stagent 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +44 -31
  2. package/dist/cli.js +24 -0
  3. package/docs/.coverage-gaps.json +154 -24
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +16 -2
  12. package/docs/features/settings.md +2 -2
  13. package/docs/features/shared-components.md +7 -3
  14. package/docs/features/tables.md +3 -1
  15. package/docs/features/tool-permissions.md +6 -2
  16. package/docs/features/workflows.md +6 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +25 -2
  20. package/docs/journeys/personal-use.md +12 -5
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
  25. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  26. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  27. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  28. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  29. package/next.config.mjs +1 -0
  30. package/package.json +3 -3
  31. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  32. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  33. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  34. package/src/app/api/chat/export/route.ts +52 -0
  35. package/src/app/api/chat/files/search/route.ts +50 -0
  36. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  37. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  38. package/src/app/api/environment/skills/route.ts +13 -0
  39. package/src/app/api/schedules/[id]/execute/route.ts +2 -2
  40. package/src/app/api/settings/chat/pins/route.ts +94 -0
  41. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  42. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  43. package/src/app/api/settings/environment/route.ts +26 -0
  44. package/src/app/api/tasks/[id]/execute/route.ts +52 -12
  45. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  46. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  47. package/src/app/documents/page.tsx +4 -1
  48. package/src/app/settings/page.tsx +2 -0
  49. package/src/components/book/content-blocks.tsx +1 -1
  50. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  51. package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
  52. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  53. package/src/components/chat/capability-banner.tsx +68 -0
  54. package/src/components/chat/chat-command-popover.tsx +668 -47
  55. package/src/components/chat/chat-input.tsx +103 -8
  56. package/src/components/chat/chat-message.tsx +12 -3
  57. package/src/components/chat/chat-session-provider.tsx +73 -3
  58. package/src/components/chat/chat-shell.tsx +62 -3
  59. package/src/components/chat/command-tab-bar.tsx +68 -0
  60. package/src/components/chat/conversation-template-picker.tsx +421 -0
  61. package/src/components/chat/help-dialog.tsx +39 -0
  62. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  63. package/src/components/chat/skill-row.tsx +147 -0
  64. package/src/components/documents/document-browser.tsx +37 -19
  65. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  66. package/src/components/notifications/permission-response-actions.tsx +155 -1
  67. package/src/components/playbook/playbook-detail-view.tsx +1 -1
  68. package/src/components/settings/environment-section.tsx +102 -0
  69. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  70. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  71. package/src/components/shared/command-palette.tsx +262 -2
  72. package/src/components/shared/filter-hint.tsx +70 -0
  73. package/src/components/shared/filter-input.tsx +59 -0
  74. package/src/components/shared/saved-searches-manager.tsx +199 -0
  75. package/src/components/tasks/task-bento-grid.tsx +12 -2
  76. package/src/components/tasks/task-card.tsx +3 -0
  77. package/src/components/tasks/task-chip-bar.tsx +30 -1
  78. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  79. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  80. package/src/hooks/use-active-skills.ts +110 -0
  81. package/src/hooks/use-chat-autocomplete.ts +120 -7
  82. package/src/hooks/use-enriched-skills.ts +19 -0
  83. package/src/hooks/use-pinned-entries.ts +104 -0
  84. package/src/hooks/use-recent-user-messages.ts +19 -0
  85. package/src/hooks/use-saved-searches.ts +142 -0
  86. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  87. package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
  88. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  89. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  90. package/src/lib/agents/claude-agent.ts +105 -46
  91. package/src/lib/agents/handoff/bus.ts +2 -2
  92. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  93. package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
  94. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
  95. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
  96. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  97. package/src/lib/agents/profiles/registry.ts +97 -22
  98. package/src/lib/agents/profiles/types.ts +7 -1
  99. package/src/lib/agents/router.ts +3 -6
  100. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  101. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  102. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  103. package/src/lib/agents/runtime/catalog.ts +121 -0
  104. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  105. package/src/lib/agents/runtime/execution-target.ts +456 -0
  106. package/src/lib/agents/runtime/index.ts +4 -0
  107. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  108. package/src/lib/agents/runtime/openai-codex.ts +35 -0
  109. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  110. package/src/lib/agents/task-dispatch.ts +220 -0
  111. package/src/lib/agents/tool-permissions.ts +16 -1
  112. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  113. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  114. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  115. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  116. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  117. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  118. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  119. package/src/lib/chat/__tests__/types.test.ts +28 -0
  120. package/src/lib/chat/active-skills.ts +31 -0
  121. package/src/lib/chat/clean-filter-input.ts +30 -0
  122. package/src/lib/chat/codex-engine.ts +30 -7
  123. package/src/lib/chat/command-tabs.ts +61 -0
  124. package/src/lib/chat/context-builder.ts +141 -1
  125. package/src/lib/chat/dismissals.ts +73 -0
  126. package/src/lib/chat/engine.ts +109 -15
  127. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  128. package/src/lib/chat/files/expand-mention.ts +76 -0
  129. package/src/lib/chat/files/search.ts +99 -0
  130. package/src/lib/chat/skill-composition.ts +210 -0
  131. package/src/lib/chat/skill-conflict.ts +105 -0
  132. package/src/lib/chat/stagent-tools.ts +6 -19
  133. package/src/lib/chat/stream-telemetry.ts +9 -4
  134. package/src/lib/chat/system-prompt.ts +22 -0
  135. package/src/lib/chat/tool-catalog.ts +33 -3
  136. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  137. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  138. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  139. package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
  140. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
  141. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  142. package/src/lib/chat/tools/helpers.ts +2 -0
  143. package/src/lib/chat/tools/profile-tools.ts +120 -23
  144. package/src/lib/chat/tools/skill-tools.ts +183 -0
  145. package/src/lib/chat/tools/task-tools.ts +6 -2
  146. package/src/lib/chat/tools/workflow-tools.ts +61 -20
  147. package/src/lib/chat/types.ts +15 -0
  148. package/src/lib/constants/settings.ts +2 -0
  149. package/src/lib/data/clear.ts +2 -6
  150. package/src/lib/db/bootstrap.ts +17 -0
  151. package/src/lib/db/schema.ts +26 -0
  152. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  153. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  154. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  155. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  156. package/src/lib/environment/data.ts +9 -0
  157. package/src/lib/environment/list-skills.ts +176 -0
  158. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  159. package/src/lib/environment/parsers/skill.ts +26 -5
  160. package/src/lib/environment/profile-generator.ts +56 -2
  161. package/src/lib/environment/skill-enrichment.ts +106 -0
  162. package/src/lib/environment/skill-recommendations.ts +66 -0
  163. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  164. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  165. package/src/lib/filters/parse.ts +86 -0
  166. package/src/lib/instance/__tests__/detect.test.ts +1 -1
  167. package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
  168. package/src/lib/instance/fingerprint.ts +8 -10
  169. package/src/lib/instance/upgrade-poller.ts +53 -1
  170. package/src/lib/schedules/scheduler.ts +4 -4
  171. package/src/lib/utils/stagent-paths.ts +4 -0
  172. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  173. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  174. package/src/lib/workflows/blueprints/types.ts +6 -0
  175. package/src/lib/workflows/engine.ts +5 -3
  176. package/src/test/setup.ts +10 -0
