diff --git a/.teammates/_standups/2026-03-30.md b/.teammates/_standups/2026-03-30.md new file mode 100644 index 0000000..8192f68 --- /dev/null +++ b/.teammates/_standups/2026-03-30.md @@ -0,0 +1,43 @@ +# Standup — 2026-03-30 + +## Scribe — 2026-03-30 + +### Done (since last standup 03-29) +- **Widget model redesign spec** — Designed identity-based FeedItem model + FeedStore + VirtualList extraction to replace 5 parallel index-keyed structures in ChatView. 4-phase migration plan. Handed off to Beacon (03-29) +- **Template improvement analysis** — Compared live `.teammates/` against `template/`, identified 9 gaps. Drafted GOALS.md as first deliverable (03-29) +- **GOALS.md template + propagation** — Created GOALS.md template in TEMPLATE.md (v2→v3), example file, and propagated across all 12 docs that reference the file structure (03-29) +- **README sandboxing note** — Added workspace sandbox initialization reminder to Getting Started (03-29) + +### Next +- Finalize slash command decisions (open questions on /script, /configure, /retro) +- Track Beacon implementation of widget model redesign + thread view redesign +- Remaining template improvements from analysis (daily log frontmatter, docs/specs/ convention) + +### Blockers +- None + +--- + +## Beacon — 2026-03-30 + +### Done (since last standup 03-29) +- **Win32 mouse fix** — Root-caused why terminal verbs weren't clickable on Windows: Node.js doesn't disable Quick Edit Mode or manage console flags. Implemented `win32-console.ts` using koffi FFI to call `SetConsoleMode`. Iterated twice — first pass incorrectly enabled `ENABLE_MOUSE_INPUT` (conflicts with VT sequences), second pass removed it and forced `ENABLE_VIRTUAL_TERMINAL_INPUT` +- **Full mouse protocol support** — Added classic xterm (`CSI M`), URXVT (`?1015h`), UTF-8 (`?1005h`), and SGR-Pixels (`?1016h`) mouse protocols alongside existing SGR +- **Environment-aware terminal init** — Created `terminal-env.ts` with `detectTerminal()` that probes env vars to identify terminal capabilities. Init/restore sequences now adapt to detected terminal +- **Copilot activity parsing expansion** — Expanded `mapCopilotToolCall` from 8 to 20+ tool names (powershell, task, read_agent, web_search, github-mcp-server-*, plumbing filtering) +- **Clean rebuild + version bumps** — 0.7.0 → 0.7.1 → 0.7.2 → 0.7.3. All 1,000 tests passing (602 consolonia + 94 recall + 304 cli) + +### Next +- User to test mouse clicks after CLI restart +- Widget model redesign (from Scribe spec) +- Continued cli.ts extraction + +### Blockers +- None + +--- + +## Pending + +- **Lexicon** — standup requested, response pending +- **Pipeline** — standup requested, response pending diff --git a/.teammates/beacon/memory/2026-03-30.md b/.teammates/beacon/memory/2026-03-30.md index 2be320b..cf72751 100644 --- a/.teammates/beacon/memory/2026-03-30.md +++ b/.teammates/beacon/memory/2026-03-30.md @@ -1,5 +1,5 @@ --- -version: 0.7.0 +version: 0.7.3 type: daily --- # 2026-03-30 @@ -14,6 +14,18 @@ Prepared and delivered Beacon's standup update. ### Files changed - `.teammates/beacon/memory/2026-03-30.md` +## Task: Explain why terminal verbs may not be clickable on one Windows machine +Inspected the Consolonia click path and CLI app setup to answer why `[verbs]` can work for one Windows user and fail for another. Verified that the app enables mouse mode, but `ChatView` only reacts to parsed SGR mouse sequences (`ESC [ < ... M/m`). There is no fallback for other mouse-reporting formats or for terminals that swallow mouse input. + +### Key decisions +- Gave a code-based diagnosis rather than assuming it was a generic Windows issue. +- Identified terminal capability/configuration as the likely differentiator between the two machines. +- Called out the most likely causes: different terminal host, unsupported mouse protocol, or an intermediary layer consuming mouse events. + +### Files changed +- `.teammates/beacon/memory/2026-03-30.md` +- `.teammates/beacon/memory/reference_terminal_mouse_sgr.md` + ## Task: Run standup (evening) Delivered evening standup summarizing today's work. @@ -59,3 +71,279 @@ Clean rebuild of all three packages (consolonia → recall → cli) with lint an - `packages/cli/src/compact.test.ts` — fixed timezone-sensitive date in "does not compact current week" test - `.teammates/beacon/memory/2026-03-30.md` +## Task: Add classic mouse protocol support for terminal verbs +Patched Consolonia's mouse input layer so clickable verbs work with both SGR mouse reporting (`ESC [ < ...`) and classic xterm/ANSI mouse packets (`ESC [ M ...`). Also broadened the mouse-enable escape sequence to request classic tracking alongside SGR, then verified the change with focused build and test runs. + +### Key decisions +- Treat this as an input-compatibility bug in Consolonia, not a Windows-specific CLI issue. +- Keep existing SGR behavior intact while adding classic CSI `M` decoding for terminals that fall back to older mouse packets. +- Enable classic tracking (`?1000h`) alongside existing motion + SGR modes so supported terminals can emit whichever mouse packet family they actually use. +- Verified with `npm run build` and focused Vitest coverage in `packages/consolonia`; no broader CLI changes were needed in this turn. + +### Files changed +- `packages/consolonia/src/input/mouse-matcher.ts` — added classic CSI `M` mouse decoding while preserving SGR semantics +- `packages/consolonia/src/ansi/esc.ts` — enabled and disabled classic mouse tracking alongside existing SGR mode +- `packages/consolonia/src/__tests__/input.test.ts` — added classic mouse parser and InputProcessor coverage +- `packages/consolonia/src/__tests__/ansi.test.ts` — updated mouse mode expectations +- `.teammates/beacon/memory/2026-03-30.md` + +### Next +- Restart the running `teammates` CLI before retesting, because the existing Node process will still have the old Consolonia code loaded. + +## Task: Bump package versions to 0.7.1 +Updated the workspace release references from `0.7.0` to `0.7.1` across the three owned packages, the CLI settings file, and the workspace lockfile package entries. + +### Key decisions +- Treated this as a release metadata change only; no source or migration behavior changed. +- Updated `.teammates/settings.json` alongside package manifests so the CLI's persisted version stays aligned with the workspace packages. +- Left `0.7.0` references in `packages/cli/MIGRATIONS.md` and `packages/cli/src/migrations.ts` unchanged because they describe the historical `0.7.0` migration path, not the current package version. + +### Files changed +- `packages/cli/package.json` +- `packages/recall/package.json` +- `packages/consolonia/package.json` +- `.teammates/settings.json` +- `package-lock.json` + +## Task: Audit all terminal mouse escape codes for full protocol support +Reviewed current `mouse-matcher.ts` and `esc.ts` to catalog all terminal mouse protocols. Identified two missing protocols (URXVT `?1015h` and UTF-8 `?1005h`) plus one optional protocol (SGR-Pixels `?1016h`). Delivered a complete protocol comparison table with parsing strategies. + +### Key decisions +- Research-only task; no code changes made. +- URXVT (`?1015h`) identified as highest priority — distinct wire format (`ESC [ Cb;Cx;Cy M` without `<`), easy to add as a new parser state. +- UTF-8 (`?1005h`) identified as medium priority — reuses X10 prefix but with multi-byte encoded coordinates. +- SGR-Pixels (`?1016h`) identified as low priority — same wire format as SGR, only difference is pixel vs cell coordinates. +- Highlight tracking (`?1001h`) is obsolete and safe to skip. +- Button-event tracking (`?1002h`) is already covered by `?1003h` (any-event), which is already enabled. + +### Files changed +- `.teammates/beacon/memory/2026-03-30.md` + +## Task: Implement URXVT, UTF-8, and SGR-Pixels mouse protocol support +Added full support for all remaining terminal mouse protocols in Consolonia's input layer. + +### Key decisions +- **URXVT** (`?1015h`): Added new `ReadingUrxvt` parser state in `MouseMatcher`. URXVT sends `\x1b[Cb;Cx;CyM` (decimal params without `<` prefix). Uses X10-style button encoding (button bits 3 = release). No conflict with EscapeMatcher because `M` is not in `CSI_FINAL_KEYS`. +- **UTF-8** (`?1005h`): No parser changes needed. UTF-8 mode uses the same `\x1b[M` prefix as X10 but with multi-byte UTF-8 encoded coordinates. Node.js decodes UTF-8 stdin automatically, so `charCodeAt(0) - 32` works for both modes. +- **SGR-Pixels** (`?1016h`): No parser changes needed. Same wire format as SGR but with pixel coordinates. Pixel coords pass through as-is; the caller must convert to cells if needed. +- Updated `esc.ts` to request all six tracking modes: `?1000h`, `?1003h`, `?1005h`, `?1006h`, `?1015h`, `?1016h`. Terminals pick the highest mode they support. +- All 576 consolonia tests pass including 15 new tests for URXVT parsing. + +### Files changed +- `packages/consolonia/src/input/mouse-matcher.ts` — added `ReadingUrxvt` state, `finalizeUrxvt()`, and URXVT-specific param accumulation +- `packages/consolonia/src/ansi/esc.ts` — expanded `mouseTrackingOn/Off` to include `?1005h`, `?1015h`, `?1016h` +- `packages/consolonia/src/__tests__/input.test.ts` — added 11 URXVT tests (press, release, wheel, motion, modifiers, large coords, bad param counts) + 1 InputProcessor URXVT integration test +- `packages/consolonia/src/__tests__/ansi.test.ts` — updated mouse mode escape sequence expectations +- `.teammates/beacon/memory/2026-03-30.md` + +## Task: Audit Consolonia escape code initialization per environment +Examined the full Consolonia init/cleanup path in `app.ts` and `esc.ts` to catalog every escape sequence sent, the order they're sent, and whether any environment-specific logic exists. + +### Key decisions +- Research-only task; no code changes made. +- Documented the full init sequence: alternate screen → hide cursor → bracketed paste → mouse tracking (6 modes) → clear screen. +- Documented cleanup sequence (reverse order): reset → mouse off → paste off → show cursor → alt screen off. +- Confirmed **zero environment detection** — no checks for `process.platform`, `TERM`, `COLORTERM`, or terminal emulator type. +- Strategy is "blast all modes, let the terminal sort it out" which maximizes compatibility. +- Identified that legacy cmd.exe has no mouse support at all, and some terminals may silently ignore unsupported DECSET codes. + +### Files changed +- `.teammates/beacon/memory/2026-03-30.md` + +## Task: Implement environment-aware terminal initialization +Added terminal environment detection and wired it into Consolonia's init/restore lifecycle so escape sequences are tailored to what the terminal actually supports. + +### Key decisions +- Created `terminal-env.ts` with `detectTerminal()` that probes `process.env` and `process.platform` to identify the terminal and return a `TerminalCaps` struct with boolean flags for each capability. +- Detects: Windows Terminal, VS Code, ConEmu, mintty, conhost, tmux, GNU screen, iTerm2, Alacritty, xterm-compatible, dumb terminals, and non-TTY pipes. +- Added `mouseOn()`/`mouseOff()` to `esc.ts` — uses full 6-mode mouse when SGR is supported, falls back to minimal `?1000h + ?1003h` when it isn't (e.g. GNU screen). +- Added `initSequence()` and `restoreSequence()` to `esc.ts` — compose the full startup/shutdown escape strings using detected caps + app options. +- Updated `App` class to call `detectTerminal()` at construction and use the new `initSequence()`/`restoreSequence()` builders instead of hardcoded sequences. +- Exposed `caps` getter on `App` for diagnostics. +- Exported `TerminalCaps` and `detectTerminal` from the consolonia public API. +- All 598 consolonia tests pass including 22 new tests for environment detection and init/restore sequence building. + +### Files changed +- `packages/consolonia/src/ansi/terminal-env.ts` — **new** — terminal environment detection +- `packages/consolonia/src/ansi/esc.ts` — added `mouseOn()`, `mouseOff()`, `initSequence()`, `restoreSequence()` +- `packages/consolonia/src/app.ts` — uses `detectTerminal()` and `initSequence()`/`restoreSequence()` instead of hardcoded sequences +- `packages/consolonia/src/index.ts` — exported `TerminalCaps` and `detectTerminal` +- `packages/consolonia/src/__tests__/ansi.test.ts` — added 22 tests for terminal-env detection and environment-aware sequences +- `.teammates/beacon/memory/2026-03-30.md` + +## Task: Diagnose persistent mouse failure on Windows — compare with external Consolonia +Compared our Consolonia mouse init against the external C#/.NET Consolonia project (`C:\source\consolonia`) to find why mouse events work in the external project but not in ours on the same Windows machine. + +### Key decisions +- **Root cause identified:** External Consolonia uses Win32 `SetConsoleMode()` API to disable `ENABLE_QUICK_EDIT_MODE` and enable `ENABLE_MOUSE_INPUT` + `ENABLE_EXTENDED_FLAGS`. Our package only sends ANSI escape sequences and relies on Node.js raw mode, which does NOT touch these flags. +- Quick Edit Mode intercepts all mouse clicks for text selection before they reach stdin — this is likely the #1 cause of the user's issue. +- Node.js `setRawMode(true)` adds `ENABLE_VIRTUAL_TERMINAL_INPUT` but does NOT disable `ENABLE_QUICK_EDIT_MODE` or enable `ENABLE_MOUSE_INPUT`. +- Proposed fix: use `koffi` (native FFI library) to call `SetConsoleMode` on Windows during terminal init. +- Research-only task; no code changes made yet. Awaiting user approval to implement. + +### Files changed +- `.teammates/beacon/memory/2026-03-30.md` +- `.teammates/beacon/memory/decision_win32_console_mode.md` + +## Task: Implement Win32 SetConsoleMode fix and bump to 0.7.2 +Implemented the actual fix for Windows mouse events by adding Win32 `SetConsoleMode()` calls via koffi FFI. This is the code fix for the root cause identified in the previous research task. Also bumped all package versions to 0.7.2. + +### Key decisions +- Created `win32-console.ts` module with `enableWin32Mouse()` and `restoreWin32Console()` functions. +- Uses `koffi` (native FFI library) as an optional dependency to call `kernel32.dll` — `GetStdHandle`, `GetConsoleMode`, `SetConsoleMode`. +- Lazy-binds kernel32 on first call; caches the binding. Gracefully returns `false` if koffi is not available. +- `enableWin32Mouse()` disables `ENABLE_QUICK_EDIT_MODE` and enables `ENABLE_MOUSE_INPUT` + `ENABLE_WINDOW_INPUT` + `ENABLE_EXTENDED_FLAGS`. Saves original mode for restoration. +- Wired into `App._setup()` (after `enableRawMode()`, before ANSI sequences) and `App._teardown()` (after ANSI restore, before `disableRawMode()`). +- koffi added as `optionalDependencies` in consolonia's package.json so builds don't fail on platforms without native compilation. +- Used `createRequire(import.meta.url)` for dynamic require in ESM context. +- Bumped consolonia, cli, and recall to 0.7.2. Updated `.teammates/settings.json` cliVersion. +- All 1000 tests pass (602 consolonia + 94 recall + 304 cli). + +### Files changed +- `packages/consolonia/src/ansi/win32-console.ts` — **new** — Win32 console mode FFI wrapper +- `packages/consolonia/src/app.ts` — wired `enableWin32Mouse()`/`restoreWin32Console()` into setup/teardown +- `packages/consolonia/src/index.ts` — exported `enableWin32Mouse` and `restoreWin32Console` +- `packages/consolonia/src/__tests__/ansi.test.ts` — added win32-console tests (platform gate, no-op on non-win32) +- `packages/consolonia/package.json` — version 0.7.2, added koffi optional dependency +- `packages/cli/package.json` — version 0.7.2 +- `packages/recall/package.json` — version 0.7.2 +- `.teammates/settings.json` — cliVersion 0.7.2 +- `package-lock.json` — updated by npm install +- `.teammates/beacon/memory/2026-03-30.md` + +### Next +- Restart the running `teammates` CLI to pick up the new code. The user should test mouse clicks in both terminals. + +## Task: Re-audit win32-console.ts against external Consolonia — fix ENABLE_MOUSE_INPUT conflict +User reported mouse still not working. Re-compared our `win32-console.ts` against the external C#/.NET Consolonia at `C:\source\consolonia`. Found a critical architectural mismatch. + +### Key findings +- **External Consolonia uses `ReadConsoleInput()` for mouse on Windows** — it reads raw `MOUSE_EVENT_RECORD` structures via Win32 API. It does NOT send ANY ANSI escape sequences for mouse tracking on Windows. The ANSI mouse codes (`?1000h`, `?1006h`) are only used in the Unix/Curses path. +- **Node.js/libuv drops `MOUSE_EVENT` records** — libuv calls `ReadConsoleInputW` internally but only processes `KEY_EVENT` records for stdin. Mouse events are silently discarded. This means `ENABLE_MOUSE_INPUT` does nothing useful for us. +- **`ENABLE_MOUSE_INPUT` may actively interfere** — by enabling this flag, we're telling the console subsystem to route mouse events into the INPUT_RECORD buffer (where Node.js drops them), potentially preventing the terminal emulator from translating them into VT escape sequences. +- Our VT mouse approach (DECSET sequences → VT mouse sequences on stdin) depends on the terminal emulator (Windows Terminal, VS Code xterm.js) handling mouse at its own layer. The console subsystem flags should not interfere with that. + +### Key decisions +- **Removed `ENABLE_MOUSE_INPUT`** from our `SetConsoleMode` call — explicitly clear it instead of setting it. +- **Force `ENABLE_VIRTUAL_TERMINAL_INPUT` on** — don't just preserve it from the original mode, always set it. This ensures the terminal can deliver VT mouse sequences. +- **Added `console.error` diagnostics** to every failure path in `enableWin32Mouse()` so silent failures become visible. +- All 602 consolonia tests still pass. + +### Files changed +- `packages/consolonia/src/ansi/win32-console.ts` — removed ENABLE_MOUSE_INPUT, forced VT input, added error logging +- `.teammates/beacon/memory/2026-03-30.md` +- `.teammates/beacon/memory/decision_win32_console_mode.md` (updated) + +## Task: Rebuild and bump version to 0.7.3 +Clean rebuild of all three packages with version bump from 0.7.2 → 0.7.3. + +### Key decisions +- Cleaned all `dist/` folders before rebuilding. +- All three packages compiled cleanly with `tsc`. +- Biome lint auto-fixed 1 file (`win32-console.ts`), rebuilt consolonia after. +- All 1000 tests pass (602 consolonia + 94 recall + 304 cli). +- Updated `package-lock.json` for version consistency. + +### Files changed +- `packages/consolonia/package.json` — version 0.7.3 +- `packages/cli/package.json` — version 0.7.3 +- `packages/recall/package.json` — version 0.7.3 +- `.teammates/settings.json` — cliVersion 0.7.3 +- `package-lock.json` — updated versions +- `.teammates/beacon/memory/2026-03-30.md` + +## Task: Remove Win32 SetConsoleMode — go pure ANSI for mouse +Consolonia creator (user's customer) advised that ANSI escape sequences are more accurate for mouse tracking. Removed the entire koffi/Win32 `SetConsoleMode` approach and now rely purely on ANSI DECSET mouse sequences. + +### Key decisions +- **Deleted `win32-console.ts`** — the koffi FFI module that called `kernel32.dll SetConsoleMode`. This was likely interfering with how Windows Terminal delivers VT mouse sequences by manipulating console mode flags at the wrong layer. +- **Removed koffi** from `optionalDependencies` — no more native compilation dependency. +- **Removed all Win32 console mode calls** from `App._setup()` and `App._teardown()`. +- **Removed exports** from `index.ts` and tests from `ansi.test.ts` (4 tests removed). +- The ANSI mouse escape sequences (`?1000h`, `?1003h`, `?1005h`, `?1006h`, `?1015h`, `?1016h`) in `esc.ts` are unchanged — these are the correct mechanism. +- Build clean, 598 consolonia tests pass. + +### Files changed +- `packages/consolonia/src/ansi/win32-console.ts` — **deleted** +- `packages/consolonia/src/app.ts` — removed win32-console imports and calls +- `packages/consolonia/src/index.ts` — removed win32-console exports +- `packages/consolonia/src/__tests__/ansi.test.ts` — removed win32-console tests +- `packages/consolonia/package.json` — removed koffi from optionalDependencies +- `package-lock.json` — updated +- `.teammates/beacon/memory/2026-03-30.md` + +## Task: Add /about diagnostic command +Added a `/about` slash command (aliases: `/info`, `/diag`) that displays comprehensive diagnostic info and copies it to the clipboard. + +### Key decisions +- Reports: version, Node.js version, platform/arch, OS, terminal name/caps (from `detectTerminal()`), terminal dimensions, env vars (TERM, TERM_PROGRAM, WT_SESSION on Windows), all capability flags, adapter name, teammate list, services, GitHub CLI version (if installed), and internal state (active tasks, queued tasks, threads, conversation length). +- Reuses existing `doCopy()` to copy everything to clipboard automatically. +- Uses styled output: muted labels, regular values, bold title. +- Added `execSync` import (merged with existing `exec as execCb`) for `gh --version`. +- Added `detectTerminal` import from `@teammates/consolonia`. +- All 304 CLI tests pass. Clean build with lint. + +### Files changed +- `packages/cli/src/commands.ts` — added `/about` command registration and `cmdAbout()` implementation +- `.teammates/beacon/memory/2026-03-30.md` + +## Task: Replace Unicode emoji symbols with ASCII to fix terminal tracers +User reported rendering tracers (ghost characters) in the `/about` diagnostic output caused by Unicode dingbat characters (`✔`, `✖`, `⚠`, `✓`, `ℹ`) having ambiguous width — some terminals treat them as double-width but the buffer assumes single-width. + +### Key decisions +- Replaced all Unicode status symbols with ASCII equivalents across the entire CLI package: `✔`/`✓` → `+`, `✖` → `x`, `⚠` → `!`, `ℹ` → `-`. +- In `/about` capability flags, changed `✔`/`✖` to `yes`/`no` for clarity. +- Left braille spinner characters (`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`) alone — they are single-width and don't cause tracers. +- Left consolonia test file's `✔` reference alone — it's testing width calculation, not displayed to users. +- All 304 CLI tests pass after changes. + +### Files changed +- `packages/cli/src/commands.ts` — replaced `✔`, `✖`, `⚠` with ASCII +- `packages/cli/src/cli.ts` — replaced `✖` with `x` +- `packages/cli/src/feed-renderer.ts` — replaced `⚠` with `!` +- `packages/cli/src/handoff-manager.ts` — replaced `✖`, `⚠` with ASCII +- `packages/cli/src/onboard-flow.ts` — replaced `✔`, `⚠` with ASCII +- `packages/cli/src/service-config.ts` — replaced `✓` with `+` +- `packages/cli/src/startup-manager.ts` — replaced `✔`, `✖`, `ℹ` with ASCII +- `.teammates/beacon/memory/2026-03-30.md` + +## Task: Fix charWidth for text-presentation symbols — restore emojis +User wanted emojis back, not ASCII replacements. Root cause: `charWidth()` treated entire Miscellaneous Symbols (0x2600-0x26FF) and Dingbats (0x2702-0x27B0) ranges as width 2, but characters like ✔ ✖ ⚠ ✓ ℹ have text presentation by default and render as 1 cell in terminals. The buffer allocated 2 cells (with continuation marker), but the terminal only used 1 — the continuation cell appeared as a "tracer" ghost character. + +### Key decisions +- **Fixed `charWidth()` in `symbol.ts`** — replaced the two broad emoji ranges with specific per-character/subrange checks for only the characters that have `Emoji_Presentation=Yes` in Unicode (e.g. ☔☕✅❌❗⚽⛽ etc.). Characters with text presentation (✔ ✖ ⚠ ✓ ℹ ✂ © ®) now correctly return width 1. +- Also removed other overly broad ranges: media controls (⏸-⏺), play/reverse buttons (▶◀), copyright/registered (©®), supplemental arrows, wavy dash — none have `Emoji_Presentation=Yes`. +- **Reverted all ASCII replacements** across 7 CLI source files back to original Unicode symbols. +- Updated consolonia `styled.test.ts` to expect width 1 for ✔ (was 2). +- Added 16 new tests in `pixel.test.ts`: 8 for text-presentation chars (width 1) and 8 for emoji-presentation chars (width 2). +- All 600 consolonia + 304 CLI tests pass. + +### Files changed +- `packages/consolonia/src/pixel/symbol.ts` — narrowed emoji width ranges to Emoji_Presentation=Yes only +- `packages/consolonia/src/__tests__/pixel.test.ts` — added text/emoji presentation width tests +- `packages/consolonia/src/__tests__/styled.test.ts` — updated ✔ width expectation from 2 to 1 +- `packages/cli/src/cli.ts` — restored ✖ symbols +- `packages/cli/src/commands.ts` — restored ✔ ✖ ⚠ symbols +- `packages/cli/src/feed-renderer.ts` — restored ⚠ symbols +- `packages/cli/src/handoff-manager.ts` — restored ✖ ⚠ symbols +- `packages/cli/src/onboard-flow.ts` — restored ✔ ⚠ symbols +- `packages/cli/src/service-config.ts` — restored ✓ symbols +- `packages/cli/src/startup-manager.ts` — restored ✔ ✖ ℹ symbols +- `.teammates/beacon/memory/2026-03-30.md` + +## Task: Add Windows Terminal wide overrides to charWidth from user screenshots +User screenshotted the dingbat test list and circled (red boxes) 17 characters that have tracers on Windows Terminal. These are text-presentation characters that Windows Terminal renders as double-width. Added them to `charWidth()` as a separate override section. + +### Key decisions +- Added 17 characters to width 2 in a new "Windows Terminal wide" section in `charWidth()`, separate from the Emoji_Presentation=Yes entries. +- Characters added: ℹ ★ ☆ ♠ ♣ ♥ ♦ ⚐ ⚑ ⚙ ⚠ ✔ ✖ ➜ ➤ ▶ ⏱ +- ⏳ and ⚡ were already width 2 (Emoji_Presentation=Yes) — no change needed. +- Updated pixel.test.ts: split "text-presentation" test into chars that remain width 1 (✓ ✂ © ®) and new "Windows Terminal wide" test for the 17 chars. +- Updated styled.test.ts: ✔ is now width 2, so `"✔ done"` span length changed from 6 to 7. +- All 601 consolonia tests pass. + +### Files changed +- `packages/consolonia/src/pixel/symbol.ts` — added 17 Windows Terminal wide overrides +- `packages/consolonia/src/__tests__/pixel.test.ts` — updated text-presentation test, added Windows Terminal wide test +- `packages/consolonia/src/__tests__/styled.test.ts` — updated ✔ width expectation from 1 to 2 +- `.teammates/beacon/memory/2026-03-30.md` diff --git a/.teammates/beacon/memory/decision_terminal_mouse_protocols.md b/.teammates/beacon/memory/decision_terminal_mouse_protocols.md new file mode 100644 index 0000000..ef9440c --- /dev/null +++ b/.teammates/beacon/memory/decision_terminal_mouse_protocols.md @@ -0,0 +1,26 @@ +--- +version: 0.7.0 +name: terminal_mouse_protocols +description: Consolonia should support both SGR and classic xterm mouse packets for clickable terminal actions. +type: decision +--- +# Terminal Mouse Protocols + +## Context +Clickable verbs in the terminal UI were working on one Windows machine but not another. The terminal setup already enabled mouse mode, but Consolonia's parser only accepted SGR mouse packets (`ESC [ < Cb ; Cx ; Cy M/m`). + +## Decision +Support both mouse packet families in Consolonia: + +- SGR mouse packets (`ESC [ < ...`) remain the preferred path. +- Classic xterm/ANSI mouse packets (`ESC [ M Cb Cx Cy`) are also decoded. +- Mouse mode enablement requests classic tracking (`?1000h`) in addition to the existing motion (`?1003h`) and SGR (`?1006h`) modes. + +## Why +- Some terminals or terminal configurations fall back to classic mouse packets even when newer modes are requested. +- Parsing only SGR makes clickable actions appear dead even though mouse mode is technically enabled. +- Supporting both formats keeps the UI terminal-agnostic without changing higher-level widgets. + +## Verification +- `npm run build` in `packages/consolonia` +- `npx vitest run src\__tests__\input.test.ts src\__tests__\ansi.test.ts` in `packages/consolonia` diff --git a/.teammates/beacon/memory/decision_win32_console_mode.md b/.teammates/beacon/memory/decision_win32_console_mode.md new file mode 100644 index 0000000..5cd45df --- /dev/null +++ b/.teammates/beacon/memory/decision_win32_console_mode.md @@ -0,0 +1,20 @@ +--- +version: 0.7.3 +name: Win32 mouse — pure ANSI, no SetConsoleMode +description: Consolonia creator confirmed ANSI escape sequences are the correct and more accurate approach for mouse tracking. Removed koffi/SetConsoleMode entirely. +type: feedback +--- + +# Mouse Input Strategy: Pure ANSI + +## Decision + +Use **ANSI DECSET escape sequences only** for mouse tracking. Do not call Win32 `SetConsoleMode()` via koffi or any FFI. + +**Why:** The Consolonia creator (external project author) explicitly advised that ANSI codes are more accurate for mouse tracking. Our koffi/`SetConsoleMode` approach was manipulating console mode flags at the kernel32 layer, which likely interfered with how the terminal emulator (Windows Terminal, VS Code xterm.js) delivers VT mouse sequences through ConPTY. + +**How to apply:** The ANSI sequences in `esc.ts` (`?1000h`, `?1003h`, `?1005h`, `?1006h`, `?1015h`, `?1016h`) are the single source of truth for mouse tracking. Do not add Win32 API calls for mouse. If mouse doesn't work, investigate the ANSI path — don't reach for native FFI. + +## Background + +The external C#/.NET Consolonia uses `ReadConsoleInput()` + `MOUSE_EVENT_RECORD` on Windows — a completely different input path. Node.js/libuv drops `MOUSE_EVENT` records from `ReadConsoleInputW`, so that path is not viable for us. The Win32 `SetConsoleMode` approach (clearing `ENABLE_QUICK_EDIT_MODE`, forcing `ENABLE_VIRTUAL_TERMINAL_INPUT`) was an attempt to work around this, but it was counterproductive — the Consolonia creator confirmed ANSI is the right approach. diff --git a/.teammates/beacon/memory/reference_terminal_mouse_sgr.md b/.teammates/beacon/memory/reference_terminal_mouse_sgr.md new file mode 100644 index 0000000..e8fabf2 --- /dev/null +++ b/.teammates/beacon/memory/reference_terminal_mouse_sgr.md @@ -0,0 +1,26 @@ +--- +version: 0.7.0 +name: terminal_mouse_sgr +description: ChatView clickable verbs depend on SGR mouse tracking support from the host terminal. +type: reference +--- +# Terminal Mouse SGR + +`@teammates/consolonia` enables mouse tracking in the app shell, but clickable action verbs only work when the terminal sends SGR mouse escape sequences. + +## Facts + +- `App` enables mouse tracking with `esc.mouseTrackingOn` when created with `mouse: true`. +- The CLI creates `App` with `mouse: true`. +- `MouseMatcher` only parses SGR extended mouse sequences in the form `\x1b[ ` — focus text narrows analysis. (4) Full prompt in debug logs via `fullPrompt` on TaskResult. Key: system tasks use unique `sys--` IDs for concurrent execution. Files: types.ts, cli.ts, cli-proxy.ts, copilot.ts + +## Task: System tasks fully background + standardized progress format +Removed system tasks from progress bar and /status. Added `startTime` to activeTasks. `formatElapsed()`: `(5s)` → `(2m 5s)` → `(1h 2m 5s)`. Format targets 80 chars. Files: cli.ts + +## Task: Fix system tasks blocking user task progress bar +Root cause: `silentAgents` was agent-level, suppressing ALL events including user tasks. Added `system?: boolean` to TaskAssignment/TaskResult. Filter by task flag, not agent. `silentAgents` retained only for defensive retry. Files: types.ts, orchestrator.ts, cli.ts + +## Task: Progress bar format — 80 chars, time in parens, cycling tag +`formatElapsed()` with escalating format. Dynamic task text truncation. Multiple tasks: `(1/3 - 2m 5s)`. Files: cli.ts + +## Task: Version bump to 0.5.2 +All 3 packages 0.5.1 → 0.5.2. Files: 3 package.json + +## Task: Footer — full project path with smart truncation +`truncatePath()` keeps first + last segments with `...` for long paths. Files: cli.ts + +## Task: Changelog v0.5.0→v0.5.2 +Compiled from daily logs + git diff. Identified cleanup targets in scribe/pipeline logs. + +## Task: Fix conversation history — store full message bodies +Root cause: storeResult stored `result.summary` (subject only). Now stores full cleaned `rawOutput` (protocol artifacts stripped). `buildConversationContext()` formats multi-line entries. 24k budget + auto-summarization handles increased size. Files: cli.ts + +## Task: v0.5.2 changelog summary + docs update +Updated CLI README (7 sections) and recall README (2 sections) for v0.5.0→v0.5.2 changes. Files: cli/README.md, recall/README.md + +## Task: Unit tests for conversation history functions +Extracted 5 pure functions to cli-utils.ts: cleanResponseBody, formatConversationEntry, buildConversationContext, findSummarizationSplit, buildSummarizationPrompt. 28 new tests. Files: cli-utils.ts, cli-utils.test.ts, cli.ts + +## Task: Version bump to 0.5.3 +All 3 packages 0.5.2 → 0.5.3. Files: 3 package.json + +## Task: Lint after build feedback + lint fixes +Fixed 3 lint issues. Saved feedback memory (always lint after build). Files: cli.ts, feedback_lint_after_build.md + +## Task: Sandbox & Isolation design spec +Created spec at `.teammates/scribe/docs/specs/F-sandbox-isolation.md`. Two tiers: worktrees (~300 LOC) and containers (~500 LOC). Separate `@teammates/sandbox` package. Files: F-sandbox-isolation.md + +## Task: Cross-folder write boundary enforcement (Layers 1 & 2) +Layer 1: prompt rule in adapter.ts for `type: "ai"` teammates. Layer 2: `auditCrossFolderWrites()` post-task with [revert]/[allow] actions. Allows own folder, `_` prefix, `.` prefix, root-level .teammates/ files. Files: adapter.ts, cli.ts + +## 2026-03-27 + +--- +version: 0.7.0 +type: daily +compressed: true +--- +# 2026-03-27 + +## Task: Implement interrupt-and-resume (Phase 1) +`/interrupt [message]` (alias: `/int`). Kills running agent, parses conversation log, queues resumed task with ``. New `InterruptState` type. `killAgent?()` on AgentAdapter. CliProxyAdapter: `activeProcesses` map, deferred promise pattern in `spawnAndProxy`. New `log-parser.ts`: parseClaudeDebugLog, parseCodexOutput, parseRawOutput, formatLogTimeline (groups 4+ consecutive), buildConversationLog. `buildResumePrompt()` in cli.ts. `getAdapter()` on Orchestrator. Key: deferred promise shared between executeTask and killAgent, log parser extracts paths not content, resume uses normal buildTeammatePrompt wrapping. 11 new tests. Files: types.ts, adapter.ts, orchestrator.ts, cli-proxy.ts, log-parser.ts, log-parser.test.ts, cli.ts, index.ts + +## Task: Version bump to 0.6.0 + migration logic +All 3 packages 0.5.3 → 0.6.0. `findUncompressedDailies()` and `buildMigrationCompressionPrompt()` in compact.ts. `semverLessThan()` in cli.ts. Migration queues system tasks with `migration: true` for uncompressed dailies, re-indexes after all complete via `pendingMigrationSyncs` counter. Files: 3 package.json, compact.ts, cli.ts, types.ts, index.ts + +## Task: Attention dilution fixes + daily compression + version tracking +5 fixes for Scribe's attention problem: (1) dedup recall against daily logs, (2) cut DAILY_LOG_BUDGET_TOKENS 24K→12K, (3) echo user's request at bottom of instructions (<500 chars verbatim, ≥500 chars pointer), (4) task-first priority statement at top of instructions, (5) conversation history to file when >8K chars. Daily compression: `buildDailyCompressionPrompt()` queued as system task on startup. Version tracking: `checkVersionUpdate()` reads/writes cliVersion in settings.json. Files: adapter.ts, adapter.test.ts, cli.ts, compact.ts, index.ts + +## Task: Version field for memory files + fix migration ordering +Added `version: 0.6.0` frontmatter to all 22 beacon memory files (dailies, weeklies, typed). `stripVersionField()` in registry.ts and adapter.ts strips the version line before injecting content into prompts. Updated compact.ts: `buildWeeklySummary`, `buildMonthlySummary`, and both compression prompt templates now include version in frontmatter. Updated adapter.ts memory instructions to tell agents to include `version: 0.6.0` in new files. Split `checkVersionUpdate()` into read-only check + `commitVersionUpdate()` write — version is persisted LAST, only after all migration tasks complete (or immediately if no migration needed). Files: registry.ts, adapter.ts, compact.ts, cli.ts, 22 memory files + +## Task: Add `type: daily` to daily memories + remove version stripping +User reversed the "strip metadata from prompts" decision — metadata fields are fine to return to the model. Added `type: daily` to all 13 daily log frontmatter files. Removed `stripVersionField()` from both registry.ts and adapter.ts — daily logs, weekly logs, and recall results now pass through with full frontmatter intact. Updated compression prompts in compact.ts to include `type: daily` in generated frontmatter. Updated adapter.ts memory instructions to tell agents to include `type: daily` in new daily logs. Files: registry.ts, adapter.ts, compact.ts, 13 daily memory files + +## Task: Add /script command +New `/script` command lets users create and run reusable scripts via the coding agent. Scripts stored under the user's twin folder (`.teammates//scripts/`). Three modes: `/script list` (show saved scripts), `/script run ` (execute existing), `/script ` (create + run new). Added `"script"` queue entry type. Wordwheel completions for subcommands and script filenames. The coding agent always handles `/script` tasks — routes to `selfName`. Files: types.ts, cli.ts + +## Task: Pre-dispatch conversation compression + 128k target context +Target context window increased to 128k tokens. Conversation history budget derived dynamically: `(TARGET_CONTEXT_TOKENS - PROMPT_OVERHEAD_TOKENS) * CHARS_PER_TOKEN` = 96k tokens = 384k chars. New `preDispatchCompress()` runs before every task dispatch — if history exceeds budget, mechanically compresses oldest entries into bullet summaries via `compressConversationEntries()`. Async agent summarization (`maybeQueueSummarization`) still runs post-task for quality. `CONV_FILE_THRESHOLD` raised from 8k→64k chars. 6 new tests. Files: cli-utils.ts, cli-utils.test.ts, cli.ts + +## Task: Diagnose @everyone context loss +Diagnosed why 3/6 teammates failed during @everyone voting dispatch. Four root causes: (1) `preDispatchCompress()` mutates shared `conversationHistory` — first drain loop can destroy context for concurrent drains, (2) `buildConversationContext()` is called per-drain-loop with no snapshot isolation, (3) conversation.md is a single shared file — concurrent writes/reads race, (4) agents may not read the file pointer. Proposed fixes: atomic snapshot at queue time, per-agent temp files, inline small contexts for @everyone, and injecting relevant conversation segments directly into the task prompt. + +## Task: Fix @everyone context loss (Fixes 1-3) +Implemented three fixes for concurrent dispatch race conditions. (1) **Atomic snapshot:** Added `contextSnapshot` field on agent `QueueEntry` — `queueTask()` freezes `conversationHistory` + `conversationSummary` once before pushing all @everyone entries, each entry gets a shallow copy. (2) **Per-agent temp files:** `buildConversationContext()` now accepts optional `teammate` param and writes `conversation-.md` instead of shared `conversation.md`. (3) **Snapshot bypass in drain:** `drainAgentQueue()` skips `preDispatchCompress()` when entry has a snapshot, passes snapshot directly to `buildConversationContext()`. Fix 4 (relevant segment injection) deferred — deeper architectural change. Files: types.ts, cli.ts + +## Task: Remove conversation.md file offload +Removed the file offload path from `buildConversationContext()` — conversation context is now always inlined in the prompt. Removed `CONV_FILE_THRESHOLD` (64k) constant. Pre-dispatch compression already keeps history within the 384k char budget, making the file offload redundant. This eliminates Bug 4 (agents not reading the temp file). Files: cli.ts + +## Task: Version bump to 0.6.1 +All 3 packages 0.6.0 → 0.6.1. Files: 3 package.json + +## Task: Fix /interrupt placeholder and teammate dropdown +Changed usage from `` to `[teammate]` (3 sites: command definition, JSDoc, error hint). Added `interrupt` and `int` (alias) to `TEAMMATE_ARG_POSITIONS` so the wordwheel shows teammate dropdown like `/debug` does. Files: cli.ts + +## Task: Fix [copy] action copying wrong response +Bug: `[copy]` buttons all shared static `id: "copy"`, so every click copied `lastCleanedOutput` (most recent response). Fix: each `[copy]` now gets unique ID (`copy--`) with its cleaned text stored in `_copyContexts` map. Handler looks up by ID, falls back to `lastCleanedOutput`. Files: cli.ts + +## Task: Fix user avatar leaking into other projects +Bug: when importing teammates from a project, user avatar folders (e.g., `stevenic/`) were imported as teammates — and USER.md was copied from the source. Three fixes: (1) `importTeammates()` now skips folders where SOUL.md has `**Type:** human`. (2) Removed USER.md copying from import (user-specific, gitignored). (3) Fixed case-sensitivity in `needsUserSetup()` — template has `` but check was for lowercase ``. User said don't touch .gitignore — just skip humans during import. Files: onboard.ts, cli.ts + +## Task: Fix ChatView scrollbar hanging under concurrent task load +Root cause: spinner animating at 80ms calling `app.refresh()` → `_fullRender()` which re-measures **every** feed line via `_renderFeed()` O(N) loop on every frame. With hundreds of accumulated lines and 12.5 FPS, the event loop saturates. Three fixes: (1) **Feed line height cache** — `_feedHeightCache[]` stores measured heights per line, invalidated on width change or content mutation. New lines measured once, cached forever. (2) **Coalesced refresh** — new `app.scheduleRefresh()` uses `setImmediate` coalescing (like `_scheduleRender`) so rapid progress updates collapse into a single render. Progress bar spinner switched from `refresh()` to `scheduleRefresh()`. (3) **Spinner interval 80ms→200ms** — still smooth for terminal, cuts render frequency 60%. Cache invalidation wired into `clear()`, `updateFeedLine()`, and hover state toggles. Files: chat-view.ts, app.ts, cli.ts + +## Task: Fix /script placeholder hint not clearing +Bug: typing `/script make a build tool` still showed dim placeholder text from the usage string. Root cause: usage was `"/script [list | run | what should the script do?]"` — `getCommandHint()` splits by whitespace, producing 10 tokens that never get consumed by typed args. Fix: simplified usage to `"/script [description]"` — single placeholder token that clears after first arg. Detailed help still shown via `/script` (no args). Files: cli.ts + +## Task: Version bump to 0.6.2 +All 3 packages 0.6.1 → 0.6.2. Files: 3 package.json + +## 2026-03-28 + +## Task: Fix `this.app.scheduleRefresh is not a function` error +Bug: pinned workspace deps `"@teammates/consolonia": "0.6.0"` resolved to registry version missing `scheduleRefresh()`. Fix: changed both workspace deps to `"*"`. Files: cli/package.json + +## Task: Version bump to 0.6.3 +All 3 packages 0.6.2 → 0.6.3. Updated cliVersion in settings.json. Clean build + lint + 924 tests pass. Files: 3 package.json, settings.json + +## Task: Threaded task view — Phase 1 (data model + thread tracking) +`TaskThread`/`ThreadEntry` interfaces, `threadId` on all `QueueEntry` variants, `createThread`/`getThread`/`appendThreadEntry` methods, `#id` prefix parsing, handoff threadId propagation. Thread IDs are session-scoped auto-incrementing integers. Files: types.ts, cli.ts, index.ts + +## Task: Threaded task view — Phase 2 (feed rendering) +ChatView: `insertToFeed`/`insertStyledToFeed`/`insertActionList`, `_shiftFeedIndices`, visibility API for collapse. CLI: thread feed ranges, working placeholders, `displayFlatResult`/`displayThreadedResult` split, collapse toggles. Files: chat-view.ts, cli.ts + +## Task: Threaded task view — Phase 3 (routing + context) +`buildThreadContext()` for thread-local conversation context, auto-focus routing via `focusedThreadId`, `#id` wordwheel completion, thread-aware `/status`. Thread context fully replaces global when threadId is set. Files: cli-utils.ts, cli.ts, index.ts + +## Task: Fix agents producing lazy responses from stale session/log state +3 prompt guardrails in adapter.ts: (1) "Task completed" not valid body, (2) prior session entries ≠ user received output, (3) only log work from THIS turn. Files: adapter.ts + +## Task: Thread format redesign — in-place rendering +Merged dispatch info into thread header (`#1 → @names`). Working placeholders show `@name - working on task...`. Responses render in-place at placeholder position via `_threadInsertAt` override. Files: cli.ts + +## Task: Thread dispatch line — merge into user message block +Changed `renderThreadHeader` from standalone action to `feedUserLine` with dark bg (matches user message). Files: cli.ts + +## Task: Thread rendering polish — 5 visual fixes +(1) Blank line between user msg and thread header. (2) Placeholder format `@name: working...` + `completeWorkingPlaceholder()`. (3) Blank line after each reply. (4) Removed `#id` input seeding. (5) Response ordering fix. Files: cli.ts + +## Task: Fix blank line between dispatch line and working placeholders +Changed `feedLine()` to `threadFeedLine(tid, "")` in all 3 `queueTask` paths — ensures blank line stays within thread range. Files: cli.ts + +## Task: Thread reorder + smart auto-scroll +Completed responses now insert before remaining working placeholders (first-to-finish at top). `_userScrolledAway` flag in ChatView prevents auto-scroll when user scrolled up. Files: chat-view.ts, cli.ts + +## Task: Fix thread rendering — #id in header + endIdx double-increment +(1) Added `#id` prefix to dispatch line. (2) Fixed `endIdx` double-increment in `threadFeedLine`/`threadFeedActionList` — saved `oldEnd` before shift, only manual increment if shift didn't extend range. Files: cli.ts + +## Task: Fix ChatView _shiftFeedIndices off-by-one — response headers hidden +All 3 insert methods called `_shiftFeedIndices(clamped + 1, 1)` but should be `clamped` — off-by-one corrupted hidden set, height cache, hover state. Fixed all 3 sites. Files: chat-view.ts + +## Task: Thread view redesign — Phase 1 (ThreadContainer class) +New `ThreadContainer` class (~230 LOC) encapsulating per-thread feed-line index management. Replaced 5 scattered maps + 10+ methods in cli.ts. Files: thread-container.ts (NEW), cli.ts, index.ts + +## Task: Thread view redesign — Phase 2 (verb relocation) +Per-item `[reply]`/`[copy]` → inline subject-line actions (`[show/hide] [copy]`). Thread-level `[reply] [copy thread]` at container bottom. `insertThreadActions` on ThreadContainer. Files: thread-container.ts, cli.ts + +## Task: Thread view redesign — Phase 3 (input routing update) +`@mention` breaks focus + creates new thread. Auto-focus fallback uses `focusedAt` timestamp. `updateFooterHint()` shows `replying to #N`. Files: cli.ts + +## Task: Fix [reply] verb not working in threaded task view +(1) `getInsertPoint()` fallback to `endIdx` was past `replyActionIdx` — now checks `replyActionIdx` first. (2) `renderWorkingPlaceholder` moved outside `threadId == null` guard. Files: thread-container.ts, cli.ts + +## Task: Fix [reply] verb + [show/hide] toggle display +(1) `addPlaceholder()` now inserts before `replyActionIdx`. (2) New `updateActionList()` on ChatView swaps `[show]`/`[hide]` text dynamically. Files: chat-view.ts, thread-container.ts, cli.ts + +## Task: Fix thread reply rendering — 4 issues +(1) `renderThreadReply()` for user messages inside threads. (2) Dispatch line for replies. (3) `hideThreadActions()`/`showThreadActions()` during work. (4) Thread verbs position fix. Files: thread-container.ts, cli.ts + +## Task: Version bump to 0.7.0 +All 3 packages 0.6.3 → 0.7.0. Updated cliVersion in settings.json. Files: 3 package.json, settings.json + +## 2026-03-29 + +## Task: Fix _shiftFeedIndices off-by-one — actually apply the fix +All three ChatView insert methods still had `clamped + 1` instead of `clamped` — logged as fixed on 03-28 but never committed. Applied the fix to all 3 sites. Files: chat-view.ts + +## Task: Fix [reply] verb — copy #id into input box +Added `chatView.inputValue = #${tid}` to the `thread-reply-*` action handler. Files: cli.ts + +## Task: Fix [reply] [copy thread] position + reply indentation +(1) Added `peekInsertPoint()` to ThreadContainer — non-destructive read vs `getInsertPoint()` which auto-increments. (2) Added 4-space indent to all continuation/wrapped lines in `renderThreadReply()`. Files: thread-container.ts, cli.ts + +## Task: Extract modules from cli.ts — Phase 1 +Extracted 5 modules (6815 → 5549 lines): `status-tracker.ts`, `handoff-manager.ts`, `retro-manager.ts`, `wordwheel.ts`, `service-config.ts`. Each receives deps via typed interface. Closure-based getters bridge private state. Files: 5 new modules, cli.ts, index.ts + +## Task: Extract modules from cli.ts — Phase 2 +Extracted 2 more modules (5549 → 4159 lines, -39% total): `thread-manager.ts` (579 lines), `onboard-flow.ts` (1089 lines). Slash commands NOT extracted — too entangled with cli.ts private state. Files: 2 new modules, cli.ts, index.ts + +## Task: Create migration guide for version upgrades +New `migrations.ts` module with `Migration` interface, `semverLessThan()`, `getMigrationsForUpgrade()`. Two migrations: 0.5→0.6 (compress logs), 0.6→0.7 (update frontmatter). Startup loop runs programmatic + agent migrations in order. Files: migrations.ts (NEW), cli.ts, adapter.ts, compact.ts, index.ts + +## Task: Simplify migrations to MIGRATIONS.md +Replaced typed Migration system with plain markdown file. `buildMigrationPrompt()` parses sections, filters by version, one agent task per teammate. Files: MIGRATIONS.md (NEW), migrations.ts, cli.ts, index.ts + +## Task: Migration progress indicator + interruption guard +`setProgress("Upgrading to v0.7.0...")` during migration. `commitVersionUpdate()` only fires when all migrations complete — interrupted CLI re-runs on next startup. Files: cli.ts + +## Task: Move MIGRATIONS.md to CLI package +Moved from `.teammates/` to `packages/cli/`. Resolves via `import.meta.url` (ESM). Added to `files` in package.json. Files: MIGRATIONS.md, migrations.ts, cli.ts, package.json + +## Task: Fix migration not triggering — __dirname in ESM +`__dirname` undefined in ESM — `readFileSync` silently caught error, skipping all migrations. Fixed with `fileURLToPath(new URL("../MIGRATIONS.md", import.meta.url))`. Files: migrations.ts + +## Task: Fix thread reply indentation + header spacing +Thread reply indent 4→2 chars. Header double space → single space. Files: thread-manager.ts + +## Task: Remove @ prefix from thread names + fix item copy +Removed `@` prefix from all thread rendering (headers, dispatch, placeholders, subjects, clipboard). Item `[copy]` now includes `teammate: subject` header. Files: thread-manager.ts + +## Task: Fix migration progress to use standard StatusTracker +User feedback: migration spinner was custom code duplicating StatusTracker's job. Removed custom spinner (fields, constant, methods). New `startMigrationProgress()` adds a synthetic `activeTasks` entry with teammate="teammates", then calls `startStatusAnimation()`. Now renders as standard `⠋ teammates... Upgrading to v0.7.0... (5s)` format, rotating with any concurrent tasks. Files: cli.ts + +## Task: Remove stuck "Upgraded" feed lines after migration +Two permanent `feedLine()` calls left "✔ Updated from v0.5.0 → v0.7.0" and "✔ Upgraded to v0.7.0" stuck at the bottom of the feed. Removed both — the progress spinner already communicates the upgrade, and the version shows in the banner. Removed: `feedLine` in `commitVersionUpdate()` (line 3658) and migration completion handler (line 1077). Files: cli.ts + +## Task: Redesign progress API — startTask/stopTask/showNotification +Rewrote StatusTracker with clean 3-method public API: `startTask(id, teammate, description)`, `stopTask(id)`, `showNotification(content: StyledLine)`. Tasks rotate with spinner + elapsed time. Notifications are one-shot styled messages that show once then auto-purge on next rotation. Animation lifecycle is fully private — callers never manage start/stop. Updated all 15+ call sites in cli.ts. Removed 6 wrapper methods. Clipboard and compact now use `showNotification()` instead of manual setProgress/setTimeout chains. Files: status-tracker.ts, cli.ts + +## Task: Fix handoffs rendering outside thread container +Handoff boxes were appended to the global feed via `feedLine()`, placing them AFTER the thread container's `[reply] [copy thread]` verbs. Root cause: `HandoffManager.renderHandoffs()` had no concept of thread containers — it always used global feed append. Fix: added `HandoffContainerCtx` interface with `insertLine()`/`insertActions()` methods. When `containerCtx` is provided, handoff lines are inserted via the container (staying within the thread range) instead of appended globally. ThreadManager now creates and passes a `containerCtx` wrapping the thread's `ThreadContainer`. Files: handoff-manager.ts, thread-manager.ts, cli.ts + +## Task: Fix system tasks polluting daily logs (wisdom distillation, compaction, etc.) +Root cause: `buildTeammatePrompt()` always included memory update instructions telling agents to write daily logs — even for system tasks like wisdom distillation. The `system` flag on `TaskAssignment` was propagated to event handlers but never passed to the prompt builder. Fix: added `system?: boolean` option to `buildTeammatePrompt()` and the `AgentAdapter.executeTask()` interface. When `system` is true, the Memory Updates section tells the agent "Do NOT update daily logs, typed memories, or WISDOM.md." Threaded through orchestrator → all 3 adapters (cli-proxy, copilot, echo). Files: adapter.ts, orchestrator.ts, cli-proxy.ts, copilot.ts, echo.ts + +## Task: Add real-time activity tracking + [show activity] [cancel] verbs +New feature: working placeholders now show `[show activity]` and `[cancel]` clickable verbs. Activity events are parsed from Claude's debug log in real-time via file polling. The `[show activity]` toggle inserts timestamped `MM:SS Tool detail` lines below the placeholder — updates live as the agent works. `[cancel]` kills the running agent process. + +### Architecture +1. **activity-watcher.ts** (NEW) — `parseClaudeActivity()` extracts tool use events from Claude debug logs (PostToolUse hook lines, tool errors, file write events). `watchDebugLog()` polls the file and calls a callback with new events. `formatActivityTime()` formats elapsed time as `MM:SS`. +2. **ActivityEvent type** in types.ts — `{ elapsedMs, tool, detail?, isError? }` +3. **onActivity callback** on `TaskAssignment` — threaded through orchestrator → adapter → cli-proxy's `spawnAndProxy()`. Watcher starts after process spawn, stops on process close. +4. **Working placeholder as action list** — changed `ThreadContainer.addPlaceholder()` from `insertStyledToFeed` to `insertActionList` with `[show activity]` and `[cancel]` verbs. Added `getPlaceholderIndex()` public method. +5. **Activity buffer in cli.ts** — `_activityBuffers` (events per teammate), `_activityShown` (toggle state), `_activityLineIndices` (feed line indices for hiding), `_activityThreadIds`. Activity lines inserted via `insertStyledToFeed` + `shiftAllContainers`. +6. **Action handlers** for `activity-*` (toggle show/hide) and `cancel-*` (kill agent) in the `chatView.on("action")` handler. + +### Key decisions +- File polling via `fs.watchFile` (1s interval) for Windows reliability +- Only tracks "work" tools (Read, Write, Edit, Bash, Grep, Glob, Search, Agent, WebFetch, WebSearch) — filters out settings, config, hooks, autocompact noise +- Activity line format: ` MM:SS Tool detail` with dimmed text, errors highlighted +- Cancel uses existing `killAgent()` on the adapter (SIGTERM → SIGKILL escalation) +- Started with Claude only — Codex can be added later via JSONL stdout parsing + +### Files changed +- activity-watcher.ts (NEW ~150 lines) +- types.ts (ActivityEvent interface, onActivity on TaskAssignment) +- adapter.ts (onActivity in executeTask options) +- orchestrator.ts (thread onActivity through to adapter) +- cli-proxy.ts (start/stop watcher in spawnAndProxy) +- echo.ts, copilot.ts (accept broader options type) +- thread-container.ts (addPlaceholder → action list, getPlaceholderIndex) +- thread-manager.ts (placeholder with [show activity] [cancel] verbs) +- cli.ts (activity state, handleActivityEvents, toggleActivity, cancelTask, action handlers) +- index.ts (export new module + ActivityEvent type) + +## Task: Fix 3 activity tracking bugs +Three issues reported by user: (1) No visual feedback when clicking [show activity] — added `insertActivityHeader()` that inserts an "Activity" header line in accent color immediately on first toggle. (2) Missing tool detail for Read/Edit — rewrote `parseClaudeActivity()` to merge "written atomically"/"Renaming" file paths into the subsequent PostToolUse event, eliminating duplicate Write+Edit entries and showing filenames like `Edit WISDOM.md`. Read still can't show file paths (Claude debug log doesn't log them). (3) Activity lines persisted after teammate responds — added `cleanupActivityLines()` that hides all activity feed lines before deleting state. Called from both task completion and cancel paths. Files: activity-watcher.ts, cli.ts + +## Task: Fix activity tracking — add tool details via PostToolUse hook +Root cause: Claude's debug log only contains tool names (`PostToolUse with query: Read`), never tool parameters. Activity lines showed `00:07 Read` with no file path. + +### Solution: PostToolUse hook system +1. **`scripts/activity-hook.mjs`** (NEW) — Node.js hook script that Claude fires after every tool call. Reads JSON from stdin (`{tool_name, tool_input}`), extracts relevant detail (file_path for Read/Edit/Write, command for Bash, pattern for Grep/Glob), appends one-line entry to `$TEAMMATES_ACTIVITY_LOG` file. +2. **`activity-hook.ts`** (NEW) — `ensureActivityHook(projectDir)` auto-installs the hook in `.claude/settings.local.json` at CLI startup. Idempotent — checks for existing hook before adding. +3. **`activity-watcher.ts`** — New `parseActivityLog()` and `watchActivityLog()` for the hook log format. New `watchDebugLogErrors()` watches debug log for errors only. Legacy `parseClaudeActivity()`/`watchDebugLog()` retained for backward compat. +4. **`cli-proxy.ts`** — Sets `TEAMMATES_ACTIVITY_LOG` env var on spawned agent process pointing to per-agent activity file in `.teammates/.tmp/activity/`. Watches both activity log (tool details) and debug log (errors). +5. **`cli.ts`** — Calls `ensureActivityHook()` in `startupMaintenance()`. Cleans old activity log files (>1 day). + +### Expected result +Activity lines will now show: `00:07 Read WISDOM.md`, `00:21 Bash npm run build`, `00:29 Grep /pattern/` instead of bare tool names. + +### Files changed +- scripts/activity-hook.mjs (NEW) +- activity-hook.ts (NEW) +- activity-watcher.ts (rewritten — hook log parser + error-only debug parser + legacy compat) +- cli-proxy.ts (env var + dual watcher setup) +- cli.ts (hook install at startup + activity dir cleanup) +- index.ts (new exports) + +## Task: Fix activity lines persisting after task completion + add trailing blank line +Root cause: `shiftAllContainers` shifted thread container and handoff indices but NOT `_activityLineIndices`. When other feed inserts happened, stored activity indices went stale — `cleanupActivityLines` hid wrong lines, leaving real activity lines visible. Fix: wrapped `shiftAllContainers` getter to also shift all `_activityLineIndices` and `_activityBlankIdx` entries. Removed manual same-teammate index shift in `insertActivityLines` (would double-shift). Also added trailing blank line after activity block: new `_activityBlankIdx` map tracks one blank line per teammate, inserted in `insertActivityHeader`, toggled in `toggleActivity`, cleaned in `cleanupActivityLines`. Files: cli.ts + +## Task: Fix activity tracking not writing anything — add debug log fallback +Root cause: `cli-proxy.ts` only watched the activity hook log (`watchActivityLog`) and debug log errors (`watchDebugLogErrors`). The PostToolUse hook script (`activity-hook.mjs`) doesn't fire — Claude Code doesn't propagate `TEAMMATES_ACTIVITY_LOG` env var to hook subprocesses. Debug log has 45+ `PostToolUse` entries per task, but the legacy full parser (`watchDebugLog`) was never wired up as a fallback. Fix: added `watchDebugLog` as a fallback alongside the hook watcher. Dedup wrapper: if the hook produces events, suppress the legacy parser to avoid duplicates. Three watchers now: (1) hook log for rich detail, (2) legacy debug log for tool names when hook doesn't fire, (3) debug log errors. Files: cli-proxy.ts + +## Task: Chat widget redesign — Phase 1 (FeedStore) +Implemented Phase 1 of the widget model redesign spec (F-widget-model-redesign.md). Created `FeedStore` class that replaces the five parallel index-keyed data structures in ChatView with a single identity-based item collection. + +### What was done +1. **New `feed-store.ts`** (~110 lines) — `FeedItem` interface with `id`, `content`, `actions`, `hidden`, `cachedHeight` fields. `FeedStore` class with `push()`, `insert()`, `get()`, `at()`, `indexOf()`, `clear()`, `invalidateAllHeights()`, `hasActions` getter. +2. **Refactored ChatView** — replaced `_feedLines[]`, `_feedActions` Map, `_hiddenFeedLines` Set, `_feedHeightCache[]`, and `_hoveredAction` index with a single `_store: FeedStore` + `_hoveredItemId: string | null`. +3. **Deleted `_shiftFeedIndices()` entirely** — no more parallel structure shifting on insert. FeedStore handles the single array splice. +4. **Height cache on FeedItem** — went beyond spec (which said keep index-based) by storing `cachedHeight` directly on each `FeedItem`, eliminating the last parallel array. +5. **Moved types** — `FeedActionItem` and `FeedActionEntry` now defined in feed-store.ts, re-exported from chat-view.ts for backward compat. +6. **Updated exports** in consolonia's index.ts — exports `FeedStore`, `FeedItem`, `FeedActionEntry`. + +### Key decisions +- External API stays index-based (callers still pass indices) — no breaking changes for cli.ts or thread-container.ts +- FeedStore is internal to ChatView for Phase 1 — public API unchanged +- Hover tracking switched from index (`_hoveredAction: number`) to ID (`_hoveredItemId: string | null`) +- `cachedHeight` on FeedItem instead of separate array — simpler, no parallel structure + +### Build & test +- TypeScript compiles cleanly (strict mode) — consolonia + cli +- 561 consolonia tests pass, 269 CLI tests pass (830 total) + +### Files changed +- `packages/consolonia/src/widgets/feed-store.ts` (NEW ~110 lines) +- `packages/consolonia/src/widgets/chat-view.ts` (refactored — removed ~50 lines net) +- `packages/consolonia/src/index.ts` (new exports) + +## Task: Collapse activity log — smart event grouping +Activity log was showing every raw tool call individually (30+ noisy lines for a typical task). Added `collapseActivityEvents()` that groups events into a compact summary. + +### Collapsing rules +- Consecutive research tools (Read, Grep, Glob, Search, Agent) → single "Exploring (14× Read, 2× Grep, ...)" line in accent color +- Bash without detail → folded into research; Bash with detail → shown individually +- Consecutive Edit/Write to same file → single "Edit chat-view.ts (×7)" line +- TodoWrite and ToolSearch filtered out entirely (internal plumbing) +- Errors never collapsed + +### Rendering changes +- `handleActivityEvents()` now calls `rerenderActivityLines()` which hides stale display lines and re-inserts fresh collapsed view +- `toggleActivity()` shows collapsed events on first toggle +- Removed TodoWrite from WORK_TOOLS (no longer tracked) + +### Build & test +- TypeScript compiles cleanly (strict mode) +- 269 CLI tests pass + +### Files changed +- `packages/cli/src/activity-watcher.ts` — RESEARCH_TOOLS set, `collapseActivityEvents()` function +- `packages/cli/src/cli.ts` — import, `rerenderActivityLines()`, updated `handleActivityEvents`/`toggleActivity`/`insertActivityLines` +- `packages/cli/src/index.ts` — new export + +## Task: Widget refactor Phase 2 — VirtualList extraction +Extracted the scrollable list rendering from ChatView into a reusable `VirtualList` widget per the F-widget-model-redesign spec. + +### What was done +1. **New `virtual-list.ts`** (~280 lines) — `VirtualListItem` interface, `VirtualListOptions`, `VirtualList` class (extends Control). Manages: scroll state, ID-keyed height cache (Map), screen-to-item mapping, scrollbar rendering/interaction, overlay callback. +2. **FeedStore cleanup** — Removed `cachedHeight` from `FeedItem` interface and `invalidateAllHeights()` from `FeedStore`. Height caching now lives solely in VirtualList. +3. **ChatView refactored** — Removed 16 private fields (scroll offset, scrollbar geometry, screen maps, drag state, feed geometry). Deleted `_renderFeed()` (~175 lines). Added `_buildVirtualListItems()` that assembles banner + separator + FeedStore items into `VirtualListItem[]`. Added `_feedLineAtScreen()` helper for hit-testing. All scroll/invalidation methods delegate to VirtualList. +4. **Selection overlay** — Rendered via `VirtualList.onRenderOverlay` callback (called after items render, before clip pop). +5. **Updated exports** — `VirtualList`, `VirtualListItem`, `VirtualListOptions` exported from consolonia index.ts. + +### Key decisions +- VirtualList is a generic reusable widget (VirtualListItem interface, not coupled to FeedStore) +- FeedItem satisfies VirtualListItem structurally (TypeScript duck typing) — no wrapper needed +- Banner + separator are prefix items with fixed IDs (`__banner__`, `__topsep__`) +- `_feedItemOffset` tracks count of prefix items for feed line index math +- Items array rebuilt each render() call (cheap — just references, heights cached by ID) +- Mouse handling stays in ChatView — VirtualList exposes scrollbar geometry + interaction methods + +### Build & test +- TypeScript compiles cleanly (strict mode) — consolonia + cli +- 561 consolonia tests pass, 269 CLI tests pass (830 total) + +### Files changed +- `packages/consolonia/src/widgets/virtual-list.ts` (NEW ~280 lines) +- `packages/consolonia/src/widgets/feed-store.ts` (removed cachedHeight, invalidateAllHeights) +- `packages/consolonia/src/widgets/chat-view.ts` (major refactor — net ~170 lines removed) +- `packages/consolonia/src/index.ts` (new exports) + +## Task: Fix banner animation not rendering after VirtualList extraction +Root cause: VirtualList caches item heights by ID. The banner (`__banner__`) changes height during animation (adds lines per phase), but the cached height from the first measurement was never invalidated — so the banner appeared frozen/invisible. Fix: added `this._feed.invalidateItem("__banner__")` in `_buildVirtualListItems()` so the banner is re-measured every render. Files: chat-view.ts + +## Task: Change progress message separator from `...` to `-` +Changed progress message format from `... ` to ` - ` at all 3 rendering sites in StatusTracker: TUI width-calc prefix, TUI styled output, and fallback chalk output. Files: status-tracker.ts + +## Task: Fix "Error: write EOF" crash with Codex adapter +Unhandled `EPIPE`/`EOF` error on `child.stdin` when Codex closes its stdin pipe before the prompt write completes. Added `child.stdin.on("error", () => {})` error swallower at all 3 stdin write sites in `cli-proxy.ts`: the `executeTask` spawn path, the `spawnAndProxy` stdin-prompt path, and the interactive stdin forwarding path. Files: cli-proxy.ts + +## Task: Split adapters into separate files per coding agent +User requested each coding agent have its own adapter file instead of all presets in cli-proxy.ts. Created 3 new files: + +### Architecture +- **`presets.ts`** (NEW) — Defines `CLAUDE_PRESET`, `CODEX_PRESET`, `AIDER_PRESET` and aggregates them into `PRESETS`. Separated from cli-proxy.ts to break circular imports (adapter files extend CliProxyAdapter). +- **`claude.ts`** (NEW) — `ClaudeAdapter` extends `CliProxyAdapter` with Claude-specific preset. `ClaudeAdapterOptions` interface. +- **`codex.ts`** (NEW) — `CodexAdapter` extends `CliProxyAdapter` with Codex-specific preset. `CodexAdapterOptions` interface. +- **`copilot.ts`** — Already existed as a separate adapter (SDK-based, not CLI-proxy). +- **`cli-proxy.ts`** — Shared base class (`CliProxyAdapter`), spawn logic, output parsing. Presets removed, re-exported from `presets.ts`. + +### Key decisions +- Preset definitions extracted to `presets.ts` to avoid circular dependency: claude.ts/codex.ts → cli-proxy.ts (for CliProxyAdapter) and cli-proxy.ts → presets.ts (for PRESETS). If presets stayed in cli-proxy.ts, the circular import would cause `CliProxyAdapter` to be undefined when adapter files try to extend it. +- `resolveAdapter()` in cli-args.ts now instantiates `ClaudeAdapter`/`CodexAdapter` directly instead of `new CliProxyAdapter({ preset: name })`. Aider still goes through the generic path. +- All adapter classes + option types exported from index.ts (including CopilotAdapter which wasn't exported before). + +### Files changed +- `adapters/presets.ts` (NEW ~75 lines) +- `adapters/claude.ts` (NEW ~30 lines) +- `adapters/codex.ts` (NEW ~35 lines) +- `adapters/cli-proxy.ts` (removed preset definitions, re-exports from presets.ts) +- `cli-args.ts` (imports ClaudeAdapter/CodexAdapter, direct instantiation) +- `index.ts` (new exports) + +## Task: Fix progress line trailing `)` — dynamic width + suffix omission +Root cause: `renderTaskFrame()` in StatusTracker budgeted task text against hardcoded `80` columns. When actual terminal width was narrower, the suffix (elapsed time tag) would overflow and get clipped, leaving a stray `)`. Fix: (1) replaced `80` with `process.stdout.columns || 80` for real terminal width, (2) added `useSuffix` guard — if remaining budget after prefix+suffix is ≤3 chars, omit the suffix entirely instead of letting it clip. Applied to both TUI (styled) and fallback (chalk) render paths. Files: status-tracker.ts + +## Task: Add Codex live activity parsing from `--json` stdout +Investigated `C:\source\teammates\.teammates\.tmp\debug\scribe-2026-03-29T11-49-03-148Z.md` and confirmed it is only a post-task markdown summary plus stderr, not a live event stream. Verified `codex exec --json` emits JSONL lifecycle events on stdout. Added `parseCodexJsonlLine()` to map Codex `tool_call` events into existing activity labels and hooked `CliProxyAdapter` to parse stdout incrementally during Codex runs. Current mappings normalize `shell_command` to `Read`/`Grep`/`Glob`/`Bash` based on the command text and `apply_patch` to `Edit` using patch targets. Added parser tests. Build + typecheck passed; `vitest` startup failed in this sandbox with Vite `spawn EPERM`. Files: activity-watcher.ts, cli-proxy.ts, activity-watcher.test.ts + +## Task: Assess `codex-tui.log` as a live activity source +Inspected `C:\Users\stevenickman\.codex\log\codex-tui.log` after user pointed to it as the realtime log. Confirmed it contains Codex runtime telemetry (`thread_spawn`, `session_init`, websocket/model startup, shell snapshot warning, shutdown) but no tool-level entries like `tool_call`, `item.completed`, `shell_command`, or `apply_patch`. Decision: detailed `[show activity]` should continue to come from streaming `codex exec --json` stdout; `codex-tui.log` is only useful as an optional coarse lifecycle/error side channel. Files changed: `.teammates/beacon/memory/2026-03-29.md`, `.teammates/beacon/memory/decision_codex_tui_log_not_activity_source.md`, `.teammates/.tmp/sessions/beacon.md` + +## Task: Verify whether the Codex adapter uses JSONL output +Confirmed yes by reading the current adapter code. `CODEX_PRESET` runs `codex exec - ... --json`, `parseOutput()` extracts the last `agent_message` from the JSONL stream, and `CliProxyAdapter` incrementally parses stdout with `parseCodexJsonlLine()` for live activity events. Parser tests already cover the Codex JSONL mapping. Files changed: `.teammates/beacon/memory/2026-03-29.md`, `.teammates/.tmp/sessions/beacon.md` + +## Task: Fix Codex live activity parser to accept begin events +Root cause: Codex live activity parsing was too narrow. The adapter streamed `codex exec --json` stdout, but `parseCodexJsonlLine()` only accepted `item.completed` tool-call events. The installed Codex binary exposes live event names like `exec_command_begin` and `patch_apply_begin`, plus tool arguments may arrive as stringified JSON. Fix: broadened the parser to accept `exec_command_begin`, `patch_apply_begin`, `web_search_begin`, `item.started`, and stringified `item.arguments`. Added a short de-dup window in `cli-proxy.ts` so start/completed pairs do not double-render. Build passed; Vitest still failed to start in this sandbox with Vite `spawn EPERM`. Files: `packages/cli/src/activity-watcher.ts`, `packages/cli/src/adapters/cli-proxy.ts`, `packages/cli/src/activity-watcher.test.ts` + +## Task: Run standup +Prepared and delivered Beacon's standup update for the user's `@everyone run a standup` request. + +### Key decisions +- No code changes were needed. +- No teammate handoffs were needed; this turn was a status report only. + +### Files changed +- `.teammates/.tmp/sessions/beacon.md` +- `.teammates/beacon/memory/2026-03-29.md` + +### Next +- If the user follows up, continue on CLI/Codex live-activity work from today's latest parser changes. + +## Task: Broaden Codex activity parsing beyond begin events +User reported Codex still showed no lines under `[show activity]`. Broadened `parseCodexJsonlLine()` to accept additional Codex JSONL shapes: `response.output_item.added`, `response.output_item.done`, and tool-call item types `custom_tool_call` / `function_call`, with arguments coming from `arguments`, `input`, `payload`, `parameters`, or stringified JSON. Also flush the final buffered Codex stdout line on process close so the last activity event is not lost. + +### Key decisions +- Treat Codex activity as a broader multi-shape stream than the earlier begin-event fix covered. +- Keep event-shape normalization in `activity-watcher.ts`; keep de-dup and trailing-buffer flush in `cli-proxy.ts`. +- Verification used a clean CLI build plus a direct Node import of built `dist/activity-watcher.js` because `vitest` still fails to start in this sandbox with Vite `spawn EPERM`. + +### Files changed +- `packages/cli/src/activity-watcher.ts` +- `packages/cli/src/activity-watcher.test.ts` +- `packages/cli/src/adapters/cli-proxy.ts` +- `.teammates/beacon/memory/decision_codex_activity_requires_begin_events.md` + +### Next +- Have the user restart the CLI so the rebuilt adapter code is loaded, then re-test a Codex task with `[show activity]`. + +## Task: Verify whether the Codex activity fix is actually present +Checked the current source instead of relying on the earlier memory entry. Confirmed the Codex path still contains the intended live-activity logic: `CODEX_PRESET` runs `codex exec - --json`, `parseCodexJsonlLine()` accepts `exec_command_begin`, `patch_apply_begin`, `mcp_tool_call_begin`, `web_search_begin`, `item.started`, `item.completed`, and `response.output_item.{added,done}`, and `CliProxyAdapter` streams stdout through that parser with a short de-dup window plus close-time trailing-buffer flush. + +### Key decisions +- Treat this as "present in source" but not "fully verified shipped" because live verification should include a clean build and a real CLI run. +- Report the current blocker plainly: the CLI package does not compile right now due to unrelated `sessionFile` / `sessionFiles` typing errors, so the Codex activity fix could not be re-validated end-to-end in this turn. + +### Verification +- Read: `packages/cli/src/activity-watcher.ts`, `packages/cli/src/adapters/cli-proxy.ts`, `packages/cli/src/adapters/presets.ts`, `packages/cli/src/activity-watcher.test.ts` +- Ran: `npm run build` in `packages/cli` +- Result: build failed with unrelated TypeScript errors in `adapter.test.ts`, `cli-proxy.ts`, and `copilot.ts` about missing `sessionFile` and `sessionFiles`. + +### Files changed +- `.teammates/.tmp/sessions/beacon.md` +- `.teammates/beacon/memory/2026-03-29.md` + +### Next +- Fix the current CLI compile errors, rebuild, restart the CLI, and run a real Codex task to confirm `[show activity]` renders live lines in practice. + +## Task: Remove session file system entirely +User reported debug/session logs appearing for teammates not being talked to. Root cause: session files (`.teammates/.tmp/sessions/*.md`) were still being created per teammate in `startSession()`, populated by agents during tasks, and the path was injected into every prompt via the `### Session State` section — wasting tokens. + +### What was removed +1. **adapter.ts** — Removed `sessionFile` option from `buildTeammatePrompt()`, removed `getSessionFile?()` from `AgentAdapter` interface, deleted entire `### Session State` prompt section +2. **cli-proxy.ts** — Removed `sessionFiles` Map, `sessionsDir` field, session file creation in `startSession()`, session file passing in `executeTask()`, `getSessionFile()` method, session file cleanup in `destroySession()` +3. **copilot.ts** — Same removals as cli-proxy (sessionFiles, sessionsDir, startSession file creation, executeTask passing, getSessionFile) +4. **adapter.test.ts** — Removed "includes session file when provided" test +5. **cli.ts** — Removed `sessions` from dir-deletion skip list in `cleanOldTempFiles()` +6. Deleted leftover `.teammates/.tmp/sessions/` directory + +### Key decisions +- `.tmp` gitignore guard retained (still needed for debug/activity dirs) — moved to a `_tmpInitialized` flag pattern +- Orchestrator `sessions` Map (session IDs, not file paths) left intact — needed for session continuity +- Clean compile + build verified + +### Files changed +- adapter.ts, adapter.test.ts, cli-proxy.ts, copilot.ts, cli.ts + +## Task: Restructure debug/activity logging — unified prompt + log files per adapter +User wasn't seeing `[show activity]` output for Codex. Restructured all adapters to write two persistent files per task under `.teammates/.tmp/debug/`: +- `--prompt.md` — the full prompt sent to the agent +- `-.md` — adapter-specific activity/debug log + +### Architecture +1. **cli-proxy.ts** — `executeTask()` creates both file paths in `.teammates/.tmp/debug/`, writes prompt immediately, passes logFile to `spawnAndProxy()`. For Claude: logFile passed as `--debug-file` so Claude writes directly. For Codex/others: raw stdout dumped to logFile on process close. Removed old temp-file-in-tmpdir pattern — prompt file now persists in debug dir. +2. **copilot.ts** — Same two-file pattern. Wraps `session.emit` to capture all Copilot SDK events into an activity log array, written to logFile after task completes. +3. **types.ts** — Added `promptFile` and `logFile` fields on `TaskResult`. +4. **cli.ts** — `lastDebugFiles` changed from `Map` to `Map`. `writeDebugEntry()` simplified to just record the adapter-provided paths (no longer writes its own mega debug file). `queueDebugAnalysis()` reads both files and includes both in the analysis prompt sent to `/debug`. + +### Key decisions +- Both files share a `-` base name for easy pairing +- Files stored in `.teammates/.tmp/debug/` (cleaned by startup maintenance after 24h) +- Claude's `--debug-file` pointed directly at the logFile (no copy/rename needed) +- Copilot event capture uses `(session as any).emit` wrapper since CopilotSession type doesn't expose emit + +### Build & test +- TypeScript compiles cleanly (strict mode) +- 277 CLI tests pass + +### Files changed +- types.ts, cli-proxy.ts, copilot.ts, cli.ts + +## Task: Fix wisdom distillation firing on every startup +Root cause: `buildWisdomPrompt()` in `compact.ts` only checked if typed memories or daily logs existed (they always do). It never checked whether wisdom had already been distilled today. Result: 4 agent calls (one per AI teammate) on every CLI startup, burning tokens for no reason. + +Fix: added a `Last compacted: YYYY-MM-DD` date check — if WISDOM.md already shows today's date, return `null` (skip). This matches the pattern already used by `buildDailyCompressionPrompt()` which checks `compressed: true`. + +### Files changed +- compact.ts (added today-check guard in `buildWisdomPrompt()`) + +## Task: Explain .tmp/activity folder creation on startup +User asked why `.teammates/.tmp/activity/` keeps appearing. Traced it to `spawnAndProxy()` in `cli-proxy.ts:479-481` — every agent task spawn creates the directory via `mkdirSync`. The folder was appearing on every startup because the wisdom distillation bug (fixed above) spawned 4 agent processes. With that fix in place, the activity dir will only be created when the user actually assigns a task. + +### Key decisions +- No code changes needed — the wisdom distillation fix resolves the symptom +- The activity dir creation in `spawnAndProxy()` is correct behavior for actual tasks + +## Task: Remove .tmp/activity disk intermediary — pipe activity events in-memory +User feedback: activity log files in `.tmp/activity/` are "on-disk turds" that serve no purpose. The PostToolUse hook (`activity-hook.mjs`) never worked because Claude Code doesn't propagate custom env vars to hook subprocesses. Activity events were always coming from the debug log watcher (Claude) or in-memory stdout parsing (Codex). + +### What was removed +1. **`activity-hook.ts`** — DELETED. Hook installer that wrote to `.claude/settings.local.json`. Dead code since env vars never propagated. +2. **`scripts/activity-hook.mjs`** — DELETED. The PostToolUse hook script itself. +3. **`.tmp/activity/` directory creation** — Removed `mkdirSync(activityDir)` and `activityFile` path construction from `spawnAndProxy()`. +4. **`TEAMMATES_ACTIVITY_LOG` env var** — Removed from spawned process environment. +5. **`watchActivityLog()`** — Removed from `activity-watcher.ts` and all call sites. It watched files that were never written to. +6. **`parseActivityLog()`** — Removed. Only parsed the hook log format. +7. **`ACTIVITY_LINE_RE`** — Removed. Regex for the hook log format. +8. **`ensureActivityHook()` call** — Removed from `startupMaintenance()` in `cli.ts`. +9. **Activity dir cleanup** — Removed from `startupMaintenance()`. +10. **Public exports** — Removed `ensureActivityHook`, `parseActivityLog`, `watchActivityLog` from `index.ts`. +11. **`activityFile` field** — Removed from `SpawnResult` type in `cli-proxy.ts`. +12. **Hook/legacy dedup wrapper** — Removed `hookFired`/`hookCallback`/`legacyCallback` from watcher setup. Now just direct `watchDebugLog()` + `watchDebugLogErrors()`. + +### What remains (correctly) +- **Claude**: `watchDebugLog()` + `watchDebugLogErrors()` — parse Claude's debug log (which Claude writes via `--debug-file`). This file is unavoidable since Claude owns it, and it's already needed for `/debug` analysis. +- **Codex**: In-memory stdout JSONL parsing via `parseCodexJsonlLine()` — events piped directly to `onActivity` callback with no disk I/O. +- **In-memory buffer**: `_activityBuffers` Map in cli.ts stores all events. `collapseActivityEvents()` groups them for display. + +### Key decisions +- Claude's debug log is the only remaining disk dependency for activity, and it was already being created for `/debug` — no new files. +- The "legacy" comment labels in activity-watcher.ts were updated since the debug log parser is now the primary (and only) Claude parser. + +### Files changed +- `activity-hook.ts` (DELETED) +- `scripts/activity-hook.mjs` (DELETED) +- `activity-watcher.ts` (removed hook log parsing, updated comments) +- `adapters/cli-proxy.ts` (removed activity dir/file/env var/hook watcher, simplified watcher setup) +- `adapters/copilot.ts` (comment fix) +- `cli.ts` (removed ensureActivityHook import/call, removed activity dir cleanup) +- `index.ts` (removed dead exports) + +## Task: Shorten working placeholder text +Changed `working on task...` → `working...` at all 4 sites: `cli.ts:3008, 3013` (non-threaded) and `thread-manager.ts:350, 355` (threaded). Clean build + lint cycle completed. Files: cli.ts, thread-manager.ts + +## Task: Make Codex debug log file appear immediately +User reported Codex runs were creating `-prompt.md` but not the paired `.md` while the task was running. Root cause: non-Claude adapters only wrote `logFile` on process close, so the file did not exist during execution and could be missing entirely on early spawn failures. + +### Key decisions +- Pre-create the non-Claude `logFile` in `executeTask()` right after writing the prompt file so the pair exists immediately in `.teammates/.tmp/debug/`. +- Append non-Claude stdout and stderr to the log file incrementally during execution for live inspection. +- Still keep the close-time final write, and append a `[SPAWN ERROR]` marker if process spawn fails before normal shutdown. + +### Build & verification +- `npm run build` in `packages/cli` passed cleanly. +- Source inspection confirmed the new eager-create and incremental append paths in `packages/cli/src/adapters/cli-proxy.ts`. + +### Files changed +- `packages/cli/src/adapters/cli-proxy.ts` + +## Task: Update focused-thread footer hint wording +Changed the focused thread footer hint from `replying to #` to `replying to task #` so the status bar language is clearer while keeping the existing thread ID behavior. + +### Key decisions +- Updated the single source of truth in `ThreadManager.updateFooterHint()` instead of layering a formatting override elsewhere. +- Kept the text lowercase to match the existing footer style. + +### Build & verification +- `npm run build` in `packages/cli` passed cleanly. +- A clean `dist` removal before build was attempted but blocked by the sandbox policy in this session. + +### Files changed +- `packages/cli/src/thread-manager.ts` + +## Task: Fix Codex [show activity] by tailing the debug JSONL file +Read the current `.teammates\.tmp\debug\*.md` Codex log and found the live tool stream was not using the older `tool_call` JSONL shapes. The real file is dominated by `item.started` / `item.completed` entries with `item.type: command_execution`, so the existing parser was dropping them and the feed stayed empty under `[show activity]`. + +### Key decisions +- Switched Codex live activity to follow the same file-watcher model as Claude, but against the paired JSONL debug log file instead of stdout-only parsing. +- Added `watchCodexDebugLog()` with trailing-line buffering so partial JSONL writes are not lost during polling. +- Taught `parseCodexJsonlLine()` to map `item.started` + `item.type: command_execution` into existing `Read` / `Grep` / `Glob` / `Bash` activity labels. +- Normalized wrapped PowerShell commands by unwrapping the `-Command "..."` payload before classification, so entries like `"powershell.exe" -Command "Get-Content ..."` collapse to `Read SOUL.md` instead of a generic Bash line. +- Kept the existing collapse/render path intact so Codex activity still renders as the same compact `Exploring` / `Edit file.ts (×N)` feed format. + +### Verification +- `npm run build` in `packages/cli` passed. +- Direct Node import of built `dist/activity-watcher.js` confirmed a real `command_execution` JSONL line now parses to `[{ tool: "Read", detail: "SOUL.md" }]`. +- Attempted clean `dist` removal before rebuild, but the sandbox blocked the recursive delete command in this session. + +### Files changed +- `packages/cli/src/activity-watcher.ts` +- `packages/cli/src/adapters/cli-proxy.ts` +- `packages/cli/src/activity-watcher.test.ts` +- `packages/cli/src/index.ts` + +### Next +- Restart the CLI so the rebuilt Codex adapter is loaded, then run a Codex task and toggle `[show activity]` to confirm live lines appear in the thread feed. + +## Task: Fix Codex activity watcher gate to use logFile +User reported Codex debug logs were visibly updating on disk while `[show activity]` stayed empty. Root cause was not the parser this time, but the watcher startup gate in `packages/cli/src/adapters/cli-proxy.ts`: watchers only started when `debugFile` existed. That works for Claude because Claude supports `--debug-file`, but Codex does not, so `debugFile` was always `undefined` even though the paired `.teammates\.tmp\debug\*.md` `logFile` was being appended live. + +### Key decisions +- Keep Claude on the existing `debugFile` watcher path. +- Start Codex activity watching from `logFile`, which is the file the adapter actually appends during execution. +- Verify the compiled output too, not just the TypeScript source, so the fix is present in `dist`. + +### Verification +- Rebuilt `packages/cli` successfully with `npm run build`. +- Ran Biome on `packages/cli/src/adapters/cli-proxy.ts`; no changes were needed. +- Confirmed both source and compiled output contain `watchCodexDebugLog(logFile, taskStartTime, onActivity)`. +- Attempted to clean `packages/cli/dist` first, but the sandbox blocked the recursive delete command in this session. + +### Files changed +- `packages/cli/src/adapters/cli-proxy.ts` + +### Next +- Restart the running CLI process so it loads the rebuilt adapter code. +- Re-run a Codex task and toggle `[show activity]`; the live lines should now render from the same debug log file you already see updating. + +## Task: Make /btw skip memory updates +User clarified that `/btw` is an ephemeral side question passed to the coding agent and must not update daily logs or typed memories. Implemented a dedicated `skipMemoryUpdates` flag instead of reusing `system`, so `/btw` keeps normal user-task behavior while suppressing memory-writing instructions in the teammate prompt. + +### Key decisions +- Added a dedicated `skipMemoryUpdates` prompt/assignment flag rather than overloading `system`; `/btw` is not maintenance work and should not inherit system-task semantics. +- Threaded the flag through `TaskAssignment` → `Orchestrator` → all adapters → `buildTeammatePrompt()`. +- Gave ephemeral tasks their own `### Memory Updates` text telling the agent to answer only and not touch memory files. +- Wired the `/btw` dispatch path in `cli.ts` to set `skipMemoryUpdates: entry.type === "btw"`. + +### Build & verification +- `npm run build` in `packages/cli` passed cleanly. +- Attempted a clean `dist` removal first, but the sandbox blocked the recursive delete command in this session. + +### Files changed +- `packages/cli/src/types.ts` +- `packages/cli/src/adapter.ts` +- `packages/cli/src/orchestrator.ts` +- `packages/cli/src/adapters/cli-proxy.ts` +- `packages/cli/src/adapters/copilot.ts` +- `packages/cli/src/adapters/echo.ts` +- `packages/cli/src/cli.ts` +- `packages/cli/src/adapter.test.ts` + +### Next +- Restart the CLI before testing `/btw`, since the running Node process will still have the old adapter code loaded. + +## Task: Extract modules from cli.ts — Phase 3 +Codex hung for 27 minutes on cli.ts (4494 lines) due to repeated `apply_patch` failures — the file is too large for reliable patching. Extracted 2 new modules to continue shrinking it (Phase 1 extracted 5, Phase 2 extracted 2). + +### New modules +1. **`activity-manager.ts`** (~330 lines) — All activity tracking state (5 Maps) + 8 methods: handleActivityEvents, rerenderActivityLines, toggleActivity, insertActivityHeader, insertActivityLines, cleanupActivityLines, updatePlaceholderVerb, cancelRunningTask, initForTask. Typed `ActivityManagerDeps` interface. +2. **`startup-manager.ts`** (~370 lines) — Startup maintenance lifecycle: startupMaintenance, cleanOldTempFiles, checkVersionUpdate, commitVersionUpdate, runCompact. Typed `StartupManagerDeps` interface. + +### Results +- cli.ts: 4494 → 3970 lines (-524, -12%) +- Total reduction across all 3 phases: 6815 → 3970 (-42%) +- TypeScript compiles cleanly (strict mode) +- 279 CLI tests pass, 561 consolonia tests pass (840 total) + +### Key decisions +- Activity state (5 Maps) moved to public readonly fields on ActivityManager so the shiftAllContainers wrapper in cli.ts can still adjust indices during feed insertions. +- Startup manager's `pendingMigrationSyncs` bridged via getter/setter closures since it's a primitive that needs to be shared with cli.ts's runSystemTask migration counter. +- Slash commands NOT extracted — too entangled with cli.ts private state. Each command accesses a different subset of REPL state, making a unified deps interface impractically large. +- Removed unused imports from cli.ts after extraction: ora, readdir, rm, unlink, DAILY_LOG_BUDGET_TOKENS, buildMigrationPrompt, autoCompactForBudget, buildDailyCompressionPrompt, buildWisdomPrompt, compactEpisodic, purgeStaleDailies. + +### Files changed +- `packages/cli/src/activity-manager.ts` (NEW ~330 lines) +- `packages/cli/src/startup-manager.ts` (NEW ~370 lines) +- `packages/cli/src/cli.ts` (refactored — replaced implementations with delegations, removed unused imports) +- `packages/cli/src/index.ts` (new exports: ActivityManager, StartupManager + types) + +## Task: Unify /cancel and /interrupt to work by task-id +Rewrote both commands to use thread IDs (`#N`) as the primary identifier instead of queue position or teammate name. + +### /cancel [task-id] [teammate?] +- Takes a thread ID and optional teammate name +- Without teammate: cancels ALL teammates in the task (both queued and running) +- With teammate: cancels just that teammate +- Each canceled teammate gets a `: canceled` subject line rendered in the thread via `displayCanceledInThread()` +- Kills running agents via `adapter.killAgent()`, removes queued tasks from queue +- Shared `cancelTeammateInThread()` helper handles both paths + +### /interrupt [task-id] [teammate] [message] +- Takes a thread ID, teammate name, and interruption text +- Kills the running agent or removes queued task +- Re-queues with original task text + `\n\nUPDATE:\n` appended +- Cascading: subsequent interruptions keep appending UPDATE sections to the accumulated task text +- Shows new working placeholder in the thread for the re-queued task +- Removed old RESUME_CONTEXT / buildResumePrompt / buildConversationLog approach + +### Cleanup +- Removed `buildResumePrompt()` method (no longer needed) +- Removed `buildConversationLog` import from log-parser.js (no longer used in cli.ts) +- Removed `InterruptState` type from types.ts and index.ts (dead code) +- Biome removed unused `mkdirSync`/`writeFileSync` imports +- Added `cancel` to `TEAMMATE_ARG_POSITIONS` in wordwheel.ts (position 1 — after task-id) +- Changed `interrupt`/`int` from position 0 to position 1 in wordwheel + +### Key decisions +- Thread ID is the task identifier for both commands (matches `#N` UI convention) +- No resume context or conversation log capture — interrupt simply restarts with updated instructions, relying on the agent to check filesystem state +- UPDATE sections cascade naturally because `entry.task` accumulates them + +### Files changed +- `packages/cli/src/cli.ts` — rewrote cmdCancel/cmdInterrupt, added cancelTeammateInThread/getThreadTeammates helpers +- `packages/cli/src/thread-manager.ts` — added `displayCanceledInThread()` method +- `packages/cli/src/wordwheel.ts` — updated TEAMMATE_ARG_POSITIONS +- `packages/cli/src/types.ts` — removed InterruptState +- `packages/cli/src/index.ts` — removed InterruptState export + +## Task: Fix missing working placeholder — renderTaskPlaceholder before container creation +Root cause: In `queueTask()`, both the `@everyone` and `mentioned` paths called `renderTaskPlaceholder()` BEFORE `renderThreadHeader()`. But `renderThreadHeader()` is what creates the `ThreadContainer` — so `renderTaskPlaceholder` found no container and silently returned without rendering anything. The single-match path (no explicit @mention) had the correct order. Fix: moved `renderThreadHeader`/`renderThreadReply` calls before the entry loop in both paths so the container exists when placeholders are rendered. Files: cli.ts + +## Task: Run standup +Prepared and delivered Beacon's standup update for the user's `@everyone do a standup` request. + +### Key decisions +- No code changes were needed. +- No teammate handoffs were needed; this turn was a status report only. + +### Files changed +- `.teammates/beacon/memory/2026-03-29.md` + +### Next +- Continue on CLI thread/orchestration reliability and Codex live activity verification if the user follows up there. + +## Task: Change [cancel] verb to populate input box instead of direct cancel +User reported clicking `[cancel]` wasn't actually cancelling. Changed the action handler to populate the input box with `/cancel # ` instead of calling `cancelTask()` directly. The user just presses enter to confirm. Looks up the entry from both `taskQueue` and `agentActive` to resolve threadId and teammate name. + +### Files changed +- `packages/cli/src/cli.ts` (cancel action handler at ~line 1662) + +## Task: Fix /cancel and /interrupt not accepting #id format +User reported `/cancel` didn't work. Root cause: the `[cancel]` verb populates the input with `/cancel #1 beacon` (with `#` prefix), but `cmdCancel` did `parseInt("#1", 10)` which returns `NaN`. Same bug in `cmdInterrupt`. Fix: added `.replace(/^#/, "")` before `parseInt()` in both commands so `#1` and `1` both parse correctly. + +### Files changed +- `packages/cli/src/cli.ts` (cmdCancel ~line 2691, cmdInterrupt ~line 2822) + +## Task: Fix /cancel not actually preventing task result display +User reported `/cancel` showed "canceled" but the agent's response still rendered. Root cause: `drainAgentQueue()` awaits `orchestrator.assign()` — when `killAgent()` terminates the process, the promise resolves with whatever output was produced before death. `drainAgentQueue` then continued to `displayTaskResult()` without checking if the task had been canceled. Fix: after `assign()` returns, check `this.agentActive.has(agent)` — if `cancelTeammateInThread` already removed it, skip result display (just clean up activity lines and `continue`). + +### Files changed +- `packages/cli/src/cli.ts` (~line 3061, added agentActive guard after assign) + +## Task: Replace killAgent with AbortSignal-based cancellation +User requested a cancellation object pattern instead of adapter-level `killAgent()`. Implemented using Node's built-in `AbortController`/`AbortSignal`. + +### Architecture +1. **Caller owns the lifecycle:** `cli.ts` creates an `AbortController` per running task in `drainAgentQueue()`, stores it in `abortControllers` Map keyed by teammate name. +2. **Signal flows through:** `TaskAssignment.signal` → `orchestrator.assign()` → `adapter.executeTask()` options → `spawnAndProxy()` / Copilot session. +3. **Adapters react to abort:** + - `CliProxyAdapter.spawnAndProxy()` — `signal.addEventListener("abort", ...)` → `child.kill("SIGTERM")` with 5s → SIGKILL escalation. Listener and kill timer cleaned up in the process close handler. + - `CopilotAdapter.executeTask()` — `signal.addEventListener("abort", ...)` → `session.disconnect()`. Listener cleaned up in finally block. + - `EchoAdapter` — accepts signal in signature (no-op). +4. **Cancel paths simplified:** `cancelTeammateInThread()` and `cmdInterrupt()` now call `controller.abort()` (synchronous) instead of `await adapter.killAgent(teammate)`. +5. **Reset path:** `cmdClear()` aborts all controllers before clearing state. + +### What was removed +- `killAgent()` method from `AgentAdapter` interface (adapter.ts) +- `killAgent()` implementation from `CliProxyAdapter` (cli-proxy.ts) +- `activeProcesses` Map from `CliProxyAdapter` (no longer needed — signal listener handles kill internally) +- `getAdapter()` dependency from `ActivityManagerDeps` interface +- `cancelRunningTask()` from `ActivityManager` (dead code — `[cancel]` verb now populates input box instead) +- `cancelTask()` from cli.ts (dead code — never called after `[cancel]` verb change) +- `getAdapter()` call from activity manager deps wiring in cli.ts +- Unused `tp` import from activity-manager.ts (caught by Biome) + +### Key decisions +- Used Node's built-in `AbortController`/`AbortSignal` instead of a custom EventEmitter — standard API, no dependencies, well-understood lifecycle. +- Cancel is now synchronous from the caller's perspective — `controller.abort()` fires immediately, the adapter reacts asynchronously, and the existing `agentActive` guard in `drainAgentQueue` handles skipping the result. +- Kept `getAdapter()` on Orchestrator (still useful for other purposes) — just removed the cancel-path usage. +- `activeProcesses` Map fully removed from `CliProxyAdapter` — the only purpose was external kill access, which the signal now handles internally. + +### Build & test +- TypeScript compiles cleanly (strict mode) +- 279 CLI tests pass +- Biome clean (removed one unused import) + +### Files changed +- `packages/cli/src/types.ts` (added `signal?: AbortSignal` to TaskAssignment) +- `packages/cli/src/adapter.ts` (added `signal` to executeTask options, removed `killAgent` from interface) +- `packages/cli/src/orchestrator.ts` (passes signal through to adapter) +- `packages/cli/src/adapters/cli-proxy.ts` (signal listener in spawnAndProxy, removed killAgent + activeProcesses) +- `packages/cli/src/adapters/copilot.ts` (signal listener for session.disconnect) +- `packages/cli/src/adapters/echo.ts` (accepts signal in signature) +- `packages/cli/src/activity-manager.ts` (removed getAdapter dep, cancelRunningTask, unused import) +- `packages/cli/src/cli.ts` (abortControllers map, abort in cancel/interrupt, removed dead cancelTask) + +## Task: Extract slash commands from cli.ts — Phase 4 +Extracted all slash commands into a new `commands.ts` module (1612 lines). cli.ts went from 3990 → 2606 lines (-35%). + +### New module +**`commands.ts`** — `CommandManager` class with typed `CommandsDeps` interface. Contains: +- `registerCommands()` — all 16 slash command definitions +- `dispatch()` — command routing +- All `cmd*` methods: Status, Debug, Cancel, Interrupt, Init, Clear, Compact, Retro, Copy, Help, User, Btw, Script, Theme +- Supporting helpers: `queueDebugAnalysis`, `cancelTeammateInThread`, `getThreadTeammates`, `getThreadTaskCounts`, `buildSessionMarkdown`, `doCopy`, `feedCommand`, `printBanner` +- Public methods used by cli.ts: `doCopy()`, `feedCommand()`, `printBanner()`, `dispatch()`, `registerCommands()`, `buildSessionMarkdown()`, `cancelTeammateInThread()`, `getThreadTeammates()` + +### Key decisions +- CommandManager receives deps via typed `CommandsDeps` interface with closure-backed getters for shared mutable state (same pattern as other extracted modules) +- `const repl = this` in `start()` captures the REPL instance for getter-based dep closures +- `cmdClear` calls `this.printBanner()` on its own class (not a dep) since the method was co-extracted +- Added `clearPastedTexts()` to deps since `pastedTexts` is only used by `cmdClear` among commands, but the Map itself stays in cli.ts +- `serviceView` getter stays in cli.ts since it's referenced by the deps object +- `refreshTeammates()` stays in cli.ts since `displayTaskResult()` (non-command) calls it +- Removed unused imports from cli.ts: `existsSync`, `readdirSync`, `readFileSync`, `stat`, `basename`, `resolve`, `colorToHex`, `relativeTime`, `cmdConfigure`, `importTeammates`, `copyTemplateFiles`, `buildImportAdaptationPrompt` + +### Cumulative extraction totals +- Phase 1: 6815 → 5549 (5 modules) +- Phase 2: 5549 → 4159 (2 modules) +- Phase 3: 4494 → 3970 (2 modules) +- Phase 4: 3990 → 2606 (1 module, this task) +- Total reduction: 6815 → 2606 (-62%) + +### Build & test +- TypeScript compiles cleanly (strict mode) +- Biome lint clean (auto-fixed 2 unused imports: `ActivityEvent` in commands.ts, one in cli.ts) +- 279 CLI tests pass, 561 consolonia tests pass (840 total) + +### Files changed +- `packages/cli/src/commands.ts` (NEW ~1612 lines) +- `packages/cli/src/cli.ts` (refactored — 3990 → 2606 lines) +- `packages/cli/src/index.ts` (new exports: CommandManager, CommandsDeps) + +## Task: Extract modules from cli.ts — Phase 5 (under 2000 lines) +Three extractions to bring cli.ts from 2606 → 1986 lines (-24%, **under the 2000-line target**). + +### New modules +1. **`conversation.ts`** (~119 lines) — `ConversationManager` class with typed `ConversationManagerDeps` interface. Contains: conversation history + summary state, `storeInHistory()`, `buildContext()`, `maybeQueueSummarization()`, `preDispatchCompress()`, and the context budget constants (`TARGET_CONTEXT_TOKENS`, `CONV_HISTORY_CHARS`). + +2. **`feed-renderer.ts`** (~400 lines) — `FeedRenderer` class with typed `FeedRendererDeps` interface. Contains: `feedLine()`, `feedMarkdown()`, `feedUserLine()`, `printUserMessage()`, `makeSpan()`, `wordWrap()`, `displayTaskResult()`, `displayFlatResult()`. Owns the `_userBg` color constant and the markdown theme config. + +### Delegation boilerplate elimination +Removed ~225 lines of pure pass-through delegation methods across 7 managers: +- **ThreadManager**: 15 methods (threads, focusedThreadId, containers, createThread, getThread, appendThreadEntry, renderThreadHeader, renderThreadReply, renderTaskPlaceholder, toggleThreadCollapse, toggleReplyCollapse, updateFooterHint, buildThreadClipboardText, threadFeedMarkdown, updateThreadHeader) +- **HandoffManager**: 9 methods (renderHandoffs, showHandoffDropdown, handleHandoffAction, auditCrossFolderWrites, showViolationWarning, handleViolationAction, handleBulkHandoff, pendingHandoffs, autoApproveHandoffs) +- **RetroManager**: 5 methods +- **OnboardFlow**: 9 methods (replaced with direct calls + inline closures for printAgentOutput) +- **Wordwheel**: 7 methods +- **ActivityManager**: 4 methods +- **StartupManager**: 4 methods + +All call sites updated to use `this.manager.method()` directly. CommandsDeps updated to receive `conversation: ConversationManager` instead of separate `conversationHistory`/`conversationSummary` fields. + +### Key decisions +- `feedLine`, `feedMarkdown`, `feedUserLine`, `makeSpan` kept as thin 1-line delegations in cli.ts (called from 30+ sites across cli.ts and passed as closures to 8 module deps — renaming every site would be churn) +- `shiftAllContainers` getter kept in cli.ts because it adds activity-index shifting logic on top of the base ThreadManager method +- FeedRenderer deps use getters for lazy resolution (chatView/threadManager don't exist when the renderer is constructed) +- `registerUserAvatar` delegation removed — inlined the 2-line body (onboardFlow call + userAlias assignment) at the single call site + +### Cumulative extraction totals +- Phase 1: 6815 → 5549 (5 modules) +- Phase 2: 5549 → 4159 (2 modules) +- Phase 3: 4494 → 3970 (2 modules) +- Phase 4: 3990 → 2606 (1 module) +- Phase 5: 2606 → 1986 (2 modules + boilerplate cleanup, this task) +- **Total reduction: 6815 → 1986 (-71%)** + +### Build & test +- TypeScript compiles cleanly (strict mode) +- Biome lint clean (auto-fixed unused imports) +- 279 CLI tests pass, 561 consolonia tests pass (840 total) + +### Files changed +- `packages/cli/src/conversation.ts` (NEW ~119 lines) +- `packages/cli/src/feed-renderer.ts` (NEW ~400 lines) +- `packages/cli/src/cli.ts` (refactored — 2606 → 1986 lines) +- `packages/cli/src/commands.ts` (updated deps: conversation instead of conversationHistory/Summary) +- `packages/cli/src/index.ts` (new exports: ConversationManager, FeedRenderer + types) + +## Task: Add GOALS.md to the teammate prompt stack +Handoff from Scribe: GOALS.md was added as a new standard teammate file (tracks active objectives and priorities). Injected it into the CLI prompt stack as item 2, between SOUL.md (IDENTITY) and WISDOM.md. + +### What was done +1. **types.ts** — Added `goals: string` field to `TeammateConfig` interface +2. **registry.ts** — Reads `GOALS.md` via `readFileSafe()` alongside SOUL.md and WISDOM.md +3. **adapter.ts** — Injects `` section between `` and `` in `buildTeammatePrompt()`. Added section reinforcement line for goals. Conditional on non-empty content (same pattern as WISDOM). +4. **commands.ts** — Updated `/retro` prompt to include GOALS.md in the review list +5. **Test files** — Added `goals: ""` to all 7 `TeammateConfig` object literals across 4 test files +6. **Inline configs** — Added `goals: ""` to 4 inline TeammateConfig constructions (cli-proxy.ts, cli.ts, onboard-flow.ts ×2) + +### Prompt stack order +`` (SOUL.md) → `` (GOALS.md) → `` (WISDOM.md) → `` → ... + +### Build & test +- TypeScript compiles cleanly (strict mode) +- 279 CLI tests pass +- Biome lint clean + +### Files changed +- types.ts, registry.ts, adapter.ts, commands.ts, cli.ts, cli-proxy.ts, onboard-flow.ts, adapter.test.ts, echo.test.ts, orchestrator.test.ts, registry.test.ts + +## Task: Fix missing [reply] [copy thread] verbs at bottom of threads +Root cause: when `displayThreadedResult()` was extracted from `cli.ts` into `thread-manager.ts` during the Phase 2 module extraction, the `insertThreadActions()` call that renders [reply] [copy thread] verbs was dropped. The `displayCanceledInThread()` method in the same file still had the correct code. Fix: added the missing `insertThreadActions()` block plus the show/hide logic and `updateThreadHeader()` call, matching the pattern in `displayCanceledInThread()`. + +### Files changed +- `packages/cli/src/thread-manager.ts` + +## Task: Fix "Cannot read properties of undefined (reading 'trim')" crash +Root cause: `registerUserAvatar()` in `onboard-flow.ts` constructed a `TeammateConfig` without the `goals` field (added in today's GOALS.md integration). When the user's avatar config was later passed to `buildTeammatePrompt()`, `teammate.goals.trim()` threw because `goals` was `undefined`. Fix: added `goals` field (read from `GOALS.md` via `readFileSync` with catch fallback to `""`) to the registered config, matching the pattern already used for `soul` and `wisdom`. + +### Files changed +- `packages/cli/src/onboard-flow.ts` + +## Task: Rework bundled personas into folder-based SOUL/WISDOM templates +Changed the bundled CLI persona templates from single markdown files under `packages/cli/personas/*.md` to per-persona folders under `packages/cli/personas//` containing `SOUL.md` and `WISDOM.md`. Updated `loadPersonas()` to discover directories, parse metadata from each `SOUL.md` frontmatter, and load the paired `WISDOM.md`. Updated `scaffoldFromPersona()` to copy both template files with `` replacement instead of synthesizing wisdom inline. Also updated persona tests to validate the new `Persona` shape (`soul` + `wisdom`) and the new scaffold behavior. + +### Key decisions +- Kept persona metadata in `SOUL.md` frontmatter so onboarding still reads `persona`, `alias`, `tier`, and `description` from one source of truth. +- Made `WISDOM.md` a first-class template file per persona so persona-specific wisdom can evolve independently instead of being hardcoded in the scaffolder. +- Created the new folder assets for all bundled personas, and updated the prompt-engineer template's internal references from `packages/cli/personas/*.md` to `packages/cli/personas/**`. +- `npm run build` for `packages/cli` passed. `vitest` still could not start in this sandbox due Vite `spawn EPERM`. +- Sandbox policy blocked deletion of the old top-level `packages/cli/personas/*.md` files in this session, so the new folder layout is in place and the loader ignores those files, but the stale legacy files still need to be removed outside this sandboxed run. + +### Files changed +- `packages/cli/src/personas.ts` +- `packages/cli/src/personas.test.ts` +- `packages/cli/personas/*/SOUL.md` +- `packages/cli/personas/*/WISDOM.md` + +### Next +- Remove the legacy top-level `packages/cli/personas/*.md` files in a non-blocked session so the repo contains only the folder-based templates. + +## Task: Make persona aliases the canonical bundled persona names +Updated persona loading and onboarding so the alias is now the primary bundled persona identity. `loadPersonas()` only accepts persona folders whose directory name matches the `alias:` in `SOUL.md`, sorts by tier then alias, and now tolerates Windows CRLF frontmatter when parsing persona files. Onboarding now shows `@alias` as the primary label and prompts for alias overrides using the alias as the default installed teammate name. Seeded real role-specific starter wisdom into the alias folders (`packages/cli/personas//WISDOM.md`) instead of the previous placeholder text. + +### Key decisions +- Treat the alias folder name as canonical and ignore legacy role-slug folders if both exist. +- Keep `persona` as the secondary human-readable role label; use alias as the selectable/installable persona name. +- Fix the loader's frontmatter regex to accept `\r\n` as well as `\n`; otherwise Windows persona files silently fail to load. +- Copy persona assets into alias-named folders for now because this sandbox still blocks clean removal/move of the legacy role-slug folders and top-level `packages/cli/personas/*.md` files. + +### Files changed +- `packages/cli/src/personas.ts` +- `packages/cli/src/personas.test.ts` +- `packages/cli/src/onboard-flow.ts` +- `packages/cli/personas/beacon/WISDOM.md` +- `packages/cli/personas/blueprint/WISDOM.md` +- `packages/cli/personas/engine/WISDOM.md` +- `packages/cli/personas/forge/WISDOM.md` +- `packages/cli/personas/pipeline/WISDOM.md` +- `packages/cli/personas/pixel/WISDOM.md` +- `packages/cli/personas/prism/WISDOM.md` +- `packages/cli/personas/neuron/WISDOM.md` +- `packages/cli/personas/orbit/WISDOM.md` +- `packages/cli/personas/tempo/WISDOM.md` +- `packages/cli/personas/scribe/WISDOM.md` +- `packages/cli/personas/lexicon/WISDOM.md` +- `packages/cli/personas/sentinel/WISDOM.md` +- `packages/cli/personas/shield/WISDOM.md` +- `packages/cli/personas/watchtower/WISDOM.md` +- `packages/cli/personas/quill/WISDOM.md` + +### Verification +- `npm run build` in `packages/cli` +- Node import of `packages/cli/dist/personas.js` confirmed `loadPersonas()` returns the alias-backed persona set + +## Task: Verify and attempt cleanup of stale bundled persona folders +User pointed out that the old bundled persona folders were still present after the alias-based conversion. I inspected `packages/cli/personas`, identified the remaining stale entries by comparing each folder name to the `alias:` in its `SOUL.md`, and confirmed the current leftover set is `qa`, `security`, `sre`, `swe`, `tech-writer`, plus the legacy top-level `*.md` persona files. I attempted cleanup through PowerShell deletion, `apply_patch`, and direct .NET file APIs, but this sandbox denied file deletions at the filesystem level. `npm run build` in `packages/cli` still passed. + +### Key decisions +- Treat a persona directory as stale when `alias:` in `SOUL.md` does not match the directory name. +- Report the exact remaining stale entries instead of claiming cleanup succeeded. +- Do not log deletion as completed because the sandbox blocked it. + +### Files changed +- `.teammates/beacon/memory/2026-03-29.md` + +### Next +- Remove the remaining stale persona folders/files in a session that permits deletions, then re-list `packages/cli/personas` to confirm only alias-backed folders remain. + +## Task: Fix Codex activity collapsing to one "Exploring" line +Analyzed recent Codex JSONL logs under `.teammates\.tmp\debug\`. Root cause: the live parser handled `command_execution` and older tool-call events, but ignored Codex `item.type: file_change` entries. That meant the UI only saw reads/greps and collapsed the whole run into one `Exploring` line. Added `file_change` parsing in `parseCodexJsonlLine()` so `item.started` emits grouped `Edit`/`Write` activity and failed `item.completed` emits error activity. Added focused parser tests for started/completed `file_change` events. Verified with a real recent log that the collapsed output now includes `Exploring`, `Edit`, `Write`, and error phases instead of only research. `npm run build` in `packages/cli` passed; `vitest` still could not start in this sandbox due Vite `spawn EPERM`. + +### Key decisions +- Treat `file_change` as the missing Codex activity shape, not a rendering bug in the activity UI. +- Emit grouped activity per change kind: `add` -> `Write`, `update`/`delete` -> `Edit`. +- Emit normal activity on `item.started` for live feedback, and only emit `item.completed` for failed file changes so successful completions do not double-render. + +### Files changed +- `packages/cli/src/activity-watcher.ts` +- `packages/cli/src/activity-watcher.test.ts` +- `.teammates/beacon/memory/decision_codex_activity_from_debug_jsonl.md` + +### Next +- Restart the running CLI so it loads the rebuilt parser, then rerun a Codex task and confirm `[show activity]` shows edit/write phases live. + +## Task: Update CLI adapter passthrough defaults for Claude and Codex +Ran `claude --help`, `codex --help`, and `codex exec --help` to capture the current option sets before changing adapter argv construction. Kept the existing passthrough flow (`agentPassthrough` -> `extraFlags`) and changed the built-in Codex defaults from `--full-auto` plus `workspace-write` to explicit `-a never -s danger-full-access`, while preserving `--ephemeral --json`. Added focused preset tests and updated CLI usage text to make passthrough visible. + +### Key decisions +- Treat passthrough as an adapter argv concern, not an orchestrator change. +- Use explicit Codex flags instead of `--full-auto` so approval and sandbox behavior are deterministic and match the requested defaults. +- Keep teammate-level sandbox config as a higher-priority default source; if no teammate sandbox is set, Codex now falls back to `danger-full-access`. +- Leave Copilot for a follow-up task, per the user's request. + +### Build & verification +- `npm run build` in `packages/cli` passed cleanly. +- `npx biome check --write ...` on the touched CLI files reported no fixes needed. +- `npx vitest run src/cli-args.test.ts src/adapters/echo.test.ts src/adapters/presets.test.ts` failed to start in this sandbox because Vite hit `spawn EPERM`. + +### Files changed +- `packages/cli/src/adapters/presets.ts` +- `packages/cli/src/adapters/presets.test.ts` +- `packages/cli/src/adapters/codex.ts` +- `packages/cli/src/adapters/cli-proxy.ts` +- `packages/cli/src/cli-args.ts` + +### Next +- Restart the CLI before manual testing so the rebuilt adapter code is loaded. +- Extend the same pass-through treatment to the Copilot adapter next. + +## Task: Fix Codex activity under-reporting in the renderer +User clarified the debug JSONL already contained both reads and edits, so the bug was in activity rendering. Fixed two renderer-side gaps: (1) the per-task Claude/Codex file watchers were skipping whatever bytes were already in the file when the watcher attached, which could drop the earliest Codex commands; (2) the collapse logic converted even a single `Read`/`Grep` event into a generic `Exploring` line, hiding concrete evidence that parsing was working. + +### Key decisions +- Treat this as a display/watcher timing bug, not an execution bug in Codex. +- Start per-task debug-log watchers from byte `0` instead of the current file size, because each task log is unique and should be parsed in full from the moment activity watching begins. +- Preserve single research events (`Read`, `Grep`, etc.) as first-class lines; only collapse consecutive research runs of 2+ events into `Exploring`. + +### Build & verification +- `npm run build` in `packages/cli` passed cleanly. +- `npx biome check --write src\\activity-watcher.ts src\\activity-watcher.test.ts` reported no fixes needed. +- Verified the compiled watcher against the current Codex debug log in `.teammates\\.tmp\\debug\\beacon-2026-03-29T21-34-44-206Z.md`; the first parsed events now include the early `Read` lines instead of starting after the watcher attach point. + +### Files changed +- `packages/cli/src/activity-watcher.ts` +- `packages/cli/src/activity-watcher.test.ts` +- `.teammates/beacon/memory/decision_codex_activity_from_debug_jsonl.md` + +### Next +- Restart the running CLI so Node loads the rebuilt watcher code. +- Re-run a Codex task and toggle `[show activity]`; early reads should now appear, and a lone read/grep will render directly instead of as `Exploring (1× ...)`. + +## Task: Fix Codex "unexpected argument '-a'" error +Root cause: `CODEX_PRESET` in `presets.ts` was passing `-a never` to `codex exec`, but `codex exec` has no `-a` flag. The flag was added earlier today when switching from `--full-auto` to explicit flags, but `-a` (approval mode) doesn't exist in Codex's CLI — only `-s` (sandbox) does. Fix: removed `args.push("-a", "never")` from the Codex preset. Updated preset test expectations to match. + +### Files changed +- `packages/cli/src/adapters/presets.ts` +- `packages/cli/src/adapters/presets.test.ts` + +### Next +- Restart the CLI so the rebuilt preset is loaded, then re-test a Codex task. + +## Task: Switch Copilot adapter from SDK to CLI +Replaced the `@github/copilot-sdk`-based `CopilotAdapter` with a thin `CliProxyAdapter` wrapper that spawns `copilot -p - --allow-all -s`. This unifies all three coding agents (Claude, Codex, Copilot) under the same subprocess-based adapter pattern. + +### What was done +1. **`presets.ts`** — Added `COPILOT_PRESET`: spawns `copilot -p - --allow-all -s` with `stdinPrompt: true`. `-p -` reads prompt from stdin, `--allow-all` enables full permissions, `-s` gives clean text output. Model override via `--model`. Added to `PRESETS` registry. +2. **`copilot.ts`** — Rewrote from 391-line SDK adapter to 28-line `CliProxyAdapter` wrapper (same pattern as `claude.ts` and `codex.ts`). `CopilotAdapterOptions` now matches the CLI pattern: `model`, `extraFlags`, `commandPath`. +3. **`cli-args.ts`** — Changed from dynamic SDK import to direct `CopilotAdapter` import. Now passes `agentPassthrough` as `extraFlags` (was missing before). Updated usage text to list copilot. Removed `"copilot"` from hardcoded available list (now discovered via `PRESETS`). +4. **`package.json`** — Removed `@github/copilot-sdk` dependency. Removed `postinstall` script (was patching SDK ESM imports). +5. **`presets.test.ts`** — Added 2 tests for `COPILOT_PRESET` (default args + model override). Added 1 test for Codex model override. + +### What was removed +- `@github/copilot-sdk` dependency (and transitive `vscode-jsonrpc`) +- `scripts/patch-copilot-sdk.cjs` (SDK ESM patch — no longer needed) +- SDK-specific code: `CopilotClient`, `CopilotSession`, `SessionEvent`, `approveAll`, event monkey-patching, `ensureClient()`, session management, JSON-RPC communication +- ~360 lines of SDK-specific adapter code + +### Key decisions +- Used `-p -` (dash convention) for stdin prompt reading — avoids command-line length limits with large prompts +- Used `--allow-all` instead of individual `--allow-all-tools --allow-all-paths --allow-all-urls` — equivalent but shorter +- Used `-s` (silent) for clean text output — no stats or progress noise to parse +- Did NOT use `--output-format json` yet — start simple with text, can add JSON later for activity tracking +- All CLI passthrough flags now work for copilot (e.g. `teammates copilot --reasoning-effort high --add-dir /tmp`) + +### Build & test +- TypeScript compiles cleanly (strict mode) +- Biome lint clean +- 6 preset tests pass (including 2 new copilot tests) + +### Files changed +- `packages/cli/src/adapters/presets.ts` +- `packages/cli/src/adapters/copilot.ts` (rewritten) +- `packages/cli/src/cli-args.ts` +- `packages/cli/package.json` +- `packages/cli/src/adapters/presets.test.ts` + +### Next +- Restart the CLI so it loads the new adapter code +- Test with `teammates copilot` to verify `copilot -p -` reads from stdin correctly +- `scripts/patch-copilot-sdk.cjs` can be deleted in a non-sandboxed session +- Consider adding `--output-format json` + `parseOutput` later for structured activity tracking + +## Task: Fix Copilot empty debug log + missing [show activity] +Root cause: the new Copilot CLI preset was wrong in two ways. (1) `copilot -p -` does not read stdin, so Copilot treated the prompt as empty and returned the same "empty message" text the user saw in the debug log/UI. (2) The preset ran in plain text mode, so there was no structured tool stream for `[show activity]` to parse. + +### What was changed +1. **`presets.ts`** — Switched Copilot to JSONL output (`--output-format json --stream off --no-color`) and added `parseOutput()` to extract the last non-empty `assistant.message`. On Windows, the preset now launches through `powershell.exe -EncodedCommand` so it can read the full prompt from the temp prompt file and pass it to `copilot -p` without stdin or shell-quoting issues. Added `-NonInteractive` plus `$ProgressPreference = 'SilentlyContinue'` to keep stderr clean. +2. **`activity-watcher.ts`** — Added `parseCopilotJsonlLine()` and `watchCopilotDebugLog()` for Copilot JSONL events. Maps `tool.execution_start` events like `view`, `shell`, `grep`, `glob`, `edit`, and `write` into the existing activity labels. +3. **`cli-proxy.ts`** — Wired the Copilot adapter to tail its paired `.teammates\\.tmp\\debug\\*.md` log file during execution, the same way Codex tails its JSONL debug log. +4. **Tests** — Added focused Copilot parser/preset tests. Clean `npm run build` passed after the change. Direct runtime sanity check against the compiled preset confirmed the Windows wrapper now emits JSONL with `tool.execution_start`, includes `toolName:\"view\"`, and returns the final `assistant.message`. + +### Key decisions +- Do not keep trying to force stdin into Copilot prompt mode; use the prompt file as the source of truth and wrap the Copilot call on Windows. +- Use Copilot's native JSONL mode instead of inventing a text parser; the tool stream is already there. +- Tail the existing paired debug log file in-memory for activity rather than creating any new disk intermediary. + +### Files changed +- `packages/cli/src/adapters/presets.ts` +- `packages/cli/src/adapters/copilot.ts` +- `packages/cli/src/activity-watcher.ts` +- `packages/cli/src/adapters/cli-proxy.ts` +- `packages/cli/src/activity-watcher.test.ts` +- `packages/cli/src/adapters/presets.test.ts` +- `packages/cli/src/index.ts` + +### Next +- Restart the running CLI process so it loads the rebuilt Copilot preset/watcher code. +- Re-run a Copilot task and toggle `[show activity]`; you should now see real `Read` / `Glob` / `Grep` / `Edit` lines instead of an empty block. + +## Task: Fix Copilot "filename or extension is too long" error on Windows +Root cause: the PowerShell `-EncodedCommand` wrapper read the prompt file into `$prompt`, then passed it as `-p $prompt` to copilot. When PowerShell called the `copilot` npm shim, which in turn called `node.exe copilot.exe -p `, the command line exceeded Windows' ~32K character limit for `CreateProcessW`. + +### Investigation +- Tested `copilot.exe -p -` with stdin piping → `-` treated as literal prompt text (not stdin). Dead end. +- Tested `copilot.exe -p @filepath` → `@filepath` treated as literal prompt text. Dead end. +- Tested copilot in **interactive mode** (no `-p` flag) with stdin piping → **works**. Copilot reads one message from stdin, processes it, and exits on EOF. +- The copilot CLI has NO `--prompt-file` or stdin flag. Interactive mode is the only path. + +### Fix +Replaced the entire PowerShell `-EncodedCommand` wrapper with simple stdin piping: +- `command: "copilot"` (same on all platforms — no more Windows-specific PowerShell wrapper) +- `stdinPrompt: true` — `spawnAndProxy()` pipes the prompt to copilot's stdin +- No `-p` flag — copilot reads stdin in interactive mode, processes the message, exits on EOF +- All other flags preserved: `--allow-all --output-format json --stream off --no-color` + +### Key decisions +- Stdin piping is the only way to get large prompts to copilot without hitting the command-line limit. +- The `-p` flag is fundamentally incompatible with large prompts on Windows since the text becomes a command-line argument. +- Interactive mode + stdin + EOF behaves identically to `-p` mode for single-message tasks. +- This simplifies the preset from 30 lines (with platform branching) to 15 lines (uniform across platforms). + +### Build & verification +- `npm run build` in `packages/cli` passed cleanly +- `npx biome check --write` on changed files — no fixes needed +- Verified via direct `copilot.exe` invocation that stdin piping with `--output-format json --stream off` returns proper JSONL with `assistant.message` events + +### Files changed +- `packages/cli/src/adapters/presets.ts` +- `packages/cli/src/adapters/presets.test.ts` + +### Next +- Restart the CLI so the rebuilt preset is loaded, then re-test a Copilot task. + +## Task: Diagnose Copilot "filename or extension is too long" error +User reported CLIXML PowerShell error from Copilot adapter: `Program 'node.exe' failed to run: The filename or extension is too long`. Error originated at `copilot.ps1:24`. + +### Root cause +User's running CLI process was still loaded with the old Copilot preset code that used `powershell.exe -EncodedCommand` to pass the full teammate prompt as a Base64-encoded command-line argument. For large prompts (~50-100KB), this exceeds Windows' 32,767-character command-line limit. The current source and built `dist/` already use stdin piping (`stdinPrompt: true`) which has no length limit — confirmed working with 116KB test prompts. The user just needed to restart the CLI to pick up the rebuilt code. + +### Key decisions +- No code changes needed — the fix (stdin piping) was already present in source and dist. +- Root cause was stale cached modules from a pre-rebuild CLI session. +- Confirmed via direct spawn test that copilot handles 116KB stdin prompts without error. + +### Files changed +- None (diagnosis only) diff --git a/.teammates/lexicon/memory/weekly/2026-W13.md b/.teammates/lexicon/memory/weekly/2026-W13.md new file mode 100644 index 0000000..3cffce4 --- /dev/null +++ b/.teammates/lexicon/memory/weekly/2026-W13.md @@ -0,0 +1,96 @@ +--- +version: 0.7.1 +type: weekly +week: 2026-W13 +period: 2026-03-23 to 2026-03-29 +--- + +# Week 2026-W13 + +## 2026-03-23 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-23 + +## Notes + +- **Reinforcement block misplaced in SOUL.md:** Added 10-line reinforcement block to SOUL.md, but stevenic corrected — SOUL.md lands in ``, not ``. Removed the block. +- **Re-handed off instruction-reinforcement-spec to Beacon** for implementation in adapter.ts `instrLines` array. Spec was correct, just placed in wrong file. + +## 2026-03-25 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-25 + +## Notes + +- **Removed "text response first" instruction** from `` in `packages/cli/src/adapter.ts`. stevenic flagged it as confusing — instructions should constrain *what*, not *when* relative to tool calls. Simplified REMINDER, changed "After writing your text response" → "After completing the task" in 2 places. + +## 2026-03-26 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-26 + +## Notes + +- No user-requested work performed this day. + +## 2026-03-27 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-27 + +## Notes + +- **Attention dilution diagnosis:** Scribe failed to process conversation history in loreweaver brainstorm. Diagnosed 3 broken layers: (1) distance — task buried after 3K+ words, (2) compression — bloated daily logs + recall duplication, (3) decompression — continuity housekeeping hijacked all 4 tool calls before reaching task. Wrote 5-fix spec at `docs/specs/attention-dilution-fix.md`. Handed off to Beacon. + +## 2026-03-28 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-28 + +## Notes + +- **Standup (×8):** Delivered standup reports. No active prompt work or blockers. + +## 2026-03-29 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-29 + +## Notes + +- **Standup:** Delivered standup report. No active prompt work or blockers. +- **Standup (2):** Delivered standup. Noted need to review adapter.ts prompt assembly after thread-view branch lands. +- **Standup (3):** Delivered standup. No new prompt work or blockers since earlier 2026-03-29 updates. Session file updated. Remaining watch item: review `packages\cli\src\adapter.ts` after thread-view branch lands. +- **Standup (4):** Delivered standup. Status unchanged: no new prompt architecture work completed this turn, no blockers, and the remaining watch item is still to review `packages\cli\src\adapter.ts` after the thread-view branch lands. Files changed: `.teammates\lexicon\memory\2026-03-29.md`. diff --git a/.teammates/pipeline/memory/weekly/2026-W13.md b/.teammates/pipeline/memory/weekly/2026-W13.md index a3a279a..5946be4 100644 --- a/.teammates/pipeline/memory/weekly/2026-W13.md +++ b/.teammates/pipeline/memory/weekly/2026-W13.md @@ -1,9 +1,8 @@ --- -version: 0.7.0 +version: 0.7.1 type: weekly week: 2026-W13 -period: 2026-03-23 to 2026-03-26 -partial: true +period: 2026-03-23 to 2026-03-29 --- # Week 2026-W13 @@ -54,6 +53,6 @@ partial: true ## 2026-03-26 -# Pipeline — Daily Log — 2026-03-26 +## 2026-03-28 (No user-requested tasks this day) diff --git a/.teammates/scribe/memory/2026-03-30.md b/.teammates/scribe/memory/2026-03-30.md new file mode 100644 index 0000000..b5ff69e --- /dev/null +++ b/.teammates/scribe/memory/2026-03-30.md @@ -0,0 +1,12 @@ +--- +version: 0.7.1 +type: daily +--- +# 2026-03-30 + +## Posted daily standup +- Compiled standup covering work since 03-29 for Scribe and Beacon +- Scribe highlights: widget model redesign spec, GOALS.md template + propagation, template improvement analysis, README sandboxing note +- Beacon highlights: Win32 mouse fix (koffi FFI + SetConsoleMode), full mouse protocol support (classic/URXVT/UTF-8/SGR-Pixels), environment-aware terminal init, Copilot activity parsing expansion, version bumps to 0.7.3 with all 1,000 tests passing +- Handed off to @lexicon and @pipeline for their sections +- Files changed: `.teammates/_standups/2026-03-30.md` (new) diff --git a/.teammates/scribe/memory/weekly/2026-W13.md b/.teammates/scribe/memory/weekly/2026-W13.md index 04f2783..4cfc86a 100644 --- a/.teammates/scribe/memory/weekly/2026-W13.md +++ b/.teammates/scribe/memory/weekly/2026-W13.md @@ -1,9 +1,8 @@ --- -version: 0.7.0 +version: 0.7.1 type: weekly week: 2026-W13 -period: 2026-03-23 to 2026-03-23 -partial: true +period: 2026-03-23 to 2026-03-29 --- # Week 2026-W13 @@ -40,3 +39,166 @@ partial: true ### Files changed - None (handoff only) + +## 2026-03-25 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-25 + +## Recalled brainstorm results for user (×5) +- Delivered 03-17 brainstorm table each time: 13 features, 75 vote points. No files changed. + +## Posted daily standup +- Files changed: `.teammates/_standups/2026-03-25.md` (new) + +## Created standup rollup for Teams +- Compiled all 4 teammates' standups for user to post to Teams. No files changed. + +## 2026-03-26 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-26 + +_No user-requested work this day._ + +## 2026-03-27 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-27 + +## Analyzed bard hang in loreweave +- Initial analysis (API retry loop) was wrong — user shared /debug analysis showing actual root cause: bulk file creation (42 files) exceeding 600s timeout +- Three [TIMEOUT] events (2 Beacon, 1 Bard) — all from bulk creation tasks +- Fix: break bulk creation into smaller batches or increase agent timeout +- No files changed. + +## Designed interrupt-and-resume mechanism for agent timeouts +- Checkpoint/restore using conversation log as state: kill agent, parse log, append user interruption, resume with original context +- Two-part: manual `/interrupt [message]` + automatic at 80% timeout +- No files changed (design discussion). + +## Wrote interrupt-and-resume spec + handoff to Beacon +- 3 phases: Phase 1 manual `/interrupt`, Phase 2 auto-interrupt at 80% timeout, Phase 3 log compaction for long sessions +- Key: expose `killAgent()` on adapter, log parsing per preset (Claude JSON, Codex JSONL, others raw stdout) +- Resume prompt: original prompt + condensed conversation log + interruption text + resume instructions +- Files changed: `.teammates/scribe/docs/specs/F-interrupt-and-resume.md` (new), `.teammates/CROSS-TEAM.md` + +## 2026-03-28 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-28 + +## Posted daily standup +- Wrote standup covering work since 03-25: interrupt-and-resume spec, timeout analysis, wisdom compaction, log compression +- Re-delivered twice (once with Thread View Redesign update); handed off to @beacon, @lexicon, @pipeline +- Files changed: `.teammates/_standups/2026-03-28.md` (new, then updated) + +## Thread View Redesign spec +- Created design spec for thread view architecture overhaul +- Key decisions: ThreadContainer abstraction, inline verbs on subject lines, thread-level [reply]/[copy thread], conversation history owned by threads, auto-reply to bottom thread +- Handed off to @beacon for implementation +- Files changed: `.teammates/scribe/docs/specs/F-thread-view-redesign.md` (new), `.teammates/CROSS-TEAM.md` + +## 2026-03-29 + +--- +version: 0.7.0 +type: daily +compressed: true +--- + +# 2026-03-29 + +## Thread view post-mortem analysis +- User asked why thread view took so many rounds; analyzed Beacon's 03-28/03-29 daily logs (18 thread-related tasks), 58 debug logs, WISDOM.md (167 lines, 7 thread entries) +- Root causes: (1) spec came after code not before, (2) feed index management has no tests, (3) serial visual feedback instead of batched, (4) cli.ts at 6,800 lines too large for AI, (5) WISDOM bloated with implementation recipes, (6) logged-but-not-committed fix wasted a round +- Recommendations delivered: WISDOM cap, verify-before-logging rule, spec-first for UI, batch feedback, mandatory index tests, cli.ts extraction +- No files changed (analysis only) + +## Migration v0.5.0 → v0.7.0 +- Applied 0.6.0 upgrade: compressed `2026-03-29.md` (only uncompressed daily log) +- Applied 0.7.0 upgrade: bumped `version` to `0.7.0` across all 22 memory files (16 daily, 3 weekly, 4 typed) +- Scrubbed system task entries (wisdom compaction, log compression) from 6 daily logs: 03-21, 03-25, 03-26, 03-27, 03-28, 03-29 +- Capped WISDOM.md: removed "Context window has a token budget" (implementation recipe); 18 entries remain +- Files changed: all `memory/*.md`, `memory/weekly/*.md`, `WISDOM.md` + +## Posted daily standup +- Wrote standup covering work since 03-28: thread view post-mortem, v0.7.0 migration +- Handed off to @beacon, @lexicon, @pipeline +- Files changed: `.teammates/_standups/2026-03-29.md` (new) + +## Widget model redesign spec +- Analyzed consolonia widget architecture (1,623-line ChatView, 5 parallel index-keyed structures, manual `_shiftFeedIndices`) +- Designed identity-based FeedItem model + FeedStore collection + VirtualList widget extraction +- Spec at `.teammates/scribe/docs/specs/F-widget-model-redesign.md`, pointer added to CROSS-TEAM.md +- 4-phase migration: FeedStore → VirtualList → Selection extraction → Dropdown extraction +- Handed off to @beacon for implementation +- Files changed: `F-widget-model-redesign.md` (new), `CROSS-TEAM.md` + +## /Command rationalization +- Audited all 16 current slash commands with user +- Proposed removing 3 (/compact, /copy, /theme), renaming 1 (/init → /setup), adding 3 (/add, /remove, /update) +- Key decisions: /compact name conflicts with Claude's context compaction; /copy obsoleted by [verbs]; teammate management needs first-class commands +- Open questions: keep /script? fold /configure into /setup? automate /retro? +- No files changed (design discussion) + +## README sandboxing note +- Updated `README.md` to tell users to run their coding agent at least once from the project's workspace folder so workspace sandboxing is initialized before using teammates +- Decision: place the note in "Getting Started" under "Launch a session" so it appears in the main startup path +- Files changed: `README.md` +- Next task should know: this was a docs-only change; no onboarding or template files were updated + +## Standup rerun +- User asked to run a standup again on 2026-03-29 +- Updated shared standup to mark the rerun, preserve current Scribe/Beacon sections, and note Lexicon/Pipeline as pending +- Files changed: `.teammates/_standups/2026-03-29.md` +- Next task should know: final response includes handoff blocks to Lexicon and Pipeline so they can post their standups + +## Standup rerun (second) +- User asked to run standup again on 2026-03-29 +- Refreshed shared standup wording for the second rerun; current sections remain Scribe and Beacon, with Lexicon and Pipeline still pending +- Files changed: `.teammates/_standups/2026-03-29.md` +- Next task should know: this turn re-issued handoff blocks to Lexicon and Pipeline in the visible response + +## Template improvement analysis +- User asked to review .teammates/ vs template/ and propose improvements based on lessons learned +- Explored all teammate folders, WISDOM files, daily logs, PROTOCOL, PROJECTS, template files +- Identified 9 gaps between template and practice: GOALS.md (user suggestion + Pipeline precedent), daily log frontmatter, decision typed memory, docs/specs/ convention, prefix naming (_standups, .tmp), PROJECTS.md, ARCHITECTURE.md, wisdom caps, verify-before-logging +- Delivered prioritized recommendation table +- No files changed (analysis only) +- Next task should know: user may ask to draft the actual template changes + +## Drafted GOALS.md template +- Added GOALS.md as a new standard file in both `template/TEMPLATE.md` (v2→v3) and `.teammates/TEMPLATE.md` +- Updated Continuity sections in both templates + example SOUL.md to reference GOALS.md in the session startup reading list +- Created `template/example/GOALS.md` (Atlas example with P0/P1/P2 tiers + Completed section) +- GOALS.md purpose: tracks intent/direction, distinct from SOUL.md (identity) and WISDOM.md (knowledge) +- Guidelines: one line per goal, link to specs, mark done with date, scannable by user +- Files changed: `template/TEMPLATE.md`, `.teammates/TEMPLATE.md`, `template/example/SOUL.md`, `template/example/GOALS.md` (new) +- Next task should know: remaining template improvements from analysis (daily log frontmatter, docs/specs/ convention, etc.) not yet drafted + +## Propagated GOALS.md across all docs +- Updated every doc that references the teammate file structure to include GOALS.md +- Files changed: `README.md`, `ONBOARDING.md`, `template/README.md`, `template/PROTOCOL.md`, `.teammates/PROTOCOL.md`, `.teammates/README.md`, `docs/index.md`, `docs/adoption-guide.md`, `docs/cookbook.md`, `docs/teammates-memory.md`, `docs/working-with-teammates.md`, `docs/teammates-vision.md` +- Key changes: added GOALS.md to file layout diagrams, context window prompt stacks (renumbered steps), session startup reading lists, "add a new teammate" recipes, verification checklists, and Key Concepts sections +- Next task should know: GOALS.md is now fully documented across the framework; the CLI prompt stack will need @beacon to add GOALS.md injection (item 2 in the prompt stack) diff --git a/.teammates/settings.json b/.teammates/settings.json index 0132c00..05f74de 100644 --- a/.teammates/settings.json +++ b/.teammates/settings.json @@ -5,5 +5,5 @@ "name": "recall" } ], - "cliVersion": "0.7.0" + "cliVersion": "0.7.3" } diff --git a/package-lock.json b/package-lock.json index 59369fe..25787ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -276,133 +276,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@github/copilot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.7.tgz", - "integrity": "sha512-KHBaJ1kbc19pqUMnB9LubPtwWVOaDCzWbzwsJss+DvHyCpr8wP8jR3GEZUnhq3rsuXI96ZKEeEozXM0NqxCAiw==", - "license": "SEE LICENSE IN LICENSE.md", - "bin": { - "copilot": "npm-loader.js" - }, - "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.7", - "@github/copilot-darwin-x64": "1.0.7", - "@github/copilot-linux-arm64": "1.0.7", - "@github/copilot-linux-x64": "1.0.7", - "@github/copilot-win32-arm64": "1.0.7", - "@github/copilot-win32-x64": "1.0.7" - } - }, - "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.7.tgz", - "integrity": "sha512-yQITowpkQYamww59CwcG5JTWV9ahj7nMH6oqObMJaeqXnG7j7dqE/YhLkujQZ3XR8VXAoIa1rZ3TahdMu94gOA==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-arm64": "copilot" - } - }, - "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.7.tgz", - "integrity": "sha512-23vP5bHaFA030nB3tr+dUUdRm2SqmQbs2fZUQ4F7JeYy59jp9hi8lBdaZp/TeQnjEirAUU9H2HZxsGRIIUWp7g==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-x64": "copilot" - } - }, - "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.7.tgz", - "integrity": "sha512-g0mB98oyXKcpd4sMNBc5n1h3UhLy9AGRlT//VL8BXPSzvlTH/dJP3fdx74pbLSgvz105to/YUMmEAFfv25VNaw==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linux-x64": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.7.tgz", - "integrity": "sha512-TRxzvTo9I4ehYJLFHTCJSJYQ4QnO/V9zebqwszxHpJRxuBd7FV4cxLmfOBqZcUpEpZgBH+VJ4OG98BPW7YEtJQ==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-x64": "copilot" - } - }, - "node_modules/@github/copilot-sdk": { - "version": "0.1.32", - "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.1.32.tgz", - "integrity": "sha512-mPWM0fw1Gqc/SW8nl45K8abrFH+92fO7y6tRtRl5imjS5hGapLf/dkX5WDrgPtlsflD0c41lFXVUri5NVJwtoA==", - "license": "MIT", - "dependencies": { - "@github/copilot": "^1.0.2", - "vscode-jsonrpc": "^8.2.1", - "zod": "^4.3.6" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.7.tgz", - "integrity": "sha512-4yFgW1K0MlKBrK5BwMIj4nMu5KSFfytNXrs8iOpVgp7erEvKVyN7VXb6SWkoU3M9TfeNlqP6Uje2rxDvgR1u5w==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-arm64": "copilot.exe" - } - }, - "node_modules/@github/copilot-win32-x64": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.7.tgz", - "integrity": "sha512-RDZlvPf/q6B54wLXJRmI39fc9+pwfcAjSwUqw0FeQruCTQgoUl8eo9NqeVWDFlr3RdzgVSMUiJHc3aiifVG6lA==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-x64": "copilot.exe" - } - }, "node_modules/@huggingface/jinja": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.6.tgz", @@ -3928,15 +3801,6 @@ } } }, - "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.11", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.11.tgz", - "integrity": "sha512-u6LElQNbSiE9OugEEmrUKwH6+8BpPz2S5MDHvQUqHL//I4Q8GPikKLOUf856UnbLkZdhxaPrExac1lA3XwpIPA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/web-streams-polyfill": { "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", @@ -4247,22 +4111,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "packages/cli": { "name": "@teammates/cli", - "version": "0.7.0", - "hasInstallScript": true, + "version": "0.7.3", "license": "MIT", "dependencies": { - "@github/copilot-sdk": "^0.1.32", "@teammates/consolonia": "*", "@teammates/recall": "*", "chalk": "^5.6.2", @@ -4283,7 +4136,7 @@ }, "packages/consolonia": { "name": "@teammates/consolonia", - "version": "0.7.0", + "version": "0.7.3", "license": "MIT", "dependencies": { "marked": "^17.0.4" @@ -4300,7 +4153,7 @@ }, "packages/recall": { "name": "@teammates/recall", - "version": "0.7.0", + "version": "0.7.3", "license": "MIT", "dependencies": { "@huggingface/transformers": "^3.0.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 5cb127b..64f05a4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@teammates/cli", - "version": "0.7.0", + "version": "0.7.3", "description": "Agent-agnostic CLI for teammates. Routes tasks, manages handoffs, and plugs into any coding agent backend.", "type": "module", "main": "dist/index.js", diff --git a/packages/cli/src/activity-watcher.test.ts b/packages/cli/src/activity-watcher.test.ts index d35cb9d..c0b927e 100644 --- a/packages/cli/src/activity-watcher.test.ts +++ b/packages/cli/src/activity-watcher.test.ts @@ -117,7 +117,12 @@ describe("parseCodexJsonlLine", () => { }); expect(parseCodexJsonlLine(line, start, receivedAt)).toEqual([ - { elapsedMs: 4_000, tool: "Edit", detail: "personas.ts (+1 files)", isError: false }, + { + elapsedMs: 4_000, + tool: "Edit", + detail: "personas.ts (+1 files)", + isError: false, + }, { elapsedMs: 4_000, tool: "Write", detail: "WISDOM.md", isError: false }, ]); }); diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 1995550..7ad3aaa 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -6,14 +6,14 @@ * getThreadTeammates, buildSessionMarkdown, doCopy, feedCommand, printBanner). */ -import { exec as execCb } from "node:child_process"; +import { exec as execCb, execSync } from "node:child_process"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { mkdir, stat } from "node:fs/promises"; import { basename, join, resolve } from "node:path"; - import { type ChatView, concat, + detectTerminal, esc, pen, type StyledSpan, @@ -243,6 +243,13 @@ export class CommandManager { description: "Show current theme colors", run: () => this.cmdTheme(), }, + { + name: "about", + aliases: ["info", "diag"], + usage: "/about", + description: "Show version, platform, and diagnostic info", + run: () => this.cmdAbout(), + }, { name: "configure", aliases: ["config"], @@ -1457,6 +1464,132 @@ Issues that can't be resolved unilaterally — they need input from other teamma d.refreshView(); } + // ── /about ───────────────────────────────────────────────────────── + + private async cmdAbout(): Promise { + const d = this.deps; + const caps = detectTerminal(); + + // Gather diagnostic info + const lines: string[] = []; + const add = (label: string, value: string) => { + lines.push(` ${label.padEnd(22)} ${value}`); + }; + + lines.push(""); + lines.push(" Teammates — Diagnostic Info"); + lines.push(` ${"─".repeat(50)}`); + lines.push(""); + + // ── Version & runtime ── + add("Version:", `v${PKG_VERSION}`); + add("Node.js:", process.version); + add("Platform:", `${process.platform} ${process.arch}`); + add( + "OS:", + `${process.env.OS || process.platform} (${process.env.PROCESSOR_ARCHITECTURE || process.arch})`, + ); + + // ── Terminal ── + lines.push(""); + add("Terminal:", caps.name); + add("TTY:", caps.isTTY ? "yes" : "no"); + add("Columns:", `${process.stdout.columns || "unknown"}`); + add("Rows:", `${process.stdout.rows || "unknown"}`); + add("TERM:", process.env.TERM || "(not set)"); + add("TERM_PROGRAM:", process.env.TERM_PROGRAM || "(not set)"); + if (process.platform === "win32") { + add("WT_SESSION:", process.env.WT_SESSION ? "yes" : "no"); + add("ConEmuPID:", process.env.ConEmuPID || "(not set)"); + } + + // ── Capabilities ── + lines.push(""); + const flag = (b: boolean) => (b ? "yes" : "no"); + add("Mouse:", flag(caps.mouse)); + add("SGR Mouse:", flag(caps.sgrMouse)); + add("Alternate Screen:", flag(caps.alternateScreen)); + add("Bracketed Paste:", flag(caps.bracketedPaste)); + add("Truecolor:", flag(caps.truecolor)); + add("256 Color:", flag(caps.color256)); + + // ── Agent / adapter ── + lines.push(""); + add("Adapter:", d.adapterName); + const registry = d.orchestrator.getRegistry(); + const teammates = registry.list(); + add("Teammates:", `${teammates.length} (${teammates.join(", ")})`); + add("Teammates Dir:", d.teammatesDir); + + // ── Services ── + lines.push(""); + for (const svc of d.serviceStatuses) { + add(`${svc.name}:`, svc.status); + } + + // GitHub CLI version (if available) + const ghSvc = d.serviceStatuses.find((s) => s.name === "GitHub"); + if ( + ghSvc && + (ghSvc.status === "configured" || ghSvc.status === "not-configured") + ) { + try { + const ghVersion = execSync("gh --version", { + stdio: "pipe", + encoding: "utf-8", + }) + .trim() + .split("\n")[0]; + add("GitHub CLI:", ghVersion); + } catch { + // already reported as missing + } + } + + // ── Internal state ── + lines.push(""); + add("Active Tasks:", `${d.agentActive.size}`); + add("Queued Tasks:", `${d.taskQueue.length}`); + add("Threads:", `${d.threads.size}`); + add( + "Focused Thread:", + d.focusedThreadId !== null ? `#${d.focusedThreadId}` : "none", + ); + add("Conversation Len:", `${d.conversation.history.length} messages`); + + lines.push(""); + + // Display + const text = lines.join("\n"); + for (const line of lines) { + if (line.includes("─")) { + d.feedLine(tp.muted(line)); + } else if ( + line.trim() === "" || + line.includes("Teammates — Diagnostic") + ) { + d.feedLine(line.includes("Diagnostic") ? tp.bold(line) : undefined); + } else { + const colonIdx = line.indexOf(":"); + if (colonIdx > 0 && colonIdx < 26) { + d.feedLine( + concat( + tp.muted(line.slice(0, colonIdx + 1)), + tp.text(line.slice(colonIdx + 1)), + ), + ); + } else { + d.feedLine(tp.text(line)); + } + } + } + + // Copy to clipboard + this.doCopy(text); + + d.refreshView(); + } + // ── printBanner (pre-TUI fallback) ───────────────────────────────── printBanner(teammates: string[]): void { diff --git a/packages/consolonia/package.json b/packages/consolonia/package.json index 568c9ff..63f308d 100644 --- a/packages/consolonia/package.json +++ b/packages/consolonia/package.json @@ -1,6 +1,6 @@ { "name": "@teammates/consolonia", - "version": "0.7.0", + "version": "0.7.3", "description": "Terminal UI rendering engine inspired by Consolonia. Pixel-level compositing with ANSI output.", "type": "module", "main": "dist/index.js", diff --git a/packages/consolonia/src/__tests__/ansi.test.ts b/packages/consolonia/src/__tests__/ansi.test.ts index d3ef4cf..ba8f96c 100644 --- a/packages/consolonia/src/__tests__/ansi.test.ts +++ b/packages/consolonia/src/__tests__/ansi.test.ts @@ -1,9 +1,10 @@ import { Writable } from "node:stream"; -import { beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import * as esc from "../ansi/esc.js"; import { AnsiOutput } from "../ansi/output.js"; import { stripAnsi, truncateAnsi, visibleLength } from "../ansi/strip.js"; +import { detectTerminal, type TerminalCaps } from "../ansi/terminal-env.js"; // ── Helpers ──────────────────────────────────────────────────────── @@ -157,12 +158,16 @@ describe("esc", () => { expect(esc.bracketedPasteOff).toBe(`${ESC}?2004l`); }); - it("mouseTrackingOn enables button-event tracking and SGR mode", () => { - expect(esc.mouseTrackingOn).toBe(`${ESC}?1003h${ESC}?1006h`); + it("mouseTrackingOn enables all mouse tracking modes", () => { + expect(esc.mouseTrackingOn).toBe( + `${ESC}?1000h${ESC}?1003h${ESC}?1005h${ESC}?1006h${ESC}?1015h${ESC}?1016h`, + ); }); - it("mouseTrackingOff disables SGR mode and button-event tracking", () => { - expect(esc.mouseTrackingOff).toBe(`${ESC}?1006l${ESC}?1003l`); + it("mouseTrackingOff disables all mouse tracking modes", () => { + expect(esc.mouseTrackingOff).toBe( + `${ESC}?1016l${ESC}?1015l${ESC}?1006l${ESC}?1005l${ESC}?1003l${ESC}?1000l`, + ); }); }); @@ -672,3 +677,323 @@ describe("AnsiOutput", () => { }); }); }); + +// ═══════════════════════════════════════════════════════════════════ +// terminal-env.ts +// ═══════════════════════════════════════════════════════════════════ + +describe("detectTerminal", () => { + const origEnv = { ...process.env }; + const origPlatform = process.platform; + const origIsTTY = process.stdout.isTTY; + + afterEach(() => { + // Restore environment + for (const key of Object.keys(process.env)) { + if (!(key in origEnv)) delete process.env[key]; + } + Object.assign(process.env, origEnv); + Object.defineProperty(process, "platform", { value: origPlatform }); + Object.defineProperty(process.stdout, "isTTY", { + value: origIsTTY, + writable: true, + }); + }); + + function setEnv(overrides: Record) { + for (const [k, v] of Object.entries(overrides)) { + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + } + + it("returns pipe caps when stdout is not a TTY", () => { + Object.defineProperty(process.stdout, "isTTY", { + value: false, + writable: true, + }); + const caps = detectTerminal(); + expect(caps.isTTY).toBe(false); + expect(caps.mouse).toBe(false); + expect(caps.alternateScreen).toBe(false); + expect(caps.name).toBe("pipe"); + }); + + it("detects Windows Terminal via WT_SESSION", () => { + Object.defineProperty(process, "platform", { value: "win32" }); + Object.defineProperty(process.stdout, "isTTY", { + value: true, + writable: true, + }); + setEnv({ WT_SESSION: "some-guid" }); + const caps = detectTerminal(); + expect(caps.name).toBe("windows-terminal"); + expect(caps.sgrMouse).toBe(true); + expect(caps.truecolor).toBe(true); + }); + + it("detects VS Code terminal on Windows", () => { + Object.defineProperty(process, "platform", { value: "win32" }); + Object.defineProperty(process.stdout, "isTTY", { + value: true, + writable: true, + }); + setEnv({ WT_SESSION: undefined, TERM_PROGRAM: "vscode" }); + const caps = detectTerminal(); + expect(caps.name).toBe("vscode"); + expect(caps.sgrMouse).toBe(true); + }); + + it("detects ConEmu on Windows", () => { + Object.defineProperty(process, "platform", { value: "win32" }); + Object.defineProperty(process.stdout, "isTTY", { + value: true, + writable: true, + }); + setEnv({ + WT_SESSION: undefined, + TERM_PROGRAM: undefined, + ConEmuPID: "1234", + }); + const caps = detectTerminal(); + expect(caps.name).toBe("conemu"); + }); + + it("detects mintty via TERM + MSYSTEM", () => { + Object.defineProperty(process, "platform", { value: "win32" }); + Object.defineProperty(process.stdout, "isTTY", { + value: true, + writable: true, + }); + setEnv({ + WT_SESSION: undefined, + TERM_PROGRAM: undefined, + ConEmuPID: undefined, + TERM: "xterm-256color", + MSYSTEM: "MINGW64", + }); + const caps = detectTerminal(); + expect(caps.name).toBe("mintty"); + }); + + it("detects tmux on Unix", () => { + Object.defineProperty(process, "platform", { value: "linux" }); + Object.defineProperty(process.stdout, "isTTY", { + value: true, + writable: true, + }); + setEnv({ TMUX: "/tmp/tmux-1000/default,1234,0", TERM: "screen-256color" }); + const caps = detectTerminal(); + expect(caps.name).toBe("tmux"); + expect(caps.sgrMouse).toBe(true); + }); + + it("detects GNU screen with limited caps", () => { + Object.defineProperty(process, "platform", { value: "linux" }); + Object.defineProperty(process.stdout, "isTTY", { + value: true, + writable: true, + }); + setEnv({ + TMUX: undefined, + TERM: "screen", + TERM_PROGRAM: undefined, + ITERM_SESSION_ID: undefined, + }); + const caps = detectTerminal(); + expect(caps.name).toBe("screen"); + expect(caps.sgrMouse).toBe(false); + expect(caps.bracketedPaste).toBe(false); + }); + + it("detects iTerm2", () => { + Object.defineProperty(process, "platform", { value: "darwin" }); + Object.defineProperty(process.stdout, "isTTY", { + value: true, + writable: true, + }); + setEnv({ + TMUX: undefined, + TERM: "xterm-256color", + TERM_PROGRAM: "iTerm.app", + }); + const caps = detectTerminal(); + expect(caps.name).toBe("iterm2"); + expect(caps.truecolor).toBe(true); + }); + + it("detects xterm-compatible with COLORTERM truecolor", () => { + Object.defineProperty(process, "platform", { value: "linux" }); + Object.defineProperty(process.stdout, "isTTY", { + value: true, + writable: true, + }); + setEnv({ + TMUX: undefined, + TERM: "xterm-256color", + TERM_PROGRAM: undefined, + ITERM_SESSION_ID: undefined, + COLORTERM: "truecolor", + }); + const caps = detectTerminal(); + expect(caps.truecolor).toBe(true); + expect(caps.color256).toBe(true); + }); + + it("detects dumb terminal with minimal caps", () => { + Object.defineProperty(process, "platform", { value: "linux" }); + Object.defineProperty(process.stdout, "isTTY", { + value: true, + writable: true, + }); + setEnv({ + TMUX: undefined, + TERM: "dumb", + TERM_PROGRAM: undefined, + ITERM_SESSION_ID: undefined, + }); + const caps = detectTerminal(); + expect(caps.name).toBe("dumb"); + expect(caps.mouse).toBe(false); + expect(caps.alternateScreen).toBe(false); + }); +}); + +// ═══════════════════════════════════════════════════════════════════ +// esc.ts — environment-aware init/restore sequences +// ═══════════════════════════════════════════════════════════════════ + +describe("esc environment-aware sequences", () => { + /** Helper to build a TerminalCaps with overrides. */ + function makeCaps(overrides: Partial = {}): TerminalCaps { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: true, + color256: true, + name: "test", + ...overrides, + }; + } + + describe("mouseOn / mouseOff", () => { + it("returns full mouse sequence when SGR is supported", () => { + const caps = makeCaps({ sgrMouse: true }); + expect(esc.mouseOn(caps)).toBe(esc.mouseTrackingOn); + expect(esc.mouseOff(caps)).toBe(esc.mouseTrackingOff); + }); + + it("returns minimal mouse sequence when SGR is not supported", () => { + const caps = makeCaps({ sgrMouse: false }); + const on = esc.mouseOn(caps); + expect(on).toContain("?1000h"); + expect(on).toContain("?1003h"); + expect(on).not.toContain("?1006h"); + expect(on).not.toContain("?1015h"); + }); + + it("returns empty string when mouse is not supported", () => { + const caps = makeCaps({ mouse: false }); + expect(esc.mouseOn(caps)).toBe(""); + expect(esc.mouseOff(caps)).toBe(""); + }); + }); + + describe("initSequence", () => { + it("includes all features for a full-caps terminal", () => { + const caps = makeCaps(); + const seq = esc.initSequence(caps, { + alternateScreen: true, + mouse: true, + }); + expect(seq).toContain(esc.alternateScreenOn); + expect(seq).toContain(esc.hideCursor); + expect(seq).toContain(esc.bracketedPasteOn); + expect(seq).toContain(esc.mouseTrackingOn); + expect(seq).toContain(esc.clearScreen); + }); + + it("skips alternate screen when not supported", () => { + const caps = makeCaps({ alternateScreen: false }); + const seq = esc.initSequence(caps, { + alternateScreen: true, + mouse: false, + }); + expect(seq).not.toContain(esc.alternateScreenOn); + }); + + it("skips alternate screen when app opts out", () => { + const caps = makeCaps(); + const seq = esc.initSequence(caps, { + alternateScreen: false, + mouse: false, + }); + expect(seq).not.toContain(esc.alternateScreenOn); + }); + + it("skips bracketed paste when not supported", () => { + const caps = makeCaps({ bracketedPaste: false }); + const seq = esc.initSequence(caps, { + alternateScreen: false, + mouse: false, + }); + expect(seq).not.toContain(esc.bracketedPasteOn); + }); + + it("skips mouse when app opts out even if caps support it", () => { + const caps = makeCaps(); + const seq = esc.initSequence(caps, { + alternateScreen: false, + mouse: false, + }); + expect(seq).not.toContain("?1000h"); + }); + + it("returns empty string for non-TTY", () => { + const caps = makeCaps({ isTTY: false }); + const seq = esc.initSequence(caps, { + alternateScreen: true, + mouse: true, + }); + expect(seq).toBe(""); + }); + }); + + describe("restoreSequence", () => { + it("includes all restore features for a full-caps terminal", () => { + const caps = makeCaps(); + const seq = esc.restoreSequence(caps, { + alternateScreen: true, + mouse: true, + }); + expect(seq).toContain(esc.reset); + expect(seq).toContain(esc.mouseTrackingOff); + expect(seq).toContain(esc.bracketedPasteOff); + expect(seq).toContain(esc.showCursor); + expect(seq).toContain(esc.alternateScreenOff); + }); + + it("mirrors initSequence — skips what init skipped", () => { + const caps = makeCaps({ bracketedPaste: false, alternateScreen: false }); + const seq = esc.restoreSequence(caps, { + alternateScreen: true, + mouse: false, + }); + expect(seq).not.toContain(esc.bracketedPasteOff); + expect(seq).not.toContain(esc.alternateScreenOff); + expect(seq).not.toContain(esc.mouseTrackingOff); + }); + + it("returns empty string for non-TTY", () => { + const caps = makeCaps({ isTTY: false }); + const seq = esc.restoreSequence(caps, { + alternateScreen: true, + mouse: true, + }); + expect(seq).toBe(""); + }); + }); +}); diff --git a/packages/consolonia/src/__tests__/input.test.ts b/packages/consolonia/src/__tests__/input.test.ts index 9c51262..94c8a33 100644 --- a/packages/consolonia/src/__tests__/input.test.ts +++ b/packages/consolonia/src/__tests__/input.test.ts @@ -41,6 +41,12 @@ function collectEvents(data: string): InputEvent[] { return collected; } +function x10Seq(cb: number, x: number, y: number): string { + return `\x1b[M${String.fromCharCode(cb + 32)}${String.fromCharCode( + x + 32, + )}${String.fromCharCode(y + 32)}`; +} + // ===================================================================== // EscapeMatcher // ===================================================================== @@ -779,6 +785,151 @@ describe("MouseMatcher", () => { expect(results[results.length - 1]).toBe(MatchResult.Complete); }); }); + + // ── URXVT sequences ──────────────────────────────────────────────── + + describe("URXVT sequences", () => { + it("parses URXVT left press at (9,19) from \\x1b[0;10;20M", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[0;10;20M")).toBe(MatchResult.Complete); + const ev = matcher.flush(); + expect(ev).not.toBeNull(); + expect(ev!.type).toBe("mouse"); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("left"); + expect(ev!.event.type).toBe("press"); + expect(ev!.event.x).toBe(9); + expect(ev!.event.y).toBe(19); + } + }); + + it("parses URXVT right press", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[2;5;5M")).toBe(MatchResult.Complete); + const ev = matcher.flush(); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("right"); + expect(ev!.event.type).toBe("press"); + expect(ev!.event.x).toBe(4); + expect(ev!.event.y).toBe(4); + } + }); + + it("parses URXVT release (button bits = 3)", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[3;10;20M")).toBe(MatchResult.Complete); + const ev = matcher.flush(); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("none"); + expect(ev!.event.type).toBe("release"); + } + }); + + it("parses URXVT wheel up (cb=64)", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[64;5;5M")).toBe(MatchResult.Complete); + const ev = matcher.flush(); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("none"); + expect(ev!.event.type).toBe("wheelup"); + } + }); + + it("parses URXVT wheel down (cb=65)", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[65;5;5M")).toBe(MatchResult.Complete); + const ev = matcher.flush(); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("none"); + expect(ev!.event.type).toBe("wheeldown"); + } + }); + + it("parses URXVT motion with left button (cb=32)", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[32;10;10M")).toBe(MatchResult.Complete); + const ev = matcher.flush(); + if (ev!.type === "mouse") { + expect(ev!.event.type).toBe("move"); + expect(ev!.event.button).toBe("left"); + } + }); + + it("parses URXVT shift+left press (cb=4)", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[4;1;1M")).toBe(MatchResult.Complete); + const ev = matcher.flush(); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("left"); + expect(ev!.event.type).toBe("press"); + expect(ev!.event.shift).toBe(true); + expect(ev!.event.ctrl).toBe(false); + expect(ev!.event.alt).toBe(false); + } + }); + + it("parses URXVT with large coordinates (beyond X10 range)", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[0;300;200M")).toBe(MatchResult.Complete); + const ev = matcher.flush(); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("left"); + expect(ev!.event.type).toBe("press"); + expect(ev!.event.x).toBe(299); + expect(ev!.event.y).toBe(199); + } + }); + + it("rejects URXVT with wrong param count (2 params)", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[0;10M")).toBe(MatchResult.NoMatch); + }); + + it("rejects URXVT with wrong param count (4 params)", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, "\x1b[0;10;20;30M")).toBe(MatchResult.NoMatch); + }); + }); + + describe("classic X10 sequences", () => { + it("parses left press from classic CSI M encoding", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, x10Seq(0, 10, 20))).toBe(MatchResult.Complete); + const ev = matcher.flush(); + expect(ev).not.toBeNull(); + expect(ev!.type).toBe("mouse"); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("left"); + expect(ev!.event.type).toBe("press"); + expect(ev!.event.x).toBe(9); + expect(ev!.event.y).toBe(19); + } + }); + + it("parses release from classic CSI M encoding", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, x10Seq(3, 10, 20))).toBe(MatchResult.Complete); + const ev = matcher.flush(); + expect(ev).not.toBeNull(); + expect(ev!.type).toBe("mouse"); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("none"); + expect(ev!.event.type).toBe("release"); + } + }); + + it("parses wheel down from classic CSI M encoding", () => { + matcher = new MouseMatcher(); + expect(feedString(matcher, x10Seq(65, 5, 5))).toBe(MatchResult.Complete); + const ev = matcher.flush(); + expect(ev).not.toBeNull(); + expect(ev!.type).toBe("mouse"); + if (ev!.type === "mouse") { + expect(ev!.event.button).toBe("none"); + expect(ev!.event.type).toBe("wheeldown"); + } + }); + }); }); // ===================================================================== @@ -968,6 +1119,31 @@ describe("InputProcessor", () => { } }); + it("URXVT mouse sequences are recognized through parallel matching", () => { + // URXVT: \x1b[0;5;10M — left press at (4,9) + const events = collectEvents("\x1b[0;5;10M"); + expect(events).toHaveLength(1); + expect(events[0].type).toBe("mouse"); + if (events[0].type === "mouse") { + expect(events[0].event.button).toBe("left"); + expect(events[0].event.type).toBe("press"); + expect(events[0].event.x).toBe(4); + expect(events[0].event.y).toBe(9); + } + }); + + it("classic X10 mouse sequences are recognized through parallel matching", () => { + const events = collectEvents(x10Seq(0, 1, 1)); + expect(events).toHaveLength(1); + expect(events[0].type).toBe("mouse"); + if (events[0].type === "mouse") { + expect(events[0].event.button).toBe("left"); + expect(events[0].event.type).toBe("press"); + expect(events[0].event.x).toBe(0); + expect(events[0].event.y).toBe(0); + } + }); + it("handles text after a paste sequence", () => { const events = collectEvents("\x1b[200~pasted\x1b[201~after"); expect(events).toHaveLength(6); // 1 paste + 5 chars diff --git a/packages/consolonia/src/__tests__/pixel.test.ts b/packages/consolonia/src/__tests__/pixel.test.ts index fa9d6bd..e2dcd25 100644 --- a/packages/consolonia/src/__tests__/pixel.test.ts +++ b/packages/consolonia/src/__tests__/pixel.test.ts @@ -388,6 +388,47 @@ describe("symbol", () => { // U+1F600 = grinning face (in emoji range 0x1f000-0x1faff) expect(charWidth(0x1f600)).toBe(2); }); + + it("text-presentation symbols that remain width 1", () => { + // These render as 1 cell even on Windows Terminal + expect(charWidth(0x2713)).toBe(1); // ✓ Check Mark + expect(charWidth(0x2702)).toBe(1); // ✂ Scissors + expect(charWidth(0x00a9)).toBe(1); // © Copyright + expect(charWidth(0x00ae)).toBe(1); // ® Registered + }); + + it("emoji-presentation symbols are width 2", () => { + // These have Emoji_Presentation=Yes and render as 2 cells + expect(charWidth(0x2614)).toBe(2); // ☔ Umbrella with Rain Drops + expect(charWidth(0x2615)).toBe(2); // ☕ Hot Beverage + expect(charWidth(0x2705)).toBe(2); // ✅ White Heavy Check Mark + expect(charWidth(0x274c)).toBe(2); // ❌ Cross Mark + expect(charWidth(0x2757)).toBe(2); // ❗ Heavy Exclamation Mark + expect(charWidth(0x26bd)).toBe(2); // ⚽ Soccer Ball + expect(charWidth(0x26fd)).toBe(2); // ⛽ Fuel Pump + expect(charWidth(0x2b50)).toBe(2); // ⭐ White Medium Star + }); + + it("Windows Terminal wide symbols are width 2", () => { + // Text-presentation chars that Windows Terminal renders as double-width + expect(charWidth(0x2139)).toBe(2); // ℹ Information Source + expect(charWidth(0x2605)).toBe(2); // ★ Black Star + expect(charWidth(0x2606)).toBe(2); // ☆ White Star + expect(charWidth(0x2660)).toBe(2); // ♠ Black Spade Suit + expect(charWidth(0x2663)).toBe(2); // ♣ Black Club Suit + expect(charWidth(0x2665)).toBe(2); // ♥ Black Heart Suit + expect(charWidth(0x2666)).toBe(2); // ♦ Black Diamond Suit + expect(charWidth(0x2690)).toBe(2); // ⚐ White Flag + expect(charWidth(0x2691)).toBe(2); // ⚑ Black Flag + expect(charWidth(0x2699)).toBe(2); // ⚙ Gear + expect(charWidth(0x26a0)).toBe(2); // ⚠ Warning Sign + expect(charWidth(0x2714)).toBe(2); // ✔ Heavy Check Mark + expect(charWidth(0x2716)).toBe(2); // ✖ Heavy Multiplication X + expect(charWidth(0x279c)).toBe(2); // ➜ Heavy Round-Tipped Arrow + expect(charWidth(0x27a4)).toBe(2); // ➤ Black Right Arrowhead + expect(charWidth(0x25b6)).toBe(2); // ▶ Black Right Triangle + expect(charWidth(0x23f1)).toBe(2); // ⏱ Stopwatch + }); }); describe("sym() factory", () => { diff --git a/packages/consolonia/src/__tests__/styled.test.ts b/packages/consolonia/src/__tests__/styled.test.ts index acc5362..482a9e3 100644 --- a/packages/consolonia/src/__tests__/styled.test.ts +++ b/packages/consolonia/src/__tests__/styled.test.ts @@ -95,7 +95,7 @@ describe("concat", () => { const s = concat(pen.green("✔ "), pen.white("done")); expect(s).toHaveLength(2); expect(spanText(s)).toBe("✔ done"); - expect(spanLength(s)).toBe(7); // ✔ is width 2 (dingbat emoji) + expect(spanLength(s)).toBe(7); // ✔ is width 2 (renders wide on Windows Terminal) }); it("accepts plain strings", () => { diff --git a/packages/consolonia/src/ansi/esc.ts b/packages/consolonia/src/ansi/esc.ts index bc583f3..9b0b7c9 100644 --- a/packages/consolonia/src/ansi/esc.ts +++ b/packages/consolonia/src/ansi/esc.ts @@ -3,6 +3,8 @@ * All functions return raw escape strings — nothing is written to stdout. */ +import type { TerminalCaps } from "./terminal-env.js"; + const ESC = "\x1b["; const OSC = "\x1b]"; @@ -103,11 +105,26 @@ export const bracketedPasteOn = `${ESC}?2004h`; /** Disable bracketed paste mode. */ export const bracketedPasteOff = `${ESC}?2004l`; -/** Enable SGR mouse tracking (any-event tracking + SGR extended coordinates). */ -export const mouseTrackingOn = `${ESC}?1003h${ESC}?1006h`; +/** + * Enable mouse tracking with all supported reporting modes. + * + * Modes enabled (in order): + * ?1000h — Normal/VT200 click tracking (press + release) + * ?1003h — Any-event tracking (all mouse movement) + * ?1005h — UTF-8 coordinate encoding (extends X10 beyond col/row 223) + * ?1006h — SGR extended coordinates (";"-delimited decimals, M/m terminator) + * ?1015h — URXVT extended coordinates (decimal params, no "<" prefix) + * ?1016h — SGR-Pixels (same wire format as SGR, pixel coordinates) + * + * Terminals pick the highest mode they support. SGR is preferred by most + * modern terminals; URXVT and UTF-8 provide fallback for older ones. + */ +export const mouseTrackingOn = `${ESC}?1000h${ESC}?1003h${ESC}?1005h${ESC}?1006h${ESC}?1015h${ESC}?1016h`; -/** Disable SGR mouse tracking. */ -export const mouseTrackingOff = `${ESC}?1006l${ESC}?1003l`; +/** + * Disable all mouse tracking modes (reverse order of enable). + */ +export const mouseTrackingOff = `${ESC}?1016l${ESC}?1015l${ESC}?1006l${ESC}?1005l${ESC}?1003l${ESC}?1000l`; // ── Window ────────────────────────────────────────────────────────── @@ -115,3 +132,77 @@ export const mouseTrackingOff = `${ESC}?1006l${ESC}?1003l`; export function setTitle(title: string): string { return `${OSC}0;${title}\x07`; } + +// ── Environment-aware init/restore ───────────────────────────────── + +/** + * Mouse tracking sequence tailored to detected capabilities. + * + * When the terminal supports SGR, request all six modes so the terminal + * picks the highest one it handles. When SGR is not available (e.g. GNU + * screen), fall back to normal + any-event tracking only — UTF-8 and + * URXVT modes could confuse terminals that don't understand them. + */ +export function mouseOn(caps: TerminalCaps): string { + if (!caps.mouse) return ""; + if (caps.sgrMouse) return mouseTrackingOn; + // Minimal: normal click + any-event only + return `${ESC}?1000h${ESC}?1003h`; +} + +/** Matching disable sequence for mouseOn(). */ +export function mouseOff(caps: TerminalCaps): string { + if (!caps.mouse) return ""; + if (caps.sgrMouse) return mouseTrackingOff; + return `${ESC}?1003l${ESC}?1000l`; +} + +/** + * Build the full terminal preparation sequence for the given environment. + * + * @param caps - Detected terminal capabilities + * @param opts - Which optional features the app requested + */ +export function initSequence( + caps: TerminalCaps, + opts: { alternateScreen: boolean; mouse: boolean }, +): string { + if (!caps.isTTY) return ""; + + let seq = ""; + if (opts.alternateScreen && caps.alternateScreen) { + seq += alternateScreenOn; + } + seq += hideCursor; + if (caps.bracketedPaste) { + seq += bracketedPasteOn; + } + if (opts.mouse) { + seq += mouseOn(caps); + } + seq += clearScreen; + return seq; +} + +/** + * Build the full terminal restore sequence (mirror of initSequence). + */ +export function restoreSequence( + caps: TerminalCaps, + opts: { alternateScreen: boolean; mouse: boolean }, +): string { + if (!caps.isTTY) return ""; + + let seq = reset; + if (opts.mouse) { + seq += mouseOff(caps); + } + if (caps.bracketedPaste) { + seq += bracketedPasteOff; + } + seq += showCursor; + if (opts.alternateScreen && caps.alternateScreen) { + seq += alternateScreenOff; + } + return seq; +} diff --git a/packages/consolonia/src/ansi/terminal-env.ts b/packages/consolonia/src/ansi/terminal-env.ts new file mode 100644 index 0000000..be6d55d --- /dev/null +++ b/packages/consolonia/src/ansi/terminal-env.ts @@ -0,0 +1,247 @@ +/** + * Terminal environment detection. + * + * Probes environment variables and process state to determine which + * terminal capabilities are available. Used by App to send only the + * escape sequences the host terminal actually supports. + */ + +// ── Capability flags ──────────────────────────────────────────────── + +export interface TerminalCaps { + /** Terminal is a TTY (not piped). */ + isTTY: boolean; + /** Supports alternate screen buffer (?1049h). */ + alternateScreen: boolean; + /** Supports bracketed paste mode (?2004h). */ + bracketedPaste: boolean; + /** Supports escape-based mouse tracking (?1000h and above). */ + mouse: boolean; + /** Supports SGR extended mouse encoding (?1006h). */ + sgrMouse: boolean; + /** Supports truecolor (24-bit) SGR sequences. */ + truecolor: boolean; + /** Supports 256-color SGR sequences. */ + color256: boolean; + /** Detected terminal name (for diagnostics). */ + name: string; +} + +// ── Detection ─────────────────────────────────────────────────────── + +/** + * Detect terminal capabilities from the current environment. + * + * On Windows the main differentiator is whether we're running under + * Windows Terminal / ConPTY (full VT support) or legacy conhost + * (very limited escape handling). On Unix the TERM variable and + * TERM_PROGRAM give us enough signal. + */ +export function detectTerminal(): TerminalCaps { + const env = process.env; + const isTTY = !!process.stdout.isTTY; + + if (!isTTY) { + return { + isTTY: false, + alternateScreen: false, + bracketedPaste: false, + mouse: false, + sgrMouse: false, + truecolor: false, + color256: false, + name: "pipe", + }; + } + + // ── Windows ───────────────────────────────────────────────────── + + if (process.platform === "win32") { + // Windows Terminal sets WT_SESSION + if (env.WT_SESSION) { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: true, + color256: true, + name: "windows-terminal", + }; + } + + // VS Code's integrated terminal (xterm.js) + if (env.TERM_PROGRAM === "vscode") { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: true, + color256: true, + name: "vscode", + }; + } + + // ConEmu / Cmder + if (env.ConEmuPID) { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: true, + color256: true, + name: "conemu", + }; + } + + // Mintty (Git Bash) — sets TERM=xterm* + if (env.TERM?.startsWith("xterm") && env.MSYSTEM) { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: true, + color256: true, + name: "mintty", + }; + } + + // Fallback: modern Windows 10+ conhost with ConPTY has decent VT + // support, but mouse tracking can be unreliable. Enable everything + // and let the terminal silently ignore what it doesn't support. + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: true, + color256: true, + name: "conhost", + }; + } + + // ── Unix / macOS ──────────────────────────────────────────────── + + const term = env.TERM ?? ""; + const termProgram = env.TERM_PROGRAM ?? ""; + + // tmux — full VT support, passes through SGR mouse + if (env.TMUX || term.startsWith("tmux") || term === "screen-256color") { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: !!env.COLORTERM || term.includes("256color"), + color256: true, + name: "tmux", + }; + } + + // GNU screen — limited mouse support, no SGR + if (term === "screen" && !env.TMUX) { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: false, + mouse: true, + sgrMouse: false, + truecolor: false, + color256: false, + name: "screen", + }; + } + + // iTerm2 + if (termProgram === "iTerm.app" || env.ITERM_SESSION_ID) { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: true, + color256: true, + name: "iterm2", + }; + } + + // VS Code integrated terminal (macOS/Linux) + if (termProgram === "vscode") { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: true, + color256: true, + name: "vscode", + }; + } + + // Alacritty + if (termProgram === "Alacritty" || term === "alacritty") { + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: true, + color256: true, + name: "alacritty", + }; + } + + // xterm-compatible (most Linux/macOS terminals) + if (term.startsWith("xterm") || term.includes("256color")) { + const hasTruecolor = + env.COLORTERM === "truecolor" || env.COLORTERM === "24bit"; + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: true, + truecolor: hasTruecolor, + color256: true, + name: term, + }; + } + + // Dumb terminal — absolute minimum + if (term === "dumb" || !term) { + return { + isTTY: true, + alternateScreen: false, + bracketedPaste: false, + mouse: false, + sgrMouse: false, + truecolor: false, + color256: false, + name: term || "unknown", + }; + } + + // Unknown but it is a TTY — enable common capabilities + return { + isTTY: true, + alternateScreen: true, + bracketedPaste: true, + mouse: true, + sgrMouse: false, + truecolor: false, + color256: true, + name: term, + }; +} diff --git a/packages/consolonia/src/app.ts b/packages/consolonia/src/app.ts index 380283e..9786e1b 100644 --- a/packages/consolonia/src/app.ts +++ b/packages/consolonia/src/app.ts @@ -9,6 +9,8 @@ import type { Writable } from "node:stream"; import * as esc from "./ansi/esc.js"; import { AnsiOutput } from "./ansi/output.js"; +import { detectTerminal, type TerminalCaps } from "./ansi/terminal-env.js"; + import { DrawingContext } from "./drawing/context.js"; import type { InputEvent } from "./input/events.js"; import { createInputProcessor } from "./input/processor.js"; @@ -40,6 +42,7 @@ export class App { private readonly _alternateScreen: boolean; private readonly _mouse: boolean; private readonly _title: string | undefined; + private readonly _caps: TerminalCaps; // Subsystems — created during run() private _output!: AnsiOutput; @@ -58,11 +61,17 @@ export class App { private _sigintListener: (() => void) | null = null; private _renderScheduled = false; + /** Detected terminal capabilities (read-only, for diagnostics). */ + get caps(): Readonly { + return this._caps; + } + constructor(options: AppOptions) { this.root = options.root; this._alternateScreen = options.alternateScreen ?? true; this._mouse = options.mouse ?? false; this._title = options.title; + this._caps = detectTerminal(); } // ── Public API ─────────────────────────────────────────────────── @@ -127,8 +136,7 @@ export class App { // 2. Create ANSI output this._output = new AnsiOutput(stdout); - // 3. Prepare terminal (custom sequence instead of prepareTerminal() - // so we can conditionally enable mouse tracking) + // 3. Prepare terminal (ANSI sequences for alternate screen, mouse, etc.) this._prepareTerminal(); // 4. Set terminal title @@ -155,31 +163,20 @@ export class App { private _prepareTerminal(): void { const stream = process.stdout as Writable; - let seq = ""; - if (this._alternateScreen) { - seq += esc.alternateScreenOn; - } - seq += esc.hideCursor; - seq += esc.bracketedPasteOn; - if (this._mouse) { - seq += esc.mouseTrackingOn; - } - seq += esc.clearScreen; - stream.write(seq); + const seq = esc.initSequence(this._caps, { + alternateScreen: this._alternateScreen, + mouse: this._mouse, + }); + if (seq) stream.write(seq); } private _restoreTerminal(): void { const stream = process.stdout as Writable; - let seq = esc.reset; - if (this._mouse) { - seq += esc.mouseTrackingOff; - } - seq += esc.bracketedPasteOff; - seq += esc.showCursor; - if (this._alternateScreen) { - seq += esc.alternateScreenOff; - } - stream.write(seq); + const seq = esc.restoreSequence(this._caps, { + alternateScreen: this._alternateScreen, + mouse: this._mouse, + }); + if (seq) stream.write(seq); } private _createRenderPipeline(cols: number, rows: number): void { diff --git a/packages/consolonia/src/index.ts b/packages/consolonia/src/index.ts index 47971d8..2a9877c 100644 --- a/packages/consolonia/src/index.ts +++ b/packages/consolonia/src/index.ts @@ -99,6 +99,10 @@ export { truncateAnsi, visibleLength, } from "./ansi/strip.js"; +export { + detectTerminal, + type TerminalCaps, +} from "./ansi/terminal-env.js"; // ── Render pipeline ───────────────────────────────────────────────── diff --git a/packages/consolonia/src/input/mouse-matcher.ts b/packages/consolonia/src/input/mouse-matcher.ts index 90ffcfb..56e3987 100644 --- a/packages/consolonia/src/input/mouse-matcher.ts +++ b/packages/consolonia/src/input/mouse-matcher.ts @@ -1,8 +1,14 @@ /** - * Parses SGR extended mouse tracking sequences. + * Parses terminal mouse tracking sequences. * - * Format: \x1b[ 127. Node.js decodes + * UTF-8 stdin automatically, so the X10 parser handles both formats. + * + * Note: SGR-Pixels mode uses the same wire format as SGR but reports + * pixel coordinates instead of cell coordinates. These are passed + * through as-is (the caller must convert to cells if needed). */ import { type InputEvent, type MouseEvent, mouseEvent } from "./events.js"; @@ -26,13 +40,19 @@ enum State { GotEsc, /** Got \x1b[ */ GotBracket, - /** Got \x1b[< — now reading params until M or m */ + /** Got \x1b[< — now reading SGR/SGR-Pixels params until M or m */ Reading, + /** Got \x1b[M — now reading three encoded bytes (X10 or UTF-8) */ + ReadingX10, + /** Got \x1b[ followed by a digit — reading URXVT decimal params until M */ + ReadingUrxvt, } export class MouseMatcher implements IMatcher { private state: State = State.Idle; private params: string = ""; + private x10Bytes: string[] = []; + private urxvtParams: string = ""; private result: InputEvent | null = null; append(char: string): MatchResult { @@ -58,6 +78,17 @@ export class MouseMatcher implements IMatcher { this.params = ""; return MatchResult.Partial; } + if (char === "M") { + this.state = State.ReadingX10; + this.x10Bytes = []; + return MatchResult.Partial; + } + // URXVT: \x1b[ followed by a digit starts decimal param reading + if (char >= "0" && char <= "9") { + this.state = State.ReadingUrxvt; + this.urxvtParams = char; + return MatchResult.Partial; + } this.state = State.Idle; return MatchResult.NoMatch; @@ -77,6 +108,28 @@ export class MouseMatcher implements IMatcher { return MatchResult.NoMatch; } + case State.ReadingX10: + this.x10Bytes.push(char); + if (this.x10Bytes.length < 3) { + return MatchResult.Partial; + } + return this.finalizeX10(); + + case State.ReadingUrxvt: { + if (char === "M") { + return this.finalizeUrxvt(); + } + const c = char.charCodeAt(0); + if ((c >= 0x30 && c <= 0x39) || char === ";") { + this.urxvtParams += char; + return MatchResult.Partial; + } + // Not a valid URXVT sequence — bail out + this.state = State.Idle; + this.urxvtParams = ""; + return MatchResult.NoMatch; + } + default: return MatchResult.NoMatch; } @@ -91,6 +144,8 @@ export class MouseMatcher implements IMatcher { reset(): void { this.state = State.Idle; this.params = ""; + this.x10Bytes = []; + this.urxvtParams = ""; this.result = null; } @@ -112,40 +167,107 @@ export class MouseMatcher implements IMatcher { return MatchResult.NoMatch; } - // Decode modifiers from cb - const shift = (cb & 4) !== 0; - const alt = (cb & 8) !== 0; - const ctrl = (cb & 16) !== 0; - const isMotion = (cb & 32) !== 0; - - // Decode button from low bits (masking out modifier/motion bits) - const buttonBits = cb & 3; - const highBits = cb & (64 | 128); - - let button: MouseEvent["button"]; - let type: MouseEvent["type"]; - - if (highBits === 64) { - // Wheel events - button = "none"; - type = buttonBits === 0 ? "wheelup" : "wheeldown"; - } else if (isRelease) { - button = decodeButton(buttonBits); - type = "release"; - } else if (isMotion) { - button = buttonBits === 3 ? "none" : decodeButton(buttonBits); - type = "move"; - } else { - button = decodeButton(buttonBits); - type = "press"; + if (isRelease) { + const shift = (cb & 4) !== 0; + const alt = (cb & 8) !== 0; + const ctrl = (cb & 16) !== 0; + this.result = mouseEvent( + cx - 1, + cy - 1, + decodeButton(cb & 3), + "release", + shift, + ctrl, + alt, + ); + return MatchResult.Complete; + } + + this.result = decodeMouseEvent(cb, cx, cy, true); + return this.result ? MatchResult.Complete : MatchResult.NoMatch; + } + + private finalizeUrxvt(): MatchResult { + this.state = State.Idle; + + const parts = this.urxvtParams.split(";"); + this.urxvtParams = ""; + + if (parts.length !== 3) { + return MatchResult.NoMatch; + } + + const cb = parseInt(parts[0], 10); + const cx = parseInt(parts[1], 10); + const cy = parseInt(parts[2], 10); + + if (Number.isNaN(cb) || Number.isNaN(cx) || Number.isNaN(cy)) { + return MatchResult.NoMatch; + } + + // URXVT uses the same button encoding as X10 (button 3 = release) + this.result = decodeMouseEvent(cb, cx, cy, true); + return this.result ? MatchResult.Complete : MatchResult.NoMatch; + } + + private finalizeX10(): MatchResult { + this.state = State.Idle; + + if (this.x10Bytes.length !== 3) { + this.x10Bytes = []; + return MatchResult.NoMatch; + } + + const [cbChar, cxChar, cyChar] = this.x10Bytes; + this.x10Bytes = []; + + const cb = cbChar.charCodeAt(0) - 32; + const cx = cxChar.charCodeAt(0) - 32; + const cy = cyChar.charCodeAt(0) - 32; + + if (cb < 0 || cx <= 0 || cy <= 0) { + return MatchResult.NoMatch; } - // Convert from 1-based to 0-based coordinates - this.result = mouseEvent(cx - 1, cy - 1, button, type, shift, ctrl, alt); - return MatchResult.Complete; + this.result = decodeMouseEvent(cb, cx, cy, true); + return this.result ? MatchResult.Complete : MatchResult.NoMatch; } } +function decodeMouseEvent( + cb: number, + cx: number, + cy: number, + x10ReleaseUsesButton3: boolean, +): InputEvent | null { + const shift = (cb & 4) !== 0; + const alt = (cb & 8) !== 0; + const ctrl = (cb & 16) !== 0; + const isMotion = (cb & 32) !== 0; + + const buttonBits = cb & 3; + const highBits = cb & (64 | 128); + + let button: MouseEvent["button"]; + let type: MouseEvent["type"]; + + if (highBits === 64) { + button = "none"; + type = buttonBits === 0 ? "wheelup" : "wheeldown"; + } else if (x10ReleaseUsesButton3 && !isMotion && buttonBits === 3) { + button = "none"; + type = "release"; + } else if (isMotion) { + button = buttonBits === 3 ? "none" : decodeButton(buttonBits); + type = "move"; + } else { + button = decodeButton(buttonBits); + type = "press"; + } + + return mouseEvent(cx - 1, cy - 1, button, type, shift, ctrl, alt); +} + function decodeButton(bits: number): MouseEvent["button"] { switch (bits) { case 0: diff --git a/packages/consolonia/src/pixel/symbol.ts b/packages/consolonia/src/pixel/symbol.ts index d1e0554..a9944ae 100644 --- a/packages/consolonia/src/pixel/symbol.ts +++ b/packages/consolonia/src/pixel/symbol.ts @@ -96,35 +96,70 @@ export function charWidth(codePoint: number): 1 | 2 { // CJK Compatibility Ideographs Supplement if (codePoint >= 0x2f800 && codePoint <= 0x2fa1f) return 2; - // ── Emoji ranges (rendered as width 2 on modern terminals) ───── - // Hourglass + Watch + // ── Emoji with Emoji_Presentation=Yes (rendered as 2 cells by default) ── + // Only characters that terminals render as wide WITHOUT a variation selector. + + // ── Text-presentation characters that Windows Terminal renders as wide ── + // These have Emoji_Presentation=No in Unicode but modern terminals (Windows + // Terminal, VS Code integrated terminal) render them as double-width emoji. + if (codePoint === 0x2139) return 2; // ℹ Information Source + if (codePoint === 0x2605 || codePoint === 0x2606) return 2; // ★☆ Stars + if (codePoint === 0x2660) return 2; // ♠ Black Spade Suit + if (codePoint === 0x2663) return 2; // ♣ Black Club Suit + if (codePoint === 0x2665) return 2; // ♥ Black Heart Suit + if (codePoint === 0x2666) return 2; // ♦ Black Diamond Suit + if (codePoint === 0x2690 || codePoint === 0x2691) return 2; // ⚐⚑ Flags + if (codePoint === 0x2699) return 2; // ⚙ Gear + if (codePoint === 0x26a0) return 2; // ⚠ Warning Sign + if (codePoint === 0x2714) return 2; // ✔ Heavy Check Mark + if (codePoint === 0x2716) return 2; // ✖ Heavy Multiplication X + if (codePoint === 0x279c) return 2; // ➜ Heavy Round-Tipped Arrow + if (codePoint === 0x27a4) return 2; // ➤ Black Right Arrowhead + if (codePoint === 0x25b6) return 2; // ▶ Black Right Triangle + if (codePoint === 0x23f1) return 2; // ⏱ Stopwatch + + // Hourglass + Watch (⌚⌛) if (codePoint === 0x231a || codePoint === 0x231b) return 2; - // Player controls (⏩-⏳) - if (codePoint >= 0x23e9 && codePoint <= 0x23f3) return 2; - // Media controls (⏸-⏺) - if (codePoint >= 0x23f8 && codePoint <= 0x23fa) return 2; - // Play / reverse play buttons - if (codePoint === 0x25b6 || codePoint === 0x25c0) return 2; - // Geometric shapes used as emoji (◻◼◽◾) - if (codePoint >= 0x25fb && codePoint <= 0x25fe) return 2; - // Miscellaneous Symbols — most have emoji presentation (☀-⛿) - if (codePoint >= 0x2600 && codePoint <= 0x26ff) return 2; - // Dingbats with emoji presentation (✂-➿) - if (codePoint >= 0x2702 && codePoint <= 0x27b0) return 2; - // Curly loop - if (codePoint === 0x27bf) return 2; - // Supplemental arrows used as emoji - if (codePoint === 0x2934 || codePoint === 0x2935) return 2; - // Misc symbols used as emoji (⬛⬜⭐⭕) - if (codePoint >= 0x2b05 && codePoint <= 0x2b07) return 2; + // Fast-forward through rewind (⏩⏪⏫⏬) + if (codePoint >= 0x23e9 && codePoint <= 0x23ec) return 2; + // Alarm clock (⏰) + if (codePoint === 0x23f0) return 2; + // Hourglass flowing (⏳) + if (codePoint === 0x23f3) return 2; + // White/black medium small square with emoji pres (◽◾) + if (codePoint === 0x25fd || codePoint === 0x25fe) return 2; + // Misc Symbols — only Emoji_Presentation=Yes entries + if (codePoint === 0x2614 || codePoint === 0x2615) return 2; // ☔☕ + if (codePoint >= 0x2648 && codePoint <= 0x2653) return 2; // ♈-♓ + if (codePoint === 0x267f) return 2; // ♿ + if (codePoint === 0x2693) return 2; // ⚓ + if (codePoint === 0x26a1) return 2; // ⚡ + if (codePoint === 0x26aa || codePoint === 0x26ab) return 2; // ⚪⚫ + if (codePoint === 0x26bd || codePoint === 0x26be) return 2; // ⚽⚾ + if (codePoint === 0x26c4 || codePoint === 0x26c5) return 2; // ⛄⛅ + if (codePoint === 0x26ce) return 2; // ⛎ + if (codePoint === 0x26d4) return 2; // ⛔ + if (codePoint === 0x26ea) return 2; // ⛪ + if (codePoint === 0x26f2 || codePoint === 0x26f3) return 2; // ⛲⛳ + if (codePoint === 0x26f5) return 2; // ⛵ + if (codePoint === 0x26fa) return 2; // ⛺ + if (codePoint === 0x26fd) return 2; // ⛽ + // Dingbats — only Emoji_Presentation=Yes entries + if (codePoint === 0x2705) return 2; // ✅ + if (codePoint === 0x270a || codePoint === 0x270b) return 2; // ✊✋ + if (codePoint === 0x2728) return 2; // ✨ + if (codePoint === 0x274c) return 2; // ❌ + if (codePoint === 0x274e) return 2; // ❎ + if (codePoint >= 0x2753 && codePoint <= 0x2755) return 2; // ❓❔❕ + if (codePoint === 0x2757) return 2; // ❗ + if (codePoint === 0x2764) return 2; // ❤ + if (codePoint >= 0x2795 && codePoint <= 0x2797) return 2; // ➕➖➗ + // Curly loop / double curly loop (➰➿) + if (codePoint === 0x27b0 || codePoint === 0x27bf) return 2; + // Black large square/circle, star, hollow circle (⬛⬜⭐⭕) if (codePoint === 0x2b1b || codePoint === 0x2b1c) return 2; if (codePoint === 0x2b50 || codePoint === 0x2b55) return 2; - // Copyright / Registered / TM (when emoji-styled) - if (codePoint === 0x00a9 || codePoint === 0x00ae) return 2; - // Wavy dash, part alternation mark - if (codePoint === 0x3030 || codePoint === 0x303d) return 2; // SMP Emoji: Mahjong through Symbols & Pictographs Extended-A - // Covers emoticons, transport, flags, supplemental symbols, etc. if (codePoint >= 0x1f000 && codePoint <= 0x1faff) return 2; return 1; diff --git a/packages/recall/package.json b/packages/recall/package.json index 54701d9..2e0d9a7 100644 --- a/packages/recall/package.json +++ b/packages/recall/package.json @@ -1,6 +1,6 @@ { "name": "@teammates/recall", - "version": "0.7.0", + "version": "0.7.3", "description": "Local semantic memory search for teammates. Indexes WISDOM.md and memory files using Vectra + transformers.js.", "type": "module", "main": "dist/index.js",