Commit Graph
362 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.8 a46677facf feat(welcome): initialize a non-repo folder as a project (T-489)
Closes the new-project story (T-486). The dead-end "not a git repo" dialog now
offers to initialize the folder: project.init runs git init + a non-clobbering
scaffold, then opens + announces on projectCreatedChannel so the account
roadblock fires — the same path a brand-new project takes. Adds initExistingProject
+ the `clide project init [--dir]` verb (default: the current workspace).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:42:02 +02:00
jpmschweitzerandClaude Opus 4.8 25430686cc feat(welcome): New project flow + per-repo account roadblock (T-488)
The UI half of the new-project flow (story T-486). A "New project…" welcome
action opens a dialog (location + name) that dispatches project.new (T-487),
opens the result, and announces it on projectCreatedChannel. The Claude
extension consumes that and shows the account roadblock — the embedded
per-workspace picker + accounts list, so a fresh project gets bound to an
account (or Default) right at birth.

The two halves stay decoupled: the welcome builtin only publishes the event
(no claude import); the claude builtin owns the account dialog. Only freshly-
created projects announce, so existing opens are never prompted.

Closes T-488. The non-repo "initialize as a project" path (T-489) is next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:35:30 +02:00
jpmschweitzerandClaude Opus 4.8 adaf8966dc feat(cli): clide project new — create + git-init a new project (T-487)
The backend half of the new-project flow (story T-486). createNewProject
validates the name, makes <parent>/<name>/, runs git init (injected from the
toolchain in main.dart so the handler stays Flutter-free), and writes a minimal
scaffold (.gitignore + a CLAUDE.md stub). The project.new verb wraps it; --dir
defaults to the current workspace's parent so a new project lands beside it.

Create-only by design — opening the new workspace and the account roadblock are
the UI flow's job (T-488). Closes T-487.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:23:18 +02:00
jpmschweitzerandClaude Opus 4.8 b9fc720f97 feat(git): GitClient.init for the new-project flow (T-487)
clide treats a git repo as the workspace, so creating a new project starts with
`git init`. Add GitClient.init — `git init -b <main>` in workDir, deterministic
default branch, idempotent on an existing repo. The first primitive of the
new-project flow (T-486); the create-dir + scaffold service and the
`clide project new` verb build on it next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:19:10 +02:00
jpmschweitzerandClaude Opus 4.8 6d7b96f115 chore(plan): re-scope T-486 — account picker at new-project init, not welcome
Per the design pivot: the account choice is only a real decision at project
birth, so it belongs as a roadblock in the new-project / init-in-a-new-dir flow
(evolving the dead-end _NotARepoDialog), not a welcome-screen list. Opening an
existing project never prompts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:06:42 +02:00
jpmschweitzerandClaude Opus 4.8 996b10d3ec feat(claude): account login pane — host claude login in a modal terminal (T-485)
Completes the `login` verb + the UI add/re-login affordances. The
accountActionChannel 'login' action opens ClaudeLoginDialog — a modal hosting a
TerminalPane that runs `CLAUDE_CONFIG_DIR=<dir> claude login`, so the CLI drives
the OAuth browser flow and credentials land in that account's config dir (D-64:
one CLI-initiated browser flow, on explicit action).

TerminalPane gains optional argv/env/cwdOverride (default stays the login
shell), and its pane.spawn carries env when set. The login pane reuses the
TerminalPane *widget* — a code import, not a runtime dependency, since it spawns
through the always-present pane.spawn IPC rather than the terminal extension.

Tests cover the parameterized spawn args and the dialog's host wiring (title,
CLAUDE_CONFIG_DIR, close). Closes T-485.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 00:52:41 +02:00
jpmschweitzerandClaude Opus 4.8 222d4ae5e3 feat(claude): per-repo account badge in the Claude pane chrome (T-481)
The always-visible affordance for the multi-account epic: a compact badge in
the pane header showing which account this workspace is bound to (or "default"),
colour-tinted per account so two windows are distinguishable at a glance. Tapping
opens a picker of the registered accounts + Default; selecting binds/unbinds via
the shared bindWorkspaceAccount helper (respawn + lock-sync follow on the bus).
Hidden when no accounts are registered, so it adds no chrome for users not using
the feature.

accountAccent derives the tint by hashing the name into a fixed set of theme
tokens — never an arbitrary colour, so the palette stays theme-owned.

The welcome-view accounts section (the other half of T-481) is split to T-486:
the welcome builtin is intentionally decoupled from feature builtins, so it needs
a welcome-section contribution point rather than importing claude directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 00:43:40 +02:00
jpmschweitzerandClaude Opus 4.8 5c31190c4f feat(claude): Settings → Claude accounts registry list (T-482 part 2)
Completes the account settings surface with the global Accounts list under the
Claude category: each registered account shows a sign-in dot, name, and config
dir, with re-login and remove affordances plus an inline add-account field.

