Commit Graph
144 Commits
Author SHA1 Message Date
jpmschweitzerandClaude 3196c4957f test sweep: cover core/(root) — cell, cursor, charset, tabs, reflow (T-91)
Adds test/terminal/core/core_test.dart — 36 unit tests across the
small standalone files that sit directly under
`lib/src/terminal/src/core/*.dart`:

- CellData (constructor + empty + getHash + toString),
- CursorStyle (default ctor, every set/unset attr getter pair, all
  three colour-mode setters per channel, reset, the .empty
  singleton) + CursorPosition,
- Charset (translate, designate/use, save/restore, asciiTranslator,
  decSpecGraphicsTranslator with in-table, out-of-table, and high-
  codepoint paths),
- TabStops (default 8-column grid, find with empty-range / out-of-
  bounds / no-stop-in-range cases, setAt/clearAt/clearAll/reset),
- reflow (empty input, single-line passthrough, grow, shrink-with-
  split, wrapped-run continuation, wide-char boundary on the new
  width, inner wide-char clamp during _addPart, anchor reparent on
  the main path, anchor reparent past trimmedLength).

Two source-side cleanups folded in:
- `CursorStyle.isItalis` was a defined-but-never-called getter
  with a typo. No external callers reference it; renamed to
  `isItalic` in the same change as the test that exercises it.
- `_LineBuilder.isEmpty` in reflow.dart was dead — the only callers
  use `isNotEmpty` or check `_lines.isNotEmpty` separately.
  Removed.

Coverage delta:
- cell.dart: 3/7 → 7/7.
- charset.dart: 12/25 → 25/25.
- cursor.dart: 2/62 → 62/62.
- tabs.dart: 0/23 → 23/23.
- reflow.dart: 24/72 → 71/71 (file shrank by one line after the
  isEmpty getter removal).
- Total project: 52.72% → 54.62%; coverage_floor bumped 52 → 54.

Note for follow-up (not blocking): the post-loop "anchor.x >= to"
branch in reflow's `_addPart` reparents anchors past trimmedLength
onto whatever builder line is active at that moment. If no
subsequent content is added (no wrapped continuations after the
last shrink iteration), that builder line is never emitted by
`finish()` and the anchor lands on a dangling reference. The path
is exercised by the new test, but the contract it implements is
arguably broken — anchors that should follow the source content
end up off the visible buffer. Worth a separate ticket if real
terminals trip it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 00:03:19 +02:00
jpmschweitzerandClaude 1fb4786f18 sweep remaining analyze infos to zero
test / unit + widget + golden + a11y (push) Failing after 35s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m0s
Clears the 19 misc lint hits left after the test_app print sweep
+ libc.dart suppression. By rule:

- `withOpacity(α)` → `withValues(alpha: α)` (deprecated_member_use)
  in `painter.dart:187` and `terminal_view.dart:318`.
- `Pointer.elementAt(n)` → `Pointer + n` (deprecated_member_use)
  in `native_pty.dart:306` and `session.dart:187`.
- Brace single-statement for/if bodies in `native_pty.dart`
  (×3) and `decisions_view.dart` (curly_braces_in_flow_control_
  structures).
- `IsolateClient` and `InProcessClient` constructors switched to
  `super.log` / `super.events` parameters (use_super_parameters);
  associated unused imports of `kernel/src/log.dart` and
  `kernel/src/events/bus.dart` removed in the same files.
- `InProcessClient._dispatcher` field + getter/setter pair folded
  into a single mutable public `dispatcher` field
  (unnecessary_getters_setters).
- `_buildDispatcher` local in `lib/main.dart` renamed to
  `buildDispatcher` (no_leading_underscores_for_local_identifiers).
- `_onTapDown(_)` in `terminal_view.dart` typed as
  `TapDownDetails _` (strict_top_level_inference).
- `operator []=(...)` in `circular_buffer.dart` given an explicit
  `void` return type (strict_top_level_inference).
- `CustomKeyboardListener` and `TerminalGestureDetector` callsites
  reordered so `child:` lands last (sort_child_properties_last).
- `CustomTextEdit` constructor declared `const`
  (prefer_const_constructors_in_immutables).
- `LinkedHashMap<K, V>()` in `paragraph_cache.dart` collapsed to a
  `<K, V>{}` literal (prefer_collection_literals); the now-unused
  `dart:collection` import dropped.

Project analyze: 19 → 0 issues. `make test` stays green; coverage
unchanged at 52.72%.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 23:51:46 +02:00
jpmschweitzerandClaude 2d26af5934 suppress POSIX-shaped lint hits in libc FFI bindings
`lib/src/pty/ffi/libc.dart` carried 34 analyze infos:

