Compare commits
@@ -3,9 +3,33 @@
|
|||||||
"allow": [
|
"allow": [
|
||||||
"Bash(git add *)",
|
"Bash(git add *)",
|
||||||
"Bash(git commit *)",
|
"Bash(git commit *)",
|
||||||
|
"Bash(git status *)",
|
||||||
|
"Bash(git diff *)",
|
||||||
|
"Bash(git log *)",
|
||||||
|
"Bash(git show *)",
|
||||||
|
"Bash(git branch *)",
|
||||||
|
"Bash(git checkout *)",
|
||||||
|
"Bash(git switch *)",
|
||||||
|
"Bash(git merge *)",
|
||||||
|
"Bash(git rebase *)",
|
||||||
|
"Bash(git stash *)",
|
||||||
|
"Bash(git fetch *)",
|
||||||
|
"Bash(git pull *)",
|
||||||
|
"Bash(git push *)",
|
||||||
|
"Bash(git tag *)",
|
||||||
|
"Bash(git remote *)",
|
||||||
|
"Bash(git rev-parse *)",
|
||||||
|
"Bash(git symbolic-ref *)",
|
||||||
|
"Bash(git ls-remote *)",
|
||||||
|
"Bash(git config *)",
|
||||||
|
"Bash(git blame *)",
|
||||||
|
"Bash(git shortlog *)",
|
||||||
|
"Bash(git cherry-pick *)",
|
||||||
"Bash(dart *)",
|
"Bash(dart *)",
|
||||||
"Bash(flutter *)",
|
"Bash(flutter *)",
|
||||||
"Bash(make *)"
|
"Bash(make *)",
|
||||||
|
"Bash(pql)",
|
||||||
|
"Bash(pql *)"
|
||||||
],
|
],
|
||||||
"deny": [
|
"deny": [
|
||||||
"Bash(rm -rf /*)",
|
"Bash(rm -rf /*)",
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ Cutting a release is its own commit. In a single commit:
|
|||||||
|
|
||||||
1. Move all entries from `## [Unreleased]` under a new heading `## [X.Y.Z] — YYYY-MM-DD`.
|
1. Move all entries from `## [Unreleased]` under a new heading `## [X.Y.Z] — YYYY-MM-DD`.
|
||||||
2. Leave an empty `## [Unreleased]` section at the top with its subsection skeleton ready.
|
2. Leave an empty `## [Unreleased]` section at the top with its subsection skeleton ready.
|
||||||
3. Bump `project.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired).
|
3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired).
|
||||||
4. Commit subject: `release vX.Y.Z`.
|
4. Commit subject: `release vX.Y.Z`.
|
||||||
|
|
||||||
`project.yaml` is the single source of truth for the version — the Makefile reads it for ldflag stamping of the sidecar binary, and the Flutter app reads it for build info. Bumping `project.yaml` and the changelog out of sync is the mistake this rule prevents.
|
`pubspec.yaml` is the single source of truth for the version — the Makefile reads it for ldflag stamping of the sidecar binary, and the Flutter app reads it for build info. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents.
|
||||||
|
|
||||||
## Attribution trailer
|
## Attribution trailer
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
---
|
||||||
|
name: pql
|
||||||
|
description: >
|
||||||
|
Query and plan against a markdown vault via the pql CLI. Two surfaces:
|
||||||
|
(1) structural queries — frontmatter, wikilinks, tags, headings, Bases,
|
||||||
|
DSL — use when the user asks about vault contents ("which notes…", "find
|
||||||
|
where…", "what tags", "who links to X", "run a Base", "query the vault");
|
||||||
|
(2) planning — decision records, tickets, project status — use when the
|
||||||
|
user asks about decisions, tickets, work items, or project planning
|
||||||
|
("sync decisions", "create a ticket", "what's the plan status", "show
|
||||||
|
D-5", "board"). Requires `pql` on PATH. JSON on stdout; exit 2 = zero
|
||||||
|
matches (not an error).
|
||||||
|
---
|
||||||
|
|
||||||
|
# pql — vault queries + project planning
|
||||||
|
|
||||||
|
`pql` indexes a vault into SQLite and exposes structural queries plus a
|
||||||
|
planning layer for decision records and tickets. One binary, two surfaces.
|
||||||
|
|
||||||
|
## Precondition
|
||||||
|
|
||||||
|
```bash
|
||||||
|
command -v pql
|
||||||
|
```
|
||||||
|
|
||||||
|
If absent, tell the user to install from
|
||||||
|
https://github.com/postmeridiem/pql/releases/latest. Don't install it
|
||||||
|
yourself. Don't fall back to grep unless the user explicitly asks.
|
||||||
|
|
||||||
|
## First touch: learn the vault
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pql schema
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns one row per frontmatter key with observed types and file counts.
|
||||||
|
Run once per session before writing queries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Surface 1: Vault queries
|
||||||
|
|
||||||
|
### Subcommands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `pql files [glob]` | List indexed files; optional glob filter |
|
||||||
|
| `pql tags [--sort count]` | Distinct tags with counts |
|
||||||
|
| `pql backlinks <path>` | Files linking TO a path |
|
||||||
|
| `pql outlinks <path>` | Links FROM a file |
|
||||||
|
| `pql meta <path>` | Frontmatter + tags + outlinks + headings for one file |
|
||||||
|
| `pql schema` | Typed frontmatter schema |
|
||||||
|
| `pql base <name>` | Execute an Obsidian .base file |
|
||||||
|
| `pql shell` | Interactive REPL (indexes once, then query per line) |
|
||||||
|
| `pql query "<DSL>"` | SQL-derived DSL for complex queries |
|
||||||
|
| `pql doctor` | Resolved vault/config/DB/index state |
|
||||||
|
|
||||||
|
### DSL examples
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT name, fm.date WHERE fm.type = 'meeting' ORDER BY fm.date DESC LIMIT 10
|
||||||
|
SELECT path WHERE 'project' IN tags ORDER BY path
|
||||||
|
SELECT name, fm.prior_job WHERE fm.type = 'council-member' ORDER BY name
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--file q.pql` or `--stdin` for long queries. Don't interpolate vault
|
||||||
|
content into the command line.
|
||||||
|
|
||||||
|
### Query cookbook
|
||||||
|
|
||||||
|
- **Files in folder** → `pql files 'sessions/*'`
|
||||||
|
- **Top tags** → `pql tags --sort count --limit 20`
|
||||||
|
- **What links to X?** → `pql backlinks members/vaasa/persona.md`
|
||||||
|
- **Date range** → `pql query "SELECT name, fm.date WHERE fm.date BETWEEN '2024-01-01' AND '2024-12-31'"`
|
||||||
|
- **Run a Base** → `pql base council-sessions`
|
||||||
|
- **Inspect one file** → `pql meta members/vaasa/persona.md --pretty`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Surface 2: Planning (decisions + tickets)
|
||||||
|
|
||||||
|
Planning state lives in `<vault>/.pql/pql.db` (user-authored state, not a
|
||||||
|
cache). Decision records come from `decisions/*.md`; tickets are
|
||||||
|
SQLite-native.
|
||||||
|
|
||||||
|
### Decision subcommands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `pql decisions sync` | Parse decisions/*.md → upsert into pql.db |
|
||||||
|
| `pql decisions validate` | Dry-run parse; exits non-zero on malformed records |
|
||||||
|
| `pql decisions claim <D\|Q\|R> <domain> "title"` | Print next available ID |
|
||||||
|
| `pql decisions list [--type X] [--domain X] [--status X]` | List decisions |
|
||||||
|
| `pql decisions show <id> [--with-refs] [--with-tickets]` | Show with joins |
|
||||||
|
| `pql decisions coverage` | Confirmed decisions without tickets |
|
||||||
|
| `pql decisions refs <id>` | Cross-references involving a decision |
|
||||||
|
|
||||||
|
Always `pql decisions sync` before querying if decisions/*.md may have changed.
|
||||||
|
|
||||||
|
### Ticket subcommands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `pql ticket new <type> "title" [--decision D-NNN] [--priority P]` | Create (emits T-NNN) |
|
||||||
|
| `pql ticket list [--status S] [--team T] [--assigned A] [--label L]` | List with filters |
|
||||||
|
| `pql ticket show <id> [--with-context] [--with-blockers]` | Show with joins (context = ancestors + decisions + children) |
|
||||||
|
| `pql ticket status <id[,id,...]> <new-status>` | Transition (batch via comma-separated IDs) |
|
||||||
|
| `pql ticket assign <id> <agent>` | Set assignee |
|
||||||
|
| `pql ticket block <id> --by <other>` | Add blocker |
|
||||||
|
| `pql ticket unblock <id> --from <other>` | Remove blocker |
|
||||||
|
| `pql ticket team <id> <team>` | Set team |
|
||||||
|
| `pql ticket label <id> add\|rm <label>` | Manage labels |
|
||||||
|
| `pql ticket board [--team T]` | Kanban board view |
|
||||||
|
|
||||||
|
Ticket types: initiative, epic, story, task, bug.
|
||||||
|
Status flow: backlog → ready → in_progress → review → done (also cancelled).
|
||||||
|
|
||||||
|
### Plan subcommands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `pql plan status` | Dashboard: decision counts, open Qs, ticket summary, coverage gaps |
|
||||||
|
| `pql plan export [--to FILE]` | Snapshot planning state to JSON (default: `pql-plan.json`) |
|
||||||
|
| `pql plan import [--from FILE]` | Restore planning state from a JSON snapshot |
|
||||||
|
|
||||||
|
### Versioning planning state
|
||||||
|
|
||||||
|
Planning state lives in `pql.db` (gitignored). To version it in git,
|
||||||
|
use `pql plan export` to write a committed JSON snapshot. pql does NOT
|
||||||
|
do this automatically — the user decides when and how to trigger it:
|
||||||
|
|
||||||
|
- Pre-push hook: `.githooks/pre-push` calls `pql plan export && git add pql-plan.json`
|
||||||
|
- Sprint close: a skill or script exports + commits on milestone
|
||||||
|
- Manual: run `pql plan export` before committing when state changed
|
||||||
|
|
||||||
|
On a fresh clone, `pql plan import` restores from the snapshot.
|
||||||
|
|
||||||
|
### Planning cookbook
|
||||||
|
|
||||||
|
- **Sync and list confirmed** → `pql decisions sync && pql decisions list --type confirmed`
|
||||||
|
- **Show with refs** → `pql decisions show D-5 --with-refs --pretty`
|
||||||
|
- **Create ticket** → `pql ticket new task "implement X" --decision D-5`
|
||||||
|
- **Batch close** → `pql ticket status T-1,T-2,T-3 done`
|
||||||
|
- **Coverage gaps** → `pql decisions coverage`
|
||||||
|
- **Dashboard** → `pql plan status --pretty`
|
||||||
|
- **Snapshot for git** → `pql plan export`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output contract (both surfaces)
|
||||||
|
|
||||||
|
- **stdout:** JSON array (default); `--jsonl` for one object/line; `--pretty`; `--limit N`.
|
||||||
|
- **stderr:** JSON diagnostics `{"level":"…","code":"pql.<phase>.<kind>","msg":"…"}`.
|
||||||
|
- **Exit codes:**
|
||||||
|
- `0` — success, ≥1 result
|
||||||
|
- `2` — zero matches (not an error — say "no matches", not "failed")
|
||||||
|
- `64` — bad flag
|
||||||
|
- `65` — parse/compile error (pass stderr back)
|
||||||
|
- `66` — vault/config not found
|
||||||
|
- `69` — unavailable
|
||||||
|
- `70` — internal error
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Don't pipe to `jq` for simple projections — use `--limit`, `--pretty`, `--jsonl`.
|
||||||
|
- Don't chain `pql files` + `pql meta` — one `pql query` with WHERE.
|
||||||
|
- Don't parse errors — pass stderr diagnostics back directly.
|
||||||
|
- Don't forget `pql decisions sync` before querying decisions.
|
||||||
|
- Don't try to install or upgrade pql — instruct the user if missing.
|
||||||
|
|
||||||
|
## When NOT to use
|
||||||
|
|
||||||
|
- **Body text search** → `grep`/`rg`.
|
||||||
|
- **Reading file contents** → `Read` tool.
|
||||||
|
- **Code structure** → tree-sitter / LSP.
|
||||||
|
- **Modifying vault files** → `Write`/`Edit`. pql doesn't write to vault content.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
The consuming project's `.claude/settings.json` should allow:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": ["Bash(pql)", "Bash(pql *)"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating the skill
|
||||||
|
|
||||||
|
`pql skill status` reports drift. `pql skill install` writes/updates;
|
||||||
|
`--force` overrides hand-edits. `pql doctor` also surfaces skill state.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: testmode
|
||||||
|
description: >
|
||||||
|
Run and interpret the ClideTestApp platform integration harness.
|
||||||
|
Use after modifying toolchain resolution, IPC dispatch, extension
|
||||||
|
lifecycle, theme loading, or native platform code. Also use when
|
||||||
|
the user says "run testmode", "test the app", "smoke test", or
|
||||||
|
"verify the build". Triggers on changes to: lib/kernel/src/toolchain.dart,
|
||||||
|
lib/src/daemon/dispatcher.dart, lib/src/ipc/, lib/extension/,
|
||||||
|
lib/kernel/src/theme/, lib/builtin/*/src/extension.dart,
|
||||||
|
linux/CMakeLists.txt, macos/, Makefile (run targets), lib/main.dart.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testmode harness
|
||||||
|
|
||||||
|
`ClideTestApp` is a lightweight Flutter app that boots the real platform
|
||||||
|
layer, runs structured tests, prints results, and exits. It catches
|
||||||
|
regressions that unit tests cannot see: missing binaries, broken
|
||||||
|
subprocess wiring, IPC dispatch failures, extension activation order,
|
||||||
|
and theme parse errors.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run-testmode # all categories, 60s timeout
|
||||||
|
make run-testmode TESTMODE_CATEGORY=toolchain
|
||||||
|
make run-testmode TESTMODE_CATEGORY=ipc
|
||||||
|
make run-testmode TESTMODE_CATEGORY=extensions
|
||||||
|
make run-testmode TESTMODE_TIMEOUT=120 # longer timeout for slow builds
|
||||||
|
```
|
||||||
|
|
||||||
|
Results go to stdout **and** `/tmp/clide-testmode.log`. The last line
|
||||||
|
of testmode output is machine-readable:
|
||||||
|
|
||||||
|
```
|
||||||
|
[testmode:json] {"passed":33,"failed":0,"total":33,"failures":[]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit code is non-zero when any test fails. The Makefile verifies
|
||||||
|
`"failed":0` in the log via `grep -q`.
|
||||||
|
|
||||||
|
## When to run which category
|
||||||
|
|
||||||
|
| Changed area | Category | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| Toolchain, PATH, dugite, ptyc, shell | `toolchain` | Binary resolution + exec |
|
||||||
|
| IPC envelope, dispatcher, schema | `ipc` | Round-trip + error contract |
|
||||||
|
| Extension manifest, activate, contributions | `extensions` | Register + activate lifecycle |
|
||||||
|
| Theme YAML, loader, palette | `extensions` | Theme parse is in this category |
|
||||||
|
| Platform config (CMakeLists, pbxproj, Makefile) | `all` | Full rebuild validates everything |
|
||||||
|
| Any doubt | `all` | ~30s, cheap insurance |
|
||||||
|
|
||||||
|
## Interpreting output
|
||||||
|
|
||||||
|
- `[testmode] exec | ... | OK` — subprocess ran, exit 0 or 1.
|
||||||
|
- `[testmode] PASS | ...` — assertion passed (IPC / extension tests).
|
||||||
|
- `[testmode] FAIL | ...` — assertion failed. The output column says why.
|
||||||
|
- `[testmode] exec | ... | EXCEPTION` — binary not found or not executable.
|
||||||
|
- `[testmode] exec | ... | TIMEOUT` — subprocess hung (5s limit).
|
||||||
|
|
||||||
|
If the app never prints `[testmode]` lines, the testmode gate in
|
||||||
|
`main.dart` didn't fire — check that `CLIDE_TESTMODE` is set to a
|
||||||
|
non-empty string (not a bare bool).
|
||||||
|
|
||||||
|
## Adding tests
|
||||||
|
|
||||||
|
All test logic lives in `lib/test_app.dart`. Pattern:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// In the appropriate _run*Tests method:
|
||||||
|
await _testExec('label', binary, ['args'], workDir);
|
||||||
|
// or
|
||||||
|
_addResult('label', boolCondition, 'detail string');
|
||||||
|
```
|
||||||
|
|
||||||
|
New categories: add a `runFoo` bool from the `category` string,
|
||||||
|
gate a new `_runFooTests` method, and add the category name to
|
||||||
|
the Makefile help comment and this skill's table above.
|
||||||
|
|
||||||
|
Extension tests must include transitive dependencies — e.g.
|
||||||
|
`GitExtension` requires `DiffExtension` to activate first.
|
||||||
|
|
||||||
|
## Source files
|
||||||
|
|
||||||
|
- `lib/test_app.dart` — all test logic and UI
|
||||||
|
- `lib/main.dart:54` — testmode gate (`String.fromEnvironment`)
|
||||||
|
- `Makefile:42–56` — `run-testmode` target with category variable
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
name: theme-ui
|
||||||
|
description: >-
|
||||||
|
Token selection guide for clide UI development. Use when building or
|
||||||
|
modifying widgets, panels, pane chrome, status indicators, icons, or
|
||||||
|
any visual surface. Ensures correct background, border, text, and hover
|
||||||
|
tokens are applied per surface type. Triggers on: new widget code,
|
||||||
|
theme-related changes, "which token", "what color", color/background
|
||||||
|
questions, visual inconsistency fixes, new panel/pane/view development,
|
||||||
|
adding or looking up Phosphor icons, icon codepoints.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Theme-UI — token selection for clide surfaces
|
||||||
|
|
||||||
|
All colors come from `SurfaceTokens` via `ClideTheme.of(context).surface`.
|
||||||
|
Never hardcode colors. Never use Material/Cupertino color constants.
|
||||||
|
|
||||||
|
## Token selection by surface
|
||||||
|
|
||||||
|
Pick tokens based on **where** the widget lives, not what it does.
|
||||||
|
|
||||||
|
### Chrome (hat bar, status bar, sidebar, context panel, spines, drag handles)
|
||||||
|
|
||||||
|
```
|
||||||
|
background → chromeBackground
|
||||||
|
text → chromeForeground
|
||||||
|
border → chromeBorder (1px)
|
||||||
|
active text → globalForeground
|
||||||
|
```
|
||||||
|
|
||||||
|
### Side panels (sidebar, context panel)
|
||||||
|
|
||||||
|
```
|
||||||
|
background → chromeBackground (both sides — they're chrome frame)
|
||||||
|
text → sidebarForeground
|
||||||
|
hover → sidebarItemHover
|
||||||
|
selected → sidebarItemSelected
|
||||||
|
section head → sidebarSectionHeader (muted, used for "START", "FILES", etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
Padding: 2px on outer edges, 0px on divider edge.
|
||||||
|
|
||||||
|
### Center column (workspace, Claude pane, editor)
|
||||||
|
|
||||||
|
```
|
||||||
|
background → panelBackground
|
||||||
|
text → globalForeground
|
||||||
|
```
|
||||||
|
|
||||||
|
No padding — content fills edge to edge.
|
||||||
|
|
||||||
|
### Pane headers (`ClidePaneChrome`)
|
||||||
|
|
||||||
|
```
|
||||||
|
background → panelHeader
|
||||||
|
text (title) → panelHeaderForeground
|
||||||
|
text (sub) → globalTextMuted
|
||||||
|
```
|
||||||
|
|
||||||
|
### List items (decisions, tickets, file rows, backlinks)
|
||||||
|
|
||||||
|
```
|
||||||
|
background → (none / transparent)
|
||||||
|
hover bg → listItemHoverBackground
|
||||||
|
selected bg → listItemSelectedBackground
|
||||||
|
text → listItemForeground / sidebarForeground (in sidebar)
|
||||||
|
selected txt → listItemSelectedForeground
|
||||||
|
```
|
||||||
|
|
||||||
|
In sidebar context, use `sidebarItemHover` not `listItemHoverBackground`.
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
```
|
||||||
|
normal → buttonBackground / buttonForeground / buttonBorder
|
||||||
|
hover → buttonHoverBackground
|
||||||
|
active → buttonActiveBackground
|
||||||
|
primary → buttonActiveBackground bg + globalBackground text
|
||||||
|
subtle → listItemBackground / listItemHoverBackground (no border)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dividers and separators
|
||||||
|
|
||||||
|
```
|
||||||
|
line → dividerColor (always, everywhere)
|
||||||
|
drag handle → 8px hit area, 1px visible line, panel bg fill
|
||||||
|
hover line → panelActiveBorder
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status indicators
|
||||||
|
|
||||||
|
```
|
||||||
|
success/ok → statusSuccess (green: done, added, connected)
|
||||||
|
warning → statusWarning (amber: question, modified, missing)
|
||||||
|
error → statusError (red: deleted, rejected, cancelled)
|
||||||
|
info → statusInfo (blue: in_progress, modified)
|
||||||
|
```
|
||||||
|
|
||||||
|
Map semantic states, not visual styles:
|
||||||
|
- `done` / `added` / `ok` → `statusSuccess`
|
||||||
|
- `in_progress` / `modified` → `statusInfo`
|
||||||
|
- `question` / `warning` → `statusWarning`
|
||||||
|
- `cancelled` / `deleted` / `error` → `statusError`
|
||||||
|
|
||||||
|
### Overlays (dialogs, palette, tooltips)
|
||||||
|
|
||||||
|
```
|
||||||
|
dialog bg → modalSurfaceBackground
|
||||||
|
dialog border→ modalSurfaceBorder
|
||||||
|
backdrop → modalOverlayBackground
|
||||||
|
tooltip → tooltipBackground / tooltipForeground / tooltipBorder
|
||||||
|
dropdown → dropdownBackground / dropdownForeground / dropdownBorder
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
Three constants — never hardcode sizes or families.
|
||||||
|
|
||||||
|
```
|
||||||
|
family UI → inherited from DefaultTextStyle (JosefinSans Light 300)
|
||||||
|
family mono → clideMonoFamily (JetBrainsMono)
|
||||||
|
body size → clideFontBody (15)
|
||||||
|
caption size → clideFontCaption (14) — status bar, section headers, git info
|
||||||
|
mono size → clideFontMono (14) — terminal, code, paths, IDs
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `ClideText` for themed text. Set `muted: true` for secondary text
|
||||||
|
(resolves to `globalTextMuted`). Set `fontFamily: clideMonoFamily` for
|
||||||
|
code/paths/IDs. Don't set fontFamily for UI text — it inherits.
|
||||||
|
|
||||||
|
## Token identity rule
|
||||||
|
|
||||||
|
Every visual surface gets its own named token. Never borrow a token from
|
||||||
|
another surface just because they happen to resolve to the same color.
|
||||||
|
|
||||||
|
**Wrong:** `sidebarBackground` for the hat bar (the hat isn't a sidebar).
|
||||||
|
**Right:** Create `chromeBackground` that resolves to the same palette key.
|
||||||
|
|
||||||
|
When two surfaces share a color:
|
||||||
|
1. **If they're the same conceptual surface** (sidebar + context panel are
|
||||||
|
both "side panels") → one shared token set is fine.
|
||||||
|
2. **If they're different surfaces that happen to match** (hat bar + sidebar
|
||||||
|
+ status bar are all "chrome frame") → create a shared primitive in the
|
||||||
|
palette/semantic layer (e.g. `bgChrome`) and give each surface its own
|
||||||
|
token that maps to that primitive. This lets themes diverge them later.
|
||||||
|
|
||||||
|
The palette layer has these depth primitives:
|
||||||
|
- `bg` (`#20202C`) — outermost root, behind everything
|
||||||
|
- `bgSunken` (`#1A1A24`) — chrome frame: sidebar, hat, statusbar
|
||||||
|
- `surface` (`#242838`) — elevated: pane headers, active tabs
|
||||||
|
- `surfaceHi` (`#2C3046`) — interactive: hover states, selections
|
||||||
|
|
||||||
|
Chrome tokens (`chromeBackground`/`chromeForeground`/`chromeBorder`) are
|
||||||
|
the shared root for all frame surfaces. They resolve to `bgSunken` /
|
||||||
|
`textDim` / `border` in the palette. Themes can override them to diverge
|
||||||
|
hat from sidebar from status bar if desired.
|
||||||
|
|
||||||
|
## Extension-owned domain colors
|
||||||
|
|
||||||
|
Extensions that need domain-specific color coding (ticket types, decision
|
||||||
|
types, priority levels) should NOT add tokens to `SurfaceTokens`. Instead:
|
||||||
|
|
||||||
|
1. Create a color map class in the extension (e.g. `TicketTypeColors`)
|
||||||
|
2. Ship dark and light presets, auto-selected via `ClideTheme.of(context).dark`
|
||||||
|
3. Store user overrides under `ext.<id>.colors` in settings
|
||||||
|
4. Reference: `lib/builtin/tickets/src/ticket_colors.dart`
|
||||||
|
|
||||||
|
This keeps the core token surface lean and lets each extension own its
|
||||||
|
palette. The pattern scales to any extension needing domain colors.
|
||||||
|
|
||||||
|
## Icons — Phosphor Icons
|
||||||
|
|
||||||
|
The app bundles Phosphor Icons (v2.0.8, MIT) as TTF fonts at
|
||||||
|
`assets/fonts/phosphor/` (regular, bold, fill weights).
|
||||||
|
|
||||||
|
**Codepoint reference:** `assets/fonts/phosphor/codepoints.csv` —
|
||||||
|
full mapping of all 1512 icon codepoints to kebab-case and PascalCase
|
||||||
|
names. Read this file to look up any icon by name or codepoint.
|
||||||
|
|
||||||
|
**Adding an icon:** find the codepoint in `codepoints.csv`, then add
|
||||||
|
a `static const` entry to `PhosphorIcons` in
|
||||||
|
`lib/widgets/src/icons/phosphor.dart`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
static const arrowClockwise = PhosphorIconPainter(0xe036);
|
||||||
|
```
|
||||||
|
|
||||||
|
Only add icons we actually use — don't bulk-import the full set.
|
||||||
|
|
||||||
|
**Using an icon:** `ClideIcon(PhosphorIcons.arrowClockwise, size: 13)`
|
||||||
|
or as a `TabContribution` icon field: `icon: PhosphorIcons.lightbulb`.
|
||||||
|
|
||||||
|
**Bold weight:** pass `family: 'Phosphor-Bold'` to `PhosphorIconPainter`.
|
||||||
|
Fill weight: `family: 'Phosphor-Fill'`.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Borrowing another surface's token (`sidebarBackground` for hat bar)
|
||||||
|
- `globalBackground` for panel fill → use `panelBackground`
|
||||||
|
- `listItemHoverBackground` in sidebar → use `sidebarItemHover`
|
||||||
|
- Hardcoded `Color(0xFF...)` → use a token
|
||||||
|
- `fontSize: 14` → use `clideFontCaption` or `clideFontMono`
|
||||||
|
- `fontFamily: 'JetBrainsMono'` → use `clideMonoFamily`
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
Full token list: `lib/kernel/src/theme/tokens.dart`
|
||||||
|
Resolver fallbacks: `lib/kernel/src/theme/resolver.dart`
|
||||||
|
Theme YAML example: `lib/kernel/src/theme/themes/clide.yaml`
|
||||||
@@ -7,6 +7,7 @@ insert_final_newline = true
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
max_line_length = 160
|
||||||
|
|
||||||
[*.go]
|
[*.go]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -- Flutter / Dart (app + core at repo root) ---------------------------
|
# -- macOS sandbox (machine-specific paths; template is committed) ------
|
||||||
|
/macos/Runner/DebugProfile.entitlements
|
||||||
|
|
||||||
|
# -- Flutter / Dart (single package at repo root) -----------------------
|
||||||
/.dart_tool/
|
/.dart_tool/
|
||||||
/.packages
|
/.packages
|
||||||
/.flutter-plugins
|
/.flutter-plugins
|
||||||
@@ -16,18 +19,6 @@
|
|||||||
/bin/clide.exe
|
/bin/clide.exe
|
||||||
# pubspec.lock is committed (supply-chain policy).
|
# pubspec.lock is committed (supply-chain policy).
|
||||||
|
|
||||||
# Flutter app sub-package under app/. Same patterns, different prefix.
|
|
||||||
app/.dart_tool/
|
|
||||||
app/.flutter-plugins
|
|
||||||
app/.flutter-plugins-dependencies
|
|
||||||
app/build/
|
|
||||||
app/*.iml
|
|
||||||
app/ios/Pods/
|
|
||||||
app/macos/Pods/
|
|
||||||
app/windows/flutter/ephemeral/
|
|
||||||
app/linux/flutter/ephemeral/
|
|
||||||
app/macos/Flutter/ephemeral/
|
|
||||||
|
|
||||||
# UI harness (Playwright) — npm + output artefacts.
|
# UI harness (Playwright) — npm + output artefacts.
|
||||||
tools/ui/node_modules/
|
tools/ui/node_modules/
|
||||||
tools/ui/out/
|
tools/ui/out/
|
||||||
@@ -39,6 +30,9 @@ tools/ui/.serve.pid
|
|||||||
/ptyc/bin/
|
/ptyc/bin/
|
||||||
/ptyc/*.o
|
/ptyc/*.o
|
||||||
|
|
||||||
|
# -- dugite-native (bundled git, downloaded at build time) ---------------
|
||||||
|
/native/dugite/
|
||||||
|
|
||||||
# -- Test, coverage, profile output ------------------------------------
|
# -- Test, coverage, profile output ------------------------------------
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
@@ -50,16 +44,11 @@ coverage.*
|
|||||||
# Alchemist golden-test failure artefacts (diff images written when a
|
# Alchemist golden-test failure artefacts (diff images written when a
|
||||||
# pixel-compare fails locally; the committed fixtures live under
|
# pixel-compare fails locally; the committed fixtures live under
|
||||||
# app/test/goldens/goldens/{ci,linux}/).
|
# app/test/goldens/goldens/{ci,linux}/).
|
||||||
app/test/goldens/failures/
|
test/goldens/failures/
|
||||||
|
|
||||||
# -- pql per-repo state (clide dogfoods against itself; pql's index lands
|
# -- pql per-repo state (index + planning DB, rebuilt from markdown) ----
|
||||||
# here when running queries locally). `pql.db` is also where the
|
/.pql/*
|
||||||
# `tools/scripts/plan` stopgap writes decisions + tickets (D-040).
|
!/.pql/pql-plan.json
|
||||||
/.pql/
|
|
||||||
|
|
||||||
# -- Python stopgap tooling under tools/scripts/plan ------------------
|
|
||||||
tools/scripts/__pycache__/
|
|
||||||
tools/scripts/planning/__pycache__/
|
|
||||||
|
|
||||||
# -- SQLite index files (defensive; should never land at repo root) ----
|
# -- SQLite index files (defensive; should never land at repo root) ----
|
||||||
*.sqlite
|
*.sqlite
|
||||||
@@ -104,3 +93,5 @@ legacy/**/*.egg-info/
|
|||||||
legacy/**/htmlcov/
|
legacy/**/htmlcov/
|
||||||
legacy/**/.coverage
|
legacy/**/.coverage
|
||||||
legacy/**/.coverage.*
|
legacy/**/.coverage.*
|
||||||
|
.clide/settings.yaml
|
||||||
|
.claude/skills/pql/
|
||||||
|
|||||||
@@ -9,15 +9,307 @@ This changelog tracks the Flutter rebuild at the repo root. The Python
|
|||||||
Textual implementation's changelog is preserved under
|
Textual implementation's changelog is preserved under
|
||||||
[`legacy/CHANGELOG.md`](legacy/CHANGELOG.md).
|
[`legacy/CHANGELOG.md`](legacy/CHANGELOG.md).
|
||||||
|
|
||||||
Versions are tracked in [`project.yaml`](project.yaml) under `version:`,
|
Versions are tracked in [`pubspec.yaml`](pubspec.yaml) under `version:`,
|
||||||
which is the single source of truth. Cutting a release means (a) moving
|
which is the single source of truth. Cutting a release means (a) moving
|
||||||
the entries below from `## [Unreleased]` under a new dated version
|
the entries below from `## [Unreleased]` under a new dated version
|
||||||
heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.0.0] — 2026-05-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- PTY FFI constants now platform-dispatched: `TIOCSWINSZ` (`0x80087467`
|
||||||
|
macOS / `0x5414` Linux), `O_NONBLOCK` (`0x0004` / `0x0800`), and
|
||||||
|
`MsghdrDarwin` struct with correct 4-byte field widths for macOS
|
||||||
|
`recvmsg()`.
|
||||||
|
|
||||||
|
- App settings directory uses `~/Library/Application Support/clide` on
|
||||||
|
macOS instead of `~/.config/clide`.
|
||||||
|
|
||||||
|
- Removed hardcoded `TERMINFO=/usr/share/terminfo` from pane spawn
|
||||||
|
environment — let the system resolve terminfo per platform.
|
||||||
|
|
||||||
|
- Panel `tabsFor()` now sorts by contribution priority when no user
|
||||||
|
order is set.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Canonical upstream moved from Gitea to GitHub
|
||||||
|
(`github.com/postmeridiem/clide`).
|
||||||
|
|
||||||
|
- README rewritten to reflect current single-process Flutter
|
||||||
|
architecture, built-in extensions, and build commands.
|
||||||
|
|
||||||
|
- Renamed desktop binary from `clide_app` to `clide` (Linux + macOS).
|
||||||
|
|
||||||
|
- macOS app icon uses the black-circle variant matching the Linux
|
||||||
|
desktop icon.
|
||||||
|
|
||||||
|
- ClideTestApp expanded to three test categories (toolchain, ipc,
|
||||||
|
extensions) with JSON summary, non-zero exit on failure, and
|
||||||
|
`make run-testmode TESTMODE_CATEGORY=<cat>` for selective runs.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- `make install` / `make uninstall` — builds the release bundle and
|
||||||
|
installs it to `~/.local/` with XDG desktop entry, icon registration
|
||||||
|
at seven sizes, and icon cache refresh. macOS installs to
|
||||||
|
`~/Applications/clide.app`.
|
||||||
|
|
||||||
|
- Backend isolate — all subprocess and file I/O runs in a dedicated
|
||||||
|
isolate, keeping the merged UI/platform thread on macOS free for
|
||||||
|
rendering. Communicates via SendPort using the existing IPC protocol.
|
||||||
|
Two-phase boot: resolve toolchain on spawn, initialize services on
|
||||||
|
project open. Scheduler ticker only runs while a project is active.
|
||||||
|
|
||||||
|
- Toolchain — centralized binary resolution replacing five ad-hoc
|
||||||
|
mechanisms. Resolves git, pql, tmux, ptyc, shell once at boot via
|
||||||
|
background isolate. Status bar reads `toolchain.missing` directly.
|
||||||
|
|
||||||
|
- GitClient — typed Dart API wrapping all git operations. Every
|
||||||
|
subprocess call goes through `_run()` with toolchain-resolved path
|
||||||
|
and environment. Replaces scattered `Process.run('git', ...)` calls.
|
||||||
|
|
||||||
|
- Native directory picker — macOS NSOpenPanel via method channel in
|
||||||
|
AppDelegate, GTK file chooser on Linux. Falls back to text-input
|
||||||
|
dialog on web. Shows "No git repo found" dialog on invalid selection.
|
||||||
|
|
||||||
|
- macOS desktop target — OS-detecting Makefile (`make run` works on
|
||||||
|
macOS/Linux/Windows), 1280x720 default window, squared app icons,
|
||||||
|
sandbox entitlements with SBPL exceptions, `_DARWIN_C_SOURCE` for
|
||||||
|
ptyc compilation, native traffic dots skipped (macOS titlebar owns
|
||||||
|
them), expanded PATH for Homebrew and `~/.local/bin` on GUI apps.
|
||||||
|
|
||||||
|
- Shared ClideFilterBox widget with search icon, clear button, and
|
||||||
|
debounced input. Applied consistently across all sidebar panes:
|
||||||
|
Files (path filter with flat results), Git (filter staged/unstaged
|
||||||
|
by path), Decisions (filter by ID/title/domain), Tickets (filter
|
||||||
|
by ID/title/status), Problems (filter by source/message), and
|
||||||
|
pql Query (replaces custom input).
|
||||||
|
|
||||||
|
- Interaction model from Wireframe Flows v3: eight new D-records
|
||||||
|
(D-47 through D-54) and five Q-records (Q-26 through Q-30)
|
||||||
|
codifying layout invariants, chrome budget, editor mode, context
|
||||||
|
auto-behavior, collapse spine, focus mode, state persistence, and
|
||||||
|
the canonical keyboard map.
|
||||||
|
|
||||||
|
- Panel collapse spine — collapsed side panels render as a 12px
|
||||||
|
vertical spine with rotated label, hover highlight, and badge dot
|
||||||
|
for pending context (D-51, T-30).
|
||||||
|
|
||||||
|
- Focus mode — `Ctrl+.` takes the active panel full-window;
|
||||||
|
`Escape` restores the prior layout with collapse states and
|
||||||
|
divider positions intact (D-52, T-31).
|
||||||
|
|
||||||
|
- Canonical keyboard shortcuts from the interaction model: collapse
|
||||||
|
toggles (`Ctrl+Shift+1/3`), panel focus (`Ctrl+1/2/3`), sidebar
|
||||||
|
section switching (`Alt+1–5`), focus mode, and `Escape` dismiss
|
||||||
|
(D-54, T-33).
|
||||||
|
|
||||||
|
- Right panel (context) icon rail — bottom section switcher matching
|
||||||
|
the left sidebar rail pattern (D-47, T-34).
|
||||||
|
|
||||||
|
- Editor-above-Claude mode — `Ctrl+E` opens the editor as a split
|
||||||
|
above Claude in the middle column with a draggable divider;
|
||||||
|
`Ctrl+W` or `Escape` closes it. Prompt bar Y stays fixed
|
||||||
|
(D-49, T-35).
|
||||||
|
|
||||||
|
- Layout state persists across sessions — collapse state, sidebar
|
||||||
|
and context panel sizes, active sections, and editor split ratio
|
||||||
|
saved to `.clide/settings.yaml` (D-53, T-32).
|
||||||
|
|
||||||
|
- Phosphor Icons font (v2.0.8, MIT) — regular, bold, and fill
|
||||||
|
weights. Replaces hand-painted CustomPaint icons in sidebar and
|
||||||
|
context panel icon rails.
|
||||||
|
|
||||||
|
- Decisions panel in sidebar — lists confirmed D-records from
|
||||||
|
`pql decisions list` with ID and title (T-37).
|
||||||
|
|
||||||
|
- Tickets panel in sidebar — lists tickets from `pql ticket list`
|
||||||
|
with status dot color-coded by state (T-37).
|
||||||
|
|
||||||
|
- Markdown viewer in context panel — shows raw content of the
|
||||||
|
active .md file, auto-updating on buffer switch (T-38).
|
||||||
|
|
||||||
|
- Clickable DQRT record links in markdown — `[D-56]`, `[T-43]` etc.
|
||||||
|
rendered as tappable links that navigate to the decision or ticket
|
||||||
|
detail pane via the message bus.
|
||||||
|
|
||||||
|
- Ctrl+Plus / Ctrl+Minus / Ctrl+0 text zoom (5% steps, 60%–200%
|
||||||
|
range) via `MediaQuery.textScaler`. Resets on app restart.
|
||||||
|
|
||||||
|
- Sidebar focus indicators — selecting a ticket or decision
|
||||||
|
highlights the active card in the sidebar list, auto-expands the
|
||||||
|
accordion section if collapsed, and scrolls the card into view.
|
||||||
|
|
||||||
|
- Typography scale constants `clideFontSmall` (12) and
|
||||||
|
`clideFontBadge` (11) for sidebar metadata and status badges.
|
||||||
|
`clideLineHeight` (1.25) applied app-wide via `DefaultTextStyle`.
|
||||||
|
|
||||||
|
- `files.read` IPC command for reading file content by path.
|
||||||
|
|
||||||
|
- pql sidebar restructured: Search tab is the default left tab with
|
||||||
|
ranked text search (debounced, scored results with score bar) and
|
||||||
|
a DSL toggle for raw PQL query mode; Markdown tab (filtered to
|
||||||
|
`.md` files) on the right. Clicking a result opens it in the
|
||||||
|
context panel markdown viewer with bidirectional focus highlighting.
|
||||||
|
|
||||||
|
- Kernel scheduler service with tiered timers (1min, 10min, 15min,
|
||||||
|
1hr, midnight) running on a background isolate. Emits `SchedulerTick`
|
||||||
|
events on the `DaemonBus`. Extensions subscribe by tier (T-61).
|
||||||
|
|
||||||
|
- Auto-refresh for sidebar panels: decisions refresh on file changes
|
||||||
|
to `decisions/*.md` and on 1-minute scheduler tick; tickets refresh
|
||||||
|
on 1-minute tick and on status change events (T-62).
|
||||||
|
|
||||||
|
- Manual refresh button (arrowClockwise icon) in decisions, tickets,
|
||||||
|
and pql markdown panels (T-63).
|
||||||
|
|
||||||
|
- `ClideAccordion` shared widget — extracted from tickets and
|
||||||
|
decisions views. Supports optional `leading` widget (color dot).
|
||||||
|
Pin/focus accordion logic: manually toggled sections are pinned;
|
||||||
|
the focused item's section auto-opens; unpinned sections without
|
||||||
|
focus auto-collapse.
|
||||||
|
|
||||||
|
- Ticket status buttons wired to `pql ticket status` — clicking a
|
||||||
|
status in the detail view transitions the ticket, refreshes the
|
||||||
|
sidebar list, and scrolls the ticket into its new section.
|
||||||
|
|
||||||
|
- `pql.tickets.status` IPC command accepting a list of IDs for
|
||||||
|
batch status transitions.
|
||||||
|
|
||||||
|
- Ticket sidebar sections split into individual statuses: IN
|
||||||
|
PROGRESS, REVIEW, READY, BACKLOG, DONE, CANCELLED (was four
|
||||||
|
coarse groups).
|
||||||
|
|
||||||
|
- Phosphor Icons codepoint reference CSV at
|
||||||
|
`assets/fonts/phosphor/codepoints.csv` — full mapping of all
|
||||||
|
1512 icon glyphs to kebab-case and PascalCase names.
|
||||||
|
|
||||||
|
- Graph view in context panel — lists files with inbound/outbound
|
||||||
|
link counts from `pql search --connections` (T-39).
|
||||||
|
|
||||||
|
- Welcome screen redesigned as full-screen overlay with two-column
|
||||||
|
layout: START actions (open folder, clone, Claude session) with
|
||||||
|
keyboard shortcuts, and RECENT projects list showing path, branch,
|
||||||
|
and relative timestamps. Status line shows version, daemon
|
||||||
|
connection, and active theme.
|
||||||
|
|
||||||
|
- Recent projects history persisted to user settings (up to 10
|
||||||
|
entries with path, branch, and last-opened timestamp). Last
|
||||||
|
project auto-restored on boot; falls back to cwd, then welcome.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Workspace renders Claude as the always-visible primary surface
|
||||||
|
instead of showing a tab bar (D-47, D-48). The editor is a
|
||||||
|
split overlay, not a tab.
|
||||||
|
|
||||||
|
- Syntax highlighting via tree-sitter (dart:ffi to vendored
|
||||||
|
libtree-sitter.so with embedded wasmtime). 48 grammar WASM files,
|
||||||
|
48 highlight queries. Colors map to theme syntax tokens.
|
||||||
|
|
||||||
|
- ClideMarkdown renderer — inline grouping for tight list items,
|
||||||
|
proper HTML entity unescaping after AST parse, Josefin Sans Light
|
||||||
|
with 1.25 line height, 16px body text. Inline code sized to
|
||||||
|
`clideFontMono` to match surrounding text weight.
|
||||||
|
|
||||||
|
- Default sidebar and context panel widths widened to 400px and
|
||||||
|
420px respectively (previous maximums). Context panel max raised
|
||||||
|
to 1000px.
|
||||||
|
|
||||||
|
- Ticket detail loading uses `pql ticket show --with-context` for
|
||||||
|
a single-call fetch of ancestors, decisions, and children.
|
||||||
|
Replaces N+1 parent-chain walk. `--with-decision` and
|
||||||
|
`--with-children` flags consolidated into `--with-context`.
|
||||||
|
|
||||||
|
- Ticket descriptions render through ClideMarkdown instead of plain
|
||||||
|
text, with clickable DQRT record links.
|
||||||
|
|
||||||
|
- Decision detail view subscribes to the message bus for navigation
|
||||||
|
(same pattern as tickets), enabling navigation from any source.
|
||||||
|
|
||||||
|
- TreeSitterService is now a shared singleton — eliminates native
|
||||||
|
double-free crashes from multiple WASM engine instances.
|
||||||
|
|
||||||
|
- Code block syntax highlighting fixes overlapping tree-sitter spans
|
||||||
|
that caused duplicated text (e.g. `makemake` instead of `make`).
|
||||||
|
|
||||||
|
- Context panel drag resize fixed — dragging left now correctly
|
||||||
|
grows the right panel instead of shrinking it.
|
||||||
|
|
||||||
|
- POLICY.md — project-wide rules for runtime behavior, dependency
|
||||||
|
vetting, vendored binary management, telemetry, and licensing.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Line length set to 160 across .editorconfig and dart formatter.
|
||||||
|
|
||||||
|
- Core frame vs shipped extension boundary defined (D-46). Builtins
|
||||||
|
are frame infrastructure only; content extensions are bundled but
|
||||||
|
architecturally removable.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `builtin.jira` stub — Jira integration belongs as a third-party
|
||||||
|
extension, not a frame builtin.
|
||||||
|
- `wasm_run` and `wasm_run_flutter` dependencies — replaced by
|
||||||
|
vendored libtree-sitter.so via dart:ffi. Eliminates runtime network
|
||||||
|
download that violated POLICY.md.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- pql skill installed via `pql init --with-skill=yes`
|
||||||
|
(`.claude/skills/pql/SKILL.md`). Covers vault queries and the
|
||||||
|
planning surface (decisions + tickets).
|
||||||
|
|
||||||
|
- `Bash(pql)` and `Bash(pql *)` permissions in
|
||||||
|
`.claude/settings.json`.
|
||||||
|
|
||||||
|
- pql daemon subsystem (`lib/src/pql/`). `PqlClient` wraps the pql
|
||||||
|
CLI per D-3. IPC verbs `pql.files | meta | backlinks | outlinks
|
||||||
|
| tags | schema | query | doctor | decisions.sync | decisions.list
|
||||||
|
| decisions.show | decisions.coverage | tickets.list | tickets.show
|
||||||
|
| tickets.board | plan.status`. 15 new core tests.
|
||||||
|
|
||||||
|
- `builtin.pql` — sidebar panel with four views: Files (pql-indexed
|
||||||
|
file listing), Query (PQL DSL input + results), Decisions (synced
|
||||||
|
D/Q/R records colour-coded by type), Tickets (kanban board columns).
|
||||||
|
Context panel tab showing backlinks + outlinks for the active file,
|
||||||
|
auto-refreshing on `editor.active-changed` events.
|
||||||
|
|
||||||
|
- `builtin.problems` — sidebar panel aggregating diagnostics from
|
||||||
|
`pql.doctor` and `pql.decisions.sync`. Surfaces missing index DB,
|
||||||
|
stale skill installs, and broken decision cross-references with
|
||||||
|
actionable hints.
|
||||||
|
|
||||||
|
- Git subsystem in the daemon (`lib/src/git/`). Status parser
|
||||||
|
(`git status --porcelain`), unified-diff parser, and operations
|
||||||
|
(stage, unstage, stage-hunk, discard, commit, stash, log, pull,
|
||||||
|
push). IPC verbs `git.status | diff | stage | stage-all | unstage
|
||||||
|
| stage-hunk | unstage-hunk | discard | commit | stash | stash-pop
|
||||||
|
| log | pull | push` with `git.changed` events on mutations.
|
||||||
|
42 new core tests cover parsing, operations, and dispatcher
|
||||||
|
round-trips.
|
||||||
|
|
||||||
|
- `clide git …` CLI shortcuts: `git status`, `git diff [--staged]`,
|
||||||
|
`git stage <paths>`, `git stage-all`, `git unstage`, `git discard`,
|
||||||
|
`git commit "<msg>"`, `git log [--count N]`, `git stash`,
|
||||||
|
`git stash-pop`, `git pull`, `git push`.
|
||||||
|
|
||||||
|
- `builtin.git` — sidebar panel showing staged, unstaged, untracked,
|
||||||
|
and conflicted file groups. Per-file stage/unstage/discard on hover.
|
||||||
|
Inline commit message input with Commit button. Branch + ahead/behind
|
||||||
|
display with Pull/Push actions. Auto-refreshes on `git.changed`
|
||||||
|
events.
|
||||||
|
|
||||||
|
- `builtin.diff` — workspace tab rendering unified diffs with
|
||||||
|
old/new line numbers, addition/removal colouring, and binary/rename
|
||||||
|
metadata. Staged/Unstaged toggle toolbar. Auto-refreshes on
|
||||||
|
`git.changed` events.
|
||||||
|
|
||||||
- `builtin.editor` — Tier-2 editor tab wired up. Contributes a
|
- `builtin.editor` — Tier-2 editor tab wired up. Contributes a
|
||||||
single `Editor` workspace tab that renders the daemon's active
|
single `Editor` workspace tab that renders the daemon's active
|
||||||
buffer via a new `EditorController`. Hydrates on mount
|
buffer via a new `EditorController`. Hydrates on mount
|
||||||
@@ -27,7 +319,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
echo-suppression guard avoids clobbering the caret when the
|
echo-suppression guard avoids clobbering the caret when the
|
||||||
daemon's authoritative edit echo comes back. Text surface is
|
daemon's authoritative edit echo comes back. Text surface is
|
||||||
Flutter's `EditableText` primitive — no `TextField` / Material —
|
Flutter's `EditableText` primitive — no `TextField` / Material —
|
||||||
so the D-007 "no Material root" stance carries into the editor;
|
so the D-7 "no Material root" stance carries into the editor;
|
||||||
JetBrainsMono via the shared `clideMonoFamily` constants, cursor
|
JetBrainsMono via the shared `clideMonoFamily` constants, cursor
|
||||||
+ selection colours bind to the theme.
|
+ selection colours bind to the theme.
|
||||||
|
|
||||||
@@ -60,7 +352,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
dispatcher round-trips.
|
dispatcher round-trips.
|
||||||
|
|
||||||
- `builtin.claude` — Tier-1 stub upgraded to the real Claude pane per
|
- `builtin.claude` — Tier-1 stub upgraded to the real Claude pane per
|
||||||
D-041. Contributes a primary `Claude` tab in the workspace slot that
|
D-41. Contributes a primary `Claude` tab in the workspace slot that
|
||||||
spawns `tmux new-session -A -s clide-claude-<hash> -- claude` via
|
spawns `tmux new-session -A -s clide-claude-<hash> -- claude` via
|
||||||
IPC `pane.spawn`, with `<hash>` derived from the git root path so
|
IPC `pane.spawn`, with `<hash>` derived from the git root path so
|
||||||
reopening the app re-attaches to the running conversation. Primary
|
reopening the app re-attaches to the running conversation. Primary
|
||||||
@@ -68,8 +360,8 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
Command `claude.new-secondary` is registered for the palette wiring
|
Command `claude.new-secondary` is registered for the palette wiring
|
||||||
that's coming next. If tmux isn't on PATH, falls back to spawning
|
that's coming next. If tmux isn't on PATH, falls back to spawning
|
||||||
`claude` directly and surfaces "no-tmux · fresh every launch" in
|
`claude` directly and surfaces "no-tmux · fresh every launch" in
|
||||||
the header subtitle. Accompanied by D-041 in
|
the header subtitle. Accompanied by D-41 in
|
||||||
[`decisions/architecture.md`](decisions/architecture.md#d-041-claude-panes-one-primary-per-repo-tmux-backed).
|
[`decisions/architecture.md`](decisions/architecture.md#d-41-claude-panes-one-primary-per-repo-tmux-backed).
|
||||||
|
|
||||||
- `builtin.files` — workspace filesystem panel in the sidebar. Lazy
|
- `builtin.files` — workspace filesystem panel in the sidebar. Lazy
|
||||||
tree rooted at the git root, expand/collapse, click-to-open plumbed
|
tree rooted at the git root, expand/collapse, click-to-open plumbed
|
||||||
@@ -79,7 +371,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
with ignore-file filtering. Ignore set composes clide's built-in
|
with ignore-file filtering. Ignore set composes clide's built-in
|
||||||
hide list (`.git/`, `.pql/`, `.clide/`, `.dart_tool/`, `build/`,
|
hide list (`.git/`, `.pql/`, `.clide/`, `.dart_tool/`, `build/`,
|
||||||
`node_modules/`) with `.gitignore` / `.clideignore` at the root per
|
`node_modules/`) with `.gitignore` / `.clideignore` at the root per
|
||||||
D-004. `IgnoreSet` + `IgnorePattern` support line-per-pattern, `#`
|
D-4. `IgnoreSet` + `IgnorePattern` support line-per-pattern, `#`
|
||||||
comments, anchored / directory-only / negated forms, and `**` across
|
comments, anchored / directory-only / negated forms, and `**` across
|
||||||
directories. 11 new unit tests on the matcher; 5 new dispatcher
|
directories. 11 new unit tests on the matcher; 5 new dispatcher
|
||||||
tests; 171 app tests still green.
|
tests; 171 app tests still green.
|
||||||
@@ -92,7 +384,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
`pane.resize` on viewport change. `initState` → spawn;
|
`pane.resize` on viewport change. `initState` → spawn;
|
||||||
`dispose` → `pane.close`. Error-state surface for "daemon not
|
`dispose` → `pane.close`. Error-state surface for "daemon not
|
||||||
connected" / "shell exited." No Claude-specific behaviour — that
|
connected" / "shell exited." No Claude-specific behaviour — that
|
||||||
lives in `builtin.claude` + D-041.
|
lives in `builtin.claude` + D-41.
|
||||||
|
|
||||||
- Shared pane widgets under `app/lib/widgets/`: `ClidePtyView` wraps
|
- Shared pane widgets under `app/lib/widgets/`: `ClidePtyView` wraps
|
||||||
`xterm.dart` with clide-theme token bindings, JetBrains Mono as the
|
`xterm.dart` with clide-theme token bindings, JetBrains Mono as the
|
||||||
@@ -104,15 +396,15 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
no IPC coupling.
|
no IPC coupling.
|
||||||
|
|
||||||
- `xterm: 4.0.0` Dart dependency on the Flutter app — MIT, listed in
|
- `xterm: 4.0.0` Dart dependency on the Flutter app — MIT, listed in
|
||||||
`licenses.yaml` per D-042. Hand-rolling a VT100 / xterm / truecolour
|
`licenses.yaml` per D-42. Hand-rolling a VT100 / xterm / truecolour
|
||||||
parser + renderer would be weeks for no fidelity win.
|
parser + renderer would be weeks for no fidelity win.
|
||||||
|
|
||||||
- `Q-023` — open question on SSH-remote development (run clide against
|
- `Q-23` — open question on SSH-remote development (run clide against
|
||||||
a workspace on another host). Local-first stays the Tier-1 target;
|
a workspace on another host). Local-first stays the Tier-1 target;
|
||||||
this records the constraint so the daemon / IPC / extension seams
|
this records the constraint so the daemon / IPC / extension seams
|
||||||
don't unknowingly accrete local-only assumptions.
|
don't unknowingly accrete local-only assumptions.
|
||||||
|
|
||||||
- IPC `pane` subsystem in the daemon (per D-006). Commands:
|
- IPC `pane` subsystem in the daemon (per D-6). Commands:
|
||||||
`pane.spawn | list | focus | close | write | resize | tail`. Events:
|
`pane.spawn | list | focus | close | write | resize | tail`. Events:
|
||||||
`pane.spawned`, `pane.output` (base64-framed), `pane.exit`,
|
`pane.spawned`, `pane.output` (base64-framed), `pane.exit`,
|
||||||
`pane.resized`, `pane.focused`, `pane.closed`. `PaneRegistry` owns
|
`pane.resized`, `pane.focused`, `pane.closed`. `PaneRegistry` owns
|
||||||
@@ -137,7 +429,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
verification, idempotent close.
|
verification, idempotent close.
|
||||||
|
|
||||||
- `ffi: 2.1.3` as a runtime dependency on the Dart core — justified
|
- `ffi: 2.1.3` as a runtime dependency on the Dart core — justified
|
||||||
in `pubspec.yaml` + documented in `licenses.yaml` per D-042. Used
|
in `pubspec.yaml` + documented in `licenses.yaml` per D-42. Used
|
||||||
by `lib/src/pty/ffi/` for `socketpair`, `recvmsg` with `SCM_RIGHTS`,
|
by `lib/src/pty/ffi/` for `socketpair`, `recvmsg` with `SCM_RIGHTS`,
|
||||||
`read`/`write` on raw fds, and `ioctl(TIOCSWINSZ)`.
|
`read`/`write` on raw fds, and `ioctl(TIOCSWINSZ)`.
|
||||||
|
|
||||||
@@ -168,7 +460,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
`license_file` pointer, and a one-line purpose. Bundled alongside
|
`license_file` pointer, and a one-line purpose. Bundled alongside
|
||||||
the per-dep license texts. The About screen (Tier 6) will render
|
the per-dep license texts. The About screen (Tier 6) will render
|
||||||
this file verbatim. Accompanied by
|
this file verbatim. Accompanied by
|
||||||
[`D-042`](decisions/tooling.md#d-042-bundled-dependencies-documented-in-licensesyaml):
|
[`D-42`](decisions/tooling.md#d-42-bundled-dependencies-documented-in-licensesyaml):
|
||||||
adding a dep is a two-step commit (artefact + `licenses.yaml`
|
adding a dep is a two-step commit (artefact + `licenses.yaml`
|
||||||
entry in the same changeset).
|
entry in the same changeset).
|
||||||
|
|
||||||
@@ -176,10 +468,10 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
- CLAUDE.md "Dependencies & supply chain" section gains the
|
- CLAUDE.md "Dependencies & supply chain" section gains the
|
||||||
"document every bundled dependency" rule, pointing at
|
"document every bundled dependency" rule, pointing at
|
||||||
`app/assets/licenses.yaml` and `D-042`.
|
`app/assets/licenses.yaml` and `D-42`.
|
||||||
|
|
||||||
- `ptyc/` — the C PTY-spawn helper, peer of `pql` per
|
- `ptyc/` — the C PTY-spawn helper, peer of `pql` per
|
||||||
[`D-005`](decisions/architecture.md#d-005-dart-core-sidecar-dissolved-ptyc-as-pql-peer).
|
[`D-5`](decisions/architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer).
|
||||||
One-shot, libc-only, ~400 LOC. Reads a JSON request on stdin
|
One-shot, libc-only, ~400 LOC. Reads a JSON request on stdin
|
||||||
(`argv`, optional `cwd`/`env`/`cols`/`rows`), does
|
(`argv`, optional `cwd`/`env`/`cols`/`rows`), does
|
||||||
`posix_openpt` + `fork` + `execvp`, and hands the master fd back
|
`posix_openpt` + `fork` + `execvp`, and hands the master fd back
|
||||||
@@ -192,10 +484,10 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
`ptyc-build` / `ptyc-clean`.
|
`ptyc-build` / `ptyc-clean`.
|
||||||
|
|
||||||
- Migrated the `docs/ADRs/` content into `decisions/` as D/R records:
|
- Migrated the `docs/ADRs/` content into `decisions/` as D/R records:
|
||||||
ADR 0001 → `D-001`, ADR 0002 → `R-002` (superseded by `D-005`), ADR
|
ADR 0001 → `D-1`, ADR 0002 → `R-2` (superseded by `D-5`), ADR
|
||||||
0003 → `D-003`, ADR 0004 → `D-004`, ADR 0005 → `D-005`, ADR 0006 →
|
0003 → `D-3`, ADR 0004 → `D-4`, ADR 0005 → `D-5`, ADR 0006 →
|
||||||
`D-006`. Titles preserved; ADR 0006's trailing open questions moved
|
`D-6`. Titles preserved; ADR 0006's trailing open questions moved
|
||||||
to `questions-architecture.md` as `Q-001` / `Q-002` / `Q-003`. The
|
to `questions-architecture.md` as `Q-1` / `Q-2` / `Q-3`. The
|
||||||
originals are preserved in git history.
|
originals are preserved in git history.
|
||||||
|
|
||||||
- `decisions/` at the repo root — Q&D record system ported from
|
- `decisions/` at the repo root — Q&D record system ported from
|
||||||
@@ -220,8 +512,8 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
(`tools/scripts/plan ` → `pql `). Ported from settled-reach with
|
(`tools/scripts/plan ` → `pql `). Ported from settled-reach with
|
||||||
the Scrum layer stripped; ticket IDs are `T-NNN` (TEXT PKs) and
|
the Scrum layer stripped; ticket IDs are `T-NNN` (TEXT PKs) and
|
||||||
there's no `sprints` table. Time-limited per
|
there's no `sprints` table. Time-limited per
|
||||||
[`D-040`](decisions/process.md#d-040-python-stopgap-under-toolsscriptsplan)
|
[`D-40`](decisions/process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
||||||
/ [`R-011`](decisions/rejected.md#r-011-permanent-stopgap).
|
/ [`R-11`](decisions/rejected.md#r-11-permanent-stopgap).
|
||||||
|
|
||||||
- `make decisions-validate` — cheap parser dry-run wired into
|
- `make decisions-validate` — cheap parser dry-run wired into
|
||||||
`push-check`. Catches malformed records before push.
|
`push-check`. Catches malformed records before push.
|
||||||
@@ -229,7 +521,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
- Reserved extension slots — `builtin.decisions`, `builtin.tickets`,
|
- Reserved extension slots — `builtin.decisions`, `builtin.tickets`,
|
||||||
`builtin.claude-control`. Id-reserving stubs under
|
`builtin.claude-control`. Id-reserving stubs under
|
||||||
`app/lib/builtin/` with no contributions yet. Implementations land
|
`app/lib/builtin/` with no contributions yet. Implementations land
|
||||||
once [`Q-021`](decisions/questions-architecture.md#q-021-pql-absorbs-planning-vs-keeps-separate)
|
once [`Q-21`](decisions/questions-architecture.md#q-21-pql-absorbs-planning-vs-keeps-separate)
|
||||||
resolves (decisions + tickets) or when the claude-control tier
|
resolves (decisions + tickets) or when the claude-control tier
|
||||||
arrives (`.claude/` first-class surface — distinct from the
|
arrives (`.claude/` first-class surface — distinct from the
|
||||||
existing `builtin.claude` PTY-pane stub).
|
existing `builtin.claude` PTY-pane stub).
|
||||||
@@ -245,6 +537,21 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
updated to reference `decisions/architecture.md` instead of the
|
updated to reference `decisions/architecture.md` instead of the
|
||||||
deleted `docs/ADRs/`.
|
deleted `docs/ADRs/`.
|
||||||
|
|
||||||
|
- `make decisions-validate` rewired from `tools/scripts/plan` to
|
||||||
|
`pql decisions validate`.
|
||||||
|
|
||||||
|
- Decision discipline guardrail in CLAUDE.md now points at
|
||||||
|
`pql decisions claim` instead of the Python stopgap.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `tools/scripts/plan` — Python stopgap planning scripts, superseded
|
||||||
|
by `pql` 1.0 native `decisions` and `ticket` subcommands. Sunset
|
||||||
|
condition from
|
||||||
|
[`D-40`](decisions/process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
||||||
|
met; deletion per
|
||||||
|
[`R-11`](decisions/rejected.md#r-11-permanent-stopgap).
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- `docs/ADRs/` directory — content lifted into `decisions/` as D/R
|
- `docs/ADRs/` directory — content lifted into `decisions/` as D/R
|
||||||
@@ -252,7 +559,7 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
- Go sidecar skeleton under `sidecar/` — `cmd/clide/main.go`, `go.mod`, and the `internal/*` packages (`cli`, `daemon`, `diag`, `git`, `ipc`, `pql`, `proc`, `pty`, `version`). Deleted wholesale per [ADR 0005](docs/ADRs/0005-dart-core-ptyc-peer.md): the "sidecar language: Go" premise no longer holds once the core is Dart. All functionality listed for those packages will be reimplemented under `lib/` as part of Tier 0.
|
- Go sidecar skeleton under `sidecar/` — `cmd/clide/main.go`, `go.mod`, and the `internal/*` packages (`cli`, `daemon`, `diag`, `git`, `ipc`, `pql`, `proc`, `pty`, `version`). Deleted wholesale per [ADR 0005](docs/ADRs/0005-dart-core-ptyc-peer.md): the "sidecar language: Go" premise no longer holds once the core is Dart. All functionality listed for those packages will be reimplemented under `lib/` as part of Tier 0.
|
||||||
- Go-specific Makefile targets (`lint`, `vuln`, `test-race`, `fmt`, `tidy`, `snapshot`, `tools`, `install` via Go), the `govulncheck`/`goimports`/`golangci-lint` version pins, and the pre-push hook's `GOBIN` PATH injection. Replaced with Dart/Flutter equivalents (`analyze`, `format`, `test`, `test-integration`, `build` via `dart compile exe`).
|
- Go-specific Makefile targets (`lint`, `vuln`, `test-race`, `fmt`, `tidy`, `snapshot`, `tools`, `install` via Go), the `govulncheck`/`goimports`/`golangci-lint` version pins, and the pre-push hook's `GOBIN` PATH injection. Replaced with Dart/Flutter equivalents (`analyze`, `format`, `test`, `test-integration`, `build` via `dart compile exe`).
|
||||||
- `module:` and `go_version:` from `project.yaml` — single-language core means no Go module path to track.
|
- `module:` and `go_version:` from `pubspec.yaml` — single-language core means no Go module path to track.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -298,14 +605,14 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit.
|
|||||||
[ADR 0004](docs/ADRs/0004-ignore-file-strategy.md) — Ignore file strategy (`ignore_files:` in `.pql/config.yaml`, layered).
|
[ADR 0004](docs/ADRs/0004-ignore-file-strategy.md) — Ignore file strategy (`ignore_files:` in `.pql/config.yaml`, layered).
|
||||||
- Pre-push quality gate: `.githooks/pre-push` runs `make push-check` (lint + test + test-race + test-integration + vuln + app-analyze + app-test) so bad pushes are caught locally before they hit Gitea. The app-side targets gracefully noop until Flutter is scaffolded. Install with `make hooks` (sets `git config core.hooksPath .githooks`); the hook prepends `$GOBIN`/`$HOME/go/bin` to PATH so govulncheck resolves without the user touching their shell profile.
|
- Pre-push quality gate: `.githooks/pre-push` runs `make push-check` (lint + test + test-race + test-integration + vuln + app-analyze + app-test) so bad pushes are caught locally before they hit Gitea. The app-side targets gracefully noop until Flutter is scaffolded. Install with `make hooks` (sets `git config core.hooksPath .githooks`); the hook prepends `$GOBIN`/`$HOME/go/bin` to PATH so govulncheck resolves without the user touching their shell profile.
|
||||||
- Go sidecar/CLI skeleton under `sidecar/` (module `git.schweitz.net/jpmschweitzer/clide/sidecar`): `cmd/clide/main.go`, `internal/cli` with a stdlib-flag dispatch, `internal/diag` mirroring pql's exit-code + stderr-JSON contract, `internal/version` with ldflag-stamped build info, and placeholder packages for `daemon`, `pty`, `proc`, `git`, `ipc`, `pql` awaiting their tier. `clide --version` emits JSON build-info today.
|
- Go sidecar/CLI skeleton under `sidecar/` (module `git.schweitz.net/jpmschweitzer/clide/sidecar`): `cmd/clide/main.go`, `internal/cli` with a stdlib-flag dispatch, `internal/diag` mirroring pql's exit-code + stderr-JSON contract, `internal/version` with ldflag-stamped build info, and placeholder packages for `daemon`, `pty`, `proc`, `git`, `ipc`, `pql` awaiting their tier. `clide --version` emits JSON build-info today.
|
||||||
- Root `Makefile` drives both the Go sidecar and the Flutter app under one toolchain. Version is read from `project.yaml` via awk and stamped into the sidecar via `-ldflags -X`. Flutter targets gracefully noop before the app is scaffolded so the Makefile is usable from day one. Pinned Go tooling (govulncheck, goimports, golangci-lint) installs via `make tools`.
|
- Root `Makefile` drives both the Go sidecar and the Flutter app under one toolchain. Version is read from `pubspec.yaml` via awk and stamped into the sidecar via `-ldflags -X`. Flutter targets gracefully noop before the app is scaffolded so the Makefile is usable from day one. Pinned Go tooling (govulncheck, goimports, golangci-lint) installs via `make tools`.
|
||||||
- `ci/` entry scripts: `test.sh`, `lint.sh` (includes the supply-chain gate — no green lint without a green CVE scan), `security.sh`, `release.sh` (stub).
|
- `ci/` entry scripts: `test.sh`, `lint.sh` (includes the supply-chain gate — no green lint without a green CVE scan), `security.sh`, `release.sh` (stub).
|
||||||
- Project identity files for the Flutter rebuild at the repo root: `project.yaml` (single source of truth for version + module path, version 2.0.0-dev), a fresh `README.md`, MIT `LICENSE`, and `.editorconfig`. The Python clide's manifest and README are preserved under `legacy/`.
|
- Project identity files for the Flutter rebuild at the repo root: `pubspec.yaml` (single source of truth for version + module path, version 2.0.0-dev), a fresh `README.md`, MIT `LICENSE`, and `.editorconfig`. The Python clide's manifest and README are preserved under `legacy/`.
|
||||||
- [`docs/initial-plan.md`](docs/initial-plan.md) — the north-star design document for the Flutter rebuild. Captures what we kept from Python Clide (pane model, git skills, Claude-always-visible), what we took from Obsidian (canvas and graph — no vault, no bases, no plugin inheritance), what Claudian's short experiment contributed (Go sidecar, CLI-first, pql-as-subsystem, ignore-file strategy), and the tier roadmap (Tier 0 app+sidecar handshake → Tier 5 canvas+graph).
|
- [`docs/initial-plan.md`](docs/initial-plan.md) — the north-star design document for the Flutter rebuild. Captures what we kept from Python Clide (pane model, git skills, Claude-always-visible), what we took from Obsidian (canvas and graph — no vault, no bases, no plugin inheritance), what Claudian's short experiment contributed (Go sidecar, CLI-first, pql-as-subsystem, ignore-file strategy), and the tier roadmap (Tier 0 app+sidecar handshake → Tier 5 canvas+graph).
|
||||||
- [`CLAUDE.md`](CLAUDE.md) orientation doc for future Claude Code instances: project identity, guardrails as one-liners, tier ordering, parent-project pointers, commands, dependencies & supply chain, open questions. Points at the design doc and ADRs rather than restating their content.
|
- [`CLAUDE.md`](CLAUDE.md) orientation doc for future Claude Code instances: project identity, guardrails as one-liners, tier ordering, parent-project pointers, commands, dependencies & supply chain, open questions. Points at the design doc and ADRs rather than restating their content.
|
||||||
- Claude Code configuration under `.claude/`: project-level
|
- Claude Code configuration under `.claude/`: project-level
|
||||||
allow/deny permissions and two skills — `skill-create` (generic
|
allow/deny permissions and two skills — `skill-create` (generic
|
||||||
skill authoring guidance) and `git-commit` (this repo's commit
|
skill authoring guidance) and `git-commit` (this repo's commit
|
||||||
conventions: no Conventional Commits, Keep a Changelog discipline,
|
conventions: no Conventional Commits, Keep a Changelog discipline,
|
||||||
`project.yaml`-and-changelog-bumped-together rule, attribution
|
`pubspec.yaml`-and-changelog-bumped-together rule, attribution
|
||||||
trailer, safety reminders).
|
trailer, safety reminders).
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## What clide is
|
## What clide is
|
||||||
|
|
||||||
A Flutter desktop IDE for Claude Code. One language (Dart) across the stack, plus small native supporter tools where Dart can't reach.
|
A Flutter desktop IDE for Claude Code. Single Flutter package at the repo root, plus small native supporter tools where Dart can't reach.
|
||||||
|
|
||||||
- **`app/`** — Flutter desktop application (Linux / macOS primary; Windows stretch).
|
- **`lib/`** — all Dart code. Subsystem handlers (`lib/src/daemon/`, `lib/src/pty/`, `lib/src/ipc/`, `lib/src/git/`, `lib/src/pql/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), and the extension framework (`lib/extension/`). The Flutter app hosts the IPC server in-process (D-56).
|
||||||
- **`lib/` + `bin/clide.dart`** — Dart core, shared by the app and by one AOT-compiled binary that has two modes: `clide <subcommand>` (one-shot for Claude) and `clide --daemon` (long-running for the app). Owns IPC, PTYs (via `ptyc`), subprocesses, file watchers, git shell-outs, pql invocations.
|
|
||||||
- **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
- **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
||||||
- **`ptyc`** — small C supporter tool, peer of pql. Spawns a PTY + child and hands the master fd back over `SCM_RIGHTS`. Clide shells out to it for every pane (shell, tmux, claude, LSP, debug adapter).
|
- **`ptyc/`** — small C supporter tool, peer of pql. Spawns a PTY + child and hands the master fd back over `SCM_RIGHTS`. Clide shells out to it for every pane (shell, tmux, claude, LSP, debug adapter).
|
||||||
|
|
||||||
App ↔ daemon ↔ CLI speak a single JSON-lines unix socket protocol. The daemon outlives app restarts so Claude sessions survive reopens. Native rendering — markdown, canvas, graph — is Dart/Flutter (`CustomPaint` + widgets), not third-party packages.
|
tmux owns Claude session persistence (D-41) — the app re-attaches on restart via `tmux new-session -A`. Native rendering — markdown, canvas, graph — is Dart/Flutter (`CustomPaint` + widgets), not third-party packages.
|
||||||
|
|
||||||
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`decisions/`](decisions/) (`D-NNN` confirmed, `Q-NNN` open, `R-NNN` rejected — see [`decisions/README.md`](decisions/README.md)). Python Textual predecessor under [`legacy/`](legacy/).
|
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`decisions/`](decisions/) (`D-NNN` confirmed, `Q-NNN` open, `R-NNN` rejected — see [`decisions/README.md`](decisions/README.md)). Python Textual predecessor under [`legacy/`](legacy/).
|
||||||
|
|
||||||
@@ -19,58 +18,60 @@ Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`decisio
|
|||||||
|
|
||||||
These are load-bearing. Violating any means the design is wrong, not the rule.
|
These are load-bearing. Violating any means the design is wrong, not the rule.
|
||||||
|
|
||||||
- **Flutter desktop is the host. No Electron, ever.** Web target may work as a happy accident — don't compromise desktop fidelity for it. If we ship a web build at all, prefer Flutter's **WebAssembly (CanvasKit/Skwasm) compile** over the JS/HTML renderer: it matches the desktop rendering pipeline, keeps our custom `CustomPaint` components pixel-identical, and avoids the DOM-renderer quirks around input handling and terminal-style content. `xterm.dart` is the terminal renderer (Tier 1); markdown, canvas, graph are custom `CustomPaint`/widget components (Tiers 2+, 5).
|
- **Flutter desktop is the host. No Electron, ever.** Web target may work as a happy accident — don't compromise desktop fidelity for it. If we ship a web build at all, prefer Flutter's **WebAssembly (CanvasKit/Skwasm) compile** over the JS/HTML renderer. `xterm.dart` is the terminal renderer; markdown, canvas, graph are custom `CustomPaint`/widget components.
|
||||||
- **No heavy lifting in the UI layer.** The app renders and handles input; process/PTY/IO lifecycles live in the daemon. The daemon is Dart too — the split is *process boundary*, not language boundary.
|
- **Single process.** The Flutter app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), extensions. No separate daemon binary (D-56 dissolved it). The CLI surface for Claude is a thin C client (ptyc peer).
|
||||||
- **CLI-first, not MCP.** Claude talks via Bash (`clide ...`), matching pql's contract. See [`D-001`](decisions/architecture.md#d-001-cli-first-not-mcp).
|
- **CLI-first, not MCP.** Claude talks via Bash (`clide ...`), matching pql's contract. See [`D-1`](decisions/architecture.md#d-1-cli-first-not-mcp).
|
||||||
- **Dart is the core; native supporter tools fill specific gaps.** One Dart AOT binary for CLI + daemon. `ptyc` (C) for PTY spawning. `pql` (Go) for queries. No second "core language." See [`D-005`](decisions/architecture.md#d-005-dart-core-sidecar-dissolved-ptyc-as-pql-peer) (supersedes [`R-002`](decisions/rejected.md#r-002-go-sidecar)).
|
- **Dart is the core; native supporter tools fill specific gaps.** `ptyc` (C) for PTY spawning. `pql` (Go) for queries. No second "core language." See [`D-5`](decisions/architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer) (amended by D-56).
|
||||||
- **Own the rendering stack.** PTY (via `ptyc`), markdown renderer, graph, canvas — all clide-owned, not pulled from opinionated packages. Third-party rendering is where we'd hit ceilings first; we'd rather pay the cost up front.
|
- **Own the rendering stack.** PTY (via `ptyc`), markdown renderer, graph, canvas — all clide-owned, not pulled from opinionated packages.
|
||||||
- **User/Claude parity.** Every CLI subcommand has a UI affordance in the app, and every UI action has a CLI. Events are symmetric: every UI state change is a subscribable event. See [`D-006`](decisions/architecture.md#d-006-cli-and-event-surface-contract).
|
- **User/Claude parity.** Every CLI subcommand has a UI affordance, and every UI action has a CLI. See [`D-6`](decisions/architecture.md#d-6-cli-and-event-surface-contract).
|
||||||
- **pql: wrap, don't duplicate, and treat it as a clide subsystem when present.** Pql logic only lives in `lib/src/pql/` (pure shell-outs). Clide owns pql's `ignore_files:` config key; it never touches pql's `.pql/` index/cache data. See [`D-003`](decisions/architecture.md#d-003-pql-as-supporter-tool-clide-wraps-never-duplicates).
|
- **pql: wrap, don't duplicate.** Pql logic only lives in `lib/src/pql/` (pure shell-outs). Clide owns pql's `ignore_files:` config key; it never touches pql's `.pql/` index/cache data. See [`D-3`](decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates).
|
||||||
- **Repo-is-the-workspace.** The git repo root is the workspace — no parallel "vault" concept. Clide dogfoods against its own repo.
|
- **Repo-is-the-workspace.** The git repo root is the workspace — no parallel "vault" concept.
|
||||||
- **Ignore discipline.** Single knob: `ignore_files:` in `.pql/config.yaml`, ordered layering. Default `[.gitignore]`; clide writes `[.gitignore, .clideignore]` when `.clideignore` exists. See [`D-004`](decisions/architecture.md#d-004-ignore-file-strategy).
|
- **Ignore discipline.** Single knob: `ignore_files:` in `.pql/config.yaml`, ordered layering. See [`D-4`](decisions/architecture.md#d-4-ignore-file-strategy).
|
||||||
- **Decision discipline.** All architectural choices live in `decisions/<domain>.md` as `D-NNN` records. Open questions live in `decisions/questions-<domain>.md` as `Q-NNN`. Rejected alternatives live in `decisions/rejected.md` as `R-NNN`. Before an architectural change, read the relevant domain file; before disagreeing with a guardrail, propose an amendment to the underlying `D-NNN` rather than a one-off. Claim new IDs via `tools/scripts/plan decisions claim D <domain> "title"` (until pql ships planning subcommands — see [`Q-021`](decisions/questions-architecture.md#q-021-pql-absorbs-planning-vs-keeps-separate)). See [`decisions/README.md`](decisions/README.md).
|
- **Decision discipline.** All architectural choices live in `decisions/<domain>.md` as `D-NNN` records. Open questions as `Q-NNN`. Rejected alternatives as `R-NNN`. Claim new IDs via `pql decisions claim D <domain> "title"`. See [`decisions/README.md`](decisions/README.md).
|
||||||
|
|
||||||
## Tier ordering (don't skip ahead)
|
## Repo layout
|
||||||
|
|
||||||
1. **Tier 0** — Flutter app + Dart daemon handshake, empty IDE shell.
|
```
|
||||||
2. **Tier 1** — Claude in an `xterm.dart` pane backed by `ptyc`-spawned PTYs; session persists across app restarts.
|
lib/
|
||||||
3. **Tier 2** — Pane model + active-file awareness + `clide open/active/insert/replace-selection/tail`.
|
main.dart # Flutter app entry point
|
||||||
4. **Tier 3** — Git panel (staged/unstaged, hunk stage, conflict UI) + diff tab + `clide git …`.
|
app.dart # Root layout, workspace, panels
|
||||||
5. **Tier 4** — pql integration: Query panel, file tree, backlinks, problems — all drawing from pql.
|
clide.dart # Barrel: shared types (IPC envelope, pane kinds, etc.)
|
||||||
6. **Tier 5** — Canvas (`CustomPaint` + `InteractiveViewer`) and graph view.
|
src/ # Core subsystems (IPC server, PTY, git, files, pql, panes, editor)
|
||||||
7. **Tier 6** — Dart extension API, settings, theming, distributable builds.
|
kernel/ # Kernel services (theme, i18n, settings, panels, commands, focus)
|
||||||
|
builtin/ # Built-in extensions (claude, editor, files, git, terminal, etc.)
|
||||||
See `docs/initial-plan.md` for the full tier definitions and acceptance criteria.
|
widgets/ # Custom widget primitives (no Material/Cupertino)
|
||||||
|
extension/ # Extension contract and registration
|
||||||
## Parent projects
|
lua/ # Lua runtime support (Tier 6)
|
||||||
|
test/ # All tests (core subsystems + widgets + goldens + a11y)
|
||||||
- **`legacy/`** — Python Textual clide v1.2.0. Feature-frozen. Reference for the pane model, panel set, git skills (`/commit`, `/stash`, `/pull`, `/push` — rewire to `clide git …`), TODO.md parsing format.
|
assets/ # Fonts, themes, grammars, licenses, logo
|
||||||
- **`projects/claudian`** (April 2026, discarded) — 2-day experiment with an Obsidian-plugin approach. Its architectural patterns (CLI-first, pql-as-subsystem, ignore-file strategy, supply-chain gate, changelog discipline, commit conventions) are the records you see under `decisions/` and the skills under `.claude/skills/`. It first proposed a Go sidecar; [`D-005`](decisions/architecture.md#d-005-dart-core-sidecar-dissolved-ptyc-as-pql-peer) reversed that in favour of a Dart core.
|
linux/, macos/, web/ # Flutter platform directories
|
||||||
- **[`projects/pql`](https://github.com/postmeridiem/pql)** — active supporter tool. Clide depends on it; never duplicates it.
|
ptyc/ # C PTY helper
|
||||||
|
native/ # Vendored native libs (libtree-sitter.so)
|
||||||
|
decisions/ # D/Q/R records
|
||||||
|
docs/ # Design docs, wireframes
|
||||||
|
legacy/ # Python Textual clide v1.2 (frozen)
|
||||||
|
```
|
||||||
|
|
||||||
## Dependencies & supply chain
|
## Dependencies & supply chain
|
||||||
|
|
||||||
- **Dart (core + app):** prefer-zero-deps. Flutter-SDK widgets first; third-party packages need justification. What stays is exact-pinned in `pubspec.yaml` (no caret ranges). Advisories reviewed before every bump; `pubspec.lock` committed.
|
- **Prefer-zero-deps.** Flutter-SDK widgets first; third-party packages need justification. What stays is exact-pinned in `pubspec.yaml` (no caret ranges). Advisories reviewed before every bump; `pubspec.lock` committed.
|
||||||
- **Document every bundled dependency.** Every third-party artefact that ships in the clide binary — Dart packages, fonts, bundled assets, native supporter tools — is listed in [`app/assets/licenses.yaml`](app/assets/licenses.yaml) with name, kind, version, homepage, license, relative path to the bundled license text, and a one-line purpose. Adding a dep is a two-step commit: add the artefact **and** the `licenses.yaml` entry in the same changeset. The About screen (Tier 6) renders the file verbatim; until then, the list being accurate is the contract. See [`D-042`](decisions/tooling.md#d-042-bundled-dependencies-documented-in-licensesyaml).
|
- **Document every bundled dependency.** Listed in [`assets/licenses.yaml`](assets/licenses.yaml) with name, kind, version, homepage, license, and purpose. Adding a dep is a two-step commit: add the artefact **and** the `licenses.yaml` entry. See [`D-42`](decisions/tooling.md#d-42-bundled-dependencies-documented-in-licensesyaml).
|
||||||
- **`ptyc` and any future native supporter tool:** no dep graph by design (libc-only for `ptyc`). "Audit" is reading the source before each bump.
|
- **`ptyc` and any future native supporter tool:** no dep graph by design (libc-only for `ptyc`). "Audit" is reading the source before each bump.
|
||||||
- `pubspec.lock` is always committed.
|
|
||||||
- `make security` runs the Dart advisory review; `ci/security.sh` is the CI entry.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
(Placeholder until Tier 0 commits land these targets in real form.)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
make build # dart compile exe bin/clide.dart -o bin/clide
|
make run # launch Flutter desktop app
|
||||||
make test # flutter test
|
|
||||||
make test-integration# daemon + CLI + fixture-repo suite
|
|
||||||
make analyze # flutter analyze
|
make analyze # flutter analyze
|
||||||
make format # dart format --set-exit-if-changed
|
make format # dart format --set-exit-if-changed
|
||||||
|
make test # fast test suite (analyze + format + unit + widget + golden)
|
||||||
|
make test-core # core subsystem tests (IPC, PTY, git, pane registry)
|
||||||
|
make test-a11y # accessibility contract tests
|
||||||
|
make test-integration# real app boot integration tests
|
||||||
make build-linux # flutter build linux
|
make build-linux # flutter build linux
|
||||||
make build-macos # flutter build macos
|
make build-macos # flutter build macos
|
||||||
make ptyc-build # build the ptyc PTY-spawn helper
|
make ptyc-build # build the ptyc PTY-spawn helper
|
||||||
make security # Dart advisory review + ptyc source review
|
make push-check # pre-push gate: decisions + core + fast tests + a11y
|
||||||
make push-check # pre-push gate: analyze + format + test
|
|
||||||
make hooks # install the repo's git hooks (one-time setup)
|
make hooks # install the repo's git hooks (one-time setup)
|
||||||
make clean # remove build artefacts
|
make clean # remove build artefacts
|
||||||
```
|
```
|
||||||
@@ -79,7 +80,7 @@ One-time setup on a fresh clone: `make hooks && flutter pub get` once Flutter is
|
|||||||
|
|
||||||
## Changelog discipline
|
## Changelog discipline
|
||||||
|
|
||||||
[Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). Every user-visible commit adds an entry under `## [Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md). Cutting a release means moving Unreleased entries under a new dated version heading **and** bumping `project.yaml` `version:` in the same commit — see [`.claude/skills/git-commit/SKILL.md`](.claude/skills/git-commit/SKILL.md) for the full rule.
|
[Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). Every user-visible commit adds an entry under `## [Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md). Cutting a release means moving Unreleased entries under a new dated version heading **and** bumping `pubspec.yaml` `version:` in the same commit — see [`.claude/skills/git-commit/SKILL.md`](.claude/skills/git-commit/SKILL.md) for the full rule.
|
||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
See [`decisions/README.md`](decisions/README.md).
|
|
||||||
@@ -1,80 +1,90 @@
|
|||||||
# clide — local dev targets. CI scripts in ci/ shell out to these.
|
# clide — local dev targets. CI scripts in ci/ shell out to these.
|
||||||
#
|
#
|
||||||
# One toolchain: Flutter + Dart. Native supporter tools (`ptyc`, future
|
# Single Flutter package at the repo root. Native supporter tools
|
||||||
# peers) live in their own directories with their own build targets and
|
# (ptyc, future peers) live in their own directories.
|
||||||
# compose in under `make build`.
|
|
||||||
|
|
||||||
INSTALL_DIR ?= $(HOME)/.local/bin
|
INSTALL_DIR ?= $(HOME)/.local/bin
|
||||||
|
|
||||||
# Version stamping. Source of truth: project.yaml `version:` field. Local
|
# -- OS detection ------------------------------------------------------------
|
||||||
# builds augment with git short SHA + dirty marker (semver build metadata).
|
# Maps uname to Flutter device/build targets: linux, macos, windows.
|
||||||
VERSION_BASE ?= $(shell awk -F': *' '/^version:/ {gsub(/[" ]/,"",$$2); print $$2; exit}' project.yaml)
|
|
||||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
UNAME_S := $(shell uname -s)
|
||||||
DIRTY := $(shell git diff --quiet HEAD 2>/dev/null || echo .dirty)
|
ifeq ($(UNAME_S),Linux)
|
||||||
VERSION ?= $(VERSION_BASE)+$(COMMIT)$(DIRTY)
|
FLUTTER_OS := linux
|
||||||
|
else ifeq ($(UNAME_S),Darwin)
|
||||||
|
FLUTTER_OS := macos
|
||||||
|
else
|
||||||
|
# Windows (MSYS2, Git Bash, Cygwin all report variants with "MINGW"/"MSYS"/"CYGWIN")
|
||||||
|
FLUTTER_OS := windows
|
||||||
|
endif
|
||||||
|
|
||||||
|
VERSION ?= $(shell awk -F': *' '/^version:/ {gsub(/[" ]/,"",$$2); print $$2; exit}' pubspec.yaml)
|
||||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## Show this help.
|
help: ## Show this help.
|
||||||
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_-]+:.*##/ {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_-]+:.*##/ {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
# -- app + core (Flutter / Dart) ----------------------------------------
|
# -- app (Flutter) -------------------------------------------------------
|
||||||
|
|
||||||
# App and core targets gracefully noop if the Dart package isn't
|
.PHONY: run
|
||||||
# scaffolded yet (pre-Tier-0 state) or if flutter isn't installed
|
run: ## Launch the Flutter desktop app.
|
||||||
# locally.
|
ifeq ($(FLUTTER_OS),linux)
|
||||||
APP_PRESENT := $(shell test -f pubspec.yaml && echo yes || echo no)
|
GDK_BACKEND=x11 LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} flutter run -d linux --dart-define=CLIDE_PROJECT=$(CURDIR)
|
||||||
HAS_FLUTTER := $(shell command -v flutter >/dev/null && echo yes || echo no)
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build: ## Build the Dart AOT `clide` binary (CLI + --daemon modes in one binary).
|
|
||||||
ifeq ($(APP_PRESENT),yes)
|
|
||||||
dart compile exe bin/clide.dart -o bin/clide \
|
|
||||||
--define=clideVersion=$(VERSION) \
|
|
||||||
--define=clideCommit=$(COMMIT) \
|
|
||||||
--define=clideDate=$(DATE)
|
|
||||||
else
|
else
|
||||||
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
flutter run -d $(FLUTTER_OS) --dart-define=CLIDE_PROJECT=$(CURDIR)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: install
|
TESTMODE_CATEGORY ?= all
|
||||||
install: build ## Install bin/clide into $(INSTALL_DIR).
|
TESTMODE_TIMEOUT ?= 60
|
||||||
ifeq ($(APP_PRESENT),yes)
|
|
||||||
install -m 0755 bin/clide $(INSTALL_DIR)/clide
|
.PHONY: run-testmode
|
||||||
|
run-testmode: ## Launch ClideTestApp (TESTMODE_CATEGORY=toolchain|ipc|extensions|all).
|
||||||
|
ifeq ($(FLUTTER_OS),linux)
|
||||||
|
@GDK_BACKEND=x11 LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} \
|
||||||
|
flutter run -d linux \
|
||||||
|
--dart-define=CLIDE_PROJECT=$(CURDIR) \
|
||||||
|
--dart-define=CLIDE_TESTMODE=$(TESTMODE_CATEGORY) 2>&1 \
|
||||||
|
| tee /tmp/clide-testmode.log & PID=$$!; \
|
||||||
|
(sleep $(TESTMODE_TIMEOUT) && kill $$PID 2>/dev/null) & TIMER=$$!; \
|
||||||
|
wait $$PID 2>/dev/null; kill $$TIMER 2>/dev/null; \
|
||||||
|
grep -q '"failed":0' /tmp/clide-testmode.log
|
||||||
else
|
else
|
||||||
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
@flutter run -d $(FLUTTER_OS) \
|
||||||
|
--dart-define=CLIDE_PROJECT=$(CURDIR) \
|
||||||
|
--dart-define=CLIDE_TESTMODE=$(TESTMODE_CATEGORY) 2>&1 \
|
||||||
|
| tee /tmp/clide-testmode.log & PID=$$!; \
|
||||||
|
(sleep $(TESTMODE_TIMEOUT) && kill $$PID 2>/dev/null) & TIMER=$$!; \
|
||||||
|
wait $$PID 2>/dev/null; kill $$TIMER 2>/dev/null; \
|
||||||
|
grep -q '"failed":0' /tmp/clide-testmode.log
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: pubget
|
.PHONY: pubget
|
||||||
pubget: ## flutter pub get (hydrate the pub cache).
|
pubget: ## flutter pub get.
|
||||||
ifeq ($(APP_PRESENT),yes)
|
|
||||||
flutter pub get
|
flutter pub get
|
||||||
|
|
||||||
|
.PHONY: build-check
|
||||||
|
build-check: ## Verify native + Dart build compiles (no run).
|
||||||
|
ifeq ($(FLUTTER_OS),linux)
|
||||||
|
LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} flutter build linux
|
||||||
else
|
else
|
||||||
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
flutter build $(FLUTTER_OS)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: analyze
|
.PHONY: analyze
|
||||||
analyze: ## dart analyze / flutter analyze.
|
analyze: ## flutter analyze.
|
||||||
ifeq ($(APP_PRESENT),yes)
|
|
||||||
flutter analyze
|
flutter analyze
|
||||||
else
|
|
||||||
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: format
|
.PHONY: format
|
||||||
format: ## dart format --set-exit-if-changed.
|
format: ## dart format --set-exit-if-changed.
|
||||||
ifeq ($(APP_PRESENT),yes)
|
|
||||||
dart format --set-exit-if-changed .
|
dart format --set-exit-if-changed .
|
||||||
else
|
|
||||||
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: ## Fast: analyze + format + unit + widget + golden (<60s).
|
test: ## Fast: analyze + format + unit + widget + golden (<60s).
|
||||||
ci/test.sh
|
ci/test.sh
|
||||||
|
|
||||||
.PHONY: test-core
|
.PHONY: test-core
|
||||||
test-core: ## Flutter-free core tests (IPC, daemon, PTY) with hard timeout.
|
test-core: ## Core subsystem tests (IPC, PTY, git, pane registry).
|
||||||
ci/test_core.sh
|
ci/test_core.sh
|
||||||
|
|
||||||
.PHONY: test-a11y
|
.PHONY: test-a11y
|
||||||
@@ -86,7 +96,7 @@ test-integration: ## Integration tests (real app boot; xvfb on headless Linux).
|
|||||||
ci/test_integration.sh
|
ci/test_integration.sh
|
||||||
|
|
||||||
.PHONY: test-e2e
|
.PHONY: test-e2e
|
||||||
test-e2e: build ## Daemon subprocess + web WASM Playwright smoke.
|
test-e2e: ## End-to-end Playwright smoke.
|
||||||
ci/test_e2e.sh
|
ci/test_e2e.sh
|
||||||
|
|
||||||
.PHONY: test-all
|
.PHONY: test-all
|
||||||
@@ -100,7 +110,7 @@ coverage: ## flutter test --coverage + lcov summary.
|
|||||||
smoke-bundle: ## Build Linux release bundle and run it under xvfb for 5s.
|
smoke-bundle: ## Build Linux release bundle and run it under xvfb for 5s.
|
||||||
ci/smoke_bundle.sh
|
ci/smoke_bundle.sh
|
||||||
|
|
||||||
# -- web UI harness (for Claude Code to drive the app) -----------------
|
# -- web UI harness ------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: ui-dev
|
.PHONY: ui-dev
|
||||||
ui-dev: ## Build web WASM + start localhost:4280 in the background.
|
ui-dev: ## Build web WASM + start localhost:4280 in the background.
|
||||||
@@ -117,25 +127,114 @@ ui-smoke: ## Build + serve + run Playwright smoke + stop.
|
|||||||
tools/ui/serve.sh
|
tools/ui/serve.sh
|
||||||
@sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts'
|
@sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts'
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: ## flutter build for the current OS.
|
||||||
|
flutter build $(FLUTTER_OS)
|
||||||
|
|
||||||
.PHONY: build-linux
|
.PHONY: build-linux
|
||||||
build-linux: ## flutter build linux (desktop bundle).
|
build-linux: ## flutter build linux (desktop bundle).
|
||||||
ifeq ($(APP_PRESENT),yes)
|
|
||||||
flutter build linux
|
flutter build linux
|
||||||
else
|
|
||||||
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: build-macos
|
.PHONY: build-macos
|
||||||
build-macos: ## flutter build macos (desktop bundle).
|
build-macos: ## flutter build macos (desktop bundle).
|
||||||
ifeq ($(APP_PRESENT),yes)
|
|
||||||
flutter build macos
|
flutter build macos
|
||||||
else
|
|
||||||
@echo "(pubspec.yaml not scaffolded yet; skipping)"
|
# -- install / uninstall -----------------------------------------------------
|
||||||
|
|
||||||
|
# Install prefix. Bundle lands at $(INSTALL_PREFIX)/clide/ with a
|
||||||
|
# symlink at $(INSTALL_DIR)/clide pointing into it.
|
||||||
|
INSTALL_PREFIX ?= $(HOME)/.local/lib
|
||||||
|
|
||||||
|
ifeq ($(FLUTTER_OS),linux)
|
||||||
|
BUNDLE_DIR := build/linux/x64/release/bundle
|
||||||
|
else ifeq ($(FLUTTER_OS),macos)
|
||||||
|
BUNDLE_DIR := build/macos/Build/Products/Release/clide.app
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# -- ptyc (C supporter tool) --------------------------------------------
|
ICON_SIZES := 16 32 48 128 192 256 512
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install: build ## Build + install clide to ~/.local (INSTALL_PREFIX, INSTALL_DIR).
|
||||||
|
ifeq ($(FLUTTER_OS),linux)
|
||||||
|
@mkdir -p $(INSTALL_PREFIX) $(INSTALL_DIR)
|
||||||
|
rm -rf $(INSTALL_PREFIX)/clide
|
||||||
|
cp -a $(BUNDLE_DIR) $(INSTALL_PREFIX)/clide
|
||||||
|
ln -sf $(INSTALL_PREFIX)/clide/clide $(INSTALL_DIR)/clide
|
||||||
|
@for size in $(ICON_SIZES); do \
|
||||||
|
dir=$(HOME)/.local/share/icons/hicolor/$${size}x$${size}/apps; \
|
||||||
|
mkdir -p $$dir; \
|
||||||
|
cp assets/logo/appicon-$${size}.png $$dir/clide.png; \
|
||||||
|
done
|
||||||
|
@mkdir -p $(HOME)/.local/share/applications
|
||||||
|
@sed 's|Exec=clide|Exec=$(INSTALL_PREFIX)/clide/clide|' linux/clide.desktop \
|
||||||
|
> $(HOME)/.local/share/applications/clide.desktop
|
||||||
|
@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
|
||||||
|
@update-desktop-database $(HOME)/.local/share/applications 2>/dev/null || true
|
||||||
|
@echo "installed: $(INSTALL_DIR)/clide -> $(INSTALL_PREFIX)/clide/clide"
|
||||||
|
@echo "desktop: ~/.local/share/applications/clide.desktop"
|
||||||
|
@echo "version: $(VERSION)"
|
||||||
|
else ifeq ($(FLUTTER_OS),macos)
|
||||||
|
@mkdir -p $(HOME)/Applications
|
||||||
|
rm -rf $(HOME)/Applications/clide.app
|
||||||
|
cp -a $(BUNDLE_DIR) $(HOME)/Applications/clide.app
|
||||||
|
@echo "installed: ~/Applications/clide.app"
|
||||||
|
@echo "version: $(VERSION)"
|
||||||
|
else
|
||||||
|
@echo "install not yet supported on $(FLUTTER_OS)"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: uninstall
|
||||||
|
uninstall: ## Remove installed clide.
|
||||||
|
ifeq ($(FLUTTER_OS),linux)
|
||||||
|
rm -f $(INSTALL_DIR)/clide
|
||||||
|
rm -rf $(INSTALL_PREFIX)/clide
|
||||||
|
rm -f $(HOME)/.local/share/applications/clide.desktop
|
||||||
|
@for size in $(ICON_SIZES); do \
|
||||||
|
rm -f $(HOME)/.local/share/icons/hicolor/$${size}x$${size}/apps/clide.png; \
|
||||||
|
done
|
||||||
|
@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
|
||||||
|
@update-desktop-database $(HOME)/.local/share/applications 2>/dev/null || true
|
||||||
|
@echo "uninstalled"
|
||||||
|
else ifeq ($(FLUTTER_OS),macos)
|
||||||
|
rm -rf $(HOME)/Applications/clide.app
|
||||||
|
@echo "uninstalled"
|
||||||
|
endif
|
||||||
|
|
||||||
|
# -- dugite-native (bundled git) ------------------------------------------
|
||||||
|
|
||||||
|
DUGITE_VERSION := v2.53.0-3
|
||||||
|
DUGITE_COMMIT := f49d009
|
||||||
|
|
||||||
|
ifeq ($(FLUTTER_OS),macos)
|
||||||
|
ifeq ($(shell uname -m),arm64)
|
||||||
|
DUGITE_PLATFORM := macOS-arm64
|
||||||
|
else
|
||||||
|
DUGITE_PLATFORM := macOS-x64
|
||||||
|
endif
|
||||||
|
else ifeq ($(FLUTTER_OS),linux)
|
||||||
|
DUGITE_PLATFORM := ubuntu-x64
|
||||||
|
else
|
||||||
|
DUGITE_PLATFORM := windows-x64
|
||||||
|
endif
|
||||||
|
|
||||||
|
DUGITE_TAR := dugite-native-v2.53.0-$(DUGITE_COMMIT)-$(DUGITE_PLATFORM).tar.gz
|
||||||
|
DUGITE_URL := https://github.com/desktop/dugite-native/releases/download/$(DUGITE_VERSION)/$(DUGITE_TAR)
|
||||||
|
DUGITE_DIR := native/dugite
|
||||||
|
|
||||||
|
.PHONY: dugite-fetch
|
||||||
|
dugite-fetch: ## Download and extract the dugite-native git distribution.
|
||||||
|
@if [ -f $(DUGITE_DIR)/bin/git ]; then echo "dugite already present at $(DUGITE_DIR)/bin/git"; exit 0; fi
|
||||||
|
@mkdir -p $(DUGITE_DIR)
|
||||||
|
curl -sL "$(DUGITE_URL)" | tar xz -C $(DUGITE_DIR)
|
||||||
|
@echo "dugite-native $(DUGITE_VERSION) ($(DUGITE_PLATFORM)) extracted to $(DUGITE_DIR)/"
|
||||||
|
|
||||||
|
.PHONY: dugite-clean
|
||||||
|
dugite-clean: ## Remove the dugite-native directory.
|
||||||
|
rm -rf $(DUGITE_DIR)
|
||||||
|
|
||||||
|
# -- ptyc (C supporter tool) ---------------------------------------------
|
||||||
|
|
||||||
# Compiled alongside the main binary. Tiny, no deps beyond libc.
|
|
||||||
PTYC_PRESENT := $(shell test -f ptyc/Makefile && echo yes || echo no)
|
PTYC_PRESENT := $(shell test -f ptyc/Makefile && echo yes || echo no)
|
||||||
|
|
||||||
.PHONY: ptyc-build
|
.PHONY: ptyc-build
|
||||||
@@ -162,33 +261,32 @@ else
|
|||||||
@echo "(ptyc/ not scaffolded yet; skipping)"
|
@echo "(ptyc/ not scaffolded yet; skipping)"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# -- security ------------------------------------------------------------
|
# -- security -------------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: security
|
.PHONY: security
|
||||||
security: ## Dart advisory review + ptyc source review (manual — no floating deps).
|
security: ## Dart advisory review + ptyc source review.
|
||||||
@echo "security: Dart advisories reviewed manually before pubspec.yaml bumps;"
|
@echo "security: Dart advisories reviewed manually before pubspec.yaml bumps;"
|
||||||
@echo " ptyc is reviewed by reading it (tiny libc-only C)."
|
@echo " ptyc is reviewed by reading it (tiny libc-only C)."
|
||||||
@echo " Automated Dart CVE tooling lands here when a reliable option exists."
|
|
||||||
|
|
||||||
# -- pre-push gate -------------------------------------------------------
|
# -- pre-push gate --------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: decisions-validate
|
.PHONY: decisions-validate
|
||||||
decisions-validate: ## Parser dry-run over decisions/*.md (cheap pre-push gate).
|
decisions-validate: ## Parser dry-run over decisions/*.md.
|
||||||
tools/scripts/plan decisions validate
|
pql decisions validate
|
||||||
|
|
||||||
.PHONY: push-check
|
.PHONY: push-check
|
||||||
push-check: decisions-validate test-core test test-a11y ## Pre-push gate: decisions + core + fast unit + widget + golden + a11y (<90s).
|
push-check: decisions-validate test-core test test-a11y ## Pre-push gate.
|
||||||
|
|
||||||
.PHONY: hooks
|
.PHONY: hooks
|
||||||
hooks: ## Install the repo's git hooks (points core.hooksPath at .githooks/).
|
hooks: ## Install the repo's git hooks.
|
||||||
git config core.hooksPath .githooks
|
git config core.hooksPath .githooks
|
||||||
@echo "hooks installed (core.hooksPath = .githooks)"
|
@echo "hooks installed (core.hooksPath = .githooks)"
|
||||||
|
|
||||||
# -- housekeeping --------------------------------------------------------
|
# -- housekeeping ---------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: ## Remove build artefacts.
|
clean: ## Remove build artefacts.
|
||||||
rm -rf bin build .dart_tool
|
rm -rf build .dart_tool
|
||||||
$(MAKE) ptyc-clean
|
$(MAKE) ptyc-clean
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
# clide Policy
|
||||||
|
|
||||||
|
This document governs what clide is allowed to do at runtime, what it's allowed to depend on, and how contributors — human and agent — introduce code into the project. It is binding on all contributors. When in doubt, stop and ask.
|
||||||
|
|
||||||
|
Rationale for specific architectural choices referenced here and in code comments (the D-### markers) lives in `decisions/`. This document sets the rules; `decisions/` records why the rules produced the code they did in a given case. If the two ever disagree, the rule in this document wins until the document itself is changed.
|
||||||
|
|
||||||
|
## Why this document exists
|
||||||
|
|
||||||
|
clide's security model rests on a simple claim: the behavior of the app on a user's machine is fully determined by the signed release artifact and the files in this repository at the time of that release. Every file the app reads, every binary it executes, and every wasm component it instantiates on the default launch path was present, reviewed, and pinned when we built the release.
|
||||||
|
|
||||||
|
The moment that claim stops being true — the moment the app fetches something from the network that wasn't audited at build time — the entire sandboxing and trust story collapses. It doesn't matter how good the capability model around wasm components is if the components themselves are downloaded from the internet at first launch.
|
||||||
|
|
||||||
|
This policy exists to keep that claim true.
|
||||||
|
|
||||||
|
## The core rule: no network on the default launch path
|
||||||
|
|
||||||
|
clide does not perform network I/O during app startup, library initialization, or first use of any API, unless the user has explicitly asked for it.
|
||||||
|
|
||||||
|
"Explicitly asked for it" means the user pressed a button, ran a command, or otherwise took an action whose stated purpose is to cause a network fetch. Opening the app is not such an action. Opening a file is not such an action. Typing in a buffer is not such an action.
|
||||||
|
|
||||||
|
### What this prohibits
|
||||||
|
|
||||||
|
- Libraries that download their own native binaries on first import or first call (the `wasm_run` pattern).
|
||||||
|
- Language servers, formatters, or grammars that auto-install from the network when a file of that language is opened.
|
||||||
|
- Fonts, icons, or assets fetched from a CDN rather than bundled.
|
||||||
|
- Telemetry, crash reporting, or analytics that connect on startup regardless of user consent.
|
||||||
|
- Auto-update checks that run without the user asking.
|
||||||
|
- Any transitive dependency behavior that does the above, regardless of whether our code triggered it.
|
||||||
|
|
||||||
|
### What this allows, grudgingly
|
||||||
|
|
||||||
|
Signed, pinned fetches are permitted for a narrow set of cases where the alternative is materially worse. To qualify, all of the following must be true:
|
||||||
|
|
||||||
|
1. The artifact URL is hardcoded in the repository, not constructed at runtime.
|
||||||
|
2. The artifact is verified against a cryptographic hash committed to the repository, or a signature verified against a public key committed to the repository.
|
||||||
|
3. The fetch is cached; subsequent launches do not re-fetch.
|
||||||
|
4. Fetch failure produces a clear error, not a silent degradation.
|
||||||
|
5. The first launch after install can complete its primary function without the fetch succeeding. If the fetch is required for the app to work at all, this is not a "grudging allowance" case — vendor it instead.
|
||||||
|
|
||||||
|
If you cannot satisfy all five, the answer is vendoring or explicit user action, not a signed fetch.
|
||||||
|
|
||||||
|
### What this allows freely
|
||||||
|
|
||||||
|
Network I/O is unrestricted when the user has explicitly initiated it. Examples:
|
||||||
|
|
||||||
|
- "Install the Rust grammar" — fetches on button press, fine.
|
||||||
|
- Cloning a repository the user typed the URL of.
|
||||||
|
- Connecting to an LSP server the user configured.
|
||||||
|
- Any feature whose stated purpose is to talk to the network (HTTP client, git, SSH).
|
||||||
|
|
||||||
|
The rule is about the *default* path, not about forbidding network ever.
|
||||||
|
|
||||||
|
## Dependencies: zero by design
|
||||||
|
|
||||||
|
clide's default answer to "should we add a dependency" is **no**. Every dependency is a liability — code we didn't write, running in our users' address space, that we are nonetheless responsible for. The right number of dependencies is the number that makes clide meaningfully better than writing the code ourselves, and that number is small.
|
||||||
|
|
||||||
|
This is not a preference. It is the project's architectural stance, documented in `CLAUDE.md` as "no dep graph by design, exact-pinned, CVE-checked, audited." Every dependency that exists in clide is there because someone argued for it, someone reviewed the argument, and the answer was yes. Dependencies do not accumulate by accident.
|
||||||
|
|
||||||
|
### What "no dep graph by design" means in practice
|
||||||
|
|
||||||
|
- **Prefer writing it.** A 100-line utility written in-repo is cheaper than a 100-line dependency, even though the dependency looks free. We own the code either way; the dependency just hides that fact behind a pub.dev page.
|
||||||
|
- **Prefer the standard library.** Dart, Flutter, and Rust all have large standard libraries. Reach for them before reaching for a package. "There's a package that does this" is not a reason; "the standard library cannot do this reasonably" is.
|
||||||
|
- **Prefer inlining over depending.** If a package is small and does one thing we need, copy the relevant code (with attribution) rather than taking on the dependency. License permitting, this is usually the right call for utilities under a few hundred lines.
|
||||||
|
- **Prefer vendoring over fetching.** When a dependency is unavoidable and small enough to own, fork it into the repo and maintain it ourselves. The dep graph stops at our fence.
|
||||||
|
- **Reject deep trees.** A direct dependency that itself has fifteen transitive dependencies is fifteen dependencies we are taking on, not one. Evaluate the whole subtree, not just the top.
|
||||||
|
- **Format engines clear the bar.** Packages that parse or render external file formats (SVG, markdown, HTML, terminal escapes, tree-sitter grammars) are adoptable — they are not shortcuts for lazy coding but maintained renderers for specs we didn't write. The distinction: UI chrome (panels, tabs, canvas, layout) we own; someone else's file format we adopt and sandbox. See D-58.
|
||||||
|
|
||||||
|
### Every dependency is audited
|
||||||
|
|
||||||
|
For every dependency that does land, the following must be true and documented:
|
||||||
|
|
||||||
|
- **Exact-pinned.** `1.2.3`, never `^1.2.3` or `>=1.2.0 <2.0.0`. Lockfiles are committed. Updates are deliberate, reviewed PRs, not automatic.
|
||||||
|
- **CVE-checked.** The dependency is checked against known vulnerability databases at the time of adoption and on every subsequent CI run via the pipeline's automated CVE gate. A dependency with unpatched high-severity CVEs does not land; a dependency that develops an unpatched high-severity CVE after it lands becomes a release blocker until resolved. The CVE check is part of the build, not a periodic chore — if CI is green, the dep tree was clean at build time.
|
||||||
|
- **Source-reviewed.** Someone on the project has read the source of the dependency, or of the relevant subset, before it lands. "I trust this maintainer" is not a substitute. For large deps, the review can be scoped to the parts we actually use.
|
||||||
|
- **Recorded.** The reason for adoption, the reviewer, the source SHA or version, and the alternatives considered are captured in the PR that adds it.
|
||||||
|
- **Justified in place.** Every dependency in `pubspec.yaml` (and equivalent manifests) carries an inline comment explaining what it does, why we took it rather than writing the code, and its license. PR descriptions are not a substitute — the justification must survive in the manifest where future contributors will actually look. If a dependency can't be justified in two or three lines next to its version pin, it probably shouldn't land.
|
||||||
|
|
||||||
|
### The "is this worth it" test
|
||||||
|
|
||||||
|
Before adding any dependency, answer honestly:
|
||||||
|
|
||||||
|
1. What specifically does this do that we cannot reasonably do ourselves?
|
||||||
|
2. How much code would we write if we did it ourselves? (If the answer is under ~200 lines, strongly prefer writing it.)
|
||||||
|
3. What is the full transitive dep tree we are accepting?
|
||||||
|
4. Who maintains it, and what happens to clide if they stop?
|
||||||
|
5. Is the dependency's scope larger than our need? (Most packages are — we take on all the code, not just the parts we use.)
|
||||||
|
|
||||||
|
A dependency that passes this test and the vetting checklist below is welcome. A dependency that passes neither is a no. A dependency that passes one but not the other is a conversation, not a commit.
|
||||||
|
|
||||||
|
## Dependency vetting checklist
|
||||||
|
|
||||||
|
Before adding any new dependency (direct or transitive) to clide, verify:
|
||||||
|
|
||||||
|
**Network behavior**
|
||||||
|
- Does the package perform network I/O during import, initialization, or first call? If yes, reject or vendor.
|
||||||
|
- Does the package have a `postinstall`, `setup`, or equivalent script that downloads binaries? If yes, reject or vendor.
|
||||||
|
- Does any transitive dependency do either of the above? `flutter pub deps` and read the code of anything unfamiliar.
|
||||||
|
|
||||||
|
**Binary provenance**
|
||||||
|
- If the package ships native binaries, are they built from source in the same repo, or fetched from a release artifact? If fetched, treat as a network-at-launch violation.
|
||||||
|
- If the package expects to download binaries on a different machine (CI, developer setup), that is also disqualifying for runtime deps.
|
||||||
|
|
||||||
|
**Maintainership**
|
||||||
|
- Single-maintainer packages require explicit sign-off and a documented fallback plan. Note the maintainer and last meaningful commit in the PR.
|
||||||
|
- Packages with no activity in 12+ months require either a fork we control or a decision to own the code ourselves.
|
||||||
|
|
||||||
|
**Surface area**
|
||||||
|
- Prefer packages that do one thing. A dependency that adds 15 transitive deps to solve a 100-line problem should be inlined instead.
|
||||||
|
|
||||||
|
**Version pinning, lockfiles, and CVE review**
|
||||||
|
- See "Dependencies: zero by design" above. All the requirements there apply: exact-pinned, CVE-checked, source-reviewed, justified in place, recorded in the PR.
|
||||||
|
- `pubspec.lock` (and equivalent lockfiles for other toolchains) is committed to the repository and regenerated on every dependency change. A stale lockfile is a supply-chain hazard: a dependency removed from `pubspec.yaml` that persists in `pubspec.lock` will continue to install in CI and on fresh clones. Lockfile regeneration is part of the dep-change PR, not a follow-up.
|
||||||
|
|
||||||
|
**License**
|
||||||
|
- See License and attribution rules below. A dependency with an incompatible or unclear license is disqualifying regardless of technical merit.
|
||||||
|
|
||||||
|
If you can't answer any of these questions, you haven't vetted the dependency yet.
|
||||||
|
|
||||||
|
## Removing a dependency
|
||||||
|
|
||||||
|
Removal is not the reverse of addition. Adding a dependency involves a single decision and a single commit; removing one involves finding every place the dependency or its transitive siblings have leaked into the codebase and making sure none of them outlive the removal PR.
|
||||||
|
|
||||||
|
When removing a dependency:
|
||||||
|
|
||||||
|
1. **Grep the entire repository** for references to the package, its exports, and any type names it contributed. `rg '<package>|<PackageType>|<prefix_>'` across the repo. Zero hits outside git history is the goal. A single lingering import will break the build; a single lingering FFI stub or type alias will compile fine and fail at runtime.
|
||||||
|
2. **Regenerate the lockfile** as part of the same PR. A `pubspec.yaml` with the dep removed but a `pubspec.lock` that still pins it is a partial removal, and CI or a fresh clone will happily continue installing the package.
|
||||||
|
3. **Update `app/assets/licenses.yaml`** to drop the removed package and any transitive deps it brought in that aren't pulled by anything else. If the license manifest is auto-generated on release, verify the generation script sees the change; if it's maintained by hand, edit it in the same PR.
|
||||||
|
4. **Remove any vendored artifacts** tied to the dep — binaries, prebuilt assets, generated bindings — and delete their `BUILD.md` records. An orphaned vendored binary is worse than a removed one because it looks legitimate.
|
||||||
|
5. **Check for architectural assumptions** that the dep was carrying. If the removed package was the thing that justified a specific data flow, build step, or platform strategy, either the replacement picks up those responsibilities or the architecture has actually changed and the relevant design decision (see `decisions/`) needs updating.
|
||||||
|
|
||||||
|
A dependency is not removed until all five are true. "I deleted the line from pubspec.yaml" is the start of the removal, not the end.
|
||||||
|
|
||||||
|
## Vendored binary rebuild process
|
||||||
|
|
||||||
|
clide ships pre-built native libraries (currently `libtree-sitter.so` and per-platform equivalents) vendored into the repository. These binaries are part of the trust boundary: the signed release contains exactly these bytes, and users running clide are running exactly this code.
|
||||||
|
|
||||||
|
Every vendored binary has a corresponding `BUILD.md` (or equivalent) next to it in the repo that records:
|
||||||
|
|
||||||
|
1. The exact upstream source (git URL and commit SHA, not a version tag).
|
||||||
|
2. The full build command, including all compile flags.
|
||||||
|
3. The toolchain version used (compiler, linker, target triple).
|
||||||
|
4. The expected output size and a SHA-256 hash of the resulting binary.
|
||||||
|
5. Any patches applied to the source, stored as `.patch` files in the same directory.
|
||||||
|
|
||||||
|
Rebuilds are deliberate events, not background maintenance:
|
||||||
|
|
||||||
|
- Rebuilds happen in CI, not on a contributor's laptop. A matrix job cross-compiles for all supported platforms and produces the full set of binaries from a single source SHA.
|
||||||
|
- The rebuild PR updates `BUILD.md`, the binaries, and the hashes in one atomic change. The hash change is the signal in review that binary content changed and must be re-reviewed.
|
||||||
|
- No binary is committed without a corresponding source SHA and build log. If you can't reproduce the binary from the recorded inputs, the binary is not trustworthy and must be rebuilt.
|
||||||
|
- Security patches to vendored dependencies are tracked with the same urgency as source-level vulnerabilities. "We're on an old libtree-sitter because rebuilding is annoying" is not acceptable.
|
||||||
|
|
||||||
|
Supported platforms and their binary artifacts are documented in the repository root. Dropping a platform requires a policy decision, not a build convenience. Adding a platform requires adding it to the CI matrix and rebuilding all vendored binaries for it before the platform ships.
|
||||||
|
|
||||||
|
## Telemetry and phone-home rules
|
||||||
|
|
||||||
|
clide does not phone home. The app does not report to any server — ours or anyone else's. This is not a "default off" setting; it is an architectural commitment. clide is a space to think, not a surface for data collection.
|
||||||
|
|
||||||
|
This means:
|
||||||
|
|
||||||
|
- No analytics SDKs. Not Firebase, not Sentry, not Mixpanel, not a hand-rolled metrics endpoint. Not in debug builds, not in release builds, not behind a feature flag.
|
||||||
|
- No crash reporters that upload automatically. Crashes produce local logs the user can read and, if they choose, attach to a bug report they submit manually.
|
||||||
|
- No "check for updates" that runs without the user asking. Update checks are a button.
|
||||||
|
- No license validation that calls home. clide is MIT; there is nothing to validate.
|
||||||
|
- No feature flags fetched from a server. Configuration is local.
|
||||||
|
- No A/B testing, no experiments, no remote config, no "anonymous usage statistics."
|
||||||
|
|
||||||
|
If a future contributor proposes telemetry under any framing — opt-in, anonymized, debug-only, "just errors," "just for us," "just to improve the product" — the answer is no. The architectural commitment is the feature. Users installing clide are choosing a tool that does not watch them, and that promise is worth more than any data we could collect.
|
||||||
|
|
||||||
|
This rule does not bend. Proposals to add telemetry are out of scope for this project, full stop. If you want telemetry in your editor, clide is not the editor for you, and that's fine — fork it.
|
||||||
|
|
||||||
|
## License and attribution rules
|
||||||
|
|
||||||
|
clide itself is MIT-licensed. Every dependency, vendored binary, bundled font, and bundled asset must be:
|
||||||
|
|
||||||
|
1. Compatible with clide's license.
|
||||||
|
2. Attributed in the about screen's license manifest.
|
||||||
|
3. Attributed correctly — the license text and copyright notice must match upstream, not a paraphrase.
|
||||||
|
|
||||||
|
### Compatibility matrix
|
||||||
|
|
||||||
|
Compatible with clide (permissive): MIT, Apache-2.0, BSD-2/3, ISC, Zlib, Unlicense, CC0.
|
||||||
|
|
||||||
|
Compatible with care (copyleft): MPL-2.0 is acceptable for libraries; LGPL is acceptable only for dynamically-linked vendored binaries where we can demonstrate users can replace the library.
|
||||||
|
|
||||||
|
Not compatible: GPL (any version) for linked code, AGPL for anything, SSPL, "commercial use prohibited" licenses, custom licenses not reviewed by someone qualified to review them.
|
||||||
|
|
||||||
|
When in doubt about a license, the dependency does not land until the question is resolved.
|
||||||
|
|
||||||
|
### Attribution requirements
|
||||||
|
|
||||||
|
- The license manifest at `app/assets/licenses.yaml` lists every dependency with its license, copyright notice, and upstream URL.
|
||||||
|
- Transitive dependencies are listed, not just direct ones. If `wasm_run` pulls in `wasmtime` which pulls in `cranelift`, all three appear.
|
||||||
|
- Apache-2.0 dependencies get their `NOTICE` file content preserved verbatim, not summarized.
|
||||||
|
- Apache-2.0-with-LLVM-exception (e.g., Cranelift, parts of LLVM) requires the LLVM exception text specifically, not just the Apache-2.0 boilerplate.
|
||||||
|
- Fonts and icon sets get attributed even if the license doesn't strictly require it. It's the right thing to do.
|
||||||
|
- `app/assets/licenses.yaml` is regenerated as part of the release build, not maintained by hand. A release that ships a stale manifest is a release defect.
|
||||||
|
|
||||||
|
Adding a dependency means updating the license manifest in the same PR. No exceptions.
|
||||||
|
|
||||||
|
## Changelog and commit conventions
|
||||||
|
|
||||||
|
clide follows [Keep a Changelog 1.1](https://keepachangelog.com/en/1.1.0/) for `CHANGELOG.md` and [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages. Enforcement is handled by the project's git skill; this section exists so human contributors know the standard before their first PR, and so the connection between these conventions and the rest of the policy is explicit.
|
||||||
|
|
||||||
|
Security-relevant changes — CVE responses, dependency-driven vulnerability fixes, the removal of a phoning-home transitive dep, anything where the rules in this document were the reason for the change — go under the `Security` heading of the release's changelog entry, regardless of whether the code change itself looks security-shaped. That heading is the trail future-us follows to reconstruct why a dep was bumped or removed. Lumping security fixes under `Fixed` because the diff looks like a normal bug fix loses that signal and is the wrong choice even when it's technically accurate.
|
||||||
|
|
||||||
|
## When this policy is wrong
|
||||||
|
|
||||||
|
This document is a set of rules, but rules encode judgment calls that may not survive contact with a specific situation. If you think the policy is wrong for a specific case, the process is:
|
||||||
|
|
||||||
|
1. Stop. Do not ship the thing.
|
||||||
|
2. Write down the specific case and why you think the rule fails.
|
||||||
|
3. Propose a change to this document.
|
||||||
|
4. Get the change reviewed on its merits.
|
||||||
|
5. If the change lands, proceed. If not, the rule holds.
|
||||||
|
|
||||||
|
"The rule is inconvenient for this task" is not an argument for the rule being wrong. "The rule produces a worse outcome than the alternative, measured against the reasons the rule exists" is.
|
||||||
|
|
||||||
|
## Notes for agents
|
||||||
|
|
||||||
|
If you are an AI agent working on this repository, the following guidance is binding:
|
||||||
|
|
||||||
|
- The default answer to "should I add a dependency" is no. Write the code instead, or use the standard library. If you think a dependency is genuinely warranted, stop and make the case to the human before adding it — do not add it and justify it afterward.
|
||||||
|
- Do not add dependencies without walking the vetting checklist. Record your answers in the PR description.
|
||||||
|
- Do not add code that performs network I/O on the default launch path. If you're not sure whether a library does this, read its source before adding it, not after.
|
||||||
|
- Do not vendor binaries without creating the corresponding `BUILD.md` and recording the source SHA and build command. A committed binary with no reproducibility record is worse than no binary at all.
|
||||||
|
- Do not add telemetry, analytics, crash reporting, remote config, feature flags, or any other phone-home mechanism under any framing. This is not a "default off" setting; the project does not have telemetry and will not have telemetry. If you think you need it, you are wrong.
|
||||||
|
- When in doubt, stop and ask the human. Pivoting architectures in a single session to route around a principle is almost always wrong; the principle is there for a reason that may not be visible from inside the task.
|
||||||
@@ -1,24 +1,47 @@
|
|||||||
# clide
|
# clide
|
||||||
|
|
||||||
A Flutter desktop IDE for Claude Code. Markdown-first content, pql-powered queries, canvas and graph surfaces, a Go sidecar handling PTYs / subprocesses / git / pql. Claude drives the UI through a `clide` CLI.
|
A Flutter desktop IDE for Claude Code. Native rendering, terminal-first interaction, pql-powered queries, canvas and graph surfaces. Linux and macOS.
|
||||||
|
|
||||||
Currently pre-v2.0, scaffolding. The north-star design lives in [`docs/initial-plan.md`](docs/initial-plan.md); architectural decisions are captured in [`docs/ADRs/`](docs/ADRs/). The Python Textual v1.2.0 implementation is archived under [`legacy/`](legacy/) for reference.
|
## Architecture
|
||||||
|
|
||||||
## Three surfaces, one tool
|
Single Flutter package at the repo root. The app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), and the extension framework. tmux owns Claude session persistence.
|
||||||
|
|
||||||
- **`app/`** — Flutter desktop application (Linux / macOS; Windows is a stretch).
|
- **`lib/`** — all Dart code. Kernel services (theme, i18n, settings, panels, commands, focus), UI widgets, built-in extensions, and the extension contract.
|
||||||
- **`sidecar/`** — Go sidecar/CLI, single binary with two modes: `clide <subcommand>` (one-shot for Claude) and `clide --daemon` (long-running sidecar for the app). Owns PTYs, subprocesses, file watchers, git, pql invocations.
|
- **`ptyc/`** — small C helper. Spawns a PTY + child and hands the master fd back over `SCM_RIGHTS`. Every pane (shell, tmux, claude, LSP, debug adapter) goes through it.
|
||||||
- **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. clide wraps it for every query surface; never re-implements it.
|
- **[pql](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
||||||
|
|
||||||
## Why the rebuild?
|
Claude drives the UI through a `clide` CLI surface (Bash, not MCP). Every CLI subcommand has a UI affordance and every UI action has a CLI equivalent.
|
||||||
|
|
||||||
The Python Textual implementation (`legacy/`) proved the pane model but capped at terminal-only rendering. A short-lived experiment (`claudian`, April 2026) explored an Obsidian-plugin approach and was abandoned because Obsidian is Electron, and the user's history with terminal-in-Electron ruled that host out.
|
## Built-in extensions
|
||||||
|
|
||||||
Flutter desktop resolves both constraints: native Skia rendering avoids Electron's failure modes, `xterm.dart` gives us a solid terminal surface, and the pane model can be rebuilt natively. The Obsidian ideas worth keeping (canvas, graph) fold in; everything else (vault concept, inline query tables, plugin ecosystem inheritance) doesn't.
|
canvas, claude, claude_control, decisions, diff, editor, extensions_ui, files, git, graph, grammars_core, ipc_status, keybindings_ui, markdown, pql, problems, settings_ui, terminal, theme_picker, tickets, todos, welcome.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Requires Flutter (stable channel) on the host. One-time setup:
|
||||||
|
|
||||||
|
```
|
||||||
|
make hooks && flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```
|
||||||
|
make run # launch the desktop app
|
||||||
|
make test # fast suite: analyze + format + unit + widget + golden
|
||||||
|
make test-core # core subsystem tests (IPC, PTY, git, pane registry)
|
||||||
|
make test-integration # real app boot integration tests
|
||||||
|
make build-linux # flutter build linux
|
||||||
|
make build-macos # flutter build macos
|
||||||
|
make ptyc-build # build the ptyc PTY-spawn helper
|
||||||
|
make push-check # pre-push gate: decisions + core + fast tests
|
||||||
|
```
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
No build yet beyond the repo scaffold. Tier 0 acceptance: sidecar daemon runs, Flutter app connects, empty IDE shell appears with placeholders.
|
Pre-v2.0 (`2.0.0-dev`). Interaction model and panel system landed. The Python Textual v1.2.0 predecessor is archived under [`legacy/`](legacy/).
|
||||||
|
|
||||||
|
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Architectural decisions: [`decisions/`](decisions/).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
formatter:
|
||||||
|
page_width: 160
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Miscellaneous
|
|
||||||
*.class
|
|
||||||
*.log
|
|
||||||
*.pyc
|
|
||||||
*.swp
|
|
||||||
.DS_Store
|
|
||||||
.atom/
|
|
||||||
.build/
|
|
||||||
.buildlog/
|
|
||||||
.history
|
|
||||||
.svn/
|
|
||||||
.swiftpm/
|
|
||||||
migrate_working_dir/
|
|
||||||
|
|
||||||
# IntelliJ related
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
*.iws
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
|
||||||
# is commented out by default.
|
|
||||||
#.vscode/
|
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
|
||||||
**/doc/api/
|
|
||||||
**/ios/Flutter/.last_build_id
|
|
||||||
.dart_tool/
|
|
||||||
.flutter-plugins-dependencies
|
|
||||||
.pub-cache/
|
|
||||||
.pub/
|
|
||||||
/build/
|
|
||||||
/coverage/
|
|
||||||
|
|
||||||
# Symbolication related
|
|
||||||
app.*.symbols
|
|
||||||
|
|
||||||
# Obfuscation related
|
|
||||||
app.*.map.json
|
|
||||||
|
|
||||||
# Android Studio will place build artifacts here
|
|
||||||
/android/app/debug
|
|
||||||
/android/app/profile
|
|
||||||
/android/app/release
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# clide_app
|
|
||||||
|
|
||||||
A new Flutter project.
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
import 'package:clide_app/extension/src/contribution.dart';
|
|
||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
import 'package:clide_app/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class ClideApp extends StatelessWidget {
|
|
||||||
const ClideApp({super.key, required this.services});
|
|
||||||
|
|
||||||
final KernelServices services;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ClideKernel(
|
|
||||||
services: services,
|
|
||||||
child: ClideTheme(
|
|
||||||
controller: services.theme,
|
|
||||||
child: _AppRoot(services: services),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppRoot extends StatelessWidget {
|
|
||||||
const _AppRoot({required this.services});
|
|
||||||
final KernelServices services;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return WidgetsApp(
|
|
||||||
title: 'clide',
|
|
||||||
color: const Color(0xFF000000),
|
|
||||||
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
|
|
||||||
PageRouteBuilder<T>(
|
|
||||||
settings: settings,
|
|
||||||
pageBuilder: (ctx, _, __) => builder(ctx),
|
|
||||||
),
|
|
||||||
home: _RootShell(services: services),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RootShell extends StatefulWidget {
|
|
||||||
const _RootShell({required this.services});
|
|
||||||
final KernelServices services;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_RootShell> createState() => _RootShellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RootShellState extends State<_RootShell> {
|
|
||||||
late final FocusNode _keyFocus;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_keyFocus = FocusNode()..requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_keyFocus.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final tokens = ClideTheme.of(context).surface;
|
|
||||||
return DefaultTextStyle(
|
|
||||||
style: TextStyle(
|
|
||||||
color: tokens.globalForeground,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: clideUiDefaultWeight,
|
|
||||||
fontFamily: clideUiFamily,
|
|
||||||
fontFamilyFallback: clideUiFamilyFallback,
|
|
||||||
),
|
|
||||||
child: KeyboardListener(
|
|
||||||
focusNode: _keyFocus,
|
|
||||||
autofocus: true,
|
|
||||||
onKeyEvent: _onKey,
|
|
||||||
child: ColoredBox(
|
|
||||||
color: tokens.globalBackground,
|
|
||||||
child: DialogHost(
|
|
||||||
router: widget.services.dialog,
|
|
||||||
child: const RootLayout(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onKey(KeyEvent event) {
|
|
||||||
final binding = KeybindingResolver.fromKeyEvent(
|
|
||||||
event,
|
|
||||||
HardwareKeyboard.instance,
|
|
||||||
);
|
|
||||||
if (binding == null) return;
|
|
||||||
final commandId = widget.services.keybindings.commandFor(binding);
|
|
||||||
if (commandId == null) return;
|
|
||||||
widget.services.commands.execute(commandId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RootLayout extends StatelessWidget {
|
|
||||||
const RootLayout({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final kernel = ClideKernel.of(context);
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: Listenable.merge([kernel.panels, kernel.arrangement]),
|
|
||||||
builder: (ctx, _) {
|
|
||||||
final a = kernel.arrangement;
|
|
||||||
final sidebarVisible = a.isVisible(Slots.sidebar);
|
|
||||||
final contextVisible = a.isVisible(Slots.contextPanel);
|
|
||||||
final statusVisible = a.isVisible(Slots.statusbar);
|
|
||||||
final sidebarSize = a.sizeOf(Slots.sidebar) ?? 240;
|
|
||||||
final contextSize = a.sizeOf(Slots.contextPanel) ?? 280;
|
|
||||||
final statusHeight = a.sizeOf(Slots.statusbar) ?? 26;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (sidebarVisible) ...[
|
|
||||||
SizedBox(
|
|
||||||
width: sidebarSize,
|
|
||||||
child: SlotHost(slot: Slots.sidebar),
|
|
||||||
),
|
|
||||||
DragResizeHandle(
|
|
||||||
arrangement: a,
|
|
||||||
slot: Slots.sidebar,
|
|
||||||
axis: Axis.horizontal,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const Expanded(child: SlotHost(slot: Slots.workspace)),
|
|
||||||
if (contextVisible) ...[
|
|
||||||
DragResizeHandle(
|
|
||||||
arrangement: a,
|
|
||||||
slot: Slots.contextPanel,
|
|
||||||
axis: Axis.horizontal,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: contextSize,
|
|
||||||
child: SlotHost(slot: Slots.contextPanel),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (statusVisible)
|
|
||||||
SizedBox(
|
|
||||||
height: statusHeight,
|
|
||||||
child: const StatusbarHost(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SlotHost extends StatelessWidget {
|
|
||||||
const SlotHost({super.key, required this.slot});
|
|
||||||
final SlotId slot;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final kernel = ClideKernel.of(context);
|
|
||||||
final tokens = ClideTheme.of(context).surface;
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: Listenable.merge([kernel.panels, kernel.i18n]),
|
|
||||||
builder: (ctx, _) {
|
|
||||||
final tabs = kernel.panels.tabsFor(slot);
|
|
||||||
if (tabs.isEmpty) {
|
|
||||||
return Container(color: tokens.panelBackground);
|
|
||||||
}
|
|
||||||
final activeId = kernel.panels.activeTabIn(slot) ?? tabs.first.id;
|
|
||||||
final active = tabs.firstWhere(
|
|
||||||
(t) => t.id == activeId,
|
|
||||||
orElse: () => tabs.first,
|
|
||||||
);
|
|
||||||
return Container(
|
|
||||||
color: tokens.panelBackground,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ClideTabBar(
|
|
||||||
items: [
|
|
||||||
for (final t in tabs)
|
|
||||||
ClideTabItem(id: t.id, title: _resolveTitle(ctx, t)),
|
|
||||||
],
|
|
||||||
activeId: active.id,
|
|
||||||
onSelect: (id) => kernel.panels.activateTab(slot, id),
|
|
||||||
),
|
|
||||||
ClideDivider(),
|
|
||||||
Expanded(child: active.build(ctx)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _resolveTitle(BuildContext context, TabContribution t) {
|
|
||||||
final key = t.titleKey;
|
|
||||||
final ns = t.i18nNamespace;
|
|
||||||
if (key == null || ns == null) return t.title;
|
|
||||||
return ClideKernel.of(context).i18n.string(
|
|
||||||
key,
|
|
||||||
namespace: ns,
|
|
||||||
placeholder: t.title,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusbarHost extends StatelessWidget {
|
|
||||||
const StatusbarHost({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final kernel = ClideKernel.of(context);
|
|
||||||
final tokens = ClideTheme.of(context).surface;
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: kernel.panels,
|
|
||||||
builder: (ctx, _) {
|
|
||||||
final items = kernel.panels
|
|
||||||
.contributionsFor(Slots.statusbar)
|
|
||||||
.whereType<StatusItemContribution>()
|
|
||||||
.toList();
|
|
||||||
final left = items.where((i) => i.priority < 100).toList();
|
|
||||||
final right = items.where((i) => i.priority >= 100).toList();
|
|
||||||
return Container(
|
|
||||||
color: tokens.statusBarBackground,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
for (final item in left) item.build(ctx),
|
|
||||||
const Spacer(),
|
|
||||||
for (final item in right) item.build(ctx),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import 'package:clide/clide.dart';
|
|
||||||
import 'package:clide_app/builtin/claude/src/claude_pane.dart';
|
|
||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
|
|
||||||
/// Claude pane. Primary per repo (tmux-persisted per D-041); optional
|
|
||||||
/// secondaries spawned via the `claude.new-secondary` command.
|
|
||||||
class ClaudeExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.claude';
|
|
||||||
@override
|
|
||||||
String get title => 'Claude';
|
|
||||||
@override
|
|
||||||
String get version => '0.1.0';
|
|
||||||
@override
|
|
||||||
List<String> get dependsOn => const [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => [
|
|
||||||
TabContribution(
|
|
||||||
id: 'claude.primary',
|
|
||||||
slot: Slots.workspace,
|
|
||||||
title: 'Claude',
|
|
||||||
titleKey: 'tab.title',
|
|
||||||
i18nNamespace: id,
|
|
||||||
priority: 90, // just before terminal (100)
|
|
||||||
build: (_) => const ClaudePane(isPrimary: true),
|
|
||||||
),
|
|
||||||
CommandContribution(
|
|
||||||
id: 'claude.new-secondary',
|
|
||||||
command: 'claude.new-secondary',
|
|
||||||
title: 'Claude: open a secondary session',
|
|
||||||
run: (_) async {
|
|
||||||
// Secondary spawn is a UI-side concern (tab creation in
|
|
||||||
// the workspace slot). Returning OK signals the palette
|
|
||||||
// that the command exists; wiring the workspace-slot tab
|
|
||||||
// manager to open `ClaudePane(isPrimary: false,
|
|
||||||
// secondaryIndex: N)` on this command lands in a follow-up.
|
|
||||||
// D-041 policy is captured in the decision record either
|
|
||||||
// way.
|
|
||||||
return IpcResponse.ok(
|
|
||||||
id: '',
|
|
||||||
data: const {
|
|
||||||
'status': 'accepted',
|
|
||||||
'note': 'UI-side tab manager wires this up in a '
|
|
||||||
'follow-up commit.',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/// Derive deterministic tmux session names for Claude panes (D-041).
|
|
||||||
///
|
|
||||||
/// The primary session name for a repo is `clide-claude-<hash>` where
|
|
||||||
/// `<hash>` is an 8-char FNV-1a-ish hex hash of the canonical
|
|
||||||
/// (absolute, symlink-resolved) repo path. Not cryptographic — we just
|
|
||||||
/// need short, stable, collision-resistant-enough strings. Secondary
|
|
||||||
/// sessions append `-N` for monotonically increasing `N`.
|
|
||||||
library;
|
|
||||||
|
|
||||||
/// Stable session name for the primary Claude pane of [repoRoot].
|
|
||||||
String primarySessionName(String repoRoot) {
|
|
||||||
return 'clide-claude-${_hash(repoRoot)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Nth secondary session name. [n] starts at 1.
|
|
||||||
String secondarySessionName(String repoRoot, int n) {
|
|
||||||
return '${primarySessionName(repoRoot)}-$n';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 8-char hex hash. FNV-1a over UTF-16 code units; independent of
|
|
||||||
/// platform endianness. Collision rate at N=1000 repos is still
|
|
||||||
/// vanishingly small (≈0.0001%).
|
|
||||||
String _hash(String s) {
|
|
||||||
var h = 2166136261; // FNV offset basis (32-bit)
|
|
||||||
for (var i = 0; i < s.length; i++) {
|
|
||||||
h ^= s.codeUnitAt(i);
|
|
||||||
h = (h * 16777619) & 0xffffffff;
|
|
||||||
}
|
|
||||||
return h.toRadixString(16).padLeft(8, '0');
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
|
|
||||||
/// Tier-reserved stub. Will surface a sidebar tab (filter by domain /
|
|
||||||
/// status, backlinks from current file) + commands (`decisions.open`,
|
|
||||||
/// `decisions.claim`, `decisions.amend`). Data source: `pql decisions
|
|
||||||
/// …` once `Q-021` resolves; `tools/scripts/plan decisions …` until
|
|
||||||
/// then.
|
|
||||||
class DecisionsExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.decisions';
|
|
||||||
@override
|
|
||||||
String get title => 'Decisions';
|
|
||||||
@override
|
|
||||||
String get version => '0.0.0-stub';
|
|
||||||
@override
|
|
||||||
List<String> get dependsOn => const [];
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => const [];
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import 'package:clide/clide.dart';
|
|
||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
|
|
||||||
class DefaultLayoutExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.default-layout';
|
|
||||||
@override
|
|
||||||
String get title => 'Default layout';
|
|
||||||
@override
|
|
||||||
String get version => '0.1.0';
|
|
||||||
|
|
||||||
LayoutPresetContribution? _preset;
|
|
||||||
ClideExtensionContext? _ctx;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => [
|
|
||||||
_preset ?? classicPreset(),
|
|
||||||
CommandContribution(
|
|
||||||
id: 'layout.reset',
|
|
||||||
command: 'layout.reset',
|
|
||||||
title: 'Layout: Reset to Classic',
|
|
||||||
run: _reset,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> activate(ClideExtensionContext ctx) async {
|
|
||||||
_ctx = ctx;
|
|
||||||
_preset = classicPreset();
|
|
||||||
// Register the preset's slots into the panel registry and apply
|
|
||||||
// the arrangement so every SlotHost has something to render.
|
|
||||||
ctx.arrangement.registerSlotsInto(ctx.panels, _preset!);
|
|
||||||
ctx.arrangement.applyPreset(_preset!);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<IpcResponse> _reset(List<String> args) async {
|
|
||||||
final preset = _preset;
|
|
||||||
final ctx = _ctx;
|
|
||||||
if (preset == null || ctx == null) {
|
|
||||||
return IpcResponse.err(
|
|
||||||
id: '',
|
|
||||||
error: IpcError(
|
|
||||||
code: IpcExitCode.toolError,
|
|
||||||
kind: IpcErrorKind.toolError,
|
|
||||||
message: 'default-layout not activated',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ctx.arrangement.applyPreset(preset);
|
|
||||||
return IpcResponse.ok(id: '', data: {'preset': preset.id});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export 'src/extension.dart';
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
|
|
||||||
/// Tier-0 stub. Real implementation lands in a later tier; the extension
|
|
||||||
/// is registered so the extensions-ui surface can list it as "installed,
|
|
||||||
/// not yet implemented" and its id is reserved.
|
|
||||||
class DiffExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.diff';
|
|
||||||
@override
|
|
||||||
String get title => 'Diff';
|
|
||||||
@override
|
|
||||||
String get version => '0.0.0-stub';
|
|
||||||
@override
|
|
||||||
List<String> get dependsOn => const [];
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => const [];
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export 'src/extension.dart';
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
|
|
||||||
/// Tier-0 stub. Real implementation lands in a later tier; the extension
|
|
||||||
/// is registered so the extensions-ui surface can list it as "installed,
|
|
||||||
/// not yet implemented" and its id is reserved.
|
|
||||||
class GitExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.git';
|
|
||||||
@override
|
|
||||||
String get title => 'Git';
|
|
||||||
@override
|
|
||||||
String get version => '0.0.0-stub';
|
|
||||||
@override
|
|
||||||
List<String> get dependsOn => const ['builtin.diff'];
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => const [];
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import 'package:clide_app/builtin/ipc_status/src/status_item.dart';
|
|
||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
|
|
||||||
class IpcStatusExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.ipc-status';
|
|
||||||
@override
|
|
||||||
String get title => 'Daemon connection';
|
|
||||||
@override
|
|
||||||
String get version => '0.1.0';
|
|
||||||
|
|
||||||
DaemonClient? _ipc;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> activate(ClideExtensionContext ctx) async {
|
|
||||||
_ipc = ctx.ipc;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions {
|
|
||||||
final ipc = _ipc;
|
|
||||||
if (ipc == null) return const [];
|
|
||||||
return [
|
|
||||||
StatusItemContribution(
|
|
||||||
id: 'ipc-status.indicator',
|
|
||||||
priority: 100, // right-side
|
|
||||||
listenable: ipc,
|
|
||||||
build: (_) => IpcStatusItem(ipc: ipc),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
import 'package:clide_app/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class IpcStatusItem extends StatelessWidget {
|
|
||||||
const IpcStatusItem({super.key, required this.ipc});
|
|
||||||
|
|
||||||
final DaemonClient ipc;
|
|
||||||
|
|
||||||
static const _ns = 'builtin.ipc-status';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final kernel = ClideKernel.of(context);
|
|
||||||
final tokens = ClideTheme.of(context).surface;
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: Listenable.merge([ipc, kernel.i18n]),
|
|
||||||
builder: (ctx, _) {
|
|
||||||
final connected = ipc.isConnected;
|
|
||||||
final color = connected ? tokens.statusSuccess : tokens.statusError;
|
|
||||||
final i = kernel.i18n;
|
|
||||||
final label = connected
|
|
||||||
? i.string('connected', namespace: _ns, placeholder: 'connected')
|
|
||||||
: i.string('disconnected',
|
|
||||||
namespace: _ns, placeholder: 'disconnected');
|
|
||||||
final hint = connected
|
|
||||||
? i.string('connected.hint',
|
|
||||||
namespace: _ns,
|
|
||||||
placeholder: 'clide daemon is reachable over the local socket')
|
|
||||||
: i.string('disconnected.hint',
|
|
||||||
namespace: _ns,
|
|
||||||
placeholder:
|
|
||||||
'clide daemon is not running — start it with `clide --daemon`');
|
|
||||||
return Semantics(
|
|
||||||
label: label,
|
|
||||||
hint: hint,
|
|
||||||
liveRegion: true,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
ClideIcon(const PlugIcon(), size: 12, color: color),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
ClideText(label, fontSize: 12, color: color),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export 'src/extension.dart';
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
|
|
||||||
/// Tier-0 stub. Real implementation lands in a later tier; the extension
|
|
||||||
/// is registered so the extensions-ui surface can list it as "installed,
|
|
||||||
/// not yet implemented" and its id is reserved.
|
|
||||||
class JiraExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.jira';
|
|
||||||
@override
|
|
||||||
String get title => 'Jira';
|
|
||||||
@override
|
|
||||||
String get version => '0.0.0-stub';
|
|
||||||
@override
|
|
||||||
List<String> get dependsOn => const [];
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => const [];
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
|
|
||||||
/// Tier-0 stub. Real implementation lands in a later tier; the extension
|
|
||||||
/// is registered so the extensions-ui surface can list it as "installed,
|
|
||||||
/// not yet implemented" and its id is reserved.
|
|
||||||
class MarkdownExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.markdown';
|
|
||||||
@override
|
|
||||||
String get title => 'Markdown';
|
|
||||||
@override
|
|
||||||
String get version => '0.0.0-stub';
|
|
||||||
@override
|
|
||||||
List<String> get dependsOn => const ['builtin.editor'];
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => const [];
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export 'src/extension.dart';
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
|
|
||||||
/// Tier-0 stub. Real implementation lands in a later tier; the extension
|
|
||||||
/// is registered so the extensions-ui surface can list it as "installed,
|
|
||||||
/// not yet implemented" and its id is reserved.
|
|
||||||
class PqlExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.pql';
|
|
||||||
@override
|
|
||||||
String get title => 'pql';
|
|
||||||
@override
|
|
||||||
String get version => '0.0.0-stub';
|
|
||||||
@override
|
|
||||||
List<String> get dependsOn => const [];
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => const [];
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export 'src/extension.dart';
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
|
|
||||||
/// Tier-0 stub. Real implementation lands in a later tier; the extension
|
|
||||||
/// is registered so the extensions-ui surface can list it as "installed,
|
|
||||||
/// not yet implemented" and its id is reserved.
|
|
||||||
class ProblemsExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.problems';
|
|
||||||
@override
|
|
||||||
String get title => 'Problems';
|
|
||||||
@override
|
|
||||||
String get version => '0.0.0-stub';
|
|
||||||
@override
|
|
||||||
List<String> get dependsOn => const [];
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => const [];
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
|
|
||||||
/// Tier-reserved stub. Will surface a sidebar tab (filtered list) + a
|
|
||||||
/// workspace tab (kanban board) + commands (`tickets.open`,
|
|
||||||
/// `tickets.new`, `tickets.move`, `tickets.block`). Data source: `pql
|
|
||||||
/// ticket …` once `Q-021` resolves; `tools/scripts/plan ticket …` until
|
|
||||||
/// then. Ticket persistence strategy open at `Q-022`.
|
|
||||||
class TicketsExtension extends ClideExtension {
|
|
||||||
@override
|
|
||||||
String get id => 'builtin.tickets';
|
|
||||||
@override
|
|
||||||
String get title => 'Tickets';
|
|
||||||
@override
|
|
||||||
String get version => '0.0.0-stub';
|
|
||||||
@override
|
|
||||||
List<String> get dependsOn => const [];
|
|
||||||
@override
|
|
||||||
List<ContributionPoint> get contributions => const [];
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
import 'package:clide_app/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class WelcomeView extends StatelessWidget {
|
|
||||||
const WelcomeView({super.key});
|
|
||||||
|
|
||||||
static const _ns = 'builtin.welcome';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final kernel = ClideKernel.of(context);
|
|
||||||
final tokens = ClideTheme.of(context).surface;
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: kernel.i18n,
|
|
||||||
builder: (ctx, _) {
|
|
||||||
final i = kernel.i18n;
|
|
||||||
return ClideSurface(
|
|
||||||
color: tokens.globalBackground,
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
ClideText(
|
|
||||||
i.string('title', namespace: _ns, placeholder: 'clide'),
|
|
||||||
fontSize: 40,
|
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
color: tokens.panelActiveBorder,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
ClideText(
|
|
||||||
i.string(
|
|
||||||
'subtitle',
|
|
||||||
namespace: _ns,
|
|
||||||
placeholder: 'Flutter desktop IDE for Claude Code',
|
|
||||||
),
|
|
||||||
muted: true,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
ClideButton(
|
|
||||||
label: i.string(
|
|
||||||
'open-project',
|
|
||||||
namespace: _ns,
|
|
||||||
placeholder: 'Open project',
|
|
||||||
),
|
|
||||||
semanticHint: i.string(
|
|
||||||
'open-project.hint',
|
|
||||||
namespace: _ns,
|
|
||||||
placeholder:
|
|
||||||
'Pick a git repository to open as the workspace',
|
|
||||||
),
|
|
||||||
variant: ClideButtonVariant.primary,
|
|
||||||
onPressed: () {
|
|
||||||
// project open UI lands with the project picker tier
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:clide_app/kernel/src/events/types.dart';
|
|
||||||
|
|
||||||
class EventBus {
|
|
||||||
EventBus();
|
|
||||||
|
|
||||||
final StreamController<ClideEventEnvelope> _controller =
|
|
||||||
StreamController<ClideEventEnvelope>.broadcast();
|
|
||||||
|
|
||||||
Stream<ClideEventEnvelope> get stream => _controller.stream;
|
|
||||||
|
|
||||||
Stream<T> on<T extends ClideEvent>() =>
|
|
||||||
_controller.stream.where((e) => e.event is T).map((e) => e.event as T);
|
|
||||||
|
|
||||||
void emit(ClideEvent event) {
|
|
||||||
if (_controller.isClosed) return;
|
|
||||||
_controller.add(ClideEventEnvelope(event, DateTime.now().toUtc()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dispose() => _controller.close();
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import 'package:clide_app/extension/src/contribution.dart';
|
|
||||||
import 'package:clide_app/kernel/src/panels/registry.dart';
|
|
||||||
import 'package:clide_app/kernel/src/panels/slot_id.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
/// Current, runtime layout state: which slots are visible, at what size,
|
|
||||||
/// and in what positions. Persisted through `settings`.
|
|
||||||
///
|
|
||||||
/// The registry knows which slots *exist* and which contributions are
|
|
||||||
/// mounted. The arrangement knows which slots are *currently* shown and
|
|
||||||
/// how big they are — user-modifiable via drag-resize.
|
|
||||||
class LayoutArrangement extends ChangeNotifier {
|
|
||||||
LayoutArrangement();
|
|
||||||
|
|
||||||
final Map<SlotId, _SlotState> _state = {};
|
|
||||||
|
|
||||||
void applyPreset(LayoutPresetContribution preset) {
|
|
||||||
_state.clear();
|
|
||||||
for (final slot in preset.slots) {
|
|
||||||
_state[slot.slot] = _SlotState(
|
|
||||||
position: slot.position,
|
|
||||||
size: slot.defaultSize,
|
|
||||||
minSize: slot.minSize,
|
|
||||||
maxSize: slot.maxSize,
|
|
||||||
visible: slot.visible,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<SlotId> get slotsInOrder => _state.keys;
|
|
||||||
|
|
||||||
SlotPosition? positionOf(SlotId id) => _state[id]?.position;
|
|
||||||
double? sizeOf(SlotId id) => _state[id]?.size;
|
|
||||||
double? minSizeOf(SlotId id) => _state[id]?.minSize;
|
|
||||||
double? maxSizeOf(SlotId id) => _state[id]?.maxSize;
|
|
||||||
bool isVisible(SlotId id) => _state[id]?.visible ?? false;
|
|
||||||
|
|
||||||
void setSize(SlotId id, double size) {
|
|
||||||
final s = _state[id];
|
|
||||||
if (s == null) return;
|
|
||||||
final clamped =
|
|
||||||
size.clamp(s.minSize ?? 0, s.maxSize ?? double.infinity).toDouble();
|
|
||||||
if (s.size == clamped) return;
|
|
||||||
_state[id] = s.copyWith(size: clamped);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVisible(SlotId id, bool visible) {
|
|
||||||
final s = _state[id];
|
|
||||||
if (s == null || s.visible == visible) return;
|
|
||||||
_state[id] = s.copyWith(visible: visible);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience for the default-layout extension: register all slots
|
|
||||||
/// it provides into a [PanelRegistry] with defaults from this preset.
|
|
||||||
void registerSlotsInto(
|
|
||||||
PanelRegistry registry,
|
|
||||||
LayoutPresetContribution preset,
|
|
||||||
) {
|
|
||||||
for (final slot in preset.slots) {
|
|
||||||
registry.registerSlot(SlotDefinition(
|
|
||||||
id: slot.slot,
|
|
||||||
position: slot.position,
|
|
||||||
defaultSize: slot.defaultSize,
|
|
||||||
minSize: slot.minSize,
|
|
||||||
maxSize: slot.maxSize,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SlotState {
|
|
||||||
const _SlotState({
|
|
||||||
required this.position,
|
|
||||||
this.size,
|
|
||||||
this.minSize,
|
|
||||||
this.maxSize,
|
|
||||||
this.visible = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
final SlotPosition position;
|
|
||||||
final double? size;
|
|
||||||
final double? minSize;
|
|
||||||
final double? maxSize;
|
|
||||||
final bool visible;
|
|
||||||
|
|
||||||
_SlotState copyWith({
|
|
||||||
SlotPosition? position,
|
|
||||||
double? size,
|
|
||||||
double? minSize,
|
|
||||||
double? maxSize,
|
|
||||||
bool? visible,
|
|
||||||
}) {
|
|
||||||
return _SlotState(
|
|
||||||
position: position ?? this.position,
|
|
||||||
size: size ?? this.size,
|
|
||||||
minSize: minSize ?? this.minSize,
|
|
||||||
maxSize: maxSize ?? this.maxSize,
|
|
||||||
visible: visible ?? this.visible,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:clide_app/kernel/src/events/bus.dart';
|
|
||||||
import 'package:clide_app/kernel/src/events/types.dart';
|
|
||||||
import 'package:clide_app/kernel/src/log.dart';
|
|
||||||
import 'package:clide_app/kernel/src/settings.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
class ProjectManager extends ChangeNotifier {
|
|
||||||
ProjectManager({
|
|
||||||
required Logger log,
|
|
||||||
required EventBus events,
|
|
||||||
required SettingsStore settings,
|
|
||||||
}) : _log = log,
|
|
||||||
_events = events,
|
|
||||||
_settings = settings;
|
|
||||||
|
|
||||||
final Logger _log;
|
|
||||||
final EventBus _events;
|
|
||||||
final SettingsStore _settings;
|
|
||||||
|
|
||||||
Directory? _current;
|
|
||||||
Directory? get current => _current;
|
|
||||||
bool get isOpen => _current != null;
|
|
||||||
|
|
||||||
/// Open a project by path. Runs `git rev-parse --show-toplevel` to
|
|
||||||
/// find the workspace root. Returns true on success.
|
|
||||||
Future<bool> open(String path) async {
|
|
||||||
final root = await resolveWorkspace(path);
|
|
||||||
if (root == null) {
|
|
||||||
_log.warn('project', 'not a git repo: $path');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_current = Directory(root);
|
|
||||||
await _settings.setProjectDir(_current);
|
|
||||||
_events.emit(ProjectOpened(path: root));
|
|
||||||
notifyListeners();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_current == null) return;
|
|
||||||
_current = null;
|
|
||||||
await _settings.setProjectDir(null);
|
|
||||||
_events.emit(const ProjectClosed());
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walks up from [path] via `git rev-parse --show-toplevel`. Returns
|
|
||||||
/// null if the path is outside a git repo or git isn't available.
|
|
||||||
Future<String?> resolveWorkspace(String path) async {
|
|
||||||
try {
|
|
||||||
final r = await Process.run(
|
|
||||||
'git',
|
|
||||||
['rev-parse', '--show-toplevel'],
|
|
||||||
workingDirectory: path,
|
|
||||||
runInShell: false,
|
|
||||||
);
|
|
||||||
if (r.exitCode != 0) return null;
|
|
||||||
final out = (r.stdout as String).trim();
|
|
||||||
return out.isEmpty ? null : out;
|
|
||||||
} catch (e) {
|
|
||||||
_log.debug('project', 'git rev-parse failed: $e');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import 'package:clide_app/app.dart';
|
|
||||||
import 'package:clide_app/builtin/canvas/canvas.dart';
|
|
||||||
import 'package:clide_app/builtin/claude/claude.dart';
|
|
||||||
import 'package:clide_app/builtin/claude_control/claude_control.dart';
|
|
||||||
import 'package:clide_app/builtin/decisions/decisions.dart';
|
|
||||||
import 'package:clide_app/builtin/default_layout/default_layout.dart';
|
|
||||||
import 'package:clide_app/builtin/diff/diff.dart';
|
|
||||||
import 'package:clide_app/builtin/editor/editor.dart';
|
|
||||||
import 'package:clide_app/builtin/extensions_ui/extensions_ui.dart';
|
|
||||||
import 'package:clide_app/builtin/files/files.dart';
|
|
||||||
import 'package:clide_app/builtin/git/git.dart';
|
|
||||||
import 'package:clide_app/builtin/grammars_core/grammars_core.dart';
|
|
||||||
import 'package:clide_app/builtin/graph/graph.dart';
|
|
||||||
import 'package:clide_app/builtin/ipc_status/ipc_status.dart';
|
|
||||||
import 'package:clide_app/builtin/jira/jira.dart';
|
|
||||||
import 'package:clide_app/builtin/keybindings_ui/keybindings_ui.dart';
|
|
||||||
import 'package:clide_app/builtin/markdown/markdown.dart';
|
|
||||||
import 'package:clide_app/builtin/pql/pql.dart';
|
|
||||||
import 'package:clide_app/builtin/problems/problems.dart';
|
|
||||||
import 'package:clide_app/builtin/settings_ui/settings_ui.dart';
|
|
||||||
import 'package:clide_app/builtin/terminal/terminal.dart';
|
|
||||||
import 'package:clide_app/builtin/theme_picker/theme_picker.dart';
|
|
||||||
import 'package:clide_app/builtin/tickets/tickets.dart';
|
|
||||||
import 'package:clide_app/builtin/todos/todos.dart';
|
|
||||||
import 'package:clide_app/builtin/welcome/welcome.dart';
|
|
||||||
import 'dart:io' show Directory, Platform;
|
|
||||||
|
|
||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
Future<void> main() async {
|
|
||||||
final binding = WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
|
|
||||||
// Enable the semantics tree at boot so screen readers (Orca, VoiceOver,
|
|
||||||
// NVDA) + the headless-browser automation harness both work out of the
|
|
||||||
// box. Production release builds could gate this on a platform check,
|
|
||||||
// but a11y-first means always-on.
|
|
||||||
binding.ensureSemantics();
|
|
||||||
|
|
||||||
final appDir = await _resolveAppDir();
|
|
||||||
final themes = await _loadBundledThemes();
|
|
||||||
|
|
||||||
final services = await KernelServices.boot(
|
|
||||||
appDir: appDir,
|
|
||||||
bundledThemes: themes,
|
|
||||||
i18nLoader: AssetCatalogLoader(bundle: rootBundle),
|
|
||||||
preloadNamespaces: _tier0Namespaces,
|
|
||||||
// The local unix-socket daemon is desktop-only. On web the
|
|
||||||
// connection indicator stays `disconnected` — that's the honest
|
|
||||||
// state: the Flutter-web build can't reach a local socket.
|
|
||||||
autoStartDaemonClient: !kIsWeb,
|
|
||||||
socketPath: kIsWeb ? '/clide-web-no-socket' : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Register every built-in. Tier 0 activates only the four that do
|
|
||||||
// real work; the rest compile in as stubs so the extensions-ui can
|
|
||||||
// list them when Tier 6 lands.
|
|
||||||
services.extensions
|
|
||||||
..register(DefaultLayoutExtension())
|
|
||||||
..register(WelcomeExtension())
|
|
||||||
..register(IpcStatusExtension())
|
|
||||||
..register(ThemePickerExtension())
|
|
||||||
..register(ClaudeExtension())
|
|
||||||
..register(TerminalExtension())
|
|
||||||
..register(FilesExtension())
|
|
||||||
..register(EditorExtension())
|
|
||||||
..register(GrammarsCoreExtension())
|
|
||||||
..register(MarkdownExtension())
|
|
||||||
..register(DiffExtension())
|
|
||||||
..register(GitExtension())
|
|
||||||
..register(PqlExtension())
|
|
||||||
..register(TodosExtension())
|
|
||||||
..register(ProblemsExtension())
|
|
||||||
..register(JiraExtension())
|
|
||||||
..register(CanvasExtension())
|
|
||||||
..register(GraphExtension())
|
|
||||||
..register(SettingsUiExtension())
|
|
||||||
..register(ExtensionsUiExtension())
|
|
||||||
..register(KeybindingsUiExtension())
|
|
||||||
..register(DecisionsExtension())
|
|
||||||
..register(TicketsExtension())
|
|
||||||
..register(ClaudeControlExtension());
|
|
||||||
|
|
||||||
await services.extensions.activateAll();
|
|
||||||
|
|
||||||
runApp(ClideApp(services: services));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve the app-settings directory.
|
|
||||||
///
|
|
||||||
/// On web we don't touch the filesystem — hand back a sentinel dir so
|
|
||||||
/// `SettingsStore.load()` silently no-ops (its read-file helper returns
|
|
||||||
/// `{}` when the file doesn't exist).
|
|
||||||
Future<Directory> _resolveAppDir() async {
|
|
||||||
if (kIsWeb) return Directory('/clide-web-no-disk');
|
|
||||||
final home = Platform.environment['HOME'] ?? '/tmp';
|
|
||||||
final xdg = Platform.environment['XDG_CONFIG_HOME'] ?? '$home/.config';
|
|
||||||
final dir = Directory('$xdg/clide');
|
|
||||||
await dir.create(recursive: true);
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<ThemeDefinition>> _loadBundledThemes() async {
|
|
||||||
const loader = ThemeLoader();
|
|
||||||
const paths = [
|
|
||||||
'lib/kernel/src/theme/themes/summer-night.yaml',
|
|
||||||
];
|
|
||||||
final out = <ThemeDefinition>[];
|
|
||||||
for (final p in paths) {
|
|
||||||
out.add(await loader.fromAsset(rootBundle, p));
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every Tier-0 extension that ships an i18n catalog. Extensions
|
|
||||||
/// registered but not active (the 17 stubs) don't preload — their
|
|
||||||
/// catalogs load lazily on activate in later tiers.
|
|
||||||
const List<String> _tier0Namespaces = [
|
|
||||||
'builtin.default-layout',
|
|
||||||
'builtin.welcome',
|
|
||||||
'builtin.ipc-status',
|
|
||||||
'builtin.theme-picker',
|
|
||||||
'builtin.terminal',
|
|
||||||
'builtin.files',
|
|
||||||
'builtin.claude',
|
|
||||||
'builtin.editor',
|
|
||||||
];
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
|
||||||
import 'package:clide_app/widgets/src/clide_text.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
enum ClideButtonVariant { normal, primary, subtle }
|
|
||||||
|
|
||||||
class ClideButton extends StatefulWidget {
|
|
||||||
const ClideButton({
|
|
||||||
super.key,
|
|
||||||
required this.label,
|
|
||||||
required this.onPressed,
|
|
||||||
this.variant = ClideButtonVariant.normal,
|
|
||||||
this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
||||||
this.semanticLabel,
|
|
||||||
this.semanticHint,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String label;
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
final ClideButtonVariant variant;
|
|
||||||
final EdgeInsetsGeometry padding;
|
|
||||||
|
|
||||||
/// Overrides [label] for screen readers (use when the visible label is
|
|
||||||
/// an icon-only glyph or a noun that reads oddly when announced).
|
|
||||||
final String? semanticLabel;
|
|
||||||
|
|
||||||
/// Screen-reader hint describing the button's effect. Optional.
|
|
||||||
final String? semanticHint;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ClideButton> createState() => _ClideButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ClideButtonState extends State<ClideButton> {
|
|
||||||
bool _hovered = false;
|
|
||||||
bool _pressed = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final tokens = ClideTheme.of(context).surface;
|
|
||||||
final enabled = widget.onPressed != null;
|
|
||||||
|
|
||||||
Color bg;
|
|
||||||
Color fg;
|
|
||||||
switch (widget.variant) {
|
|
||||||
case ClideButtonVariant.normal:
|
|
||||||
bg = _pressed
|
|
||||||
? tokens.buttonActiveBackground
|
|
||||||
: _hovered
|
|
||||||
? tokens.buttonHoverBackground
|
|
||||||
: tokens.buttonBackground;
|
|
||||||
fg = tokens.buttonForeground;
|
|
||||||
case ClideButtonVariant.primary:
|
|
||||||
bg = _pressed
|
|
||||||
? tokens.panelActiveBorder
|
|
||||||
: _hovered
|
|
||||||
? tokens.buttonActiveBackground
|
|
||||||
: tokens.buttonActiveBackground;
|
|
||||||
fg = tokens.globalBackground;
|
|
||||||
case ClideButtonVariant.subtle:
|
|
||||||
bg = _hovered
|
|
||||||
? tokens.listItemHoverBackground
|
|
||||||
: tokens.listItemBackground;
|
|
||||||
fg = tokens.listItemForeground;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Semantics(
|
|
||||||
button: true,
|
|
||||||
enabled: enabled,
|
|
||||||
label: widget.semanticLabel ?? widget.label,
|
|
||||||
hint: widget.semanticHint,
|
|
||||||
onTap: enabled ? widget.onPressed : null,
|
|
||||||
excludeSemantics: true,
|
|
||||||
child: MouseRegion(
|
|
||||||
cursor:
|
|
||||||
enabled ? SystemMouseCursors.click : SystemMouseCursors.forbidden,
|
|
||||||
onEnter: (_) => setState(() => _hovered = true),
|
|
||||||
onExit: (_) => setState(() => _hovered = false),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTapDown: (_) => setState(() => _pressed = true),
|
|
||||||
onTapCancel: () => setState(() => _pressed = false),
|
|
||||||
onTapUp: (_) {
|
|
||||||
setState(() => _pressed = false);
|
|
||||||
widget.onPressed?.call();
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: widget.padding,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: bg,
|
|
||||||
border: Border.all(color: tokens.buttonBorder, width: 1),
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
|
||||||
child: ClideText(
|
|
||||||
widget.label,
|
|
||||||
color: fg,
|
|
||||||
fontWeight: widget.variant == ClideButtonVariant.primary
|
|
||||||
? FontWeight.w600
|
|
||||||
: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
#include "my_application.h"
|
|
||||||
|
|
||||||
#include <flutter_linux/flutter_linux.h>
|
|
||||||
#ifdef GDK_WINDOWING_X11
|
|
||||||
#include <gdk/gdkx.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "flutter/generated_plugin_registrant.h"
|
|
||||||
|
|
||||||
struct _MyApplication {
|
|
||||||
GtkApplication parent_instance;
|
|
||||||
char** dart_entrypoint_arguments;
|
|
||||||
};
|
|
||||||
|
|
||||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
|
||||||
|
|
||||||
// Called when first Flutter frame received.
|
|
||||||
static void first_frame_cb(MyApplication* self, FlView* view) {
|
|
||||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GApplication::activate.
|
|
||||||
static void my_application_activate(GApplication* application) {
|
|
||||||
MyApplication* self = MY_APPLICATION(application);
|
|
||||||
GtkWindow* window =
|
|
||||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
|
||||||
|
|
||||||
// Use a header bar when running in GNOME as this is the common style used
|
|
||||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
|
||||||
// desktop).
|
|
||||||
// If running on X and not using GNOME then just use a traditional title bar
|
|
||||||
// in case the window manager does more exotic layout, e.g. tiling.
|
|
||||||
// If running on Wayland assume the header bar will work (may need changing
|
|
||||||
// if future cases occur).
|
|
||||||
gboolean use_header_bar = TRUE;
|
|
||||||
#ifdef GDK_WINDOWING_X11
|
|
||||||
GdkScreen* screen = gtk_window_get_screen(window);
|
|
||||||
if (GDK_IS_X11_SCREEN(screen)) {
|
|
||||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
|
||||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
|
||||||
use_header_bar = FALSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (use_header_bar) {
|
|
||||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
|
||||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
|
||||||
gtk_header_bar_set_title(header_bar, "clide_app");
|
|
||||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
|
||||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
|
||||||
} else {
|
|
||||||
gtk_window_set_title(window, "clide_app");
|
|
||||||
}
|
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 1280, 720);
|
|
||||||
|
|
||||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
|
||||||
fl_dart_project_set_dart_entrypoint_arguments(
|
|
||||||
project, self->dart_entrypoint_arguments);
|
|
||||||
|
|
||||||
FlView* view = fl_view_new(project);
|
|
||||||
GdkRGBA background_color;
|
|
||||||
// Background defaults to black, override it here if necessary, e.g. #00000000
|
|
||||||
// for transparent.
|
|
||||||
gdk_rgba_parse(&background_color, "#000000");
|
|
||||||
fl_view_set_background_color(view, &background_color);
|
|
||||||
gtk_widget_show(GTK_WIDGET(view));
|
|
||||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
|
||||||
|
|
||||||
// Show the window when Flutter renders.
|
|
||||||
// Requires the view to be realized so we can start rendering.
|
|
||||||
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
|
||||||
self);
|
|
||||||
gtk_widget_realize(GTK_WIDGET(view));
|
|
||||||
|
|
||||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
|
||||||
|
|
||||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GApplication::local_command_line.
|
|
||||||
static gboolean my_application_local_command_line(GApplication* application,
|
|
||||||
gchar*** arguments,
|
|
||||||
int* exit_status) {
|
|
||||||
MyApplication* self = MY_APPLICATION(application);
|
|
||||||
// Strip out the first argument as it is the binary name.
|
|
||||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
|
||||||
|
|
||||||
g_autoptr(GError) error = nullptr;
|
|
||||||
if (!g_application_register(application, nullptr, &error)) {
|
|
||||||
g_warning("Failed to register: %s", error->message);
|
|
||||||
*exit_status = 1;
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_application_activate(application);
|
|
||||||
*exit_status = 0;
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GApplication::startup.
|
|
||||||
static void my_application_startup(GApplication* application) {
|
|
||||||
// MyApplication* self = MY_APPLICATION(object);
|
|
||||||
|
|
||||||
// Perform any actions required at application startup.
|
|
||||||
|
|
||||||
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GApplication::shutdown.
|
|
||||||
static void my_application_shutdown(GApplication* application) {
|
|
||||||
// MyApplication* self = MY_APPLICATION(object);
|
|
||||||
|
|
||||||
// Perform any actions required at application shutdown.
|
|
||||||
|
|
||||||
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implements GObject::dispose.
|
|
||||||
static void my_application_dispose(GObject* object) {
|
|
||||||
MyApplication* self = MY_APPLICATION(object);
|
|
||||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
|
||||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void my_application_class_init(MyApplicationClass* klass) {
|
|
||||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
|
||||||
G_APPLICATION_CLASS(klass)->local_command_line =
|
|
||||||
my_application_local_command_line;
|
|
||||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
|
||||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
|
||||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void my_application_init(MyApplication* self) {}
|
|
||||||
|
|
||||||
MyApplication* my_application_new() {
|
|
||||||
// Set the program name to the application ID, which helps various systems
|
|
||||||
// like GTK and desktop environments map this running application to its
|
|
||||||
// corresponding .desktop file. This ensures better integration by allowing
|
|
||||||
// the application to be recognized beyond its binary name.
|
|
||||||
g_set_prgname(APPLICATION_ID);
|
|
||||||
|
|
||||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
|
||||||
"application-id", APPLICATION_ID, "flags",
|
|
||||||
G_APPLICATION_NON_UNIQUE, nullptr));
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import Cocoa
|
|
||||||
import FlutterMacOS
|
|
||||||
|
|
||||||
@main
|
|
||||||
class AppDelegate: FlutterAppDelegate {
|
|
||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 520 B |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,347 +0,0 @@
|
|||||||
# Generated by pub
|
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
|
||||||
packages:
|
|
||||||
alchemist:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: alchemist
|
|
||||||
sha256: "4c5f5146b7539eb9a53211d81489f09f08fc3821f754a370f493477a9ef9e390"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.12.1"
|
|
||||||
async:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: async
|
|
||||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.13.1"
|
|
||||||
boolean_selector:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: boolean_selector
|
|
||||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.2"
|
|
||||||
characters:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: characters
|
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.1"
|
|
||||||
clide:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
path: ".."
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "2.0.0-dev"
|
|
||||||
clock:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: clock
|
|
||||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.2"
|
|
||||||
collection:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: collection
|
|
||||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.19.1"
|
|
||||||
convert:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: convert
|
|
||||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.2"
|
|
||||||
equatable:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: equatable
|
|
||||||
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.8"
|
|
||||||
fake_async:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: fake_async
|
|
||||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.3"
|
|
||||||
ffi:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: ffi
|
|
||||||
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.3"
|
|
||||||
file:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: file
|
|
||||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "7.0.1"
|
|
||||||
flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
flutter_driver:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
flutter_lints:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: flutter_lints
|
|
||||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.0.0"
|
|
||||||
flutter_test:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
fuchsia_remote_debug_protocol:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
integration_test:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
leak_tracker:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker
|
|
||||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "11.0.2"
|
|
||||||
leak_tracker_flutter_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_flutter_testing
|
|
||||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.10"
|
|
||||||
leak_tracker_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_testing
|
|
||||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.2"
|
|
||||||
lints:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: lints
|
|
||||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.1.0"
|
|
||||||
matcher:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: matcher
|
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.12.19"
|
|
||||||
material_color_utilities:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: material_color_utilities
|
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.13.0"
|
|
||||||
meta:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: meta
|
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.17.0"
|
|
||||||
mocktail:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: mocktail
|
|
||||||
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.4"
|
|
||||||
path:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: path
|
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.9.1"
|
|
||||||
platform:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: platform
|
|
||||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.6"
|
|
||||||
process:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: process
|
|
||||||
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "5.0.5"
|
|
||||||
quiver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: quiver
|
|
||||||
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.2.2"
|
|
||||||
sky_engine:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
source_span:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_span
|
|
||||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.10.2"
|
|
||||||
stack_trace:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stack_trace
|
|
||||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.12.1"
|
|
||||||
stream_channel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stream_channel
|
|
||||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.4"
|
|
||||||
string_scanner:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: string_scanner
|
|
||||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.1"
|
|
||||||
sync_http:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sync_http
|
|
||||||
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.1"
|
|
||||||
term_glyph:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: term_glyph
|
|
||||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.2"
|
|
||||||
test_api:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: test_api
|
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.10"
|
|
||||||
typed_data:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: typed_data
|
|
||||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
vector_math:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: vector_math
|
|
||||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.2.0"
|
|
||||||
vm_service:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: vm_service
|
|
||||||
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "15.1.0"
|
|
||||||
webdriver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: webdriver
|
|
||||||
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.0"
|
|
||||||
xterm:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: xterm
|
|
||||||
sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.0"
|
|
||||||
yaml:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: yaml
|
|
||||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.3"
|
|
||||||
zmodem:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: zmodem
|
|
||||||
sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.6"
|
|
||||||
sdks:
|
|
||||||
dart: ">=3.9.0-0 <4.0.0"
|
|
||||||
flutter: ">=3.32.0"
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
# clide_app — Flutter desktop UI.
|
|
||||||
#
|
|
||||||
# Renders clide's IDE surfaces; depends on the root `clide` Dart core
|
|
||||||
# package (shared with the CLI + daemon). Prefer-zero-deps; every
|
|
||||||
# third-party entry below is exact-pinned and justified. No carets.
|
|
||||||
|
|
||||||
name: clide_app
|
|
||||||
description: >-
|
|
||||||
Flutter desktop IDE for Claude Code — renders the UI. Talks to the
|
|
||||||
Dart core (CLI + daemon) via IPC; all PTY/subprocess/filesystem work
|
|
||||||
lives in the daemon (ADR 0005).
|
|
||||||
publish_to: none
|
|
||||||
version: 2.0.0-dev
|
|
||||||
repository: https://git.schweitz.net/jpmschweitzer/clide
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: ">=3.5.0 <4.0.0"
|
|
||||||
flutter: ">=3.19.0"
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
|
|
||||||
# Dart core — shared types, IPC protocol, anything CLI + daemon + app
|
|
||||||
# all import. Kept Flutter-free so `dart compile exe` produces a lean
|
|
||||||
# static binary (see ADR 0005).
|
|
||||||
clide:
|
|
||||||
path: ../
|
|
||||||
|
|
||||||
# YAML — the one justified exception to prefer-zero-deps. Dart-team
|
|
||||||
# maintained, used for theme files and extension manifests.
|
|
||||||
yaml: 3.1.3
|
|
||||||
|
|
||||||
# xterm.dart — Flutter-native terminal emulator (ANSI / xterm / truecolor
|
|
||||||
# parser + renderer). Writing a vt100/ANSI parser that handles everything
|
|
||||||
# tmux / neovim / claude emit would be weeks of work for no fidelity
|
|
||||||
# win. Listed in licenses.yaml per D-042. MIT.
|
|
||||||
xterm: 4.0.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_lints: 6.0.0
|
|
||||||
|
|
||||||
# Integration tests for the real-app startup-regression gate.
|
|
||||||
integration_test:
|
|
||||||
sdk: flutter
|
|
||||||
|
|
||||||
# Mocks at IO/IPC boundaries; hand-rolled fakes for ChangeNotifiers.
|
|
||||||
mocktail: 1.0.4
|
|
||||||
|
|
||||||
# Golden-test framework (Ahem font, cross-platform pixel stability).
|
|
||||||
# Used only for widget primitives, not compositions.
|
|
||||||
alchemist: 0.12.1
|
|
||||||
|
|
||||||
flutter:
|
|
||||||
uses-material-design: false
|
|
||||||
|
|
||||||
assets:
|
|
||||||
- lib/kernel/src/theme/themes/
|
|
||||||
- lib/kernel/src/i18n/catalog/
|
|
||||||
# Licensing — read by the About screen. `licenses.yaml` is the
|
|
||||||
# manifest (self + runtime deps + dev deps); each `license_file`
|
|
||||||
# pointer must also be a bundled asset so the About screen can
|
|
||||||
# display each dependency's full license text at runtime.
|
|
||||||
# Root LICENSE is mirrored into assets/LICENSE for the same reason
|
|
||||||
# (root paths can't be referenced from a Flutter package's assets).
|
|
||||||
# See CLAUDE.md "Dependencies & supply chain" and D-042.
|
|
||||||
- assets/licenses.yaml
|
|
||||||
- assets/LICENSE
|
|
||||||
- assets/fonts/jetbrains_mono/OFL.txt
|
|
||||||
- assets/fonts/josefin_sans/OFL.txt
|
|
||||||
|
|
||||||
# Bundled fonts. OFL-licensed; license files live alongside each.
|
|
||||||
#
|
|
||||||
# JosefinSans — UI default. Variable font covers weights 100-700
|
|
||||||
# plus an italic axis companion; we default to Light (300) per the
|
|
||||||
# project's aesthetic direction.
|
|
||||||
#
|
|
||||||
# JetBrainsMono — terminal / code surfaces. Static weights keep
|
|
||||||
# rendering predictable across xterm.dart, goldens, and CI.
|
|
||||||
fonts:
|
|
||||||
- family: JosefinSans
|
|
||||||
fonts:
|
|
||||||
- asset: assets/fonts/josefin_sans/JosefinSans-VariableFont.ttf
|
|
||||||
- asset: assets/fonts/josefin_sans/JosefinSans-Italic-VariableFont.ttf
|
|
||||||
style: italic
|
|
||||||
|
|
||||||
- family: JetBrainsMono
|
|
||||||
fonts:
|
|
||||||
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-Regular.ttf
|
|
||||||
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-Italic.ttf
|
|
||||||
style: italic
|
|
||||||
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-Bold.ttf
|
|
||||||
weight: 700
|
|
||||||
- asset: assets/fonts/jetbrains_mono/JetBrainsMono-BoldItalic.ttf
|
|
||||||
weight: 700
|
|
||||||
style: italic
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:clide_app/builtin/ipc_status/ipc_status.dart';
|
|
||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import '../../helpers/kernel_fixture.dart';
|
|
||||||
import '../../helpers/widget_harness.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('IpcStatusExtension', () {
|
|
||||||
late KernelFixture f;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
f = await KernelFixture.create(
|
|
||||||
i18nCatalogs: {
|
|
||||||
'builtin.ipc-status': {
|
|
||||||
const Locale('en', 'US'): const {
|
|
||||||
'connected': {'translation': 'connected'},
|
|
||||||
'connected.hint': {'translation': 'daemon reachable'},
|
|
||||||
'disconnected': {'translation': 'disconnected'},
|
|
||||||
'disconnected.hint': {'translation': 'daemon down'},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async => f.dispose());
|
|
||||||
|
|
||||||
test('contributes a statusbar item', () async {
|
|
||||||
f.services.extensions.register(IpcStatusExtension());
|
|
||||||
await f.services.extensions.activateAll();
|
|
||||||
final items = f.services.panels
|
|
||||||
.contributionsFor(Slots.statusbar)
|
|
||||||
.whereType<StatusItemContribution>()
|
|
||||||
.toList();
|
|
||||||
expect(items, hasLength(1));
|
|
||||||
expect(items.first.priority, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('renders "disconnected" label until connected', (tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
harness(f, IpcStatusItem(ipc: f.services.ipc)),
|
|
||||||
);
|
|
||||||
expect(find.text('disconnected'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('flips to "connected" when the client reports connected',
|
|
||||||
(tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
harness(f, IpcStatusItem(ipc: f.services.ipc)),
|
|
||||||
);
|
|
||||||
f.ipc.setConnected(true);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('connected'), findsOneWidget);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:clide_app/builtin/welcome/welcome.dart';
|
|
||||||
import 'package:clide_app/builtin/welcome/src/welcome_view.dart';
|
|
||||||
import 'package:clide_app/extension/extension.dart';
|
|
||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import '../../helpers/kernel_fixture.dart';
|
|
||||||
import '../../helpers/widget_harness.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('WelcomeExtension', () {
|
|
||||||
late KernelFixture f;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
f = await KernelFixture.create(
|
|
||||||
i18nCatalogs: {
|
|
||||||
'builtin.welcome': {
|
|
||||||
const Locale('en', 'US'): const {
|
|
||||||
'title': {'translation': 'clide'},
|
|
||||||
'subtitle': {
|
|
||||||
'translation': 'Flutter desktop IDE for Claude Code'
|
|
||||||
},
|
|
||||||
'open-project': {'translation': 'Open project'},
|
|
||||||
'open-project.hint': {'translation': 'Pick a git repository'},
|
|
||||||
'tab.title': {'translation': 'Welcome'},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async => f.dispose());
|
|
||||||
|
|
||||||
test('contributes a workspace tab with an i18n title key', () {
|
|
||||||
final ext = WelcomeExtension();
|
|
||||||
final tabs = ext.contributions.whereType<TabContribution>().toList();
|
|
||||||
expect(tabs, hasLength(1));
|
|
||||||
expect(tabs.first.slot, Slots.workspace);
|
|
||||||
expect(tabs.first.titleKey, 'tab.title');
|
|
||||||
expect(tabs.first.i18nNamespace, ext.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('WelcomeView renders title + subtitle + Open-project button',
|
|
||||||
(tester) async {
|
|
||||||
await tester.pumpWidget(harness(f, const WelcomeView()));
|
|
||||||
expect(find.text('clide'), findsOneWidget);
|
|
||||||
expect(
|
|
||||||
find.text('Flutter desktop IDE for Claude Code'),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(find.text('Open project'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Open-project button is a Semantics button w/ hint',
|
|
||||||
(tester) async {
|
|
||||||
await tester.pumpWidget(harness(f, const WelcomeView()));
|
|
||||||
final semantics =
|
|
||||||
tester.getSemantics(find.bySemanticsLabel('Open project'));
|
|
||||||
expect(semantics.label, 'Open project');
|
|
||||||
expect(semantics.hint, 'Pick a git repository');
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('locale switch refreshes the visible text', (tester) async {
|
|
||||||
await tester.pumpWidget(harness(f, const WelcomeView()));
|
|
||||||
f.services.i18n
|
|
||||||
.registerCatalog('builtin.welcome', const Locale('nl'), const {
|
|
||||||
'title': {'translation': 'clide-nl'},
|
|
||||||
'subtitle': {'translation': 'Flutter IDE voor Claude Code'},
|
|
||||||
'open-project': {'translation': 'Project openen'},
|
|
||||||
'open-project.hint': {'translation': 'Kies een git repo'},
|
|
||||||
'tab.title': {'translation': 'Welkom'},
|
|
||||||
});
|
|
||||||
await f.services.i18n.setLocale(const Locale('nl'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('Project openen'), findsOneWidget);
|
|
||||||
expect(find.text('clide-nl'), findsOneWidget);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 810 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
@@ -1,50 +0,0 @@
|
|||||||
import 'package:clide_app/kernel/kernel.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('LayoutArrangement', () {
|
|
||||||
test('applyPreset sets position + size + visibility per slot', () {
|
|
||||||
final a = LayoutArrangement();
|
|
||||||
a.applyPreset(classicPreset());
|
|
||||||
expect(a.positionOf(Slots.sidebar), SlotPosition.left);
|
|
||||||
expect(a.sizeOf(Slots.sidebar), 240);
|
|
||||||
expect(a.minSizeOf(Slots.sidebar), 180);
|
|
||||||
expect(a.maxSizeOf(Slots.sidebar), 400);
|
|
||||||
expect(a.isVisible(Slots.workspace), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setSize clamps to min/max', () {
|
|
||||||
final a = LayoutArrangement()..applyPreset(classicPreset());
|
|
||||||
a.setSize(Slots.sidebar, 50); // below min 180
|
|
||||||
expect(a.sizeOf(Slots.sidebar), 180);
|
|
||||||
a.setSize(Slots.sidebar, 10000); // above max 400
|
|
||||||
expect(a.sizeOf(Slots.sidebar), 400);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setSize notifies listeners only when changing', () {
|
|
||||||
final a = LayoutArrangement()..applyPreset(classicPreset());
|
|
||||||
var count = 0;
|
|
||||||
a.addListener(() => count++);
|
|
||||||
a.setSize(Slots.sidebar, 240); // already 240, no change
|
|
||||||
expect(count, 0);
|
|
||||||
a.setSize(Slots.sidebar, 260);
|
|
||||||
expect(count, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setVisible flips the flag', () {
|
|
||||||
final a = LayoutArrangement()..applyPreset(classicPreset());
|
|
||||||
expect(a.isVisible(Slots.sidebar), true);
|
|
||||||
a.setVisible(Slots.sidebar, false);
|
|
||||||
expect(a.isVisible(Slots.sidebar), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('registerSlotsInto populates a PanelRegistry', () {
|
|
||||||
final a = LayoutArrangement();
|
|
||||||
final r = PanelRegistry();
|
|
||||||
final preset = classicPreset();
|
|
||||||
a.registerSlotsInto(r, preset);
|
|
||||||
expect(r.definitionFor(Slots.sidebar), isNotNull);
|
|
||||||
expect(r.definitionFor(Slots.statusbar), isNotNull);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 917 B |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Phosphor Icons
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||