- Sign-in status: accountIsSignedIn, a read-only probe (a .credentials.json, or
  an oauthAccount marker in .claude.json; under-reports on macOS keychain).
- Add: registers ~/.claude-<name> and publishes a login action (T-485 opens the
  pane).
- Remove: registry-remove, guarded while a workspace is bound — matching the CLI
  `account remove`. --purge dir deletion stays on the CLI flag.

All management routes through the AccountRegistry + accountActionChannel, and
the list rebuilds live off the settings notifier. en/nl strings + widget/probe
tests added. Closes T-482.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 00:35:47 +02:00
jpmschweitzerandClaude Opus 4.8 c3efcf3855 feat(claude): Settings → Claude per-workspace account picker (T-482 part 1)
A custom settings control under the Claude category: a dropdown of the
registered accounts plus Default. Picking one binds (or unbinds) this
workspace via the AccountRegistry and publishes set/unset on
accountActionChannel — the same channel the CLI verbs use, so the session
respawns onto the account (T-480) and the IDE lock re-syncs (T-479). It reads
live off the shared settings notifier, so a CLI `account set` updates the
dropdown too. Empty states cover no-workspace and no-accounts, each pointing at
the CLI.

Registry writes set the in-memory binding synchronously then flush, so the
control publishes the bus event before the disk write completes.

Part 2 (the global Accounts registry CRUD list + sign-in probe) is still open
on T-482. en/nl strings added; widget test covers the states + bind/unbind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:46:43 +02:00
jpmschweitzerandClaude Opus 4.8 e86543d476 fix(ipc): write the /ide discovery lock into bound-account config dirs (T-479)
A `claude` started with CLAUDE_CONFIG_DIR=<dir> looks for clide's /ide
discovery lock under <dir>/ide, not ~/.claude/ide — so today a per-repo
account session (T-476) can't reach clide's IDE bridge.

McpServer now reconciles a SET of lock paths instead of one: the default
~/.claude/ide always, plus the bound account's <dir>/ide when this workspace
is bound (boundConfigDir, injected from main.dart via the AccountRegistry).
syncDiscoveryLocks() writes/removes locks to match the active set; it runs on
start, once post-boot when the registry becomes resolvable, and on every
accountActionChannel event, so binding/unbinding adds/removes the account lock
at runtime. stop() sweeps every path written — no orphans. Lock content +
0600 are identical across dirs.

Acceptance 1-4 and 6 are covered by mcp_server_test (multi-dir write, no-binding
regression, dynamic add/remove, full cleanup); #5 (a live claude resolving the
bridge end-to-end) is manual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:25:19 +02:00
jpmschweitzerandClaude Opus 4.8 34b9690005 feat(claude): respawn panes on account set/unset + safe --purge (T-480 part 2)
The extension consumer for the per-repo account verbs, making set/unset
fully functional. The Claude extension subscribes to accountActionChannel:

- set / unset → ClaudeSessionOrchestrator.respawnForWorkspace(cwd): closes the
  workspace's solo sessions (awaiting real process death, T-437) and re-spawns
  each on the same id with --resume, so the conversation continues under the
  newly-bound CLAUDE_CONFIG_DIR (resolved at spawn by agentBootstrap). Team and
  forked sessions are skipped — re-joining the broker / re-forking on an account
  swap is out of scope; they adopt the account on their next natural spawn.
- remove --purge → deletes the config dir behind isPurgeableAccountDir, a strict
  guard that only ever removes a ~/.claude-* directory that is a direct child of
  $HOME. The purge payload now carries the dir (the account is gone from the
  registry by publish time).

login still only publishes its action — spawning the `claude login` terminal
pane needs argv+env terminal-pane support and is split to T-485.

Covered: respawnForWorkspace (respawn solo, skip fork/other-repo) and the purge
guard's accept/reject matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:18:49 +02:00
jpmschweitzerandClaude Opus 4.8 744b6cff43 feat(cli): clide claude account verbs — add/list/login/set/unset/remove (T-480 part 1)
The CLI half of the multi-account feature (epic T-476; D-6 parity). A new
`claude.account` dispatcher command multiplexes the six sub-verbs over an
injected, Flutter-free AccountStore port (runs under `dart test`):

- add <name> [--dir]   register (default ~/.claude-<name>); idempotent, clear
                       conflict error
- list                 {accounts, boundAccount (this workspace), detected}
- set <name>           bind this workspace (persists)
- unset                clear this workspace's binding
- remove <name> [--purge]  registry-remove; refuses while any workspace is
                       bound