- 26 × `non_constant_identifier_names` on struct field names
  (`msg_name`, `iov_base`, `msg_controllen`, etc.) that map 1:1
  to POSIX (`man 2 socketpair`, `recvmsg`, `iovec`, `msghdr`).
- 8 × `library_private_types_in_public_api` on the C / Dart
  function-signature typedefs (`_SocketpairC`, `_SocketpairDart`,
  etc.) consumed only by the `lookupFunction<...>()` calls in
  this same file.

Renaming the field names to lowerCamelCase would diverge from the
spec the file documents itself against; promoting the typedefs to
public would just add noise to the import surface. This is the
textbook FFI-binding case where the lints work against the file's
purpose.

Adds a file-wide `// ignore_for_file:` directive — explicitly
approved per the no-lint-suppression rule, with the reason
written inline above the directive so a future reader can
re-evaluate it.

Project analyze drops 65 → 31 issues.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 23:46:36 +02:00
jpmschweitzerandClaude 26cc1b3154 drop redundant single-symbol imports across the tree
12 imports flagged by `unnecessary_import` because the symbols
they bring in are also re-exported by the umbrella import already
present in the same file:

- bin/clide.dart: src/git/client.dart, src/pql/client.dart
  (covered by package:clide/clide.dart).
- lib/builtin/decisions/, lib/builtin/tickets/ (4 files):
  kernel/src/events/message_bus.dart (covered by kernel.dart).
- lib/kernel/src/ipc/in_process.dart: src/daemon/dispatcher.dart
  (covered by clide.dart).
- lib/main.dart: kernel/src/toolchain.dart (covered by kernel.dart).
- test/builtin/ipc_status/widget_test.dart:
  builtin/ipc_status/src/status_item.dart (covered by
  ipc_status.dart).
- test/daemon/{git,pql}_commands_test.dart: src/git/client.dart and
  src/pql/client.dart (covered by clide.dart).
- test/widgets/multitab_pane_test.dart: widgets/src/icons/x.dart
  (covered by widgets.dart).

Mechanical change — every removed line was already a no-op for
symbol resolution; the umbrella imports define the public surface
each file is actually using.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 23:46:24 +02:00
jpmschweitzerandClaude 0f2f5180ee route testmode output through the kernel logger
`lib/test_app.dart` printed [testmode] / [testmode:json] lines via
the bare `print` builtin, which tripped the `avoid_print` analyze
rule 38 times — by far the loudest source of analyze noise in the
tree.

Routes everything through a `Logger()` instance held on
`_ClideTestAppState`, with a small `_say(msg)` helper for
human-readable lines and a separate `'testmode:json'` source for
the structured summary the harness greps. The default Logger sink
is stderr; `make run-testmode` already pipes `2>&1`, so the
existing `grep -q '"failed":0'` check is unaffected.

Also drops the now-redundant kernel sub-imports (events/bus,
events/types, log, toolchain) — `kernel/kernel.dart` re-exports
them, and the analyzer flagged the doubles as unnecessary.

Project analyze: 107 → 65 issues. test_app.dart is now clean
(0 issues, was 42).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 23:42:34 +02:00
jpmschweitzerandClaude 257a333904 drop debug main() from keytab_default.dart
The file's only purpose is to expose the default-keytab string
constant, but it carried a `void main()` at the end that parsed
that constant and printed the result. That entry point:

- doesn't belong in `lib/` (Dart entry points live in `bin/` or
  `tool/`),
- pulls in `keytab_parse` and `keytab_token` imports that are
  unused everywhere else in the file,
- emits one of the pre-existing `avoid_print` analyze infos,
- only ever ran when a contributor manually invoked
  `dart lib/src/terminal/src/core/input/keytab/keytab_default.dart`,
  which the build never does.

Removing it unblocks the file from the coverage report (no
executable lines remain, just the string constant), drops the
unused imports, and shaves an analyze info off the pre-existing
total. If the dump-to-stdout helper turns out to be useful again,
the right home is a `tool/dump_keytab.dart` outside the package's
runtime surface.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 23:37:13 +02:00
jpmschweitzerandClaude 94abebf904 remove dead defensive throws in keytab tokenizer + parser
Four `throw` sites in `core/input/keytab/` were unreachable through
the public API:

- `keytab_token.dart` `_parseKeyboardNameDefine` and `_parseKeyDefine`
  each tested `reader.readString() == 'keyboard'` / `'key'` after
  the caller in the same file (`tokenize`) had already gated entry
  on `_isKeyboardNameDefine` / `_isKeyDefine`. Both checks
  redundantly re-derived a fact already established a function
  call earlier; the `else { throw }` was dead code.
- `keytab_parse.dart` `_parseName` and `_parseKeyDefine` checked
  the first token's type, but `addTokens` only delegates to those
  functions after `peek().type` matches the expected kind. Same
  pattern: the throw protects an invariant the caller already
  enforces.