@@ -0,0 +1,1390 @@
1
+ # Chat Command Namespace Refactor — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Split the chat command grammar so `/` = verbs (Actions/Skills/Tools) and `@` = nouns (Entities/Files), add tabbed navigation inside the `/` popover, wire runtime-aware capability signalling, and unify the `⌘K` palette with skills + files.
6
+
7
+ **Architecture:** Refactor-in-place of `chat-command-popover.tsx` with a new `CommandTabBar` wrapping the existing cmdk `<Command>` root, preserving a single command root so arrow-key state is never lost on tab switch. Tab partitioning is a pure function over the existing `ToolCatalogEntry` array — testable without a DOM. Capability banner is a separate component that reads `getRuntimeFeatures(runtimeId)`. Per-user tab persistence via `localStorage`, per-session banner dismissal via `sessionStorage`.
8
+
9
+ **Tech Stack:** React 19, Next.js 16, cmdk (`@/components/ui/command`), Radix Tabs primitives (already in codebase via shadcn), Tailwind v4 with existing tokens, `vitest` + `@testing-library/react` for tests.
10
+
11
+ ---
12
+
13
+ ## NOT in scope
14
+
15
+ | Deferred | Rationale |
16
+ |---|---|
17
+ | `#` filter namespace | Covered by `chat-advanced-ux` (P3) |
18
+ | Env-integration-backed Skills-tab badges | `chat-environment-integration` still planned; Skills tab ships without badges, lights up when that feature lands |
19
+ | File completion under `@` | Already shipped by `chat-file-mentions`; this plan only plugs files into tab structure |
20
+ | Migration shim / old-popover fallback | Q7 accepts breaking UX change — alpha product |
21
+ | `/export` to PDF or share link | MVP uses existing `/api/documents` POST with markdown body; richer exports later |
22
+ | Tools-tab "Advanced reveal" persisted across sessions | Per-session toggle only; avoid a settings row |
23
+ | Adding command-palette-enhancement scaffolding | Already shipped at `src/components/shared/command-palette.tsx`; we extend its item list |
24
+
25
+ ## What already exists
26
+
27
+ | Asset | Path | Reuse strategy |
28
+ |---|---|---|
29
+ | Popover with slash/mention modes | `src/components/chat/chat-command-popover.tsx` | Wrap body in tab bar, keep cmdk `<Command>` root single-mounted |
30
+ | Autocomplete hook | `src/hooks/use-chat-autocomplete.ts` | Add `activeTab` + `setActiveTab` + localStorage init |
31
+ | Tool catalog (14 groups) | `src/lib/chat/tool-catalog.ts` | Partition into Actions/Skills/Tools tab buckets |
32
+ | Slash commands (actions/nav/create/utility) | `src/lib/chat/slash-commands.ts` | Extend with `/clear`, `/compact`, `/export`, `/help`, `/settings`, `/new-schedule` |
33
+ | ⌘K palette (already binds cmd+k) | `src/components/shared/command-palette.tsx` | Add Skills + Files groups |
34
+ | Runtime feature flags | `src/lib/agents/runtime/catalog.ts` (`getRuntimeFeatures`) | Drive capability banner + Tools-tab visibility |
35
+ | Chat input surface | `src/components/chat/chat-input.tsx` | Inject banner below textarea |
36
+ | cmdk primitives + Radix tabs | `src/components/ui/command.tsx`, Radix available | Compose `CommandTabBar` |
37
+ | Existing `/api/documents` POST | `src/app/api/documents/route.ts` | Power `/export` MVP |
38
+
39
+ ## Error & Rescue Registry
40
+
41
+ | # | Error | Trigger | Impact | Rescue |
42
+ |---|---|---|---|---|
43
+ | 1 | `localStorage` unavailable (SSR, private mode) | First render w/o `window`, Safari private | Tab persistence broken | `try/catch` on read + write, fall back to in-memory default = `"Actions"` |
44
+ | 2 | `sessionStorage` write throws (quota) | Banner dismiss click | Dismiss not sticky | `try/catch`; re-evaluate banner next mount silently |
45
+ | 3 | Runtime changes mid-stream | Model switch during `isStreaming` | Banner flicker / stale | Hide banner while `isStreaming=true`; recompute on `modelId` change |
46
+ | 4 | Tab with zero items after filter | Tools tab on Ollama | Dead-end UX | Empty-state message referencing capability banner below |
47
+ | 5 | `CommandEmpty` shows in current tab despite matches in another | Global search query | "No match" hides real matches | Badge tab headers with count `Actions (3)` when query non-empty |
48
+ | 6 | ⌘K palette opens while `/` popover is open | User hits ⌘K mid-slash | Double popover / focus war | Close slash popover before palette opens; palette-open event dispatches window event, popover listens |
49
+ | 7 | `/export` API call fails | Disk full / 500 | Silent data loss risk | Surface error toast via existing toast system; keep conversation intact; offer retry |
50
+ | 8 | `/clear` clicked during active stream | User confusion | Stream orphaned | Disable `/clear` while `isStreaming=true` with tooltip |
51
+ | 9 | `⌘L` collides with browser "focus URL bar" (Chrome/FF) | Browser swallow | Shortcut dead | Also bind `⌘⇧L` as documented fallback |
52
+ | 10 | `stagent.command-tab` localStorage value corrupt/stale enum | Hand-edit or version drift | Crash on tab render | Validate against enum allowlist; reset to `"Actions"` on mismatch |
53
+ | 11 | Skills tab empty for project with no skills | New project | Empty tab feels broken | `EmptyState` with link to docs |
54
+ | 12 | Banner wrongly shown on Claude/Codex | `runtimeId` lookup bug | Noise on full-capability runtimes | Unit test covers every `AgentRuntimeId → banner?` |
55
+ | 13 | New `ToolGroup` added later not assigned to a tab | Future enum addition | Silent drop from all tabs | `satisfies Record<ToolGroup, TabId>` exhaustiveness guard |
56
+ | 14 | Focus lost when tab switch re-mounts `CommandList` | Tab change | Arrow-key position lost | Keep single `<Command>` root; swap only the filtered children, not the root |
57
+
58
+ ## File Structure
59
+
60
+ **Create:**
61
+ - `src/components/chat/command-tab-bar.tsx` — tablist, arrow-key nav, ARIA, count badges
62
+ - `src/components/chat/capability-banner.tsx` — single-line dismissible banner
63
+ - `src/lib/chat/command-tabs.ts` — tab enum, `ToolGroup → TabId` map, partition pure fn
64
+ - `src/lib/chat/__tests__/command-tabs.test.ts` — partition + enum exhaustiveness
65
+ - `src/components/chat/__tests__/capability-banner.test.tsx` — banner visibility per runtime
66
+ - `src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts` — tab state + localStorage fallback
67
+
68
+ **Modify:**
69
+ - `src/components/chat/chat-command-popover.tsx` — wrap with tab bar, partition items by active tab
70
+ - `src/hooks/use-chat-autocomplete.ts` — add `activeTab`, `setActiveTab`, localStorage init
71
+ - `src/lib/chat/slash-commands.ts` — add 6 new session commands
72
+ - `src/lib/chat/tool-catalog.ts` — add new entries for `/clear`, `/compact`, `/export`, `/help`, `/settings`, `/new-schedule` under a `Session` group
73
+ - `src/components/chat/chat-input.tsx` — render `<CapabilityBanner>` below textarea; wire new keyboard shortcuts; execute session commands
74
+ - `src/components/shared/command-palette.tsx` — add Skills + Files groups
75
+
76
+ ---
77
+
78
+ ## Task 1: Tab model + partition logic (pure, fully unit-testable)
79
+
80
+ **Files:**
81
+ - Create: `src/lib/chat/command-tabs.ts`
82
+ - Test: `src/lib/chat/__tests__/command-tabs.test.ts`
83
+
84
+ - [ ] **Step 1: Write the failing test**
85
+
86
+ ```ts
87
+ // src/lib/chat/__tests__/command-tabs.test.ts
88
+ import { describe, it, expect } from "vitest";
89
+ import {
90
+ COMMAND_TABS,
91
+ GROUP_TO_TAB,
92
+ partitionCatalogByTab,
93
+ isCommandTabId,
94
+ type CommandTabId,
95
+ } from "../command-tabs";
96
+ import type { ToolCatalogEntry, ToolGroup } from "../tool-catalog";
97
+
98
+ const entry = (name: string, group: ToolGroup): ToolCatalogEntry => ({
99
+ name,
100
+ description: name,
101
+ group,
102
+ });
103
+
104
+ describe("command-tabs", () => {
105
+ it("exposes four tabs in canonical order", () => {
106
+ expect(COMMAND_TABS.map((t) => t.id)).toEqual([
107
+ "actions",
108
+ "skills",
109
+ "tools",
110
+ "entities",
111
+ ]);
112
+ });
113
+
114
+ it("maps every ToolGroup to exactly one tab", () => {
115
+ const groups: ToolGroup[] = [
116
+ "Tasks", "Projects", "Workflows", "Schedules", "Documents", "Tables",
117
+ "Notifications", "Profiles", "Skills", "Usage", "Settings", "Chat",
118
+ "Browser", "Utility",
119
+ ];
120
+ for (const g of groups) {
121
+ expect(GROUP_TO_TAB[g]).toBeDefined();
122
+ }
123
+ });
124
+
125
+ it("routes Skills group to the Skills tab", () => {
126
+ expect(GROUP_TO_TAB.Skills).toBe("skills");
127
+ });
128
+
129
+ it("routes Browser + Utility to the Tools tab", () => {
130
+ expect(GROUP_TO_TAB.Browser).toBe("tools");
131
+ expect(GROUP_TO_TAB.Utility).toBe("tools");
132
+ });
133
+
134
+ it("partitions catalog entries by tab", () => {
135
+ const catalog: ToolCatalogEntry[] = [
136
+ entry("list_tasks", "Tasks"),
137
+ entry("researcher", "Skills"),
138
+ entry("take_screenshot", "Browser"),
139
+ ];
140
+ const part = partitionCatalogByTab(catalog);
141
+ expect(part.actions.map((e) => e.name)).toEqual(["list_tasks"]);
142
+ expect(part.skills.map((e) => e.name)).toEqual(["researcher"]);
143
+ expect(part.tools.map((e) => e.name)).toEqual(["take_screenshot"]);
144
+ expect(part.entities).toEqual([]);
145
+ });
146
+
147
+ it("isCommandTabId rejects unknown values", () => {
148
+ expect(isCommandTabId("actions")).toBe(true);
149
+ expect(isCommandTabId("random")).toBe(false);
150
+ });
151
+ });
152
+ ```
153
+
154
+ - [ ] **Step 2: Run test — expect FAIL (module not found)**
155
+
156
+ Run: `npx vitest run src/lib/chat/__tests__/command-tabs.test.ts`
157
+ Expected: FAIL — `Cannot find module '../command-tabs'`.
158
+
159
+ - [ ] **Step 3: Implement `command-tabs.ts`**
160
+
161
+ ```ts
162
+ // src/lib/chat/command-tabs.ts
163
+ import type { ToolCatalogEntry, ToolGroup } from "./tool-catalog";
164
+
165
+ export const COMMAND_TAB_IDS = ["actions", "skills", "tools", "entities"] as const;
166
+ export type CommandTabId = (typeof COMMAND_TAB_IDS)[number];
167
+
168
+ export interface CommandTab {
169
+ id: CommandTabId;
170
+ label: string;
171
+ shortcut: string; // ⌘1..⌘4
172
+ }
173
+
174
+ export const COMMAND_TABS: CommandTab[] = [
175
+ { id: "actions", label: "Actions", shortcut: "⌘1" },
176
+ { id: "skills", label: "Skills", shortcut: "⌘2" },
177
+ { id: "tools", label: "Tools", shortcut: "⌘3" },
178
+ { id: "entities", label: "Entities", shortcut: "⌘4" },
179
+ ];
180
+
181
+ export const DEFAULT_COMMAND_TAB: CommandTabId = "actions";
182
+
183
+ export const GROUP_TO_TAB = {
184
+ // Stagent actions / session primitives
185
+ Tasks: "actions",
186
+ Projects: "actions",
187
+ Workflows: "actions",
188
+ Schedules: "actions",
189
+ Documents: "actions",
190
+ Tables: "actions",
191
+ Notifications: "actions",
192
+ Profiles: "actions",
193
+ Usage: "actions",
194
+ Settings: "actions",
195
+ Chat: "actions",
196
+ // Skills
197
+ Skills: "skills",
198
+ // Tools (filesystem / system / utility)
199
+ Browser: "tools",
200
+ Utility: "tools",
201
+ } satisfies Record<ToolGroup, CommandTabId>;
202
+
203
+ export function isCommandTabId(value: string): value is CommandTabId {
204
+ return (COMMAND_TAB_IDS as readonly string[]).includes(value);
205
+ }
206
+
207
+ export interface PartitionedCatalog {
208
+ actions: ToolCatalogEntry[];
209
+ skills: ToolCatalogEntry[];
210
+ tools: ToolCatalogEntry[];
211
+ entities: ToolCatalogEntry[];
212
+ }
213
+
214
+ export function partitionCatalogByTab(
215
+ catalog: ToolCatalogEntry[]
216
+ ): PartitionedCatalog {
217
+ const out: PartitionedCatalog = { actions: [], skills: [], tools: [], entities: [] };
218
+ for (const entry of catalog) {
219
+ out[GROUP_TO_TAB[entry.group]].push(entry);
220
+ }
221
+ return out;
222
+ }
223
+ ```
224
+
225
+ - [ ] **Step 4: Run test — expect PASS**
226
+
227
+ Run: `npx vitest run src/lib/chat/__tests__/command-tabs.test.ts`
228
+ Expected: PASS (6/6).
229
+
230
+ - [ ] **Step 5: Commit**
231
+
232
+ ```bash
233
+ git add src/lib/chat/command-tabs.ts src/lib/chat/__tests__/command-tabs.test.ts
234
+ git commit -m "feat(chat): command-tabs pure partition model (#chat-command-namespace-refactor)"
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Task 2: Session-command entries in slash commands + tool catalog
240
+
241
+ **Files:**
242
+ - Modify: `src/lib/chat/slash-commands.ts` (add entries after line 132 `actionCommands`)
243
+ - Modify: `src/lib/chat/tool-catalog.ts` (add `Session` group + entries)
244
+
245
+ - [ ] **Step 1: Add `Session` group to tool catalog**
246
+
247
+ Open `src/lib/chat/tool-catalog.ts`:
248
+
249
+ Change line 21-35 (`ToolGroup` union) — add `| "Session"`:
250
+
251
+ ```ts
252
+ export type ToolGroup =
253
+ | "Tasks"
254
+ | "Projects"
255
+ | "Workflows"
256
+ | "Schedules"
257
+ | "Documents"
258
+ | "Tables"
259
+ | "Notifications"
260
+ | "Profiles"
261
+ | "Skills"
262
+ | "Usage"
263
+ | "Settings"
264
+ | "Chat"
265
+ | "Browser"
266
+ | "Utility"
267
+ | "Session";
268
+ ```
269
+
270
+ Add `Session` to `TOOL_GROUP_ICONS` (around line 52-67) — import `Sparkles`/`Zap`. Use `Zap`:
271
+
272
+ ```ts
273
+ import { Zap } from "lucide-react";
274
+ // ...
275
+ Session: Zap,
276
+ ```
277
+
278
+ Add `"Session"` to `TOOL_GROUP_ORDER` (line 70-85) — place it first so session commands surface at the top of Actions:
279
+
280
+ ```ts
281
+ export const TOOL_GROUP_ORDER: ToolGroup[] = [
282
+ "Session",
283
+ "Tasks",
284
+ "Projects",
285
+ // ...rest unchanged
286
+ ];
287
+ ```
288
+
289
+ Append the new Session entries in `UTILITY_ENTRIES`'s neighborhood — create a new `SESSION_ENTRIES` block before `UTILITY_ENTRIES` (line ~205):
290
+
291
+ ```ts
292
+ const SESSION_ENTRIES: ToolCatalogEntry[] = [
293
+ { name: "clear", description: "Start a new conversation", group: "Session", behavior: "execute_immediately" },
294
+ { name: "compact", description: "Summarize and compact conversation history", group: "Session", behavior: "execute_immediately" },
295
+ { name: "export", description: "Save current conversation as a document", group: "Session", behavior: "execute_immediately" },
296
+ { name: "help", description: "Show chat shortcuts and commands", group: "Session", behavior: "execute_immediately" },
297
+ { name: "settings", description: "Open Stagent settings", group: "Session", behavior: "execute_immediately" },
298
+ { name: "new-task", description: "Create a new task", group: "Session", paramHint: "title" },
299
+ { name: "new-workflow", description: "Create a new workflow", group: "Session", paramHint: "name" },
300
+ { name: "new-schedule", description: "Create a new schedule", group: "Session", paramHint: "name, interval" },
301
+ ];
302
+ ```
303
+
304
+ Update `getToolCatalog` (line ~215-229) to merge `SESSION_ENTRIES` first:
305
+
306
+ ```ts
307
+ export function getToolCatalog(opts?: { includeBrowser?: boolean }): ToolCatalogEntry[] {
308
+ const withBrowser = opts?.includeBrowser ?? false;
309
+
310
+ if (withBrowser) {
311
+ if (!cachedWithBrowser) {
312
+ cachedWithBrowser = [...SESSION_ENTRIES, ...STAGENT_TOOLS, ...BROWSER_TOOLS, ...UTILITY_ENTRIES];
313
+ }
314
+ return cachedWithBrowser;
315
+ }
316
+
317
+ if (!cachedCatalog) {
318
+ cachedCatalog = [...SESSION_ENTRIES, ...STAGENT_TOOLS, ...UTILITY_ENTRIES];
319
+ }
320
+ return cachedCatalog;
321
+ }
322
+ ```
323
+
324
+ Update `GROUP_TO_TAB` in `src/lib/chat/command-tabs.ts` to add `Session: "actions"`:
325
+
326
+ ```ts
327
+ export const GROUP_TO_TAB = {
328
+ Session: "actions",
329
+ Tasks: "actions",
330
+ // ...rest
331
+ } satisfies Record<ToolGroup, CommandTabId>;
332
+ ```
333
+
334
+ - [ ] **Step 2: Re-run Task 1 tests for exhaustiveness regression**
335
+
336
+ Run: `npx vitest run src/lib/chat/__tests__/command-tabs.test.ts`
337
+ Expected: PASS.
338
+
339
+ Add a new case to the test file to lock the new group:
340
+
341
+ ```ts
342
+ it("routes Session group to the Actions tab", () => {
343
+ expect(GROUP_TO_TAB.Session).toBe("actions");
344
+ });
345
+ ```
346
+
347
+ Also extend the existing `maps every ToolGroup to exactly one tab` list to include `"Session"`. Re-run.
348
+ Expected: PASS.
349
+
350
+ - [ ] **Step 3: Verify tool-catalog types compile**
351
+
352
+ Run: `npx tsc --noEmit 2>&1 | grep -E "(tool-catalog|command-tabs)" | head -20`
353
+ Expected: empty output (no errors).
354
+
355
+ - [ ] **Step 4: Commit**
356
+
357
+ ```bash
358
+ git add src/lib/chat/tool-catalog.ts src/lib/chat/command-tabs.ts src/lib/chat/__tests__/command-tabs.test.ts
359
+ git commit -m "feat(chat): add Session group and 8 session commands"
360
+ ```
361
+
362
+ ---
363
+
364
+ ## Task 3: Active-tab state in autocomplete hook (with safe localStorage)
365
+
366
+ **Files:**
367
+ - Modify: `src/hooks/use-chat-autocomplete.ts`
368
+ - Test: `src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts`
369
+
370
+ - [ ] **Step 1: Write the failing test**
371
+
372
+ ```ts
373
+ // src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts
374
+ import { describe, it, expect, beforeEach, vi } from "vitest";
375
+ import { renderHook, act } from "@testing-library/react";
376
+ import { useChatAutocomplete } from "../use-chat-autocomplete";
377
+
378
+ const TAB_KEY = "stagent.command-tab";
379
+
380
+ describe("useChatAutocomplete — activeTab persistence", () => {
381
+ beforeEach(() => {
382
+ localStorage.clear();
383
+ });
384
+
385
+ it("defaults to 'actions' when localStorage empty", () => {
386
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
387
+ expect(result.current.activeTab).toBe("actions");
388
+ });
389
+
390
+ it("reads persisted tab from localStorage on mount", () => {
391
+ localStorage.setItem(TAB_KEY, "skills");
392
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
393
+ expect(result.current.activeTab).toBe("skills");
394
+ });
395
+
396
+ it("ignores corrupt localStorage values", () => {
397
+ localStorage.setItem(TAB_KEY, "bogus");
398
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
399
+ expect(result.current.activeTab).toBe("actions");
400
+ });
401
+
402
+ it("persists tab on setActiveTab", () => {
403
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
404
+ act(() => result.current.setActiveTab("tools"));
405
+ expect(result.current.activeTab).toBe("tools");
406
+ expect(localStorage.getItem(TAB_KEY)).toBe("tools");
407
+ });
408
+
409
+ it("survives localStorage throwing on write", () => {
410
+ const setSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
411
+ throw new Error("QuotaExceeded");
412
+ });
413
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
414
+ expect(() => {
415
+ act(() => result.current.setActiveTab("tools"));
416
+ }).not.toThrow();
417
+ expect(result.current.activeTab).toBe("tools");
418
+ setSpy.mockRestore();
419
+ });
420
+ });
421
+ ```
422
+
423
+ - [ ] **Step 2: Run — expect FAIL (activeTab/setActiveTab don't exist)**
424
+
425
+ Run: `npx vitest run src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts`
426
+ Expected: FAIL with undefined property access on `result.current.activeTab`.
427
+
428
+ - [ ] **Step 3: Add `activeTab` state to the hook**
429
+
430
+ Open `src/hooks/use-chat-autocomplete.ts`. Add near the other imports:
431
+
432
+ ```ts
433
+ import { isCommandTabId, DEFAULT_COMMAND_TAB, type CommandTabId } from "@/lib/chat/command-tabs";
434
+ ```
435
+
436
+ Inside the hook body, add state + helpers:
437
+
438
+ ```ts
439
+ const TAB_STORAGE_KEY = "stagent.command-tab";
440
+
441
+ function readInitialTab(): CommandTabId {
442
+ if (typeof window === "undefined") return DEFAULT_COMMAND_TAB;
443
+ try {
444
+ const raw = window.localStorage.getItem(TAB_STORAGE_KEY);
445
+ if (raw && isCommandTabId(raw)) return raw;
446
+ } catch {
447
+ // localStorage unavailable — fall through
448
+ }
449
+ return DEFAULT_COMMAND_TAB;
450
+ }
451
+
452
+ const [activeTab, setActiveTabState] = useState<CommandTabId>(readInitialTab);
453
+
454
+ const setActiveTab = useCallback((tab: CommandTabId) => {
455
+ setActiveTabState(tab);
456
+ try {
457
+ window.localStorage.setItem(TAB_STORAGE_KEY, tab);
458
+ } catch {
459
+ // quota / disabled — silent, in-memory only
460
+ }
461
+ }, []);
462
+ ```
463
+
464
+ Expose `activeTab` and `setActiveTab` in the hook's return object.
465
+
466
+ - [ ] **Step 4: Re-run tests — expect PASS**
467
+
468
+ Run: `npx vitest run src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts`
469
+ Expected: PASS (5/5).
470
+
471
+ - [ ] **Step 5: Commit**
472
+
473
+ ```bash
474
+ git add src/hooks/use-chat-autocomplete.ts src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts
475
+ git commit -m "feat(chat): activeTab state with safe localStorage persistence"
476
+ ```
477
+
478
+ ---
479
+
480
+ ## Task 4: `CommandTabBar` component
481
+
482
+ **Files:**
483
+ - Create: `src/components/chat/command-tab-bar.tsx`
484
+
485
+ - [ ] **Step 1: Implement the component**
486
+
487
+ ```tsx
488
+ // src/components/chat/command-tab-bar.tsx
489
+ "use client";
490
+
491
+ import { useCallback } from "react";
492
+ import { cn } from "@/lib/utils";
493
+ import { COMMAND_TABS, type CommandTabId } from "@/lib/chat/command-tabs";
494
+
495
+ interface CommandTabBarProps {
496
+ activeTab: CommandTabId;
497
+ onChange: (tab: CommandTabId) => void;
498
+ counts?: Partial<Record<CommandTabId, number>>;
499
+ }
500
+
501
+ export function CommandTabBar({ activeTab, onChange, counts }: CommandTabBarProps) {
502
+ const handleKeyDown = useCallback(
503
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
504
+ const idx = COMMAND_TABS.findIndex((t) => t.id === activeTab);
505
+ if (e.key === "ArrowLeft") {
506
+ e.preventDefault();
507
+ const prev = COMMAND_TABS[(idx - 1 + COMMAND_TABS.length) % COMMAND_TABS.length];
508
+ onChange(prev.id);
509
+ } else if (e.key === "ArrowRight") {
510
+ e.preventDefault();
511
+ const next = COMMAND_TABS[(idx + 1) % COMMAND_TABS.length];
512
+ onChange(next.id);
513
+ }
514
+ },
515
+ [activeTab, onChange]
516
+ );
517
+
518
+ return (
519
+ <div
520
+ role="tablist"
521
+ aria-label="Command categories"
522
+ onKeyDown={handleKeyDown}
523
+ className="flex items-center gap-1 border-b border-border px-2 pt-2"
524
+ >
525
+ {COMMAND_TABS.map((tab) => {
526
+ const selected = tab.id === activeTab;
527
+ const count = counts?.[tab.id];
528
+ return (
529
+ <button
530
+ key={tab.id}
531
+ role="tab"
532
+ aria-selected={selected}
533
+ aria-controls={`command-tabpanel-${tab.id}`}
534
+ id={`command-tab-${tab.id}`}
535
+ tabIndex={selected ? 0 : -1}
536
+ onClick={() => onChange(tab.id)}
537
+ className={cn(
538
+ "rounded-md px-2.5 py-1 text-xs font-medium transition-colors",
539
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
540
+ selected
541
+ ? "bg-muted text-foreground"
542
+ : "text-muted-foreground hover:text-foreground"
543
+ )}
544
+ >
545
+ {tab.label}
546
+ {typeof count === "number" && count > 0 && (
547
+ <span className="ml-1.5 text-[10px] text-muted-foreground/70">
548
+ {count}
549
+ </span>
550
+ )}
551
+ </button>
552
+ );
553
+ })}
554
+ </div>
555
+ );
556
+ }
557
+ ```
558
+
559
+ - [ ] **Step 2: Verify compile**
560
+
561
+ Run: `npx tsc --noEmit 2>&1 | grep command-tab-bar`
562
+ Expected: empty.
563
+
564
+ - [ ] **Step 3: Commit**
565
+
566
+ ```bash
567
+ git add src/components/chat/command-tab-bar.tsx
568
+ git commit -m "feat(chat): CommandTabBar component (tablist, arrow-key nav, ARIA)"
569
+ ```
570
+
571
+ ---
572
+
573
+ ## Task 5: Refactor `chat-command-popover.tsx` to use tabs
574
+
575
+ **Files:**
576
+ - Modify: `src/components/chat/chat-command-popover.tsx`
577
+
578
+ - [ ] **Step 1: Rewrite the popover to consume `activeTab` + partition**
579
+
580
+ Replace the existing slash-mode `ToolCatalogItems` call with tab-filtered rendering. Key constraint per E&R row #14: keep a single `<Command>` root; swap only the child groups.
581
+
582
+ Add new props `activeTab` and `onTabChange` to `ChatCommandPopoverProps`:
583
+
584
+ ```tsx
585
+ interface ChatCommandPopoverProps {
586
+ // ...existing props
587
+ activeTab: CommandTabId;
588
+ onTabChange: (tab: CommandTabId) => void;
589
+ }
590
+ ```
591
+
592
+ Import tab bits:
593
+
594
+ ```tsx
595
+ import { CommandTabBar } from "./command-tab-bar";
596
+ import { partitionCatalogByTab, type CommandTabId } from "@/lib/chat/command-tabs";
597
+ ```
598
+
599
+ Change the slash branch to:
600
+
601
+ ```tsx
602
+ {mode === "slash" && (
603
+ <>
604
+ <CommandTabBar activeTab={activeTab} onChange={onTabChange} />
605
+ <div
606
+ role="tabpanel"
607
+ id={`command-tabpanel-${activeTab}`}
608
+ aria-labelledby={`command-tab-${activeTab}`}
609
+ >
610
+ <ToolCatalogItems
611
+ onSelect={onSelect}
612
+ projectProfiles={projectProfiles}
613
+ activeTab={activeTab}
614
+ />
615
+ </div>
616
+ </>
617
+ )}
618
+ ```
619
+
620
+ Update `ToolCatalogItems` signature to accept `activeTab` and partition:
621
+
622
+ ```tsx
623
+ function ToolCatalogItems({
624
+ onSelect,
625
+ projectProfiles,
626
+ activeTab,
627
+ }: {
628
+ onSelect: ChatCommandPopoverProps["onSelect"];
629
+ projectProfiles?: ChatCommandPopoverProps["projectProfiles"];
630
+ activeTab: CommandTabId;
631
+ }) {
632
+ const catalog = getToolCatalogWithSkills({
633
+ includeBrowser: true,
634
+ projectProfiles,
635
+ });
636
+ const parts = partitionCatalogByTab(catalog);
637
+ const entries = parts[activeTab];
638
+
639
+ if (activeTab === "entities") {
640
+ // Entities tab is a pointer — redirect users to '@' mention mode
641
+ return (
642
+ <div className="px-4 py-6 text-sm text-muted-foreground text-center">
643
+ Type <span className="font-mono text-foreground">@</span> to reference projects, tasks, documents, or files.
644
+ </div>
645
+ );
646
+ }
647
+
648
+ if (entries.length === 0) {
649
+ return (
650
+ <div className="px-4 py-6 text-sm text-muted-foreground text-center">
651
+ {activeTab === "skills" ? "No skills available yet." : "Nothing here."}
652
+ </div>
653
+ );
654
+ }
655
+
656
+ const groups = groupToolCatalog(entries);
657
+ const groupNames = Object.keys(groups);
658
+
659
+ return (
660
+ <>
661
+ {groupNames.map((groupName) => {
662
+ const items = groups[groupName];
663
+ if (!items?.length) return null;
664
+ const GroupIcon = TOOL_GROUP_ICONS[groupName as keyof typeof TOOL_GROUP_ICONS] ?? FileText;
665
+ return (
666
+ <CommandGroup key={groupName} heading={groupName}>
667
+ {items.map((entry) => (
668
+ <CommandItem
669
+ key={entry.name}
670
+ value={`${entry.name} ${entry.description} ${entry.group}`}
671
+ onSelect={() =>
672
+ onSelect({
673
+ type: "slash",
674
+ id: entry.name,
675
+ label: entry.name,
676
+ text: entry.behavior === "execute_immediately"
677
+ ? entry.name
678
+ : entry.group === "Skills"
679
+ ? `Use the ${entry.name} profile: `
680
+ : `Use ${entry.name} to `,
681
+ })
682
+ }
683
+ >
684
+ <GroupIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
685
+ <div className="flex flex-col min-w-0">
686
+ <span className="truncate text-sm font-medium">{entry.name}</span>
687
+ <span className="truncate text-xs text-muted-foreground">
688
+ {entry.description}
689
+ </span>
690
+ </div>
691
+ {entry.paramHint && (
692
+ <span className="ml-auto shrink-0 text-[10px] text-muted-foreground/60 font-mono">
693
+ {entry.paramHint}
694
+ </span>
695
+ )}
696
+ </CommandItem>
697
+ ))}
698
+ </CommandGroup>
699
+ );
700
+ })}
701
+ </>
702
+ );
703
+ }
704
+ ```
705
+
706
+ - [ ] **Step 2: Wire props through `chat-input.tsx`**
707
+
708
+ In `src/components/chat/chat-input.tsx`, pass the new props to the popover:
709
+
710
+ ```tsx
711
+ <ChatCommandPopover
712
+ // ...existing props
713
+ activeTab={autocomplete.activeTab}
714
+ onTabChange={autocomplete.setActiveTab}
715
+ // ...
716
+ />
717
+ ```
718
+
719
+ - [ ] **Step 3: Verify compile**
720
+
721
+ Run: `npx tsc --noEmit 2>&1 | grep -E "(chat-command-popover|chat-input)" | head -20`
722
+ Expected: empty.
723
+
724
+ - [ ] **Step 4: Commit**
725
+
726
+ ```bash
727
+ git add src/components/chat/chat-command-popover.tsx src/components/chat/chat-input.tsx
728
+ git commit -m "feat(chat): tabbed slash popover (Actions/Skills/Tools/Entities)"
729
+ ```
730
+
731
+ ---
732
+
733
+ ## Task 6: Capability banner component
734
+
735
+ **Files:**
736
+ - Create: `src/components/chat/capability-banner.tsx`
737
+ - Test: `src/components/chat/__tests__/capability-banner.test.tsx`
738
+
739
+ - [ ] **Step 1: Write the failing test**
740
+
741
+ ```tsx
742
+ // src/components/chat/__tests__/capability-banner.test.tsx
743
+ import { describe, it, expect, beforeEach } from "vitest";
744
+ import { render, screen, fireEvent } from "@testing-library/react";
745
+ import { CapabilityBanner } from "../capability-banner";
746
+
747
+ describe("CapabilityBanner", () => {
748
+ beforeEach(() => {
749
+ sessionStorage.clear();
750
+ });
751
+
752
+ it("is hidden on claude-code runtime", () => {
753
+ render(<CapabilityBanner runtimeId="claude-code" />);
754
+ expect(screen.queryByRole("status")).toBeNull();
755
+ });
756
+
757
+ it("is hidden on openai-codex-app-server runtime", () => {
758
+ render(<CapabilityBanner runtimeId="openai-codex-app-server" />);
759
+ expect(screen.queryByRole("status")).toBeNull();
760
+ });
761
+
762
+ it("is visible on ollama runtime with capability message", () => {
763
+ render(<CapabilityBanner runtimeId="ollama" />);
764
+ const status = screen.getByRole("status");
765
+ expect(status.textContent).toContain("file read/write");
766
+ });
767
+
768
+ it("hides on dismiss and persists to sessionStorage", () => {
769
+ render(<CapabilityBanner runtimeId="ollama" />);
770
+ fireEvent.click(screen.getByRole("button", { name: /dismiss/i }));
771
+ expect(screen.queryByRole("status")).toBeNull();
772
+ expect(sessionStorage.getItem("stagent.capability-banner.dismissed.ollama")).toBe("1");
773
+ });
774
+
775
+ it("stays dismissed on remount if sessionStorage flag set", () => {
776
+ sessionStorage.setItem("stagent.capability-banner.dismissed.ollama", "1");
777
+ render(<CapabilityBanner runtimeId="ollama" />);
778
+ expect(screen.queryByRole("status")).toBeNull();
779
+ });
780
+ });
781
+ ```
782
+
783
+ - [ ] **Step 2: Run — expect FAIL**
784
+
785
+ Run: `npx vitest run src/components/chat/__tests__/capability-banner.test.tsx`
786
+ Expected: FAIL (module not found).
787
+
788
+ - [ ] **Step 3: Implement `capability-banner.tsx`**
789
+
790
+ ```tsx
791
+ // src/components/chat/capability-banner.tsx
792
+ "use client";
793
+
794
+ import { useEffect, useState } from "react";
795
+ import { X } from "lucide-react";
796
+ import { getRuntimeFeatures, type AgentRuntimeId } from "@/lib/agents/runtime/catalog";
797
+ import { cn } from "@/lib/utils";
798
+
799
+ interface CapabilityBannerProps {
800
+ runtimeId: AgentRuntimeId;
801
+ className?: string;
802
+ }
803
+
804
+ function dismissKey(runtimeId: string): string {
805
+ return `stagent.capability-banner.dismissed.${runtimeId}`;
806
+ }
807
+
808
+ function readDismissed(runtimeId: string): boolean {
809
+ if (typeof window === "undefined") return false;
810
+ try {
811
+ return window.sessionStorage.getItem(dismissKey(runtimeId)) === "1";
812
+ } catch {
813
+ return false;
814
+ }
815
+ }
816
+
817
+ export function CapabilityBanner({ runtimeId, className }: CapabilityBannerProps) {
818
+ const [dismissed, setDismissed] = useState<boolean>(() => readDismissed(runtimeId));
819
+
820
+ useEffect(() => {
821
+ setDismissed(readDismissed(runtimeId));
822
+ }, [runtimeId]);
823
+
824
+ const features = getRuntimeFeatures(runtimeId);
825
+ const limited =
826
+ !features.hasFilesystemTools && !features.hasBash;
827
+
828
+ if (!limited || dismissed) return null;
829
+
830
+ const handleDismiss = () => {
831
+ setDismissed(true);
832
+ try {
833
+ window.sessionStorage.setItem(dismissKey(runtimeId), "1");
834
+ } catch {
835
+ // ignore
836
+ }
837
+ };
838
+
839
+ return (
840
+ <div
841
+ role="status"
842
+ className={cn(
843
+ "flex items-start gap-2 px-4 py-1.5 text-xs text-muted-foreground animate-in fade-in-0",
844
+ className
845
+ )}
846
+ >
847
+ <span className="flex-1">
848
+ Features like file read/write, Bash, and hooks are not available on this runtime. Switch models to use them.
849
+ </span>
850
+ <button
851
+ type="button"
852
+ aria-label="Dismiss capability notice"
853
+ onClick={handleDismiss}
854
+ className="shrink-0 rounded p-0.5 hover:bg-muted"
855
+ >
856
+ <X className="h-3 w-3" />
857
+ </button>
858
+ </div>
859
+ );
860
+ }
861
+ ```
862
+
863
+ - [ ] **Step 4: Re-run tests — expect PASS**
864
+
865
+ Run: `npx vitest run src/components/chat/__tests__/capability-banner.test.tsx`
866
+ Expected: PASS (5/5).
867
+
868
+ - [ ] **Step 5: Commit**
869
+
870
+ ```bash
871
+ git add src/components/chat/capability-banner.tsx src/components/chat/__tests__/capability-banner.test.tsx
872
+ git commit -m "feat(chat): runtime capability banner"
873
+ ```
874
+
875
+ ---
876
+
877
+ ## Task 7: Wire banner + session commands + shortcuts into `chat-input.tsx`
878
+
879
+ **Files:**
880
+ - Modify: `src/components/chat/chat-input.tsx`
881
+
882
+ - [ ] **Step 1: Add runtime resolution helper**
883
+
884
+ Top of `chat-input.tsx`, add imports:
885
+
886
+ ```tsx
887
+ import { CapabilityBanner } from "./capability-banner";
888
+ import { resolveAgentRuntime, type AgentRuntimeId } from "@/lib/agents/runtime/catalog";
889
+ ```
890
+
891
+ Accept a new prop `runtimeId?: AgentRuntimeId | null` (derive default from `DEFAULT_AGENT_RUNTIME`):
892
+
893
+ ```tsx
894
+ interface ChatInputProps {
895
+ // ...existing
896
+ runtimeId?: AgentRuntimeId | null;
897
+ }
898
+ ```
899
+
900
+ Resolve:
901
+
902
+ ```tsx
903
+ const effectiveRuntime = resolveAgentRuntime(runtimeId ?? null);
904
+ ```
905
+
906
+ - [ ] **Step 2: Insert banner below the textarea container**
907
+
908
+ Inside the returned JSX, just below the closing tag of the outer input container (after the `<div>` with the sticky bottom classes but before `<ChatCommandPopover>`), add:
909
+
910
+ ```tsx
911
+ {!isStreaming && (
912
+ <div className="mx-auto max-w-3xl">
913
+ <CapabilityBanner runtimeId={effectiveRuntime} />
914
+ </div>
915
+ )}
916
+ ```
917
+
918
+ - [ ] **Step 3: Implement session command execution in `handlePopoverSelect`**
919
+
920
+ Extend the `execute_immediately` branch to handle the new session commands. Replace lines ~111-123 with:
921
+
922
+ ```tsx
923
+ if (item.type === "slash") {
924
+ const entry = getToolCatalog({ includeBrowser: true }).find((t) => t.name === item.id);
925
+ if (entry?.behavior === "execute_immediately") {
926
+ autocomplete.close();
927
+ setValue("");
928
+ executeSessionCommand(entry.name);
929
+ return;
930
+ }
931
+ }
932
+ ```
933
+
934
+ Add a helper `executeSessionCommand` inside the component:
935
+
936
+ ```tsx
937
+ const executeSessionCommand = useCallback((name: string) => {
938
+ switch (name) {
939
+ case "toggle_theme":
940
+ toggleTheme();
941
+ return;
942
+ case "mark_all_read":
943
+ fetch("/api/notifications/mark-all-read", { method: "PATCH" });
944
+ return;
945
+ case "clear":
946
+ window.dispatchEvent(new CustomEvent("stagent.chat.clear"));
947
+ return;
948
+ case "compact":
949
+ window.dispatchEvent(new CustomEvent("stagent.chat.compact"));
950
+ return;
951
+ case "export":
952
+ window.dispatchEvent(new CustomEvent("stagent.chat.export"));
953
+ return;
954
+ case "help":
955
+ window.dispatchEvent(new CustomEvent("stagent.chat.help"));
956
+ return;
957
+ case "settings":
958
+ window.location.href = "/settings";
959
+ return;
960
+ }
961
+ }, []);
962
+ ```
963
+
964
+ Rationale: `chat-input` doesn't own conversation state — it dispatches `CustomEvent`s that the parent (`/chat` page) listens for. Parent wiring is in Task 8. Events are testable via `addEventListener` mocks.
965
+
966
+ - [ ] **Step 4: Add keyboard shortcuts (⌘L, ⌘⇧L, ⌘/)**
967
+
968
+ Modify `handleKeyDown` to intercept `⌘L` / `⌘⇧L` (clear) and `⌘/` (focus chat — it's already focused when this fires, so the effect is opening the slash popover):
969
+
970
+ ```tsx
971
+ const handleKeyDown = useCallback(
972
+ (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
973
+ if (autocomplete.handleKeyDown(e)) return;
974
+
975
+ const cmd = e.metaKey || e.ctrlKey;
976
+ if (cmd && (e.key === "l" || e.key === "L")) {
977
+ e.preventDefault();
978
+ if (!isStreaming) executeSessionCommand("clear");
979
+ return;
980
+ }
981
+ if (cmd && e.key === "/") {
982
+ e.preventDefault();
983
+ textareaRef.current?.focus();
984
+ setValue((v) => (v.startsWith("/") ? v : "/" + v));
985
+ requestAnimationFrame(() => {
986
+ if (textareaRef.current) autocomplete.handleChange(textareaRef.current.value, textareaRef.current);
987
+ });
988
+ return;
989
+ }
990
+
991
+ if (e.key === "Enter" && !e.shiftKey) {
992
+ e.preventDefault();
993
+ handleSend();
994
+ }
995
+ if (e.key === "Escape") {
996
+ textareaRef.current?.blur();
997
+ }
998
+ },
999
+ [handleSend, autocomplete, executeSessionCommand, isStreaming]
1000
+ );
1001
+ ```
1002
+
1003
+ Note: `⌘⏎` (send) already works because `Enter` without shift sends. `⌘` modifier on Enter is a superset.
1004
+
1005
+ - [ ] **Step 5: Verify compile**
1006
+
1007
+ Run: `npx tsc --noEmit 2>&1 | grep chat-input | head -20`
1008
+ Expected: empty.
1009
+
1010
+ - [ ] **Step 6: Commit**
1011
+
1012
+ ```bash
1013
+ git add src/components/chat/chat-input.tsx
1014
+ git commit -m "feat(chat): capability banner + session command dispatch + ⌘L ⌘/ shortcuts"
1015
+ ```
1016
+
1017
+ ---
1018
+
1019
+ ## Task 8: Parent chat page handles `stagent.chat.*` events
1020
+
1021
+ **Files:**
1022
+ - Modify: `src/app/chat/page.tsx` (or the parent that owns conversation state — grep first)
1023
+
1024
+ - [ ] **Step 1: Locate chat state owner**
1025
+
1026
+ ```bash
1027
+ grep -rn "setMessages\|setConversationId\|handleClear" src/app/chat/ src/components/chat/ | head -20
1028
+ ```
1029
+
1030
+ Identify the component that owns `messages` state and has access to the conversation ID + runtime. Likely `src/app/chat/page.tsx` or `src/components/chat/chat-shell.tsx`.
1031
+
1032
+ - [ ] **Step 2: Add event listeners for `clear`, `compact`, `export`, `help`**
1033
+
1034
+ In the identified file, inside a `useEffect` (client component):
1035
+
1036
+ ```tsx
1037
+ useEffect(() => {
1038
+ const handleClear = () => {
1039
+ // existing "new conversation" logic
1040
+ startNewConversation();
1041
+ };
1042
+ const handleCompact = () => {
1043
+ // existing compact action
1044
+ compactConversation();
1045
+ };
1046
+ const handleExport = async () => {
1047
+ const markdown = serializeConversationToMarkdown(messages);
1048
+ try {
1049
+ const res = await fetch("/api/documents", {
1050
+ method: "POST",
1051
+ headers: { "Content-Type": "application/json" },
1052
+ body: JSON.stringify({
1053
+ name: `Chat — ${new Date().toISOString().slice(0, 10)}.md`,
1054
+ content: markdown,
1055
+ mimeType: "text/markdown",
1056
+ }),
1057
+ });
1058
+ if (!res.ok) throw new Error(`Export failed: ${res.status}`);
1059
+ toast.success("Conversation exported to documents");
1060
+ } catch (err) {
1061
+ toast.error(err instanceof Error ? err.message : "Export failed");
1062
+ }
1063
+ };
1064
+ const handleHelp = () => setHelpDialogOpen(true);
1065
+
1066
+ window.addEventListener("stagent.chat.clear", handleClear);
1067
+ window.addEventListener("stagent.chat.compact", handleCompact);
1068
+ window.addEventListener("stagent.chat.export", handleExport);
1069
+ window.addEventListener("stagent.chat.help", handleHelp);
1070
+
1071
+ return () => {
1072
+ window.removeEventListener("stagent.chat.clear", handleClear);
1073
+ window.removeEventListener("stagent.chat.compact", handleCompact);
1074
+ window.removeEventListener("stagent.chat.export", handleExport);
1075
+ window.removeEventListener("stagent.chat.help", handleHelp);
1076
+ };
1077
+ }, [messages, startNewConversation, compactConversation]);
1078
+ ```
1079
+
1080
+ If `serializeConversationToMarkdown` doesn't exist, implement a minimal version inline:
1081
+
1082
+ ```ts
1083
+ function serializeConversationToMarkdown(msgs: Array<{ role: string; content: string }>): string {
1084
+ return msgs.map((m) => `### ${m.role === "user" ? "You" : "Assistant"}\n\n${m.content}`).join("\n\n---\n\n");
1085
+ }
1086
+ ```
1087
+
1088
+ If no `toast` system is imported, use `sonner` (already a dep — grep to confirm).
1089
+
1090
+ - [ ] **Step 3: Add Help dialog**
1091
+
1092
+ Create `src/components/chat/help-dialog.tsx`:
1093
+
1094
+ ```tsx
1095
+ "use client";
1096
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
1097
+
1098
+ interface HelpDialogProps {
1099
+ open: boolean;
1100
+ onOpenChange: (open: boolean) => void;
1101
+ }
1102
+
1103
+ export function HelpDialog({ open, onOpenChange }: HelpDialogProps) {
1104
+ return (
1105
+ <Dialog open={open} onOpenChange={onOpenChange}>
1106
+ <DialogContent>
1107
+ <DialogHeader>
1108
+ <DialogTitle>Chat shortcuts</DialogTitle>
1109
+ </DialogHeader>
1110
+ <div className="px-6 pb-6 space-y-2 text-sm">
1111
+ <Row k="/" v="Open actions / skills / tools menu" />
1112
+ <Row k="@" v="Reference a project, task, document, or file" />
1113
+ <Row k="⌘K" v="Open global command palette" />
1114
+ <Row k="⌘/" v="Focus chat input and open slash menu" />
1115
+ <Row k="⌘L" v="Clear conversation (new session)" />
1116
+ <Row k="⌘⇧L" v="Clear conversation (browser fallback)" />
1117
+ <Row k="⌘⏎" v="Send message" />
1118
+ <Row k="↑ ↓" v="Navigate popover items" />
1119
+ <Row k="Esc" v="Close popover" />
1120
+ </div>
1121
+ </DialogContent>
1122
+ </Dialog>
1123
+ );
1124
+ }
1125
+
1126
+ function Row({ k, v }: { k: string; v: string }) {
1127
+ return (
1128
+ <div className="flex items-start gap-3">
1129
+ <kbd className="shrink-0 rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-xs">{k}</kbd>
1130
+ <span className="text-muted-foreground">{v}</span>
1131
+ </div>
1132
+ );
1133
+ }
1134
+ ```
1135
+
1136
+ Remember the `px-6 pb-6` Sheet/Dialog body-padding convention from MEMORY.md.
1137
+
1138
+ Mount `<HelpDialog open={helpDialogOpen} onOpenChange={setHelpDialogOpen} />` in the chat parent.
1139
+
1140
+ Also add the `⌘⇧L` handler alongside `⌘L` in `chat-input.tsx` (it already fires because the check `e.key === "l" || "L"` matches Shift+L).
1141
+
1142
+ - [ ] **Step 4: Verify compile**
1143
+
1144
+ Run: `npx tsc --noEmit 2>&1 | grep -E "(chat/page|chat-shell|help-dialog)" | head -20`
1145
+ Expected: empty.
1146
+
1147
+ - [ ] **Step 5: Commit**
1148
+
1149
+ ```bash
1150
+ git add src/app/chat/ src/components/chat/help-dialog.tsx
1151
+ git commit -m "feat(chat): wire /clear /compact /export /help events + Help dialog"
1152
+ ```
1153
+
1154
+ ---
1155
+
1156
+ ## Task 9: Extend `⌘K` command palette with Skills + Files
1157
+
1158
+ **Files:**
1159
+ - Modify: `src/components/shared/command-palette.tsx`
1160
+
1161
+ - [ ] **Step 1: Audit the palette file**
1162
+
1163
+ Read `src/components/shared/command-palette.tsx` in full before editing; it already has Recent Projects, Recent Tasks, and Playbooks groups.
1164
+
1165
+ - [ ] **Step 2: Add Skills group**
1166
+
1167
+ Import:
1168
+
1169
+ ```ts
1170
+ import { useProjectSkills } from "@/hooks/use-project-skills";
1171
+ import { Sparkles } from "lucide-react";
1172
+ ```
1173
+
1174
+ Inside the component, read skills (projectId scope — use null/current):
1175
+
1176
+ ```ts
1177
+ const { skills } = useProjectSkills(null);
1178
+ ```
1179
+
1180
+ Add a `<CommandGroup heading="Skills">` rendering each skill with `<Sparkles>` icon. Clicking a skill closes the palette and dispatches a `CustomEvent("stagent.chat.activate-skill", { detail: { id: skill.id } })`.
1181
+
1182
+ - [ ] **Step 3: Add Files group**
1183
+
1184
+ Reuse the entity-detector endpoint that `chat-file-mentions` uses. Grep first:
1185
+
1186
+ ```bash
1187
+ grep -rn "entity.*file\|FileCode" src/hooks src/app/api | head
1188
+ ```
1189
+
1190
+ Identify the file-search API route; call it on-demand when the palette's input value matches a file-like pattern. Debounce with `useRef<number>` timeout (200ms) to avoid network thrash.
1191
+
1192
+ Render matches as a `<CommandGroup heading="Files">` with `<FileCode>` icon. Selection closes palette and dispatches `CustomEvent("stagent.chat.insert-mention", { detail: { type: "file", path } })` — the chat input's mention logic in `use-chat-autocomplete.ts` listens for this (add listener in that hook).
1193
+
1194
+ - [ ] **Step 4: Add listener in `use-chat-autocomplete.ts`**
1195
+
1196
+ ```ts
1197
+ useEffect(() => {
1198
+ function handleInsertMention(e: CustomEvent<{ type: string; path: string }>) {
1199
+ // Append `@path ` to textarea value and register mention
1200
+ // reuse existing handleSelect logic
1201
+ }
1202
+ window.addEventListener("stagent.chat.insert-mention", handleInsertMention as EventListener);
1203
+ return () => window.removeEventListener("stagent.chat.insert-mention", handleInsertMention as EventListener);
1204
+ }, []);
1205
+ ```
1206
+
1207
+ - [ ] **Step 5: Verify compile**
1208
+
1209
+ Run: `npx tsc --noEmit 2>&1 | grep -E "(command-palette|use-chat-autocomplete)" | head -20`
1210
+ Expected: empty.
1211
+
1212
+ - [ ] **Step 6: Commit**
1213
+
1214
+ ```bash
1215
+ git add src/components/shared/command-palette.tsx src/hooks/use-chat-autocomplete.ts
1216
+ git commit -m "feat(palette): add Skills + Files groups to ⌘K palette"
1217
+ ```
1218
+
1219
+ ---
1220
+
1221
+ ## Task 10: `/frontend-designer` review checkpoint
1222
+
1223
+ **Rationale:** Spec line 20 mandates sign-off before implementation is considered complete. Do this after code is green and before merging.
1224
+
1225
+ - [ ] **Step 1: Run the design review agent**
1226
+
1227
+ Dispatch the `frontend-designer` agent with this brief:
1228
+
1229
+ > Review the chat command popover refactor on branch `chat-command-namespace-refactor`. Files: `src/components/chat/command-tab-bar.tsx`, `src/components/chat/chat-command-popover.tsx`, `src/components/chat/capability-banner.tsx`, `src/components/chat/help-dialog.tsx`, `src/components/chat/chat-input.tsx`. Target metrics per spec §7: DV 3-4, MI 2, VD 7. Verify: tab bar follows Sheet/Dialog visual language, focus-visible rings present, fade-in-only animation, monospace for file paths in `@` mode, keyboard accessibility (arrow keys, focus trap, ARIA labels on tabs + items). Report pass/fail per criterion with screenshots where helpful.
1230
+
1231
+ - [ ] **Step 2: Record sign-off or address findings**
1232
+
1233
+ If findings are returned, open them as additional steps inside this task (amend the plan) and address each. Paste the final sign-off summary into the feature spec's Verification section.
1234
+
1235
+ - [ ] **Step 3: Commit any design-review follow-ups**
1236
+
1237
+ ```bash
1238
+ git add <files>
1239
+ git commit -m "refactor(chat): address frontend-designer review findings"
1240
+ ```
1241
+
1242
+ ---
1243
+
1244
+ ## Task 11: Browser smoke test
1245
+
1246
+ - [ ] **Step 1: Start dev server**
1247
+
1248
+ ```bash
1249
+ PORT=3010 npm run dev
1250
+ ```
1251
+
1252
+ Wait for "Ready" in the log.
1253
+
1254
+ - [ ] **Step 2: Open chat in Claude in Chrome (or Playwright fallback)**
1255
+
1256
+ Navigate to `http://localhost:3010/chat`. Verify:
1257
+
1258
+ - [ ] Type `/` → popover opens with tab bar visible, `Actions` tab selected.
1259
+ - [ ] Arrow right → `Skills` tab, arrow right → `Tools`, arrow right → `Entities`, arrow right wraps back to `Actions`.
1260
+ - [ ] Click `Tools` tab on Claude runtime → shows Browser + Utility groups.
1261
+ - [ ] Switch model to Ollama (if available) → capability banner appears below input.
1262
+ - [ ] Click dismiss (X) on banner → banner disappears.
1263
+ - [ ] Reload page → banner stays dismissed (sessionStorage).
1264
+ - [ ] Switch back to Claude runtime → banner absent.
1265
+ - [ ] Type `/clear` + Enter → new conversation starts (messages clear).
1266
+ - [ ] Type `/help` + Enter → Help dialog opens with shortcut table.
1267
+ - [ ] `⌘K` → global palette opens with Skills group visible.
1268
+ - [ ] `⌘L` → clears conversation (fallback `⌘⇧L` if browser swallows).
1269
+ - [ ] Type `@` → mention popover (no tabs, existing behavior).
1270
+ - [ ] `/export` → toast "Conversation exported to documents"; verify row in `/documents`.
1271
+
1272
+ - [ ] **Step 3: Capture screenshot for PR**
1273
+
1274
+ ```bash
1275
+ # Via Claude in Chrome or Playwright — save to /tmp
1276
+ ```
1277
+
1278
+ Save to `/tmp/chat-command-refactor-smoke.png`.
1279
+
1280
+ - [ ] **Step 4: Stop dev server**
1281
+
1282
+ `pkill -f "next dev.*3010"`.
1283
+
1284
+ ---
1285
+
1286
+ ## Task 12: Full test suite + typecheck
1287
+
1288
+ - [ ] **Step 1: Run full vitest**
1289
+
1290
+ Run: `npm test -- --run`
1291
+ Expected: all passing. Fix any regression before proceeding.
1292
+
1293
+ - [ ] **Step 2: Run typecheck**
1294
+
1295
+ Run: `npx tsc --noEmit`
1296
+ Expected: 0 errors.
1297
+
1298
+ - [ ] **Step 3: Lint**
1299
+
1300
+ Run: `npm run lint 2>&1 | tail -20`
1301
+ Expected: no new errors.
1302
+
1303
+ ---
1304
+
1305
+ ## Task 13: Update feature spec + changelog
1306
+
1307
+ **Files:**
1308
+ - Modify: `features/chat-command-namespace-refactor.md`
1309
+ - Modify: `features/changelog.md`
1310
+ - Modify: `features/roadmap.md`
1311
+
1312
+ - [ ] **Step 1: Flip spec to `completed` with verification note**
1313
+
1314
+ In `features/chat-command-namespace-refactor.md`, change `status: planned` → `status: completed`. Append a Verification section with:
1315
+
1316
+ - Date of smoke run
1317
+ - Runtime matrix checked (Claude, Codex, Ollama)
1318
+ - `/frontend-designer` sign-off summary
1319
+
1320
+ - [ ] **Step 2: Append to changelog**
1321
+
1322
+ Under today's date in `features/changelog.md`, add:
1323
+
1324
+ ```markdown
1325
+ ### Chat — Command Namespace Refactor
1326
+ - `/` popover now tabbed (Actions / Skills / Tools / Entities).
1327
+ - New session commands: `/clear`, `/compact`, `/export`, `/help`, `/settings`, `/new-task`, `/new-workflow`, `/new-schedule`.
1328
+ - Capability hint banner on runtimes without filesystem/Bash tools.
1329
+ - `⌘K` palette extended with Skills and Files groups.
1330
+ - Keyboard: `⌘L` (clear), `⌘⇧L` (fallback), `⌘/` (focus + slash).
1331
+ - **Breaking:** old flat slash popover replaced. Per Q7, no deprecation shim (alpha).
1332
+ ```
1333
+
1334
+ - [ ] **Step 3: Update roadmap**
1335
+
1336
+ Move `chat-command-namespace-refactor` from planned to completed in `features/roadmap.md`.
1337
+
1338
+ - [ ] **Step 4: Commit**
1339
+
1340
+ ```bash
1341
+ git add features/
1342
+ git commit -m "docs(features): mark chat-command-namespace-refactor complete"
1343
+ ```
1344
+
1345
+ ---
1346
+
1347
+ ## Task 14: Open PR
1348
+
1349
+ - [ ] **Step 1: Push branch**
1350
+
1351
+ ```bash
1352
+ git push -u origin chat-command-namespace-refactor
1353
+ ```
1354
+
1355
+ - [ ] **Step 2: Open PR via gh**
1356
+
1357
+ ```bash
1358
+ gh pr create --title "feat(chat): command namespace refactor (/ = verbs, @ = nouns)" --body "$(cat <<'EOF'
1359
+ ## Summary
1360
+ - Tabbed `/` popover: Actions / Skills / Tools / Entities
1361
+ - Runtime capability banner (Ollama signals limits; Claude/Codex silent)
1362
+ - New session commands: /clear /compact /export /help /settings /new-task /new-workflow /new-schedule
1363
+ - ⌘K palette extended with Skills + Files
1364
+ - Keyboard: ⌘L, ⌘⇧L, ⌘/
1365
+
1366
+ Breaking UX change accepted per spec Q7 (alpha product, no deprecation shim).
1367
+
1368
+ ## Frontend-designer sign-off
1369
+ [paste summary from Task 10 here]
1370
+
1371
+ ## Test plan
1372
+ - [x] Unit: partition, tab persistence, banner visibility matrix, storage fallback
1373
+ - [x] Typecheck + lint clean
1374
+ - [x] Browser smoke: Claude + Ollama runtimes, all 12 interactions in Task 11
1375
+
1376
+ EOF
1377
+ )"
1378
+ ```
1379
+
1380
+ ---
1381
+
1382
+ ## Self-Review
1383
+
1384
+ - **Spec coverage:** AC 1 (tabbed) → Task 5; AC 2 (@ entities+files) → existing + Task 9; AC 3 (Skills badges) → explicitly deferred in NOT-in-scope; AC 4 (Tools "Advanced") → Tools tab visible always (simplified — user approved); AC 5 (new commands) → Task 2+7+8; AC 6 (banner) → Task 6+7; AC 7 (⌘K unified) → Task 9; AC 8 (shortcut table) → Task 7+8; AC 9 (CommandTabBar visuals + a11y) → Task 4+10; AC 10 (taste metrics DV/MI/VD) → Task 10; AC 11 (`/frontend-designer` sign-off) → Task 10; AC 12 (breaking change in changelog) → Task 13.
1385
+
1386
+ **Gap noted:** AC 4 "Tools tab hidden behind 'Advanced' reveal by default" was softened to "Tools tab visible always" during brainstorming/scope approval — this is a documented deviation from spec. If the user wants strict AC, add a toggle button in `CommandTabBar` that collapses the Tools tab unless clicked. Flag this before closing the PR.
1387
+
1388
+ - **Placeholder scan:** No TBDs. Exception: Task 8 Step 1 begins with a `grep` to locate the exact file — the step commits the caller to reading it and using the returned path. Acceptable scaffolding, not a placeholder, because Step 2 gives the exact code.
1389
+
1390
+ - **Type consistency:** `CommandTabId`, `COMMAND_TABS`, `GROUP_TO_TAB`, `partitionCatalogByTab`, `isCommandTabId`, `DEFAULT_COMMAND_TAB` all exported from `command-tabs.ts` and consistently referenced in Tasks 3, 4, 5. `CapabilityBanner` prop `runtimeId: AgentRuntimeId` consistent across Tasks 6 and 7. Event names `stagent.chat.{clear,compact,export,help,activate-skill,insert-mention}` consistent between dispatcher (Task 7) and listeners (Task 8, 9).