- login <name>         (publishes the login action)

Registry reads/writes go through the user-scope SettingsStore; side-effects
that only the UI layer can do — respawn on set/unset, the `claude login`
terminal pane, and the --purge rm — are published on accountActionChannel for
the Claude extension to consume (that consumer is T-480 part 2). main.dart
adapts the real AccountRegistry to the port and registers the command alongside
image.show / status.

Adds SettingsStore.keysAt (binding enumeration) and AccountRegistry.boundName /
boundAccountNames. No changelog yet — set/unset don't auto-respawn until part 2,
so the feature isn't user-complete. Verb behaviour + payloads + the in-use
guard are unit-tested against a fake store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 17:30:01 +02:00
jpmschweitzerandClaude Opus 4.8 1fa7dee863 feat(claude): spawn hosted sessions under the bound account's CLAUDE_CONFIG_DIR (T-484)
The load-bearing piece of the multi-account epic (T-476): a bound workspace's
hosted claude now spawns with CLAUDE_CONFIG_DIR set to that account's dir, so
it runs under the bound account end-to-end. Every hosted session (primary /
secondary / fork / teammate) inherits it — the orchestrator already routes all
spawns through agentBootstrap.

- New pure resolver claudeConfigDirForWorkspace(cwd, boundConfigDir, env):
  bound account dir > parent CLAUDE_CONFIG_DIR (respect the launcher) > null
  (Claude defaults to ~/.claude). The registry is injected as a plain lookup
  so agent_bootstrap stays Flutter-free (its tests run under `dart test`).
- agentBootstrap merges CLAUDE_CONFIG_DIR BEFORE base, so an explicit
  SpawnSpec.env override still wins (override > binding > parent > unset); the
  key is omitted entirely when the resolver returns null.
- Orchestrator carries an optional AccountRegistry; the claude extension builds
  it from ctx.settings. Null in tests → no injection (unchanged behaviour).