Surfaced while bringing `core/input/` to ~100% coverage. Per the
"near-perfect discipline" / "no carve-outs" rules, dead defensive
code is cleaned, not skipped — the surrounding callers in the same
file are tight enough that introducing a real callsite gap would
be a localised and obvious bug, not a silent failure rescued by
these guards.

The two `else`-throw sites in keytab_token.dart fold into a single
unconditional `reader.readString()` (consume the leading word) +
`yield` of the matching token type. The two type-check throws in
keytab_parse.dart fold into an unconditional `reader.take()` to
skip the already-validated token.

All public-API ParseError paths exercised by `core/input/`'s
unit tests still throw correctly — they're guarded by the second
check in each function (the action-token type check after
modeStatus loops, and the input-token check in _parseName).

After cleanup:
- keytab_token.dart: 80 / 80
- keytab_parse.dart: 63 / 63

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 23:37:05 +02:00
jpmschweitzerandClaude 3b352fe654 remove obsolete dead-code suppression from SGR loop
`_csiHandleSgr` carried a `// ignore: dead_code` directive with the
note "workaround for a bug in the analyzer". Re-running the
analyzer with the suppression removed produces no warning — Dart's
flow analysis has caught up since the comment was added.

Per the no-lint-suppression rule the suppression needed to be
either removed or given a more substantive justification; the
analyzer's silence makes the call easy.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 23:25:59 +02:00
jpmschweitzerandClaude 9331055b44 test sweep: cover EscapeParser + EscapeEmitter (T-91)
Adds test/terminal/escape/parser_test.dart — 70 unit tests covering
the parser's dispatch surface end-to-end:

- single-byte controls (BEL, BS, HT, LF/VT/FF, CR, SO, SI),
- ESC sequences (D, E, H, M, =, >, 7, 8, ( name, ) name, unknown),
- CSI cursor moves (A/B/C/D/E/F/G/H/d/f) with default + 0-as-1
  fallback semantics,
- erase / scroll / line-insert/delete / chars (J, K, L, M, P, S, T,
  X, @, b, g, r),
- device attributes (c / >c / =c) and DSR (5, 6),
- window manipulation (CSI 8 t resize, CSI 18 t sendSize, ignored
  no-op codes, malformed CSI 8 t),
- mode set/reset (h/l, ? prefix for DEC modes — covering ?1, ?3,
  ?5, ?6, ?7, ?9, ?12, ?25, ?47, ?66, ?1000, ?1002, ?1003, ?1004,
  ?1005, ?1006, ?1007, ?1015, ?1047, ?1048, ?1049, ?2004, +
  unknown fallback),
- SGR styling (resets, set/unset for every attr, 16-colour
  foreground + background, 256-colour, 24-bit RGB, 39 / 49
  resets, unknown → unsupportedStyle),
- OSC 0/1/2 (BEL- and ST-terminated), unknown OSC, incomplete
  sequence held back across writes,
- unknown CSI final byte → unknownCSI,
- token bookkeeping (tokenBegin / tokenEnd advance with consumed
  bytes).

Plus EscapeEmitter — every reply string format (primary /
secondary / tertiary device attributes, operating status, cursor
position, bracketed paste, size).

Coverage delta:
- parser.dart: 0 / 462 → 504 / 514 (98.1%; the remaining 10 lines
  sit inside the `// ignore: dead_code` SGR loop, which I'm
  surfacing for separate review rather than extending tests
  around).
- emitter.dart: 1 / 11 → 11 / 11.
- Total project: 43.20% → 49.38%; coverage_floor bumped 43 → 49.

Two real source-code issues found while writing tests, fixed in
the same commit:

1. Swapped docstrings on `_escHandleSetAppKeypadMode` /
   `_escHandleResetAppKeypadMode`. The function names + dispatch
   table + bodies all match the VT spec correctly (ESC = enables,
   ESC > disables); only the doc-comments were swapped. Now read
   the right way around.
2. `case 10061000:` in `_setDecMode` was unreachable (no DEC mode
   has that value). Almost certainly a typo where `case 1006:`
   meant to glue onto `case 1000:` but a newline went missing.
   Mode 1006 is already handled separately at its own clause as
   `MouseReportMode.sgr`. Removed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 23:24:12 +02:00
jpmschweitzerandClaude 1d75e8e1cd fix concurrent-modification in BufferLine anchor iteration
`removeCells`, `insertCells`, and `dispose` each iterate over
`_anchors` while invoking `anchor.dispose()` on entries inside the
loop — but `dispose()` removes the anchor from the same list, which
shifts later indexes left and causes the for-loop to skip them.

Symptoms (no user-facing report yet, but real correctness bug):
- After `removeCells` with multiple anchors past the start, anchors
  that should be repositioned were silently left at their old `x`.
- After `insertCells` with anchors getting pushed past `_length`,
  ones meant to be disposed could survive.
