@classic-homes/agent-chat 0.7.0 → 0.8.0

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 CHANGED
@@ -52,6 +52,9 @@ agent-api origin that serves `/frame`; artifacts and MCP-UI resources only ever
52
52
  (`ComposerPrompt.text` may be a loader, e.g. fetching the template).
53
53
  - `ToolTimeline`: tool calls with status and duration, delegations nested; `toolLabels` maps
54
54
  tool names to readable ones (`AgentChat` passes both through).
55
+ - Agent picker: pass `agents` (from `/api/agents`); with more than one, a new conversation offers
56
+ a picker in the composer until its first message. The conversation keeps that agent; `header`
57
+ receives `{ agent, agentName }` and `onagentchange` reports it.
55
58
  - `Welcome`: the empty conversation, with a greeting (`userName`), a capability line and up to
56
59
  four `suggestions` that send when chosen. `AgentChat` shows it unless the host passes an
57
60
  `empty` snippet, a loading skeleton while an existing conversation's transcript arrives, and
@@ -2,6 +2,7 @@
2
2
  import type { AgentApiClient } from '@classic-homes/agent-client';
3
3
  import type {
4
4
  AgentKey,
5
+ AgentSummary,
5
6
  ClientKind,
6
7
  DocumentContext,
7
8
  FileRef,
@@ -13,6 +14,7 @@
13
14
  cn,
14
15
  Icon,
15
16
  ResizablePanes,
17
+ Select,
16
18
  Sheet,
17
19
  Skeleton,
18
20
  } from '@classic-homes/theme-svelte';
@@ -34,7 +36,15 @@
34
36
  conversationId?: string | null;
35
37
  /** Backend mode from `/api/me`; shows the mock banner before any turn runs. */
36
38
  mode?: BackendMode;
39
+ /** Agent for a new conversation; a loaded conversation keeps its own. */
37
40
  agent?: AgentKey;
41
+ /**
42
+ * Agents the user may address (`/api/agents`). With more than one, a new conversation offers
43
+ * a picker in the composer until its first message; the agent is fixed after that.
44
+ */
45
+ agents?: AgentSummary[];
46
+ /** The agent of this conversation changed (picked, or learned from a loaded conversation). */
47
+ onagentchange?: (agent: AgentKey) => void;
38
48
  clientKind?: ClientKind;
39
49
  variant?: 'page' | 'panel';
40
50
  getContext?: () => DocumentContext | null | undefined;
@@ -63,8 +73,8 @@
63
73
  welcomeTitle?: string;
64
74
  /** Replaces the welcome's capability line. */
65
75
  welcomeDescription?: string;
66
- /** Rendered above the transcript, e.g. a `ChatHeader`. */
67
- header?: import('svelte').Snippet;
76
+ /** Rendered above the transcript, e.g. a `ChatHeader`; receives the conversation's agent. */
77
+ header?: import('svelte').Snippet<[{ agent: AgentKey; agentName?: string }]>;
68
78
  context?: import('svelte').Snippet;
69
79
  /** Replaces the default `Welcome` on an empty conversation. */
70
80
  empty?: import('svelte').Snippet;
@@ -76,6 +86,8 @@
76
86
  conversationId = null,
77
87
  mode,
78
88
  agent = 'classic-agent',
89
+ agents,
90
+ onagentchange,
79
91
  clientKind,
80
92
  variant = 'page',
81
93
  getContext,
@@ -163,6 +175,19 @@
163
175
  const last = session.messages[session.messages.length - 1];
164
176
  if (last?.role === 'assistant') void session.retry(last.requestId);
165
177
  }
178
+ const agentName = $derived(agents?.find((a) => a.key === session.agent)?.name);
179
+ const pickAgent = $derived(
180
+ (agents?.length ?? 0) > 1 && !conversationId && session.messages.length === 0,
181
+ );
182
+ let reportedAgent: AgentKey | undefined;
183
+ $effect(() => {
184
+ const current = session.agent;
185
+ if (current !== reportedAgent) {
186
+ reportedAgent = current;
187
+ onagentchange?.(current);
188
+ }
189
+ });
190
+
166
191
  // An existing conversation whose transcript has not arrived yet: show a skeleton, not the
167
192
  // welcome (which would flash before the messages).
168
193
  const pending = $derived(
@@ -181,6 +206,17 @@
181
206
  }
