diff options
| author | Adam Malczewski <[email protected]> | 2026-08-20 19:36:12 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-08-20 19:36:12 +0900 |
| commit | fb7eb3105e1fbbdefdad9348f9132815b5bbe52d (patch) | |
| tree | deb95e97a917424ea67470b168b91349a41d1c08 /src/features/tabs/tabs.ts | |
| parent | 63588c3c69b32d180dc78a2cc187cc8aa22ad7e0 (diff) | |
| download | dispatch-web-fb7eb3105e1fbbdefdad9348f9132815b5bbe52d.tar.gz dispatch-web-fb7eb3105e1fbbdefdad9348f9132815b5bbe52d.zip | |
feat(named-agents): sidebar view, synthetic provider, auto-save, snapshot semantics
Implement the frontend of the Named Agents feature (plan §5-§9):
- New features/named-agents module: pure logic (types re-exported from
@dispatch/wire + transport-contract, validation, provider/model options,
snapshot helpers, network normalizers, auto-save status model) + an injected
auto-save controller (name debounce, immediate provider/model save, monotonic
stale-response guard) + the collapsible sidebar card view.
- Store: /named-agents CRUD + boot seed; selectModel(model, snapshot?) persists
the raw model WITH the namedAgent snapshot; draft promotion carries it;
refreshModel restores ModelResponse.namedAgent.
- Chat store setNamedAgent (sends namedAgent on chat.send); Tab.snapshot +
setSnapshot (retained across rename/remove, persisted with tabs).
- Model selector: synthetic Named Agents provider ALWAYS first; selecting one
resolves to the raw model + records a snapshot; a retained snapshot of a
renamed/removed agent still displays as unavailable (plan §7).
- Composition root wires the view (viewKinds + loaded-modules manifest) + feeds
the selector.
Tests: logic + controller + selector + app-store CRUD (1246 pass), typecheck 0,
biome clean (only pre-existing config infos pending a future migrate), build OK.
Diffstat (limited to 'src/features/tabs/tabs.ts')
| -rw-r--r-- | src/features/tabs/tabs.ts | 40 |
1 files changed, 39 insertions, 1 deletions
diff --git a/src/features/tabs/tabs.ts b/src/features/tabs/tabs.ts index 9fc9306..ff7bb4f 100644 --- a/src/features/tabs/tabs.ts +++ b/src/features/tabs/tabs.ts @@ -1,9 +1,18 @@ +import type { NamedAgentSelectionSnapshot } from "../named-agents"; + export interface Tab { readonly conversationId: string; readonly model: string; readonly title: string; /** The workspace this tab belongs to (the workspace's URL slug). */ readonly workspaceId: string; + /** + * The Named Agent selection snapshot, when this conversation was selected via + * a Named Agent (plan §7). Retained so the conversation keeps showing the + * friendly label (and its captured raw model) even after the Named Agent is + * renamed/removed. Absent for a raw model selection. + */ + readonly snapshot?: NamedAgentSelectionSnapshot; } export interface TabsState { @@ -73,7 +82,36 @@ export function closeTab(state: TabsState, conversationId: string): TabsState { } export function setModel(state: TabsState, conversationId: string, model: string): TabsState { - const tabs = state.tabs.map((t) => (t.conversationId === conversationId ? { ...t, model } : t)); + // A raw-model selection clears any Named Agent snapshot (a raw selection is + // not "via a Named Agent"). Omit the key entirely (`exactOptionalPropertyTypes`). + const tabs = state.tabs.map((t) => + t.conversationId !== conversationId ? t : omitSnapshot({ ...t, model }), + ); + return { tabs, activeConversationId: state.activeConversationId }; +} + +/** Drop the optional `snapshot` key (used to clear a Named Agent selection). */ +function omitSnapshot(tab: Tab): Tab { + if (!("snapshot" in tab)) return tab; + const { snapshot: _omit, ...rest } = tab; + void _omit; + return rest; +} + +/** + * Set (or clear) a tab's Named Agent selection snapshot (plan §7). The snapshot's + * captured `model` should equal the tab's `model` (the resolved raw model); this + * reducer does not rewrite `model` — the caller keeps them in sync. Pass `null` + * to clear (a raw model selection). + */ +export function setSnapshot( + state: TabsState, + conversationId: string, + snapshot: NamedAgentSelectionSnapshot | null, +): TabsState { + const tabs = state.tabs.map((t) => + t.conversationId === conversationId ? { ...t, ...(snapshot === null ? {} : { snapshot }) } : t, + ); return { tabs, activeConversationId: state.activeConversationId }; } |