- `BufferLine.dispose` would throw `ConcurrentModificationError` as
  soon as more than one anchor was attached.

Fix: iterate `_anchors.toList()` (a snapshot) in all three sites.
Cheap, safe, and matches the expected anchor-management semantics.

Surfaced by the unit tests added under T-91; that commit covers the
fix with regression tests.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 23:03:03 +02:00
jpmschweitzerandClaude a67a768592 restore semantic bold rendering in terminal panes (T-73)
Bold attributes from terminal escapes now render in a real bold
weight instead of being silently flattened.

- pubspec.yaml: register JetBrainsMono Bold + BoldItalic at
  weight 700 under family JetBrainsMono. Files already shipped on
  disk; only the registration was missing.
- assets/licenses.yaml: bump JetBrainsMono weights_bundled to
  [Regular, Italic, Bold, BoldItalic] per D-42 (the entry must
  match what is actually wired into the family).
- lib/src/terminal/src/ui/painter.dart: revert the `bold: false`
  override and drop the workaround comment. Bold now flows from
  CellFlags.bold to TextStyle.fontWeight.
- test/terminal/painter_bold_metrics_test.dart: load Regular and
  Bold via FontLoader and assert paragraph maxIntrinsicWidth is
  identical (cell-grid drift = 0). JetBrainsMono Bold's monospace
  by spec; this test is the canary for the day someone swaps the
  font.
- test/goldens/goldens/{ci,linux}/clide_button.png: regenerate.
  ClideButton's label renders slightly heavier on the bold variant
  (expected — 0.28% pixel diff before regen).

Earlier perception of over-bolding in the Claude pane was
synthetic-bold smearing (Flutter overpaints when no Bold.ttf is
registered for the family), not legitimate bold rendering. Visual
A/B confirms a real Bold face renders crisp emphasis without the
smear, so no per-pane renderer config is needed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 22:29:23 +02:00
jpmschweitzerandClaude bfa128f131 drop dead mouse imports from terminal_view
`mouse/button.dart` and `mouse/button_state.dart` were imported but
nothing in terminal_view referenced their symbols — analyzer
warnings, not infos. Removed.

Probable origin: a half-landed mouse-forwarding refactor (the actual
work is now scoped under T-74); the imports can come back when the
real wiring lands. Removing them in the meantime keeps the analyze
gate clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 22:28:58 +02:00
jpmschweitzerandClaude f0bb2ffcce format the tree to current dart format spec
Mechanical `dart format` sweep across files that drifted from the
formatter's output (mostly trailing-comma and line-wrap differences
from a Dart SDK / formatter version bump). No semantic changes.

Caught because the pre-push gate now actually fires.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 22:28:51 +02:00
jpmschweitzerandClaude 5ee759d0fd clear dart doc validate-links warnings
Eight unresolved doc references and broken README-rewritten links
that surfaced under `dart doc --validate-links`:

- Library-scope refs `[spawn]`, `[openProject]` qualified to
  `[Backend.spawn]` / `[Backend.openProject]`; same treatment for
  `[resolvePaths]` / `[applyResolved]` on Toolchain.
- `[D-41]` was a decision ID, not a Dart symbol — drop the brackets.
- `[from]` from I18n.interpolated qualified to `[I18nReplacer.from]`.
- `[DefaultSurfaceMap]` was a stale name (private `_defaultSurfaceMap`
  in resolver.dart); switch to backticked path reference since
  dartdoc can't link private members.
- `[icons/]` was a directory, not a symbol; backticked path.
- README links to `legacy/`, `docs/initial-plan.md`, `decisions/`,
  `LICENSE` rewritten as absolute github.com/postmeridiem/clide URLs
  so dartdoc stops re-rooting them into the doc tree.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 17:58:18 +02:00
jpmschweitzerandClaude 6601056972 finish T-87: cold-start reap, kill-all-sessions, helper tests
Three remaining acceptance criteria for T-87:

1. Cold-start reap. The Claude extension's activate() now kills
   every leftover secondary tmux session for the current repo
   before any new spawn. activate runs before any UI mounts, so
   _nextSecondary's starting value of 1 is correct even when a
   previous run died abruptly (kill -9, OOM, force-quit). The
   deactivate() hook also calls reapSecondaries as a courtesy on
   explicit extension teardown — but Flutter's deactivate doesn't
   fire on app quit, so activate is the load-bearing path.

2. claude.kill-all-sessions actually kills server-side. The
   command previously called pane.close on every claude pane,
   which only kills the tmux client. It now also calls
   tmux.killAllForRepo to kill the sessions on the clide socket.

3. Tests. test/builtin/claude/tmux_session_test.dart covers
   killSession, listClideSessions, reapSecondaries, and
   killAllForRepo via the TmuxRunner override — no real shell-out
   in tests.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 16:48:06 +02:00