182
207
  </script>
183
208
 
209
+ {#snippet agentPicker()}
210
+ <Select
211
+ value={session.agent}
212
+ onValueChange={(value) => (session.agent = value as AgentKey)}
213
+ options={(agents ?? []).map((a) => ({ value: a.key, label: a.name }))}
214
+ aria-label="Agent"
215
+ disabled={session.status !== 'idle'}
216
+ class="h-8 w-auto min-w-36 border-0 bg-transparent text-xs shadow-none hover:bg-muted"
217
+ />
218
+ {/snippet}
219
+
184
220
  {#snippet emptyState()}
185
221
  {#if pending}
186
222
  <div
@@ -220,7 +256,7 @@
220
256
 
221
257
  {#snippet chat()}
222
258
  <div class="flex h-full min-w-0 flex-1 flex-col">
223
- {#if header}{@render header()}{/if}
259
+ {#if header}{@render header({ agent: session.agent, agentName })}{/if}
224
260
  <ModeBanner mode={session.mode} />
225
261
  {#if session.loadError}
226
262
  <div class="mx-auto w-full max-w-3xl px-4 pt-3">
@@ -277,6 +313,7 @@
277
313
  onsend={send}
278
314
  oncancel={() => session.cancel()}
279
315
  {context}
316
+ tools={pickAgent ? agentPicker : undefined}
280
317
  />
281
318
  </div>
282
319
  {/snippet}
@@ -1,5 +1,5 @@
1
1
  import type { AgentApiClient } from '@classic-homes/agent-client';
2
- import type { AgentKey, ClientKind, DocumentContext } from '@classic-homes/agent-contracts';
2
+ import type { AgentKey, AgentSummary, ClientKind, DocumentContext } from '@classic-homes/agent-contracts';
3
3
  import type { BackendMode } from '@classic-homes/agent-contracts';
4
4
  import type { ComposerPrompt } from '../composer-prompts.js';
5
5
  import type { WelcomeSuggestion } from '../welcome.js';
@@ -10,7 +10,15 @@ interface Props {
10
10
  conversationId?: string | null;
11
11
  /** Backend mode from `/api/me`; shows the mock banner before any turn runs. */
12
12
  mode?: BackendMode;
13
+ /** Agent for a new conversation; a loaded conversation keeps its own. */
13
14
  agent?: AgentKey;
15
+ /**
16
+ * Agents the user may address (`/api/agents`). With more than one, a new conversation offers
17
+ * a picker in the composer until its first message; the agent is fixed after that.
18
+ */
19
+ agents?: AgentSummary[];
20
+ /** The agent of this conversation changed (picked, or learned from a loaded conversation). */
21
+ onagentchange?: (agent: AgentKey) => void;
14
22
  clientKind?: ClientKind;
15
23
  variant?: 'page' | 'panel';
16
24
  getContext?: () => DocumentContext | null | undefined;
@@ -39,8 +47,11 @@ interface Props {
39
47
  welcomeTitle?: string;
40
48
  /** Replaces the welcome's capability line. */
41
49
  welcomeDescription?: string;
42
- /** Rendered above the transcript, e.g. a `ChatHeader`. */
43
- header?: import('svelte').Snippet;
50
+ /** Rendered above the transcript, e.g. a `ChatHeader`; receives the conversation's agent. */
51
+ header?: import('svelte').Snippet<[{
52
+ agent: AgentKey;
53
+ agentName?: string;
54
+ }]>;
44
55
  context?: import('svelte').Snippet;
45
56
  /** Replaces the default `Welcome` on an empty conversation. */
46
57
  empty?: import('svelte').Snippet;
@@ -1 +1 @@
1
- {"version":3,"file":"AgentChat.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AgentChat.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAYlE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAQrD,UAAU,KAAK;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,qBAAqB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,gFAAgF;IAChF,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,IAAI,CAAC;IACX,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,iGAAiG;IACjG,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAmNH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"AgentChat.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AgentChat.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EACR,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAalE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAQrD,UAAU,KAAK;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,yEAAyE;IACzE,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,8FAA8F;IAC9F,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;IAC1C,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,qBAAqB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,gFAAgF;IAChF,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,IAAI,CAAC;IACX,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,iGAAiG;IACjG,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6FAA6F;IAC7F,MAAM,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IAC7E,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsOH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -27,6 +27,8 @@
27
27
  onremovefile?: (fileId: string) => void;
28
28
  /** Slot for a host-provided context chip (page context toggle). */
29
29
  context?: import('svelte').Snippet;
30
+ /** Toolbar controls before the context chip (e.g. the agent picker). */
31
+ tools?: import('svelte').Snippet;
30
32
  }
31
33
  let {
32
34
  disabled = false,
@@ -40,6 +42,7 @@
40
42
  oncancel,
41
43
  onremovefile,
42
44
  context,
45
+ tools,
43
46
  }: Props = $props();
44
47
 
45
48
  const uid = $props.id();
@@ -272,6 +275,7 @@
272
275
  <Icon name="paperclip" size={18} />
273
276
  </Button>
274
277
  {/if}
278
+ {#if tools}<div class="min-w-0">{@render tools()}</div>{/if}
275
279
  {#if context}<div class="min-w-0">{@render context()}</div>{/if}
276
280
  <div class="ml-auto flex items-center gap-2">
277
281
  {#if showCount}
@@ -15,6 +15,8 @@ interface Props {
15
15
  onremovefile?: (fileId: string) => void;
16
16
  /** Slot for a host-provided context chip (page context toggle). */
17
17
  context?: import('svelte').Snippet;
18
+ /** Toolbar controls before the context chip (e.g. the agent picker). */
19
+ tools?: import('svelte').Snippet;
18
20
  }
19
21
  /**
20
22
  * The message box: an autosizing textarea in a card with attach, the host's context chip and
@@ -1 +1 @@
1
- {"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAIvF,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,oEAAoE;IACpE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CACpC;AA2PH;;;;GAIG;AACH,QAAA,MAAM,QAAQ;iBA/NO,IAAI;qBA+N6B,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAIvF,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,oEAAoE;IACpE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;AA6PH;;;;GAIG;AACH,QAAA,MAAM,QAAQ;iBAhOO,IAAI;qBAgO6B,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agent-picker.svelte.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-picker.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/agent-picker.svelte.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,84 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { ChatSession } from '../session.svelte.js';
5
+ import AgentChat from './AgentChat.svelte';
6
+ afterEach(() => cleanup());
7
+ const agents = [
8
+ { key: 'classic-agent', name: 'Classic Agent' },
9
+ { key: 'elevation-studio', name: 'Elevation Studio' },
10
+ ];
11
+ function fakeClient() {
12
+ return {
13
+ fileUrl: (id) => `#${id}`,
14
+ uploadFile: vi.fn(),
15
+ cancel: vi.fn(),
16
+ resumeTurn: vi.fn(),
17
+ sendFeedback: vi.fn(),
18
+ getConversation: vi.fn().mockResolvedValue({
19
+ conversation: { conversationId: 'c-9', agent: 'elevation-studio' },
20
+ messages: [],
21
+ files: [],
22
+ }),
23
+ streamChat: vi.fn(async (req, opts) => {
24
+ [
25
+ { type: 'start', conversationId: 'c-1', mode: 'live' },
26
+ { type: 'delta', text: 'Hello' },
27
+ { type: 'done', finishReason: 'stop' },
28
+ ].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
29
+ return { kind: 'done' };
30
+ }),
31
+ };
32
+ }
33
+ const base = { sandboxOrigin: 'https://frame.test' };
34
+ describe('ChatSession agent', () => {
35
+ it('sends the chosen agent and adopts a loaded conversation’s agent', async () => {
36
+ const client = fakeClient();
37
+ const session = new ChatSession({ client: client });
38
+ expect(session.agent).toBe('classic-agent');
39
+ session.agent = 'elevation-studio';
40
+ await session.send('hi');
41
+ expect(client.streamChat.mock.calls[0][0]).toMatchObject({ agent: 'elevation-studio' });
42
+ const other = new ChatSession({ client: client, conversationId: 'c-9' });
43
+ await other.load('c-9');
44
+ expect(other.agent).toBe('elevation-studio');
45
+ });
46
+ });
47
+ describe('AgentChat agent picker', () => {
48
+ it('offers a picker for a new conversation only, and hides it after the first message', async () => {
49
+ const user = userEvent.setup();
50
+ const client = fakeClient();
51
+ const onagentchange = vi.fn();
52
+ render(AgentChat, {
53
+ props: { ...base, client: client, agents, onagentchange },
54
+ });
55
+ expect(onagentchange).toHaveBeenLastCalledWith('classic-agent');
56
+ const picker = screen.getByRole('button', { name: 'Agent' });
57
+ await user.click(picker);
58
+ await user.click(await screen.findByRole('option', { name: 'Elevation Studio' }));
59
+ await waitFor(() => expect(onagentchange).toHaveBeenLastCalledWith('elevation-studio'));
60
+ await user.type(screen.getByRole('textbox', { name: 'Message' }), 'hi{Enter}');
61
+ await screen.findAllByText('Hello');
62
+ expect(client.streamChat.mock.calls[0][0]).toMatchObject({ agent: 'elevation-studio' });
63
+ expect(screen.queryByRole('button', { name: 'Agent' })).toBeNull();
64
+ });
65
+ it('has no picker with a single agent or on an existing conversation', async () => {
66
+ const { unmount } = render(AgentChat, {
67
+ props: { ...base, client: fakeClient(), agents: agents.slice(0, 1) },
68
+ });
69
+ expect(screen.queryByRole('button', { name: 'Agent' })).toBeNull();
70
+ unmount();
71
+ const onagentchange = vi.fn();
72
+ render(AgentChat, {
73
+ props: {
74
+ ...base,
75
+ client: fakeClient(),
76
+ agents,
77
+ conversationId: 'c-9',
78
+ onagentchange,
79
+ },
80
+ });
81
+ expect(screen.queryByRole('button', { name: 'Agent' })).toBeNull();
82
+ await waitFor(() => expect(onagentchange).toHaveBeenLastCalledWith('elevation-studio'));
83
+ });
84
+ });
@@ -35,6 +35,8 @@ export declare class ChatSession {
35
35
  /** The conversation whose transcript `messages` holds (loaded or started here); null before. */
36
36
  loadedConversationId: string | null;
37
37
  mode: "mock" | "live" | undefined;
38
+ /** Who answers. Set before the first message; a loaded conversation keeps its own agent. */
39
+ agent: string;
38
40
  loadError: string | undefined;
39
41
  pendingFiles: {
40
42
  fileId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,8 @@ export class ChatSession {
11
11
  /** The conversation whose transcript `messages` holds (loaded or started here); null before. */
12
12
  loadedConversationId = $state(null);
13
13
  mode = $state(undefined);
14
+ /** Who answers. Set before the first message; a loaded conversation keeps its own agent. */
15
+ agent = $state('classic-agent');
14
16
  loadError = $state(undefined);
15
17
  pendingFiles = $state([]);
16
18
  controller;
@@ -19,6 +21,7 @@ export class ChatSession {
19
21
  this.options = options;
20
22
  this.conversationId = options.conversationId ?? null;
21
23
  this.mode = options.mode;
24
+ this.agent = options.agent ?? 'classic-agent';
22
25
  }
23
26
  get streaming() {
24
27
  return this.status === 'streaming';
@@ -51,6 +54,7 @@ export class ChatSession {
51
54
  ? { feedback: detail.feedback[m.id.replace(/^a-/, '')] }
52
55
  : {}),
53
56
  });
57
+ this.agent = detail.conversation.agent;
54
58
  const active = detail.conversation.activeRequestId;
55
59
  this.status = 'idle';
56
60
  if (active)
@@ -75,7 +79,7 @@ export class ChatSession {
75
79
  this.pendingFiles = [];
76
80
  const request = {
77
81
  requestId,
78
- agent: this.options.agent ?? 'classic-agent',
82
+ agent: this.agent,
79
83
  message,
80
84
  ...(this.conversationId ? { conversationId: this.conversationId } : {}),
81
85
  ...(context ? { context } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/agent-chat",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Svelte 5 chat UI for Classic Agent: session state, message list, composer, tool timeline, artifacts and MCP-UI sandbox frames. Consumed by the Classic App.",
@@ -17,8 +17,8 @@
17
17
  "dist"
18
18
  ],
19
19
  "dependencies": {
20
- "@classic-homes/agent-client": "0.7.0",
21
- "@classic-homes/agent-contracts": "0.7.0",
20
+ "@classic-homes/agent-client": "0.8.0",
21
+ "@classic-homes/agent-contracts": "0.8.0",
22
22
  "dompurify": "3.4.17"
23
23
  },
24
24
  "peerDependencies": {