No way to SET a binding yet (that's the CLI T-480 / settings UI T-482), so no
changelog entry — the mechanism is in place, the surface lands next. Unit tests
cover the resolver's four states and the envDelta precedence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 17:00:06 +02:00
jpmschweitzerandClaude Opus 4.8 a8d321f344 feat(claude): per-repo account registry + storage foundation (T-483)
Foundation for the multi-account epic (T-476): a Flutter-free AccountRegistry
over the app-scope SettingsStore (per-user, never committed to a repo).

- `app.claude.accounts` — the {name, dir} account list (dir = CLAUDE_CONFIG_DIR).
- `app.claude.account.<hash>` — per-workspace binding to an account name; the
  hash is the SAME FNV-1a 64-bit hex D-70 uses for the socket path, trailing
  separators stripped so /repo and /repo/ map alike.
- accountForWorkspace(cwd) resolves an account or null (a binding to a removed
  account degrades to null = Claude's default, never errors).
- probeExistingAccountDirs(home): read-only bootstrap probe for adoptable
  ~/.claude-* config dirs (welcome-view UX is T-481).

Keys are app.-prefixed because SettingsStore requires a scope prefix; app scope
already provides the per-user persistence T-483 wants (T-356 will consolidate
more state later but isn't a hard blocker). No spawn/UI/CLI — downstream
tickets. Unit tests cover CRUD, the disk round-trip, hash/trailing-slash
mapping, independent bindings, and probe filtering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 16:42:39 +02:00
jpmschweitzerandClaude Opus 4.8 679b5ba9d9 feat(cli): clide instances / instance verbs for instance discovery (T-247)
Closes the observability half of T-247: a way to find and identify running
clides. `clide instances` scans the runtime socket dir, probes each live
*.sock, and prints its identity (version/pid/workspace/socketPath) as jsonl;
dead sockets are skipped. `clide instance` reports the one you're connected
to. Combined with CLIDE_SOCK honoring (this same ticket), you can now list
instances and pin the CLI to a chosen one.

Server: a new `instance` dispatcher command (registered in buildDispatcher
with the live workspace/pid/socket) returns the identity map; added to the
argv translator's umbrella set so a bare `instance` token routes to it.
Client: a POSIX dir-scan in clide.c (Windows stub until it ships).

Tests: e2e `instances` lists the live test server with its identity; a
cc-free unit test covers the `instance` command shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:51:13 +02:00
jpmschweitzerandClaude Opus 4.8 6af980f31c fix(cli): honor CLIDE_SOCK as an explicit target, fail loudly if dead (T-247)
The `clide` shell client only ever computed the socket path from the
workspace's FNV-1a hash — it never read CLIDE_SOCK, so the var the app
exports to spawned agents was a no-op and a bogus CLIDE_SOCK was silently
ignored (it still hit the discovered instance). Now: when CLIDE_SOCK is set
the client connects to it directly, beating discovery, and a dead socket
aborts with EX_UNAVAILABLE rather than falling back to a different instance
(the split-brain footgun). Unset → the deterministic per-workspace path
(D-70) as before. The server keeps deterministic binding; this is a
client-side targeting override only.

Also fixes a pre-existing -Wstringop-truncation warning in
find_workspace_root (strncpy+manual-null → snprintf).

Tests: the e2e suite now clears the inherited CLIDE_SOCK so discovery tests
stay hermetic (the suite may run inside a clide instance), plus two new
tests — a valid CLIDE_SOCK pins from a non-git dir, a dead one fails loudly
and returns no data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 08:11:55 +02:00
jpmschweitzerandClaude Opus 4.8 3cbe0e6ec6 refactor(claude): drop the redundant Team-tab account budget (T-158)
After live review: the shared account /usage budget read as redundant on
both the Activity and Team tabs. Usage is per-account and can't be split per
member, so one place is enough — it lives only on the Activity tab, next to
the refresh control that fetches it (T-415). Removes the Team-tab account
card, its usage prop wiring, the team.section.usage/team.usage.shared i18n
keys, and the now-obsolete team_tab test. The three-tab card facelift stays.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:56:09 +02:00
jpmschweitzerandClaude Opus 4.8 026156261c Merge origin/main: T-476 per-repo-account breakdown
Union-resolved the .pql/changelog logs (append-only, idempotent INSERT … ON
CONFLICT DO UPDATE — latest-updated wins on replay), keeping both sides:
this session's T-158 changes and origin's T-476 6-child breakdown. pql.db
rebuilt clean (2785 statements).

NOTE — label collision to reconcile (record_ids intact, labels only):
the breakdown reused T-477 and T-478, already held by the window-size bug
and the Summer Night theme. Reconcile with `pql ticket relabel`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:36:03 +02:00
jpmschweitzerandClaude Opus 4.8 763d628fc9 feat(claude): shared account budget in Team tab + meta-sidebar card facelift (T-158)
Implements T-158 option A: the forwarded /usage budget (already parsed for
the Activity tab, T-415) now also renders in the Team tab as a single
ACCOUNT card, captioned "shared across the team". Usage is per-account —
every team session shares one ~/.claude login — so it's shown once, not
split per member (which would just repeat identical numbers).

Facelift: the Activity, Team, and Config tabs now render their sections as
elevated cards (panelHeader fill, dividerColor hairline, 6px radius) under
small-caps mono headers, matching the settings overlay's card design
(settings_category_view). Shared helpers — metaSectionHeader / metaCard /
metaCardRow — live in meta_sidebar/models.dart; the SESSION control strip
and the pinned Config SETTINGS block adopt the same card. The shared
ClideAccordion (also used by decisions/tickets) is left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:31:50 +02:00
Jeroen SchweitzerandClaude Opus 4.7 3f627c5b17 chore(plan): break T-476 down into 6 children under an epic
Convert T-476 from story to epic and file the implementation slices:
T-477 storage/registry/migration, T-478 spawn-time CLAUDE_CONFIG_DIR
injection, T-479 multi-dir /ide lock writer, T-480 clide CLI verbs
(D-6 parity), T-481 pane badge + welcome accounts UI, T-482 settings
rows. Blocker graph wired (T-356 -> T-477 -> fan-out -> T-480 -> UI).
Epic body refined with the proven mechanism, surveyed alternatives,
concrete code touchpoints, and acceptance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 09:50:57 +02:00
jpmschweitzerandClaude Opus 4.8 c4be179a87 chore(plan): record T-158 decision — option A (shared-account team-tab budget)
Locks scope to the single account-wide budget section in the team tab; declines
the per-member split (not meaningful under one shared account) and the
close-as-done option. Not started.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:01:21 +02:00
jpmschweitzerandClaude Opus 4.8 367792490d chore(plan): refine T-158 (team-sidebar usage budget) implementation plan
Grounded against the current tree: T-415 already ships the account budget
(parseUsageText/ClaudeUsage + Activity tab). Key finding — usage is
per-ACCOUNT (one shared ~/.claude login across all sessions), so a literal
per-member split would repeat identical numbers and isn't meaningful even
under T-476 (per-repo, not per-session, login). Refinement recommends a
single shared-account section in the team tab (resolves Q-34), lists the
exact files to touch, alternatives, and acceptance. No code yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:59:50 +02:00
jpmschweitzerandClaude Opus 4.8 259562f8cc docs(git-commit): document the release tagging step (T-393)
The "Cutting a release" ritual moved the changelog and bumped pubspec but
never mentioned tagging — which is how tags silently lapsed from v2.2.0
through v2.8.0. Add step 6: run `make release` (verify + gate + annotated
tag), then push with --follow-tags, and state the rule that every released
version must have a matching git tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:27:13 +02:00
jpmschweitzerandClaude Opus 4.8 b7db22924d feat(theme): canonical theme/namespace lists + ship Summer Night (T-371, T-478)
The bundled-theme list and the Tier-0 i18n namespace list were each
hand-maintained in 3+ places that had drifted: the testmode harness loaded
8 themes (catppuccin silently unvalidated) while the app and contrast gate
loaded 10, and the i18n gate checked 4 of the shipped namespaces. Export
one canonical const each — kBundledThemePaths and kTier0Namespaces — and
have the app, the testmode harness, and the a11y gates iterate them.

Drift-proof meta-assertions:
- contrast gate fails if any theme YAML on disk is absent from
  kBundledThemePaths (so a theme can't sit unvalidated).
- i18n gate derives its subjects from assets/i18n/en_us/ at test time and
  asserts en/nl key parity for every shipped catalog (26, was 4), plus that
  every kTier0Namespaces entry has a shipped catalog.

Surfaced summer-night.yaml: a legacy-ported palette on disk, unbundled and
never contrast-checked. Per the maintainer's call, ship it: flesh the
palette to full token + syntax coverage (honouring the v1.2 colours, clide
derivation pattern for the new keys) and add a structurally identical
summer-night-hc sibling that clears the strict extended gate. Both pass.

No licenses.yaml change: summer-night is clide's own (ported from legacy
clide v1.2 under legacy/, MIT); the only third-party palette, Catppuccin,
is already acknowledged; no fonts added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:31:16 +02:00
jpmschweitzerandClaude Opus 4.8 1e49f3e1d9 fix(editor): collapse the workspace split when the last buffer closes (T-459)
EditorRegistry.close() guarded its active-changed emit on `_activeId !=
null`, so closing the LAST buffer (active clears to null) emitted only
editor.closed — never the active-changed(id:null) the editor extension
listens for to call closeEditor(). editorOpen stayed true and the top
split sat orphaned over the Claude pane. Always emit active-changed when
the active buffer is removed, including the cleared-to-null case; the
slot renderer already collapses correctly once editorOpen flips false.

The existing extension test fabricated the null active-changed event, so
it passed despite the registry never emitting it — that gap is why the
bug shipped. Add a registry test that drives the real close() path, plus
a slot_host widget test asserting the split (drag handle) drops out and
the primary pane fills the column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:49:12 +02:00
jpmschweitzerandClaude Opus 4.8 8c9c881d81 fix(welcome): open Linux window at 1600x900 to clear footer overlap (T-477)
At the 720p default the vertically-centred welcome content slid under the
floating version/theme status line (Positioned bottom:24), overlapping the
tips card. The taller default gives the centred block clear bottom margin
and matches the macOS default size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:14:05 +02:00
jpmschweitzerandClaude Opus 4.8 1bdfd1be1c refactor(i18n): bundle catalogs under assets/i18n/<locale>/ (T-462)
Move the catalogs out of lib/kernel/src/i18n/catalog/ into assets/i18n/, and
switch the layout from a `{namespace}_{locale}.json` filename to a per-locale
DIRECTORY: assets/i18n/en_us/{namespace}.json. Adding a language is now just a
new folder (assets/i18n/nl_nl/, nl_be, en_eu, …) of the same namespace files —
no renames — which is cleaner to maintain and mirrors how an external
extension ships its own catalog. AssetCatalogLoader/FileCatalogLoader resolve
`<root>/<locale>/<namespace>.json`; pubspec bundles the locale folder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:54:14 +02:00
jpmschweitzerandClaude Opus 4.8 198c8b18b5 feat(i18n): i18n key fields on command + settings contributions (T-462)
Lets manifest labels (command-palette/menu titles, settings labels) localize,
not just displayed widget strings. Adds optional titleKey/i18nNamespace to
CommandContribution and labelKey/helpKey/titleKey + a category i18nNamespace to
the settings schema. The command palette and menu bar now resolve titles via a
shared localizedCommandTitle helper — and the palette's fuzzy search matches
the localized title too (PaletteController.titleResolver). No behaviour change
until the per-extension keys + catalog entries land (placeholder == English).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 09:44:35 +02:00
jpmschweitzerandClaude Opus 4.8 33a8a74240 refactor(i18n): route framework + shared chrome through a 'core' catalog (T-469)
Framework strings outside any extension — widget primitives (collapser, toast,
lightbox, multitab, ex-line, spine, pane chrome), the shared reader chrome, the
markdown 'Open in editor' tooltip, and the drag-resize handle a11y labels — now
resolve under a new 'core' namespace (preloaded at boot). Settles the T-469
namespace question: framework chrome gets one 'core' catalog.

Makes ClideSettings.i18n.string/.interpolated null-safe (ClideKernel.maybeOf):
primitives render kernel-less in isolated tests, returning the placeholder —
matching the D-101 fallback contract for fonts. The markdown tooltip threads
via the ClideMarkdownHooks carrier like mono/ui; drag_resize reads the kernel
i18n directly to avoid a kernel→widgets layering inversion. No en_US change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:26:02 +02:00
jpmschweitzerandClaude Opus 4.8 c5500dc235 refactor(i18n): route remaining extension labels through the catalog (T-468)
Migrate the user-facing strings in menubar, output, search, deeplink, editor,
files, terminal, markdown, and vim to ClideSettings.i18n.string/.interpolated;
extend the existing catalogs and create the missing ones (deeplink, output,
search, markdown, menubar, vim). vim mode labels now resolve through
builtin.vim (default still `-- NORMAL --`, now localizable). A dock-status test
seeds builtin.output so the widget's own i18n lookups resolve instead of
warning into the logRing it counts. No en_US behaviour change (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:16:20 +02:00
jpmschweitzerandClaude Opus 4.8 17d22b9571 refactor(i18n): route planning-pane labels through the catalog (T-467)
Migrate the user-facing strings in builtin.tickets, builtin.pql,
builtin.decisions, and builtin.problems (list/detail views, section headers,
filters, empty/loading/error states, type chips) to ClideSettings.i18n; extend
each extension's catalog. The detail/links tabs that had a bare title now wire
titleKey + i18nNamespace so the tab title resolves too. Context threaded into
the static label helpers. No en_US behaviour change (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:14:01 +02:00
jpmschweitzerandClaude Opus 4.8 f33cd603be refactor(i18n): route git + diff panel labels through the catalog (T-466)
Migrate the user-facing strings in builtin.git (panel, status groups, commit
bar, branch picker, discard dialog, row a11y) and builtin.diff (toolbar,
empty states, file meta) to ClideSettings.i18n.string/.interpolated; extend
both catalogs. _stateLabel threaded a BuildContext from its caller. No en_US
behaviour change (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:13:35 +02:00
jpmschweitzerandClaude Opus 4.8 16fe71cb3f refactor(i18n): route claude pane labels through the catalog (T-464)
Migrate the hardcoded user-facing strings across builtin.claude — composer,
conversation cards/segments, permission + AskUserQuestion prompts, task dock,
meta-sidebar (activity/config/team/roster), session/model pickers — to
ClideSettings.i18n.string/.interpolated, English kept as the placeholder;
builtin.claude_en_us.json extended to cover them. Context threaded into the
context-free render helpers (toolInputBody chain, _ConversationTurn) the same
way the mono family already is. No en_US behaviour change (D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:19:19 +02:00
jpmschweitzerandClaude Opus 4.8 93aa9bc332 fix(i18n): re-export I18nReplacer with the ClideSettings facade (T-462)
ClideSettings.i18n.interpolated takes I18nReplacer, but the type didn't travel
with the facade — every templated-lookup call site had to separately import it
from the kernel. Re-export it from clide_settings so it ships via the widgets
barrel alongside ClideSettings; the interpolated API is now self-contained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:14:20 +02:00
jpmschweitzerandClaude Opus 4.8 e1d57e7964 refactor(i18n): route welcome labels through the catalog (T-465)
Migrate the ~25 hardcoded user-facing strings in builtin.welcome (section
headers, tips, action + status lines, both dialogs) to
ClideSettings.i18n.string(...), keeping the English as the placeholder; extend
builtin.welcome_en_us.json to cover them. The _tips const widened to carry
(key, English, glyph) so the labels resolve at render. No en_US behaviour
change — strings now resolve through the catalog so they can be localized
(D-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:52:05 +02:00
jpmschweitzerandClaude Opus 4.8 3e83373258 fix(i18n): close theme-picker + diff catalog gaps (T-463)
theme-picker referenced section.appearance + toggle.highContrast with no
catalog entries (silently fell to the placeholder); add both. builtin.diff
contributes a localized tab but shipped no catalog file at all; create it with
tab.title. Both namespaces now resolve from the catalog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:56:25 +02:00
jpmschweitzerandClaude Opus 4.8 6d2361cd61 feat(i18n): load every extension's own namespace + facade lookup (T-462)
Foundation for routing hardcoded UI labels through the catalog (D-21). Two
enablers:

- ExtensionManager now eagerly loads each activated extension's own-id catalog
  namespace, not just the namespaces of localized tabs. An extension's id IS
  its catalog namespace (ClideExtension.t), so labels resolve through the
  catalog even for extensions that contribute no tab; a missing catalog file
  loads as an empty map, so it's harmless.
- ClideSettings.i18n gains string()/interpolated() lookups so widget call
  sites route through the one D-101 facade (like theme/fonts) instead of each
  re-deriving ClideKernel.of(context).i18n.

No user-visible change yet — placeholders equal the en_US catalog values; the
per-extension label migrations follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:54:56 +02:00
jpmschweitzerandClaude Opus 4.8 90546cb446 fix(fonts): markdown prose + links honour the live UI font (T-475)
The markdown span builders are context-free statics, so they pinned the
bundled clideUiFamily const and ignored the Appearance UI-font setting —
Claude's conversation prose stayed on the default face regardless of the
pick. Thread the resolved UI family through the same ClideMarkdownHooks
carrier already used for mono (T-472): build() resolves it from context and
every prose/link span reads hooks.ui. Adds a regression test asserting prose
and inline code follow the families from the ClideSettingsScope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:35:51 +02:00
jpmschweitzerandClaude Opus 4.8 18cd945380 refactor(theme): route consumer reads through ClideSettings.theme (T-473)
Migrate ClideTheme.of(context) reads across the widget, feature, and shell
layers to the unified ClideSettings.theme.of(context) facade (D-101), so
theme/i18n/fonts/settings share one widget-facing entry. The facade delegates
straight to ClideTheme, so behaviour is unchanged — goldens are unmoved.

The low-level theme provider keeps its direct ClideTheme.of: the facade is
built on it, and the two kernel sites (ClideTheme's own definition + the
panels drag-resize widget) stay direct to avoid a widgets→kernel import cycle.
Dead controller.dart/kernel.dart imports left by the sweep removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:47:15 +02:00
jpmschweitzerandClaude Opus 4.8 1c79c9c4dc refactor(i18n): route consumer reads through ClideSettings.i18n (T-474)
Migrate widget i18n reads from ClideKernel.of(context).i18n to the unified
ClideSettings.i18n.of(context) facade (D-101), so settings/theme/fonts/i18n
share one widget-facing entry point. The facade delegates to the same I18n
service, so behaviour is unchanged. Infra plumbing (KernelServices /
ExtensionManager fields, the slot-host Listenable.merge) keeps its direct
kernel reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:37:51 +02:00
jpmschweitzerandClaude Opus 4.8 c4248e3228 fix(fonts): honour the monospace setting in context-free render helpers (T-472)
Eleven leaf sites rendered monospace text from top-level/static helpers that
take no BuildContext, so they hard-coded clideMonoFamily and ignored the
Settings → Appearance Monospace choice that the D-101 facade made live
everywhere else.

Thread the resolved family in from the nearest context-bearing caller:
- claude tool bodies/results: a required `mono` field on _ConversationTurn +
  a `mono` arg on the shared toolInputBody chain;
- markdown inline `code`/record/file-ref spans: carried on ClideMarkdownHooks,
  which build() already constructs from context and threads to every static;
- search preview styles and welcome tips: a `mono` parameter on the helpers.

No behaviour change when the setting is default; these surfaces now switch
live with the rest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:35:38 +02:00
jpmschweitzerandClaude Opus 4.8 6b5317342b chore(plan): file T-472/T-473/T-474 (ClideSettings migration follow-ups)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:00:28 +02:00
jpmschweitzerandClaude Opus 4.8 b8e6629df5 feat(settings): monospace font picker + bundle Fira Mono (T-471)
Settings → Appearance gains a Monospace font select (JetBrains Mono / Fira
Mono) on the schema engine, writing app.mono.font. It applies live: root_shell
resolves the family into the ClideSettingsScope (D-101) and rebuilds on the
settings change, so the ~93 migrated mono sites — terminal, diffs, code, IDs —
re-read the new family at once.

Vendor Fira Mono (Regular + Bold, OFL) under assets/fonts/fira_mono/ with the
pubspec font family + licenses.yaml entry per D-42; JetBrains Mono stays the
default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:59:47 +02:00
jpmschweitzerandClaude Opus 4.8 a7ea38acde refactor(settings): ClideSettings — one live-preferences facade (D-101)
Introduce ClideSettings, a single widget-facing facade for the app's live
user preferences, namespaced by concern: ClideSettings.fonts.monoOf(context)
/ .fonts.uiOf(context), ClideSettings.theme.of(context),
ClideSettings.i18n.of(context). "Plumb once, use many."

Fonts are carried by a new root-provided ClideSettingsScope (resolved from the
font settings in root_shell, rebuilt on change); theme and i18n delegate to
their existing live providers (ClideTheme / the I18n service) so there's one
source of truth and their many consumers migrate incrementally rather than in
a big-bang. Reads outside a scope fall back to the bundled font defaults, so a
widget renders without a provider (isolated tests).

Migrate ~93 monospace-font call sites across 33 files from the clideMonoFamily
const to ClideSettings.fonts.monoOf(context) — pure refactor, identical family
when no override is set. 11 context-less helper sites (markdown static spans,
a few top-level/static builders) keep the const for now and are tracked in
T-472. Records D-101; updates the ui-design skill's font-family rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:57:16 +02:00
jpmschweitzerandClaude Opus 4.8 21d435d1f1 feat(settings): Inter as default UI font + UI font picker (T-460)
Vendor Inter (variable + italic, OFL) under assets/fonts/inter/ and make it
the default application UI face, replacing Josefin Sans (which stays bundled
as a selectable option). pubspec font family + licenses.yaml entry per D-42.

Settings → Appearance gains a UI-font select (Inter / Josefin Sans). The root
DefaultTextStyle reads app.ui.font (kUiFontSettingKey) over the default and
re-applies live on settings change, so a pick takes effect immediately. Bump
the default UI weight to w400 — Inter reads better at Regular than Josefin's
Light.

The monospace picker is deferred to T-471: clideMonoFamily is a const at ~38
call sites (not inherited), so a live mono picker needs the family routed
through context first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:15:01 +02:00
jpmschweitzerandClaude Opus 4.8 ea670fd077 feat(settings): Claude category — new-session defaults (T-457)
A Settings → Claude category sets per-user defaults for NEW sessions: model,
effort, and permission mode (generic schema selects persisting
app.claude.default*). The pane reads them at spawn — effort flows through the
existing --effort flag (SpawnSpec.effort); model and permission mode are sent
as control requests right after a fresh (non-resume, non-fork) session starts.
'default'/unset values are no-ops, leaving the CLI's own defaults.

The optional "settings changed — apply to current sessions?" prompt is filed
as T-470: it needs a custom control (a generic select can't prompt), so it's
a clean follow-up on the T-452 escape hatch.

Tests: defaultEffortFlag sentinel handling; the category contributes the three
default fields. The thin pane-side applySessionDefaults is covered by the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:58:11 +02:00
jpmschweitzerandClaude Opus 4.8 d9eadf295a chore(plan): sync settings-epic ticket statuses
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:11:15 +02:00
jpmschweitzerandClaude Opus 4.8 79c5294d44 feat(settings): Extensions tab — "watch this space" notice (T-456)
Built-in extensions are always on and there's no third-party install path yet,
so the Extensions tab is a notice rather than a toggle list — a toggle list
would let users brick the app by disabling load-bearing builtins. It explains
that installing/enabling/disabling arrives with third-party (Lua) extensions
and points at the records that pick it up (D-16 / T-8).

Fleshes out the builtin.extensions-ui stub via the custom-control hatch. Adds
two renderer guards — empty section/field labels render no chrome — so a
notice-only category shows just its card.

Tests: contributes the Extensions category + notice control; the notice renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:45:01 +02:00
jpmschweitzerandClaude Opus 4.8 757d6f71fa feat(settings): Appearance category + custom-control escape hatch (T-452)
Add the one bespoke control the schema engine defers to. New
SettingsControlContribution routes a WidgetBuilder into a kernel
SettingsControlRegistry under a customId; a SettingsFieldKind.custom field
names that id, and the renderer draws the registered widget full-width
(label on top, no scope tag — the control owns its own apply + scope).

The theme-picker extension uses it: an Appearance category whose theme field
is custom, backed by AppearanceThemeControl — base-theme chips + a
high-contrast toggle that apply live through ThemeController (persisted by
theme_persistence). Reuses the shared theme_families helpers.

Tests: control registry (register/dup/unregister), the renderer's custom-field
path, and the Appearance contribution + live theme apply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:04:52 +02:00
jpmschweitzerandClaude Opus 4.8 727fe8fdd4 feat(settings): Keymap category — preset select (T-451)
The keybindings-ui extension contributes a Keymap SettingsCategory: a preset
select (Default / Vim / VS Code / JetBrains) reading the active preset from
kKeymapPresetSetting. Picking one routes through a new schema affordance —
SettingsField.applyCommandPrefix — running `keymap.preset.<value>`, which calls
KeymapService.setPreset to persist and reload the layered keymap live. The
prefix path keeps the engine generic: settings a subsystem applies via a
command (rather than a bare key write) declare the prefix; the scope tag and
current-value read still use the key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:51:14 +02:00