jpmschweitzerandClaude 1c424f26e7 kill secondary Claude tmux sessions on tab close (T-87)
Adds lib/builtin/claude/src/tmux_session.dart with helpers for the
clide-socket tmux server: killSession, listClideSessions,
reapSecondaries, killAllForRepo. The runner is overrideable via a
TmuxRunner typedef so tests don't shell out for real.

Wires ClaudePane.dispose() to call killSession(sessionName) for
secondary panes. Primary panes are left alone — D-41 keeps the
primary's tmux session alive across clide restarts so the next
launch re-attaches via `tmux new-session -A`.

Imports the helpers in the Claude extension as groundwork for the
app-shutdown reap and the existing claude.kill-all-sessions
command — wiring those uses lands separately.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 16:41:33 +02:00
jpmschweitzerandClaude e329f1cc6f codify UI spacing constants (T-86)
Adds lib/widgets/src/spacing.dart with three categories of named
constants — insets (clideInsetHairline / Tight / Icon / Standard /
Text), gaps (clideGapTight / Standard / Section / SectionLarge /
Major / Column), and sizes (clideIconMicro / Caption / Standard /
HitTarget / Emphatic, clideControlHeight).

Migrates MultitabPane to consume the constants and updates the
ui-design geometry reference to point at them. Inline pixel
literals in widget code were drifting (12 here, 6 there, 28
elsewhere) — pulling them through named symbols makes the
"uniform inner spacing" rule enforceable instead of eyeballed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 15:45:07 +02:00
jpmschweitzerandClaude 71b21e1974 wire Claude pane to MultitabPane (T-24)
ClaudeSessionHost replaces its bespoke tab strip / add button /
close handler with a MultitabPane<_Session> in keepAlive mode.
The primary tab is seeded as non-closeable and non-reorderable
per D-41; secondaries spawn via the existing addSecondary()
entry point and gain drag-to-reorder for free.

Drops ~100 lines of custom _TabRow / _Tab / _AddButton code in
favour of the shared widget. Behaviour is preserved: primary
persists across clide restarts, secondaries can be closed, and
PTY state survives tab switches because IndexedStack keeps every
ClaudePane mounted.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 15:21:08 +02:00
jpmschweitzerandClaude 96c6cfc6c5 MultitabPane: keepAlive mode and tab strip polish
Adds keepAlive: when true, all entry bodies stay mounted via
IndexedStack so switching tabs doesn't tear down their state.
Hosts that own PTY-backed sessions or any long-lived widget
state opt in; callers that want fresh state on each switch use
the default single-body mode.

Polishes the tab strip itself for production use:
- bottom divider so the strip visually anchors to the body below
- Column.crossAxisAlignment.stretch so the strip fills the pane
  width instead of sizing to its content
- close button: replace the text × glyph with the CloseIcon
  painter (clean cross strokes, font-independent)
- two-column tab layout — Expanded title on the left, fixed
  16x16 close button on the right; uniform 12px left padding,
  6px right padding to match the 6px top/bottom breathing room
  around the close button

Two new widget tests cover keepAlive (state preserved across
switches) and default mode (inactive bodies disposed).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 15:20:57 +02:00
jpmschweitzerandClaude aa742eed79 MultitabPane: drag-to-reorder gesture wiring (T-84)
Each tab is wrapped in a Draggable (when allowReorder is true and the
entry itself is reorderable) and a DragTarget (always — the controller's
barrier logic decides whether the move actually happens). Drops insert
the dragged entry at the target tab's index. A 2px leading insertion
indicator highlights the active drop target.

The widget harness now wraps children in an Overlay so Draggable's
feedback can mount without each test re-wrapping. Sized by the test
view's bounds to avoid disturbing existing tests that query
find.byType(SizedBox).first.

Four widget tests cover the gesture path: drop reorders, pinned
barrier blocks, pinned tabs aren't draggable, and allowReorder=false
disables drag entirely.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 14:17:16 +02:00
jpmschweitzerandClaude 1caaf5f4dd implement MultitabPane widget + controller (T-83)
MultitabController<T> is a Flutter-free ChangeNotifier owning the
tab list, active selection, and reorder/close invariants:
- pinned (non-reorderable) entries form barriers that other tabs
  cannot cross
- non-closeable entries silently no-op on remove() so hosts don't
  need to gate the call site
- closing the active tab falls right, then left, then to null
- duplicate ids are rejected

MultitabPane<T> is the widget shell: a horizontal tab strip
followed by the active entry's body. Active tab gets the
panelHeader background and a panelActiveBorder top accent;
inactive tabs blend into the tab bar. Close × is hidden until
hover. Add button only renders when onAddRequested is wired.

