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.
- package/README.md +44 -31
- package/dist/cli.js +24 -0
- package/docs/.coverage-gaps.json +154 -24
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +16 -2
- package/docs/features/settings.md +2 -2
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +3 -1
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +6 -2
- package/docs/getting-started.md +1 -1
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +25 -2
- package/docs/journeys/personal-use.md +12 -5
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -3
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/schedules/[id]/execute/route.ts +2 -2
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -12
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/documents/page.tsx +4 -1
- package/src/app/settings/page.tsx +2 -0
- package/src/components/book/content-blocks.tsx +1 -1
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +668 -47
- package/src/components/chat/chat-input.tsx +103 -8
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +73 -3
- package/src/components/chat/chat-shell.tsx +62 -3
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/playbook/playbook-detail-view.tsx +1 -1
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/command-palette.tsx +262 -2
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/tasks/task-bento-grid.tsx +12 -2
- package/src/components/tasks/task-card.tsx +3 -0
- package/src/components/tasks/task-chip-bar.tsx +30 -1
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +105 -46
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +97 -22
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex.ts +35 -0
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +30 -7
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +141 -1
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +109 -15
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +6 -19
- package/src/lib/chat/stream-telemetry.ts +9 -4
- package/src/lib/chat/system-prompt.ts +22 -0
- package/src/lib/chat/tool-catalog.ts +33 -3
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/helpers.ts +2 -0
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/task-tools.ts +6 -2
- package/src/lib/chat/tools/workflow-tools.ts +61 -20
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +2 -0
- package/src/lib/data/clear.ts +2 -6
- package/src/lib/db/bootstrap.ts +17 -0
- package/src/lib/db/schema.ts +26 -0
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +56 -2
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/instance/__tests__/detect.test.ts +1 -1
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
- package/src/lib/instance/fingerprint.ts +8 -10
- package/src/lib/instance/upgrade-poller.ts +53 -1
- package/src/lib/schedules/scheduler.ts +4 -4
- package/src/lib/utils/stagent-paths.ts +4 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +6 -0
- package/src/lib/workflows/engine.ts +5 -3
- 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).
|