From c06f17206054e0e442edaae778043a0e40687416 Mon Sep 17 00:00:00 2001 From: ANonABento Date: Fri, 12 Jun 2026 20:21:29 -0400 Subject: [PATCH 01/13] docs: refresh roadmap + checklist-as-roadmap spec + MCP tool-count drift - ROADMAP.md: authoritative current state, ranked for daily-driving (supersedes the stale STATUS tiers + session-scoped REMAINING_WORK index) - specs/CHECKLIST_AS_ROADMAP.md: planned checklist /api routes + MCP tools so the per-workspace checklist can serve as an agent-maintainable roadmap - CLAUDE.md: drop already-fixed MCP 'known gaps' (source attribution + recursion guard shipped; mark_complete/deps route through /api), list the real remaining gaps (no checklist tools; UI-only ops without routes) - STATUS.md: MCP tool count 19 -> 25 --- .tickets/_docs/ROADMAP.md | 93 +++++++++++++++ .tickets/_docs/STATUS.md | 2 +- .tickets/_docs/specs/CHECKLIST_AS_ROADMAP.md | 114 +++++++++++++++++++ CLAUDE.md | 8 +- 4 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 .tickets/_docs/ROADMAP.md create mode 100644 .tickets/_docs/specs/CHECKLIST_AS_ROADMAP.md diff --git a/.tickets/_docs/ROADMAP.md b/.tickets/_docs/ROADMAP.md new file mode 100644 index 00000000..2cf31a19 --- /dev/null +++ b/.tickets/_docs/ROADMAP.md @@ -0,0 +1,93 @@ +# KaitenCode Roadmap + +> Authoritative "what's done / what's left" as of **2026-06-12**, verified against +> `main` HEAD. Supersedes the tier table in [STATUS.md](./STATUS.md) and the +> session-scoped [handoffs/REMAINING_WORK.md](./handoffs/REMAINING_WORK.md) (both +> kept for history). Released: **v1.0.0** (Linux `.deb` + AppImage, 2026-06-07). + +Context: the goal is to **daily-drive KaitenCode as the primary surface for +agent-driven dev work** (in place of raw terminals). Priorities below are ranked +by that goal — interactive-mode reliability first, since it's the path that +replaces the terminal. + +--- + +## ✅ Shipped & wired + +- Source-of-truth refactor (Fronts 1–3): all surfaces (UI / HTTP API / MCP / chef) + funnel through shared `pipeline::*_service`; MCP routes through `/api/*`. +- Reliability cluster (PR #256): queue-stall, worktree leak, startup false-success, + over-spawn, sentinel scan — fixed before v1. +- Interactive mode (PR #257): real `claude`/`codex` TUI in per-task tmux, opt-in + setting + per-chat toggle. +- Terminal-harness B1–B4: create-UI options, interactive promotion, chef terminal, + CLI health check. +- Local HTTP API on by default (PR #258). +- Discord MVP (PR #260): task→thread + agent-output streaming (one-way). +- Trigger UX redesign phase 1 (PR #261): sentence-builder + recipes. +- Settings runtime tab (PR #262): backend `AppSettings` via System → Runtime. +- MCP: 25 tools; source attribution + recursion guard (migration 046); + mark_complete/dependencies route through `/api/*`. + +--- + +## 🔴 Critical now (blocks comfortable daily-driving) + +1. **Interactive readiness hard-kill.** Readiness is a 5s glyph-scrape that + *hard-kills* the tmux session on slow cold start / CLI UI change + (`chat/bridge.rs` ~2469/2610). Make it quiescence-based (adaptive), not a fixed + budget. → board task "Interactive: adaptive readiness". +2. **Interactive codex parity.** `--append-system-prompt` likely invalid for + interactive codex (would fail to start); `--resume` not wired (`spawn_interactive_cli` + warns + ignores `resume_id`). Verify against a real codex binary; drop/guard the + bad flag. Claude is solid. → board task "Interactive: codex parity". +3. **Two-claude footgun (env hygiene).** Stale `/usr/local/bin/claude` (2.1.138 npm + copy) can win in some PATHs. App now resolves absolute paths correctly, but + `rm /usr/local/bin/claude` removes the ambiguity. One-liner, do it. + +## 🟡 Important (rough edges, not blockers) + +4. **Interactive completion is panel-bound.** The "agent done" advisory only fires + while the panel is attached — no persisted flag / card badge. Needs a migration + + badge so it survives panel close. → board task "Interactive: persist advisory". +5. **`effort_level` not wired into the trigger path.** It's a real DB field + has a + `thinking-selector.tsx` and works in the interactive chat panel, but no + `effort_level`→CLI plumbing in `pipeline/spawn.rs` (the headless `spawn_cli` + path). Either wire `--effort`/`-c model_reasoning_effort` into the trigger + command or document it as interactive-only. +6. **Checklist as per-workspace roadmap + MCP surface.** Checklist has zero + `/api/*` routes and zero MCP tools. Make it agent-readable/maintainable so it + can serve as the project roadmap. **Spec:** [specs/CHECKLIST_AS_ROADMAP.md](./specs/CHECKLIST_AS_ROADMAP.md). + Planned, deferred 2026-06-12. +7. **MCP UI-only gaps.** No MCP tool for `update_column`/`delete_column`/ + `reorder_columns`, `update_script`/`delete_script`, per-task runtime-mode + override, or agent control (inject/interrupt/pause/restart/switch-model). Each + needs an `/api/*` route first. Add as needed (the checklist spec establishes the + route+tool pattern). + +## ⚪ Inert / deferred (known, intentional) + +- Custom keyboard shortcuts render but are inert (`shortcuts-tab.tsx:54`). +- Providers OpenRouter / Google AI / Ollama are "Coming soon" cards. +- Voice/Whisper built but gated out of the default build (`voice` Cargo feature). +- macOS release: matrix disabled pending Apple Developer ID + notarization secrets. + Windows unsupported by design (tmux/jq/sh). +- Discord: one-way only; reply-routing / `#chef` / bidirectional are later slices + (`.tickets/discord/`). +- MCP source-attribution Part 3 (spawned-by badge + human/agent filter); interactive/ + managed spawn-path env threading (headless done). + +--- + +## Needs testing (no automated coverage) + +- Interactive codex round-trip (resume, append-system-prompt, sentinel) on a real + binary — gates item #2. +- Live CLI smoke test (throwaway-tmux round-trip) — designed in B4, not built. +- Discord thread streaming end-to-end. +- 5-agent-per-workspace concurrency under real load (queue promotion). + +## Suggested order + +`#1 readiness` → `#2 codex parity` (+ its testing) → `#3 rm stale claude` (free) → +`#4 persist advisory` → `#5 effort wiring` → `#6 checklist/roadmap MCP` → `#7 MCP gaps`. diff --git a/.tickets/_docs/STATUS.md b/.tickets/_docs/STATUS.md index 16afafbb..23011dc0 100644 --- a/.tickets/_docs/STATUS.md +++ b/.tickets/_docs/STATUS.md @@ -42,7 +42,7 @@ Phase 6 🟡 — CliSessionManager removed, but AgentRunner + PtyManager still u ### MCP Server (kaitencode-mcp) -Standalone Rust binary (19 tools), shares Cargo workspace with Tauri app for WAL-compatible concurrent DB access. 17 unit tests. +Standalone Rust binary (25 tools), shares Cargo workspace with Tauri app for WAL-compatible concurrent DB access. 18+ unit tests. ### Database diff --git a/.tickets/_docs/specs/CHECKLIST_AS_ROADMAP.md b/.tickets/_docs/specs/CHECKLIST_AS_ROADMAP.md new file mode 100644 index 00000000..55525017 --- /dev/null +++ b/.tickets/_docs/specs/CHECKLIST_AS_ROADMAP.md @@ -0,0 +1,114 @@ +# Spec — Checklist as Per-Workspace Roadmap (+ MCP/HTTP surface) + +> Status: **PLANNED, not started** (deferred 2026-06-12 — "more critical things first"). +> Owner: TBD. Effort: ~1 day (routes + MCP tools + tests + rebuild). + +## Goal + +Repurpose the existing **checklist** feature as a high-level, per-workspace +**project roadmap** that both the human (UI) and agents (MCP) can read and +maintain. Today the checklist is a release-readiness widget with *no* agent +surface; this spec closes that gap without inventing a new feature. + +## Why checklist (not pipeline templates) + +They are genuinely different things (verified 2026-06-12): + +| | Pipeline Template | **Checklist** | +|---|---|---| +| Scope | Global, stateless blueprint | **Already per-workspace** (1 per ws) | +| Captures | Column names/icons/triggers | Categories → items + state | +| Item state | — | `checked`, `notes`, `position`, `linked_task_id`, auto-detect | +| Agent surface | 3 MCP tools | **none** | + +The checklist is already workspace-scoped, ordered, supports notes, can **link an +item to a board task** (`linked_task_id`), and can **auto-detect** completion from +repo state (file-exists / file-contains / file-absent / command-succeeds). That is +a near-perfect roadmap primitive — it just isn't reachable by agents. + +## Current state (what exists) + +- **DB** (`src-tauri/src/db/checklist.rs`): full CRUD already implemented as + `(conn, …)` functions — `insert_checklist`, `get_workspace_checklist`, + `insert_checklist_category`, `list_checklist_categories`, + `insert_checklist_item`, `list_checklist_items`, `update_checklist_item` + (toggle `checked`), `update_checklist_item_details`, `delete_checklist_item`, + `link_checklist_item_to_task`, `create_checklist_item_with_detect`, … +- **Tauri commands** (`src-tauri/src/commands/checklist.rs`): 15 commands wrapping + the above (used by the UI). +- **Tables** (migration `005_checklists.sql`, `019_checklist_autodetect.sql`): + `checklists(workspace_id)`, `checklist_categories(checklist_id, name, icon, + position)`, `checklist_items(category_id, text, checked, notes, position, + detect_type, detect_config, auto_detected, linked_task_id)`. + +## Gap + +- **Zero `/api/*` routes** for checklist. MCP mutation tools route through + `/api/*`, so nothing is reachable from MCP today. +- **Zero MCP tools.** +- One structural constraint: **exactly one checklist per workspace** + (`commands/checklist.rs:134`). Fine for a *single* roadmap per project; only a + blocker if we later want multiple named roadmaps per workspace. + +## Design + +Mirror the established `create_column` pattern exactly: an `/api/*` handler that +takes `AxumState>`, grabs a conn via `get_db!(api)`, calls the +`db::checklist::*` fn directly, then emits an event and returns `ok_response`. +Add a new event `checklist:changed { workspaceId }` (typed struct, +`#[serde(rename_all = "camelCase")]`) + a `useChecklistSync` hook so UI refreshes +live when an agent edits the roadmap. + +### New HTTP routes (`src-tauri/src/api.rs`, registered in the `protected` router ~L893) + +| Route | Calls | Notes | +|---|---|---| +| `GET /api/checklist?workspace_id=` | `get_workspace_checklist` + categories + items | read; returns full nested roadmap | +| `POST /api/checklist/ensure` | `insert_checklist` if absent | idempotent; create-on-first-use | +| `POST /api/checklist/category` | `insert_checklist_category` | add roadmap phase | +| `POST /api/checklist/item` | `insert_checklist_item` / `create_checklist_item_with_detect` | add milestone (optional auto-detect) | +| `POST /api/checklist/item/update` | `update_checklist_item` (toggle) + `update_checklist_item_details` (text/notes) + `link_checklist_item_to_task` | the workhorse | +| `POST /api/checklist/item/delete` | `delete_checklist_item` | | + +All request structs `#[derive(Deserialize)]` snake_case (match existing API +convention — note: the API request structs are snake_case, e.g. `MoveTaskReq` +uses `id`/`target_column_id`, *not* camelCase). + +### New MCP tools (`mcp-server/src/main.rs`) + +| Tool | Route | Scope | +|---|---|---| +| `get_checklist` | `GET /api/checklist` | read-only — works without app | +| `checklist_update` | `POST /api/checklist/item/update` | toggle done / edit text / link task | +| `checklist_add_item` | `POST /api/checklist/item` | add milestone | +| `checklist_add_category` | `POST /api/checklist/category` | add phase | + +Read-only `get_checklist` should also support the test-only direct-DB fallback +(like the other read tools); mutations require the running app (`/api/*`). + +### Phasing + +1. **P1 (minimal, unblocks roadmap):** `GET /api/checklist` + `POST + /api/checklist/item/update` → MCP `get_checklist` + `checklist_update`. + Agents can read the roadmap and tick milestones. +2. **P2 (agent-maintainable):** add category + item create/delete routes + tools. + Agents can build/restructure the roadmap. +3. **P3 (optional):** drop the 1-per-workspace constraint + add a `name` column to + `checklists` for multiple named roadmaps per workspace (migration after 047). + Only if multi-roadmap is wanted. + +### Tests + +- MCP: extend `mcp-server/src/main.rs` test module (currently 18 tests) with a + `test_checklist_update_*` round-trip against the test-DB fallback. +- API: a handler test if the api.rs test harness supports it; otherwise rely on + the MCP round-trip + a manual smoke test after rebuild. + +## Open decisions (for whoever picks this up) + +- **Single vs multiple roadmaps per workspace** — P3 above. Default: keep single + (zero schema change) until there's a concrete need. +- **Auto-detect as roadmap signal** — should `checklist_update` allow agents to + set `detect_config` so a milestone self-checks from repo state? Powerful for a + roadmap ("done when `cargo test` passes"), but lets agents wire shell commands. + Default: P2 read-only on detect; no agent-set detect until reviewed. diff --git a/CLAUDE.md b/CLAUDE.md index 66acd611..57512ded 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -231,9 +231,11 @@ mcp-server/ **App requirement:** Most mutation tools route through the Tauri app's HTTP API (port and bearer token discovered via `~/.kaitencode/api.port`) so triggers fire and `tasks:changed` events flow to the UI. If the app isn't running, those tools error out (production builds disable direct-DB fallback; only `cfg!(test)` allows it). Read-only tools work without the app. -**Known gaps (see `.tickets/_docs/MCP_DOGFOOD_REPORT.md`):** -- `mark_complete`, `add_dependency`, `remove_dependency` bypass the API and write the DB directly — no `tasks:changed` event, so the UI doesn't refresh in real-time. -- No recursion guard / source attribution on MCP-created tasks. See `.tickets/_docs/MCP_SELF_TASK_WORKFLOW.md` for a safe self-task pattern. +**Resolved (was a gap, now fixed):** `mark_complete`, `add_dependency`, `remove_dependency` now route through `/api/mark_complete` + `/api/set_dependencies` (emit `tasks:changed`); source attribution + recursion guard shipped (migration 046 — `created_by_task_id`/`created_by_agent_session_id`, `mcp_max_recursion_depth`). See `.tickets/_docs/MCP_SELF_TASK_WORKFLOW.md` for the self-task pattern. + +**Remaining gaps:** +- **No checklist tools.** Checklist is Tauri-IPC-only (15 commands in `commands/checklist.rs`), with zero `/api/*` routes — so no MCP tool can reach it yet. Adding `checklist_update`/`get_checklist` requires a new `/api/*` route first. +- **UI-only ops without MCP coverage:** `update_column`/`delete_column`/`reorder_columns`, `update_script`/`delete_script`, per-task runtime-mode override, and agent control (`inject_message`/`interrupt`/`pause`/`restart`/`switch_model`) — each needs an `/api/*` route before an MCP tool. ## Type System From 3c4b8bf3bfc4884d964cbff7d383dba4fbf88be6 Mon Sep 17 00:00:00 2001 From: ANonABento Date: Fri, 12 Jun 2026 20:21:41 -0400 Subject: [PATCH 02/13] fix(terminal): give the embedded tmux terminal color + working clear The tmux attach client (whose output feeds xterm.js) was spawned with no TERM, so on a GUI launch (TERM unset) tmux downgraded to a colorless dumb terminal and dropped clear / alternate-screen sequences. Set TERM=xterm-256color + COLORTERM=truecolor + -2 on the attach client, default-terminal=tmux-256color + truecolor terminal-overrides on the server, and COLORTERM in the session env. Takes effect on every attach, so existing sessions get color on reopen too. --- src-tauri/src/chat/tmux_transport.rs | 37 ++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/chat/tmux_transport.rs b/src-tauri/src/chat/tmux_transport.rs index a6664daf..9f0d819e 100644 --- a/src-tauri/src/chat/tmux_transport.rs +++ b/src-tauri/src/chat/tmux_transport.rs @@ -129,6 +129,26 @@ pub fn ensure_tmux_server() -> Result<(), String> { .args(["set-option", "-g", "mouse", "on"]) .output(); + // Color fidelity. Set explicitly (not relying on the user's ~/.tmux.conf or a + // GUI-launch environment) so the hosted agent TUIs render in full color and + // alternate-screen / clear works: + // - default-terminal: what TERM panes see; tmux-256color advertises 256-color + // + smcup/rmcup (alternate screen) + clear capabilities. Only applies to + // sessions created AFTER it's set — hence here, pre-session. + // - terminal-overrides …:RGB: pass 24-bit truecolor through to the attached + // client (xterm.js attaches as xterm-256color; see attach_in_pty). + let _ = Command::new("tmux") + .args(["set-option", "-g", "default-terminal", "tmux-256color"]) + .output(); + let _ = Command::new("tmux") + .args([ + "set-option", + "-ga", + "terminal-overrides", + ",xterm-256color:RGB,*256col*:RGB", + ]) + .output(); + Ok(()) } @@ -361,6 +381,9 @@ impl TmuxTransport { let output = Command::new("tmux") .args(&args) .envs(config.env_vars.as_ref().cloned().unwrap_or_default()) + // Panes inherit TERM from default-terminal (tmux-256color); COLORTERM + // lets truecolor-aware apps inside the pane emit 24-bit color. + .env("COLORTERM", "truecolor") .output() .map_err(|e| format!("Failed to create tmux session: {}", e))?; @@ -380,9 +403,19 @@ impl TmuxTransport { let (pty, pts) = pty_process::blocking::open().map_err(|e| format!("Failed to open PTY: {}", e))?; - // Build tmux attach command + // Build tmux attach command. The attach client renders tmux's output into + // the xterm.js PTY, so ITS environment decides whether tmux emits color + + // clear/alternate-screen sequences. On a GUI launch TERM is unset, which + // makes tmux downgrade to a colorless dumb terminal — hence set it here + // explicitly. `-2` forces tmux to assume 256-color support. let mut cmd = pty_process::blocking::Command::new("tmux"); - cmd = cmd.arg("attach-session").arg("-t").arg(&name); + cmd = cmd + .env("TERM", "xterm-256color") + .env("COLORTERM", "truecolor") + .arg("-2") + .arg("attach-session") + .arg("-t") + .arg(&name); let child = cmd .spawn(pts) From 3bbd3f276dd3bdf0fa3948bad80fbd36a5b43a4c Mon Sep 17 00:00:00 2001 From: ANonABento Date: Fri, 12 Jun 2026 20:21:48 -0400 Subject: [PATCH 03/13] feat(panels): two-stage responsive header collapse Headers overlapped at narrow panel widths. New useElementWidth + PanelDensityContext drive three tiers (regular >=470 / compact / mini <360): tabs go label->icon (PanelTabs gains 'responsive'); agent right-side controls fold regular(labelled) -> compact(icons) -> mini(runtime+hold into a generalized PanelOverflowMenu). RuntimeModeToggle gains button|compact|menu variants; HoldButton extracted. Chef header gets the lighter pass (responsive tabs + Cmd+J hint hidden when tight). --- src/components/panel/agent-panel.tsx | 326 ++++++++++++++------ src/components/panel/orchestrator-panel.tsx | 18 +- src/components/shared/panel-tabs.tsx | 18 +- src/hooks/use-element-width.ts | 60 ++++ 4 files changed, 319 insertions(+), 103 deletions(-) create mode 100644 src/hooks/use-element-width.ts diff --git a/src/components/panel/agent-panel.tsx b/src/components/panel/agent-panel.tsx index 009d2827..13e0a25a 100644 --- a/src/components/panel/agent-panel.tsx +++ b/src/components/panel/agent-panel.tsx @@ -9,6 +9,12 @@ import { holdTask, killTaskSession } from '@/lib/ipc/agent' import { agentRestart, interactiveModeDevFlag } from '@/lib/ipc/agent-interactive' import { setTaskRuntimeModeOverride } from '@/lib/ipc/task' import { PanelTabs, type PanelTab } from '@/components/shared/panel-tabs' +import { + useElementWidth, + panelDensity, + PanelDensityContext, + usePanelDensity, +} from '@/hooks/use-element-width' import { ActivityIcon, TerminalIcon, ChangesIcon, FilesIcon } from '@/components/shared/tab-icons' import { WorkspaceFilesView } from './workspace-files-view' import { signalPtyInterrupt } from '@/lib/ipc/terminal' @@ -207,34 +213,18 @@ function HeadlessPanel({ task, onClose }: AgentPanelProps) { - - - { void handleKill() }} - /> - + { void handleHoldToggle() }} + killBusy={killBusy} + onKill={() => { void handleKill() }} + /> } viewSlot={ - - - { void handleKill() }} - /> - + { void handleHoldToggle() }} + killBusy={killBusy} + onKill={() => { void handleKill() }} + /> } /> @@ -525,10 +499,15 @@ function PanelHeader({ viewSlot, errorSlot, }: PanelHeaderProps) { + // Measure the controls row so tabs + right-side controls collapse in step + // (regular → icons → overflow) instead of overlapping at narrow panel widths. + const [rowRef, rowWidth] = useElementWidth() + const density = panelDensity(rowWidth) return ( +
{/* Controls row: close + runtime/session controls. */} -
+
{onClose && (
}
+ ) } @@ -582,7 +562,18 @@ const RUNTIME_CHOICES: { value: RuntimeChoice; label: string }[] = [ { value: 'interactive', label: 'Interactive (live TUI)' }, ] -function RuntimeModeToggle({ task }: { task: Task }) { +type RuntimeVariant = 'button' | 'compact' | 'menu' + +function RuntimeModeToggle({ + task, + variant = 'button', + onAfterSelect, +}: { + task: Task + /** `button` = labelled (regular), `compact` = icon-only, `menu` = flat list inside the overflow menu (mini). */ + variant?: RuntimeVariant + onAfterSelect?: () => void +}) { const [open, setOpen] = useState(false) const [busy, setBusy] = useState(false) const [interactiveAllowed, setInteractiveAllowed] = useState(true) @@ -605,6 +596,7 @@ function RuntimeModeToggle({ task }: { task: Task }) { const handleSelect = async (choice: RuntimeChoice) => { setOpen(false) + onAfterSelect?.() if (choice === current) return setBusy(true) try { @@ -621,6 +613,42 @@ function RuntimeModeToggle({ task }: { task: Task }) { } } + const choiceButtons = RUNTIME_CHOICES.map((choice) => { + const disabled = choice.value === 'interactive' && !interactiveAllowed + return ( + + ) + }) + + // Mini: render choices directly inside the overflow menu (no own dropdown). + if (variant === 'menu') { + return ( +
+
+ Runtime +
+ {choiceButtons} +
+ ) + } + + const iconOnly = variant === 'compact' return (
{open && (
- {RUNTIME_CHOICES.map((choice) => { - const disabled = choice.value === 'interactive' && !interactiveAllowed - return ( - - ) - })} + {choiceButtons}
)}
) } +function RuntimeIcon() { + return ( + + ) +} + +// Right-side header controls (runtime · hold · overflow), responsive to the +// density reported by PanelHeader: regular = labelled inline, compact = icons +// inline, mini = everything folded into the overflow (⋯) menu so nothing +// overlaps at narrow panel widths. +function AgentHeaderControls({ + task, + holdBusy, + onHoldToggle, + killBusy, + onKill, +}: { + task: Task + holdBusy: boolean + onHoldToggle: () => void + killBusy: boolean + onKill: () => void +}) { + const density = usePanelDensity() + + const killItem = (close: () => void) => ( + + ) + + if (density === 'mini') { + return ( + + {(close) => ( + <> + +
+ { close(); onHoldToggle() }} + variant="menu" + /> +
+ {killItem(close)} + + )} + + ) + } + + const compact = density === 'compact' + return ( + <> + + + {(close) => killItem(close)} + + ) +} + +type HoldVariant = 'full' | 'compact' | 'menu' + +function HoldButton({ + held, + busy, + onToggle, + variant, +}: { + held: boolean + busy: boolean + onToggle: () => void + variant: HoldVariant +}) { + const title = held ? 'Release auto-advance hold' : 'Hold auto-advance for this task' + if (variant === 'menu') { + return ( + + ) + } + const compact = variant === 'compact' + return ( + + ) +} + const AGENT_PANEL_TABS: readonly PanelTab[] = [ { value: 'activity', label: 'Activity', icon: , testId: 'agent-panel-tab-transcript' }, { value: 'terminal', label: 'Terminal', icon: , testId: 'agent-panel-tab-terminal' }, @@ -684,14 +828,16 @@ function HoldIcon() { ) } +// Generic ⋯ overflow menu shell. Children receive a `close` callback so items +// can dismiss the menu on activation. The agent header composes its contents +// (kill always; runtime + hold folded in at `mini` density). function PanelOverflowMenu({ - killBusy, - onKill, + children, }: { - killBusy: boolean - onKill: () => void + children: (close: () => void) => ReactNode }) { const [open, setOpen] = useState(false) + const close = useCallback(() => { setOpen(false) }, []) return (
@@ -701,7 +847,7 @@ function PanelOverflowMenu({ aria-expanded={open} aria-label="Agent actions" title="Agent actions" - className="flex h-7 w-7 items-center justify-center rounded-md border border-border-default bg-surface text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary" + className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border-default bg-surface text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary" style={{ cursor: 'pointer' }} > {open && ( -
- +
+ {children(close)}
)}
diff --git a/src/components/panel/orchestrator-panel.tsx b/src/components/panel/orchestrator-panel.tsx index 17f8be6b..bcfdd6d6 100644 --- a/src/components/panel/orchestrator-panel.tsx +++ b/src/components/panel/orchestrator-panel.tsx @@ -23,6 +23,7 @@ import { killTaskSession } from '@/lib/ipc/agent' import { chefSessionKey } from '@/lib/ipc/terminal' import { WorkspaceFilesView } from './workspace-files-view' import { PanelTabs, type PanelTab } from '@/components/shared/panel-tabs' +import { useElementWidth, panelDensity, PanelDensityContext } from '@/hooks/use-element-width' import { ChatIcon, TerminalIcon, FilesIcon } from '@/components/shared/tab-icons' type OrchestratorView = 'chat' | 'terminal' | 'files' @@ -106,6 +107,11 @@ export function OrchestratorPanel({ workspaceId }: OrchestratorPanelProps) { disabled: isPanelCollapsed, }) + // Measure the header so its tabs drop to icons and the Cmd+J hint hides at + // narrow widths instead of overlapping the centered "Chef" title. + const [headerRef, headerWidth] = useElementWidth() + const headerDensity = panelDensity(headerWidth) + // Local UI state const [sidebarMode, setSidebarMode] = useState<'history' | 'dashboard' | 'v2-dashboard' | null>(null) const [viewMode, setViewMode] = useState('chat') @@ -342,7 +348,9 @@ export function OrchestratorPanel({ workspaceId }: OrchestratorPanelProps) { > {/* Header - clickable to toggle. */} +
)} - - {isPanelCollapsed ? 'Cmd+J to expand' : 'Cmd+J'} - + {(isPanelCollapsed || headerDensity === 'regular') && ( + + {isPanelCollapsed ? 'Cmd+J to expand' : 'Cmd+J'} + + )}
+ {/* Content - only shown when expanded */} diff --git a/src/components/shared/panel-tabs.tsx b/src/components/shared/panel-tabs.tsx index e8a5e32e..553860dd 100644 --- a/src/components/shared/panel-tabs.tsx +++ b/src/components/shared/panel-tabs.tsx @@ -6,6 +6,7 @@ */ import type { ReactNode } from 'react' +import { usePanelDensity } from '@/hooks/use-element-width' export type PanelTab = { value: T @@ -22,6 +23,12 @@ type PanelTabsProps = { className?: string /** Icon-only (label as tooltip) — for tight headers like the agent panel. */ iconOnly?: boolean + /** + * Drop labels (icon-only) automatically when the surrounding `PanelHeader` + * reports a non-`regular` density. Shows labels when there's room, icons when + * tight — without the caller threading width through. + */ + responsive?: boolean 'aria-label'?: string } @@ -31,8 +38,11 @@ export function PanelTabs({ onChange, className = '', iconOnly = false, + responsive = false, 'aria-label': ariaLabel, }: PanelTabsProps) { + const density = usePanelDensity() + const showIconsOnly = iconOnly || (responsive && density !== 'regular') return (
({ key={tab.value} type="button" aria-pressed={active} - aria-label={iconOnly ? tab.label : undefined} - title={iconOnly ? tab.label : undefined} + aria-label={showIconsOnly ? tab.label : undefined} + title={showIconsOnly ? tab.label : undefined} data-testid={tab.testId} onClick={() => { onChange(tab.value) }} className={`relative inline-flex min-w-0 items-center gap-1.5 py-1.5 text-[11px] font-medium transition-colors ${ - iconOnly ? 'px-1.5' : 'px-2' + showIconsOnly ? 'px-1.5' : 'px-2' } ${active ? 'text-text-primary' : 'text-text-secondary hover:text-text-primary'}`} style={{ cursor: 'pointer' }} > {tab.icon} - {!iconOnly && {tab.label}} + {!showIconsOnly && {tab.label}} {active && (