Hosts route the user's add/close intent through callbacks so the
widget stays domain-free — for the Claude pane, add will spawn a
new tmux session and close will kill one. Drag-to-reorder is
controller-side only for now (the gesture wiring lands with T-24).

19 controller tests + 9 widget tests.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 12:12:42 +02:00
jpmschweitzerandClaude 2d64740007 welcome screen: add Tips card below START / RECENT row
Six common keybindings (Quick open, Command palette, Toggle
sidebar, Toggle context, Switch theme, New Claude session) shown
as a 3x2 grid card spanning the same 850px content column as the
two action columns above. LayoutBuilder gates the card on viewport
height (>640px) so on shorter windows the centered START / RECENT
columns stay the focus and the tips drop out cleanly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 11:10:17 +02:00
jpmschweitzerandClaude 5f162a6668 rename tagline to "IDE for Claude Code CLI"
clide is an IDE for the Claude Code CLI. The previous tagline
"Flutter desktop IDE for Claude Code" overemphasized the host
toolkit (Flutter is implementation detail, immediately obvious to
contributors) and was ambiguous about whether the integration
target is the CLI specifically.

Updates the welcome subtitle (i18n catalog + widget test + view),
the project description in pubspec.yaml, README and CLAUDE.md, the
CLI banner, and the web manifest/index.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 11:06:41 +02:00
jpmschweitzerandClaude 5fb3df84ed map POSIX errno to actionable IPC error kinds (T-79)
pane.spawn (via PtyException.errno) and editor.open (via
FileSystemException.osError.errorCode) now route ENOENT to
not_found, EACCES/EPERM to user_error with a permissions hint,
EISDIR/ENOTDIR/EEXIST to distinct user-error/conflict, and
EMFILE/ENFILE to tool_error with a "fd limit hit" hint. The
mapping lives in lib/src/ipc/errno_mapping.dart so other handlers
can adopt the same surface as they pick up errno-bearing failures.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 09:56:04 +02:00
jpmschweitzerandClaude 41fd586ca0 IPC server: per-request timeout, error logging, stale-socket guard (T-77)
Three hardening fixes:
- 60s per-request timeout (configurable via DaemonServer constructor)
  prevents a misbehaving handler from blocking the connection's
  read pipeline indefinitely. On timeout the client gets a clean
  tool_error response.
- broadcast() and the per-request response writeln are wrapped in
  try/catch with stderr logging. Previously write failures silently
  dropped clients with no diagnostic; events going missing was
  invisible.
- start() probes for a live daemon before unlinking a stale socket.
  If something answers within 200ms, refuse to start. Previously
  two daemons racing to bind would let the second rip the first's
  live socket out.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-05 15:12:14 +02:00
jpmschweitzerandClaude 1787147e82 PTY: fix resource leaks and reader-isolate races (T-76)
NativePty.close() now awaits the reader-isolate spawn, kills the
child first to drive EOF on the master fd, awaits the isolate's
EOF acknowledgement, and only then closes the fd. Previously the
fd-close racing with the polling isolate left a window where the
fd number could be reused and the isolate would briefly target the
wrong file.

Both NativePty and PtySession now surface reader-isolate spawn
errors via the output stream's addError instead of silently
swallowing them.

PtySession.spawn closes the master fd on any post-receive failure,
closes parentSock in finally (was leaking on every spawn), and
kills the ptyc process if recvFd fails.

PtySession._recvFdAsync uses try/finally to close the ReceivePort
and kill the spawn isolate even when Isolate.spawn itself throws.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-05 15:10:43 +02:00
jpmschweitzerandClaude 0d333e8470 PTY: surface errno from forkpty/write/ioctl failures (T-75)
forkpty failures throw PtyException with the captured errno
(previously a generic StateError). The spawned child's chdir/execve
failures write a diagnostic line to its slave PTY before _exit, so
the parent's reader sees "exec failed: <path>" instead of an
indistinguishable EOF.

NativePty.write and PtySession.write loop on short writes and throw
PtyException on hard errors (with errno). NativePty.resize sets
_dead on EBADF so subsequent calls short-circuit cleanly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-05 15:07:25 +02:00
jpmschweitzerandClaude 816e60d028 reject path traversal in files.read and files.ls (T-78)
Both handlers concatenated the request path onto the workspace root
without validating containment, letting `path: "../../../etc/passwd"`
escape the workspace. resolveUnderRoot normalizes the path and
checks containment under root.absolute.path before any filesystem
access.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-05 15:02:30 +02:00
jpmschweitzerandClaude 4ff004790d suppress bold rendering in terminal to keep cell grid stable
Flutter falls back to synthetic bold when JetBrainsMono-Bold isn't
registered, and synthetic bold drifts glyph advance widths enough
to break the monospace cell grid (cursor block lands between
characters, prompts wrap mid-word). Color is enough to convey
emphasis in TUIs; semantic italic and underline still render.

Also drop the temporary `tmux -L clide kill-server` from the
install target — the rapid-iteration loop is no longer needed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-05 14:51:04 +02:00
jpmschweitzerandClaude c378503e26 spawn Claude in fullscreen mode and route mouse-wheel to PgUp/PgDown
Spawn `claude` directly as the tmux command with
CLAUDE_CODE_NO_FLICKER=1 so Claude Code runs in its fullscreen TUI
mode (input box pinned at bottom, owns its own scrollback). Mouse
wheel events are converted to PgUp/PgDown key input — universal
scroll signal that Claude, less, vim normal mode all respect, and
sidesteps the mouse-mode-but-no-scroll dead end where TUI apps
capture mouse without binding the wheel.

Drops the 1000-row tmux canvas + SingleChildScrollView experiment
in favor of viewport-sized tmux and Claude's native bottom-pinning.

Makefile install target now kills the clide tmux server so the
new config takes effect immediately. Marked TEMP — to be removed
once we no longer need the rapid-iteration loop.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-05 13:08:47 +02:00
jpmschweitzerandClaude 8a5c626892 update licenses for terminal inlining
licenses.yaml: xterm entry changed from dart-package to
inlined-source with derivative-work description. JetBrains Mono
weights updated (Bold/BoldItalic dropped). Terminal LICENSE
clarifies this is a derivative work based on xterm.dart v4.0.0.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-05 08:52:25 +02:00
jpmschweitzerandClaude 85e275a362 wire Claude pane to clide tmux config and isolated socket
Extract bundled tmux.conf to ~/.config/clide/tmux.conf on first
spawn and pass via -f. Use -L clide for a dedicated tmux server
so clide sessions don't inherit the user's tmux settings.
Terminal maxLines bumped from 5k to 50k.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-05 08:50:50 +02:00
jpmschweitzerandClaude 9c1597a450 inline terminal emulator, drop xterm.dart dependency
Replace the xterm pub.dev package with owned code under
lib/src/terminal/. Based on xterm.dart v4.0.0 by xuty (MIT).
Quiver LRU replaced with hand-rolled LinkedHashMap cache.
Scrollable removed from TerminalView — scroll events are forwarded
via Listener.onPointerSignal instead. zmodem, debugger, and
suggestion modules stripped as unused.

Also: bundle clide.tmux.conf (no status bar, 50k scrollback,
mouse on, zero escape delay, isolated -L clide socket), bump PTY
read buffer to 64KB, add 2px terminal padding, drop bold
JetBrains Mono registration.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-05 08:50:38 +02:00
jpmschweitzerandClaude 7b30748bf5 rewire daemon services on project open
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
The daemon dispatcher (pql, git, files, editor) was built once at
boot against cwd and never updated. Opening a project from the
welcome screen or directory picker had no effect on the daemon
services — pql couldn't find its index, git operated on the wrong
repo.

Now onProjectOpen rebuilds the full dispatcher against the new
workspace root and swaps it into the InProcessClient. The IPC
client's dispatcher field is no longer final.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-03 22:29:03 +02:00
jpmschweitzerandClaude a5221792ba fix sidebar order and ptyc status bar warning
test / unit + widget + golden + a11y (push) Failing after 29s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
Revert priority sort in tabsFor() — registration order in
main.dart is the intended sidebar order, not priority. The
priority fields on extensions were dead code.

Remove ptyc from toolchain missing list since NativePty
replaced it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-03 22:22:23 +02:00
jpmschweitzerandClaude 38b017e92b dart format session.dart
test / unit + widget + golden + a11y (push) Failing after 34s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-03 21:52:53 +02:00
jpmschweitzerandClaude b45699ccd1 fix test suite — green on make test
tabsFor() sorts by contribution priority when no user order is
set. Test expectations updated for sidebar defaultSize 400 and
decision ID D-1 (no zero-padding). PTY tests tagged forkpty and
run via dart test (forkpty output unreliable inside flutter test
runner). CI script adds --no-fatal-infos and --exclude-tags.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-03 21:52:39 +02:00
jpmschweitzerandClaude 864a1062a6 remove dead code and fix analyzer warnings
Unused _resolvePtycPath (bin/clide.dart), _TrafficDot (app.dart
and clide_column_hat.dart), stale @override on _spawned field
(claude_pane.dart), duplicate import and unused local in
test_app.dart, unnecessary non-null assertions in project.dart.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-03 21:52:26 +02:00
jpmschweitzerandClaude 943ab966d2 harden PTY FFI layer for macOS
Platform-dispatched constants where Linux and macOS diverge:
TIOCSWINSZ, O_NONBLOCK, MsghdrDarwin struct (4-byte msg_iovlen
and msg_controllen vs Linux's 8-byte size_t fields), and split
recvmsg into Linux/Darwin typed variants so scm_rights uses the
correct struct per platform.

Also: app settings dir uses ~/Library/Application Support on
macOS, removed hardcoded TERMINFO from pane spawn env, removed
debug print from native_pty resize.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-03 21:52:13 +02:00
jpmschweitzerandClaude 9c7ec008dc dart format whole tree
Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-03 21:51:59 +02:00
Jeroen SchweitzerandClaude Opus 4.6 56d13c71ef use tmux resize-window for explicit window size control
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
tmux determines window size from its client, not the PTY winsize.
TIOCSWINSZ + SIGWINCH on the master fd has no effect on tmux's
internal dimensions. Use tmux resize-window -t <session> -x -y
to explicitly set the window size on resize.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 13:17:56 +02:00
Jeroen SchweitzerandClaude Opus 4.6 956279eca4 tell tmux to refresh-client on resize
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
tmux doesn't re-read the PTY winsize on SIGWINCH. After the ioctl
resize, call tmux refresh-client -C cols,rows to update tmux's
internal window dimensions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 13:11:26 +02:00
Jeroen SchweitzerandClaude Opus 4.6 a028eb7ad3 pass -x/-y to tmux new-session for correct initial size
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
tmux running inside a PTY without a traditional terminal client
defaults to a huge window size (2000+ cols, 10000 rows). Pass
explicit -x and -y flags matching the TerminalView dimensions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 13:03:51 +02:00
Jeroen SchweitzerandClaude Opus 4.6 0b3c6bc1f8 restore tmux session in Claude pane
test / unit + widget + golden + a11y (push) Failing after 34s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
Reverts the bare-shell diagnostic. Spawns tmux new-session -A -s
<sessionName> for persistence. Claude command will be added back
after tmux + resize are stable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 12:20:03 +02:00
Jeroen SchweitzerandClaude Opus 4.6 29fe6795b3 rename CLIDE_WORKSPACE to CLIDE_PROJECT
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
Missed in the workspace → project rename. Consistent with the
project.open / project.validate / ProjectOpened naming.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 12:11:15 +02:00
Jeroen SchweitzerandClaude Opus 4.6 2921adc9cb add TERMINFO env for top/ncurses, tighten output flush
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
top fails with "Error opening terminal: xterm-256color" because the
PTY child can't find terminfo. Set TERMINFO=/usr/share/terminfo
explicitly in the pane environment.

Output flush changed from 16ms Timer to Duration.zero (next event
loop turn) for tighter batching of escape sequences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 12:08:01 +02:00
Jeroen SchweitzerandClaude Opus 4.6 3e6c0279ef batch terminal output at 60fps, increase read buffer to 64KB
test / unit + widget + golden + a11y (push) Failing after 29s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
Split escape sequences caused rendering artifacts (visible % prompt
mark, garbled lines between commands). Two fixes from the legacy
Python implementation:

1. Read buffer increased from 4096 to 65536 bytes. Larger reads mean
   fewer chunk boundaries that can split multi-byte escape sequences.

2. Output writes throttled to ~60fps via a 16ms Timer. Multiple PTY
   reads within one frame window are batched into a single
   terminal.write() call, so the xterm parser sees complete sequences
   instead of fragments that render as garbage between frames.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 12:01:22 +02:00
Jeroen SchweitzerandClaude Opus 4.6 0485a36091 stagger initial scheduler ticks to avoid rebuild storm
test / unit + widget + golden + a11y (push) Failing after 33s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
Firing all tier ticks simultaneously on project open causes multiple
IPC calls (pql, git, files) to run in parallel, their responses
triggering rapid setState rebuilds that beachball the merged thread.
Stagger 500ms apart so each panel refreshes sequentially.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 11:41:42 +02:00
Jeroen SchweitzerandClaude Opus 4.6 2f7435f271 debounce terminal resize, add SIGWINCH, remove padding
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
Rapid resize during window drag sends many SIGWINCH signals causing
the shell to redraw repeatedly, corrupting xterm's render. Debounce
to 150ms so only the final size is sent.

NativePty.resize now explicitly sends SIGWINCH (signal 28) after
TIOCSWINSZ, matching the legacy Python implementation.

Removed 8px padding from ClidePtyView to eliminate potential
dimension mismatch between widget size and PTY cols/rows.

Default window size increased to 1600x900.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 11:40:12 +02:00
Jeroen SchweitzerandClaude Opus 4.6 6f12c884cd defer PTY spawn until first TerminalView resize
test / unit + widget + golden + a11y (push) Failing after 1m57s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
The PTY was spawning at 80x24 defaults before TerminalView had laid
out, then the real resize caused visual artifacts (ghost lines, broken
reflow). Now spawn triggers on the first onResize callback when real
dimensions are known — matching the pty-spike's proven pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 09:24:10 +02:00