@classic-homes/agent-chat 0.1.2 → 0.2.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.
Files changed (64) hide show
  1. package/README.md +40 -0
  2. package/dist/components/AgentChat.svelte +65 -39
  3. package/dist/components/AgentChat.svelte.d.ts +2 -0
  4. package/dist/components/AgentChat.svelte.d.ts.map +1 -1
  5. package/dist/components/ArtifactPanel.svelte +79 -96
  6. package/dist/components/ArtifactPanel.svelte.d.ts +2 -0
  7. package/dist/components/ArtifactPanel.svelte.d.ts.map +1 -1
  8. package/dist/components/ChatHeader.svelte +122 -0
  9. package/dist/components/ChatHeader.svelte.d.ts +27 -0
  10. package/dist/components/ChatHeader.svelte.d.ts.map +1 -0
  11. package/dist/components/Composer.svelte +78 -121
  12. package/dist/components/Composer.svelte.d.ts.map +1 -1
  13. package/dist/components/ConversationDialogs.svelte +134 -0
  14. package/dist/components/ConversationDialogs.svelte.d.ts +25 -0
  15. package/dist/components/ConversationDialogs.svelte.d.ts.map +1 -0
  16. package/dist/components/ConversationSidebar.svelte +135 -114
  17. package/dist/components/ConversationSidebar.svelte.d.ts +8 -2
  18. package/dist/components/ConversationSidebar.svelte.d.ts.map +1 -1
  19. package/dist/components/Markdown.svelte +48 -0
  20. package/dist/components/Markdown.svelte.d.ts +16 -0
  21. package/dist/components/Markdown.svelte.d.ts.map +1 -0
  22. package/dist/components/MessageItem.svelte +27 -116
  23. package/dist/components/MessageItem.svelte.d.ts.map +1 -1
  24. package/dist/components/MessageList.svelte +23 -34
  25. package/dist/components/ModeBanner.svelte +7 -13
  26. package/dist/components/ModeBanner.svelte.d.ts.map +1 -1
  27. package/dist/components/SandboxFrame.svelte +43 -0
  28. package/dist/components/SandboxFrame.svelte.d.ts +13 -0
  29. package/dist/components/SandboxFrame.svelte.d.ts.map +1 -0
  30. package/dist/components/ToolTimeline.svelte +17 -68
  31. package/dist/components/ToolTimeline.svelte.d.ts.map +1 -1
  32. package/dist/components/UiFrame.svelte +9 -55
  33. package/dist/components/UiFrame.svelte.d.ts.map +1 -1
  34. package/dist/components/conversation-chrome.svelte.test.d.ts +2 -0
  35. package/dist/components/conversation-chrome.svelte.test.d.ts.map +1 -0
  36. package/dist/components/conversation-chrome.svelte.test.js +127 -0
  37. package/dist/components/markdown-prose.svelte.test.d.ts +2 -0
  38. package/dist/components/markdown-prose.svelte.test.d.ts.map +1 -0
  39. package/dist/components/markdown-prose.svelte.test.js +26 -0
  40. package/dist/composer-keys.d.ts +11 -0
  41. package/dist/composer-keys.d.ts.map +1 -0
  42. package/dist/composer-keys.js +10 -0
  43. package/dist/composer-keys.test.d.ts +2 -0
  44. package/dist/composer-keys.test.d.ts.map +1 -0
  45. package/dist/composer-keys.test.js +16 -0
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -0
  49. package/dist/reducer.d.ts +10 -0
  50. package/dist/reducer.d.ts.map +1 -1
  51. package/dist/reducer.js +19 -1
  52. package/dist/reducer.test.js +29 -1
  53. package/dist/session.svelte.d.ts +6 -0
  54. package/dist/session.svelte.d.ts.map +1 -1
  55. package/dist/session.svelte.js +15 -1
  56. package/dist/ui-frame-protocol.d.ts +2 -2
  57. package/dist/ui-frame-protocol.js +2 -2
  58. package/dist/ui-frame-relay.d.ts +16 -0
  59. package/dist/ui-frame-relay.d.ts.map +1 -0
  60. package/dist/ui-frame-relay.js +22 -0
  61. package/dist/ui-frame-relay.test.d.ts +2 -0
  62. package/dist/ui-frame-relay.test.d.ts.map +1 -0
  63. package/dist/ui-frame-relay.test.js +36 -0
  64. package/package.json +8 -5
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @classic-homes/agent-chat
2
+
3
+ Svelte 5 chat UI for Classic Agent: session state, message list, composer, tool timeline,
4
+ artifacts and MCP-UI sandbox frames. The Classic App's `/agent` page and agent panel use it.
5
+
6
+ ## Requirements
7
+
8
+ - `svelte` 5 and `@classic-homes/theme-svelte` 1.39 or later (peer dependencies).
9
+ - Tailwind with `@classic-homes/theme-tailwind-preset`. The components are styled with Tailwind
10
+ utility classes and theme tokens, so the consumer's Tailwind build has to scan this package:
11
+
12
+ ```js
13
+ // tailwind.config.js
14
+ export default {
15
+ content: [
16
+ './src/**/*.{html,js,svelte,ts}',
17
+ './node_modules/@classic-homes/theme-svelte/**/*.{html,js,svelte,ts}',
18
+ './node_modules/@classic-homes/agent-chat/**/*.{js,svelte}',
19
+ ],
20
+ presets: [require('@classic-homes/theme-tailwind-preset')],
21
+ };
22
+ ```
23
+
24
+ Without that entry the chat renders unstyled. Markdown prose (rendered from model text) is the
25
+ one part styled with plain CSS, shipped inside the `Markdown` component.
26
+
27
+ ## Usage
28
+
29
+ ```svelte
30
+ <script lang="ts">
31
+ import { AgentChat } from '@classic-homes/agent-chat';
32
+ </script>
33
+
34
+ <AgentChat {client} sandboxOrigin={AGENT_SANDBOX_ORIGIN} {conversationId} mode={me.mode} />
35
+ ```
36
+
37
+ `client` is an `AgentApiClient` from `@classic-homes/agent-client`. `sandboxOrigin` is the
38
+ agent-api origin that serves `/frame`; artifacts and MCP-UI resources only ever render there.
39
+
40
+ Publishing: `docs/operations/package-publishing.md`.
@@ -7,6 +7,8 @@
7
7
  FileRef,
8
8
  } from '@classic-homes/agent-contracts';
9
9
  import type { BackendMode } from '@classic-homes/agent-contracts';
10
+ import { cn, ResizablePanes, Sheet } from '@classic-homes/theme-svelte';
11
+ import { MediaQuery } from 'svelte/reactivity';
10
12
  import { ChatSession } from '../session.svelte.js';
11
13
  import ArtifactPanel from './ArtifactPanel.svelte';
12
14
  import Composer from './Composer.svelte';
@@ -31,6 +33,8 @@
31
33
  /** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
32
34
  ontoolrequest?: (toolName: string, params?: Record<string, unknown>) => void;
33
35
  placeholder?: string;
36
+ /** Rendered above the transcript, e.g. a `ChatHeader`. */
37
+ header?: import('svelte').Snippet;
34
38
  context?: import('svelte').Snippet;
35
39
  empty?: import('svelte').Snippet;
36
40
  class?: string;
@@ -50,9 +54,10 @@
50
54
  onnotify,
51
55
  ontoolrequest,
52
56
  placeholder,
57
+ header,
53
58
  context,
54
59
  empty,
55
- class: className = '',
60
+ class: className,
56
61
  }: Props = $props();
57
62
 
58
63
  const session = new ChatSession({
@@ -96,15 +101,23 @@
96
101
  }
97
102
  });
98
103
 
104
+ // The page shows artifacts beside the chat on wide screens; narrow screens and the panel use a
105
+ // sheet so the transcript keeps its width.
106
+ const wide = new MediaQuery('(min-width: 1024px)', false);
107
+ const sidePane = $derived(variant === 'page' && wide.current);
108
+ const showArtifacts = $derived(artifactsOpen && artifacts.length > 0);
109
+ const selectedTitle = $derived(
110
+ artifacts.find((a) => a.artifactId === selectedArtifact)?.title ?? 'Artifact',
111
+ );
112
+
99
113
  async function send(text: string): Promise<void> {
100
114
  const attached = files;
101
115
  files = [];
102
116
  await session.send(text, { files: attached });
103
117
  }
104
118
  function retry(): void {
105
- const lastUser = [...session.messages].reverse().find((m) => m.role === 'user');
106
- if (lastUser && lastUser.role === 'user')
107
- void session.send(lastUser.text, { files: lastUser.attachments });
119
+ const last = session.messages[session.messages.length - 1];
120
+ if (last?.role === 'assistant') void session.retry(last.requestId);
108
121
  }
109
122
  function prompt(text: string): void {
110
123
  draft = text;
@@ -112,10 +125,11 @@
112
125
  }
113
126
  </script>
114
127
 
115
- <div class={`agent-chat agent-chat--${variant} ${className}`} data-variant={variant}>
116
- <div class="agent-chat__main">
128
+ {#snippet chat()}
129
+ <div class="flex h-full min-w-0 flex-1 flex-col">
130
+ {#if header}{@render header()}{/if}
117
131
  <ModeBanner mode={session.mode} />
118
- {#if session.loadError}<div class="agent-chat__load-error" role="alert">
132
+ {#if session.loadError}<div class="px-4 py-2 text-sm text-destructive" role="alert">
119
133
  {session.loadError}
120
134
  </div>{/if}
121
135
  <MessageList
@@ -147,42 +161,54 @@
147
161
  {context}
148
162
  />
149
163
  </div>
150
- {#if artifactsOpen && artifacts.length}
151
- <div class="agent-chat__side">
164
+ {/snippet}
165
+
166
+ <div
167
+ class={cn('flex h-full min-h-0 bg-background text-foreground', className)}
168
+ data-variant={variant}
169
+ >
170
+ {#if sidePane}
171
+ <!-- The chat stays mounted while the pane opens and closes (iframes, scroll position): a
172
+ closed pane hides the end side and the handle and gives the chat the full width. -->
173
+ <ResizablePanes
174
+ size={60}
175
+ min={35}
176
+ max={75}
177
+ persistKey="agent-chat:artifacts"
178
+ handleLabel="Resize artifact pane"
179
+ class={cn(!showArtifacts && '[&>[role=separator]]:hidden')}
180
+ startClass={cn('flex', !showArtifacts && '!basis-full')}
181
+ endClass={cn(!showArtifacts && 'hidden')}
182
+ >
183
+ {#snippet start()}{@render chat()}{/snippet}
184
+ {#snippet end()}
185
+ {#if showArtifacts}
186
+ <ArtifactPanel
187
+ {artifacts}
188
+ {sandboxOrigin}
189
+ bind:selectedId={selectedArtifact}
190
+ onclose={() => (artifactsOpen = false)}
191
+ />
192
+ {/if}
193
+ {/snippet}
194
+ </ResizablePanes>
195
+ {:else}
196
+ {@render chat()}
197
+ <Sheet
198
+ open={showArtifacts}
199
+ onOpenChange={(open) => (artifactsOpen = open)}
200
+ side="right"
201
+ size="lg"
202
+ title={selectedTitle}
203
+ hideTitle
204
+ >
152
205
  <ArtifactPanel
153
206
  {artifacts}
154
207
  {sandboxOrigin}
155
208
  bind:selectedId={selectedArtifact}
156
- onclose={() => (artifactsOpen = false)}
209
+ class="border-l-0"
210
+ headerClass="pr-12"
157
211
  />
158
- </div>
212
+ </Sheet>
159
213
  {/if}
160
214
  </div>
161
-
162
- <style>
163
- .agent-chat {
164
- display: flex;
165
- height: 100%;
166
- min-height: 0;
167
- background: hsl(var(--background));
168
- color: hsl(var(--foreground));
169
- }
170
- .agent-chat__main {
171
- flex: 1;
172
- min-width: 0;
173
- display: flex;
174
- flex-direction: column;
175
- }
176
- .agent-chat__side {
177
- width: min(45%, 40rem);
178
- min-width: 20rem;
179
- }
180
- .agent-chat--panel .agent-chat__side {
181
- display: none;
182
- }
183
- .agent-chat__load-error {
184
- padding: 0.5rem 1rem;
185
- color: hsl(var(--destructive));
186
- font-size: 0.875rem;
187
- }
188
- </style>
@@ -19,6 +19,8 @@ interface Props {
19
19
  /** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
20
20
  ontoolrequest?: (toolName: string, params?: Record<string, unknown>) => void;
21
21
  placeholder?: string;
22
+ /** Rendered above the transcript, e.g. a `ChatHeader`. */
23
+ header?: import('svelte').Snippet;
22
24
  context?: import('svelte').Snippet;
23
25
  empty?: import('svelte').Snippet;
24
26
  class?: string;
@@ -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;AAQhE,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,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,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+GH,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,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAUhE,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,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,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuIH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -1,41 +1,58 @@
1
1
  <script lang="ts">
2
- import { Button, Icon } from '@classic-homes/theme-svelte';
2
+ import { Button, cn, Icon } from '@classic-homes/theme-svelte';
3
3
  import type { Artifact } from '../reducer.js';
4
4
  import { parseMarkdown } from '../markdown.js';
5
- import { UI_FRAME_RENDER } from '../ui-frame-protocol.js';
5
+ import Markdown from './Markdown.svelte';
6
+ import SandboxFrame from './SandboxFrame.svelte';
6
7
 
7
8
  interface Props {
8
9
  artifacts: Artifact[];
9
10
  sandboxOrigin: string;
10
11
  selectedId?: string | null;
11
12
  onclose?: () => void;
13
+ class?: string;
14
+ headerClass?: string;
12
15
  }
13
- let { artifacts, sandboxOrigin, selectedId = $bindable(null), onclose }: Props = $props();
16
+ let {
17
+ artifacts,
18
+ sandboxOrigin,
19
+ selectedId = $bindable(null),
20
+ onclose,
21
+ class: className,
22
+ headerClass,
23
+ }: Props = $props();
14
24
  const selected = $derived(
15
25
  artifacts.find((a) => a.artifactId === selectedId) ?? artifacts[artifacts.length - 1],
16
26
  );
17
- let frame: HTMLIFrameElement | null = $state(null);
18
-
19
- function sendHtml(): void {
20
- if (!selected || !frame?.contentWindow) return;
21
- const html =
22
- selected.mediaType === 'application/vnd.ant.mermaid'
23
- ? mermaidPage(selected.text)
24
- : selected.text;
25
- frame.contentWindow.postMessage(
26
- { type: UI_FRAME_RENDER, html, uri: `artifact://${selected.artifactId}` },
27
- sandboxOrigin,
28
- );
29
- }
27
+ // Markdown artifacts stream like messages; re-parsing on every token is the dominant cost, so
28
+ // throttle while the artifact is still open (same budget as MessageItem).
29
+ const PARSE_THROTTLE_MS = 120;
30
+ let markdownHtml = $state('');
31
+ let lastParse = 0;
32
+ let timer: ReturnType<typeof setTimeout> | null = null;
30
33
  $effect(() => {
31
- void selected?.text;
32
- if (
33
- selected &&
34
- (selected.mediaType === 'text/html' ||
35
- selected.mediaType === 'application/vnd.ant.mermaid' ||
36
- selected.mediaType === 'image/svg+xml')
37
- )
38
- sendHtml();
34
+ if (selected?.mediaType !== 'text/markdown') return;
35
+ const text = selected.text;
36
+ const streaming = selected.open;
37
+ const now = Date.now();
38
+ if (!streaming || now - lastParse >= PARSE_THROTTLE_MS) {
39
+ if (timer) clearTimeout(timer);
40
+ timer = null;
41
+ lastParse = now;
42
+ markdownHtml = parseMarkdown(text);
43
+ } else if (!timer) {
44
+ timer = setTimeout(
45
+ () => {
46
+ timer = null;
47
+ lastParse = Date.now();
48
+ markdownHtml = parseMarkdown(selected?.text ?? '');
49
+ },
50
+ PARSE_THROTTLE_MS - (now - lastParse),
51
+ );
52
+ }
53
+ });
54
+ $effect(() => () => {
55
+ if (timer) clearTimeout(timer);
39
56
  });
40
57
 
41
58
  function mermaidPage(source: string): string {
@@ -69,93 +86,59 @@
69
86
  </script>
70
87
 
71
88
  {#if selected}
72
- <aside class="agent-artifacts" aria-label="Artifacts">
73
- <header class="agent-artifacts__header">
74
- <select class="agent-artifacts__select" bind:value={selectedId} aria-label="Artifact">
89
+ <aside
90
+ class={cn('flex h-full flex-col border-l border-border bg-card', className)}
91
+ aria-label="Artifacts"
92
+ >
93
+ <header class={cn('flex items-center gap-2 border-b border-border px-3 py-2', headerClass)}>
94
+ <select
95
+ class="min-w-0 flex-1 truncate rounded-md bg-transparent py-1 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
96
+ bind:value={selectedId}
97
+ aria-label="Artifact"
98
+ >
75
99
  {#each artifacts as a (a.artifactId)}
76
100
  <option value={a.artifactId}>{a.title ?? a.artifactId}</option>
77
101
  {/each}
78
102
  </select>
79
- <div class="agent-artifacts__actions">
80
- <Button variant="ghost" size="icon" aria-label="Copy" onclick={copy}
103
+ <div class="flex gap-0.5">
104
+ <Button variant="ghost" size="icon" class="size-8" aria-label="Copy" onclick={copy}
81
105
  ><Icon name="copy" size={16} /></Button
82
106
  >
83
- <Button variant="ghost" size="icon" aria-label="Download" onclick={download}
107
+ <Button variant="ghost" size="icon" class="size-8" aria-label="Download" onclick={download}
84
108
  ><Icon name="download" size={16} /></Button
85
109
  >
86
- {#if onclose}<Button variant="ghost" size="icon" aria-label="Close" onclick={onclose}
87
- ><Icon name="close" size={16} /></Button
110
+ {#if onclose}<Button
111
+ variant="ghost"
112
+ size="icon"
113
+ class="size-8"
114
+ aria-label="Close"
115
+ onclick={onclose}><Icon name="close" size={16} /></Button
88
116
  >{/if}
89
117
  </div>
90
118
  </header>
91
- <div class="agent-artifacts__body">
119
+ <div class="min-h-0 flex-1 overflow-auto p-3">
92
120
  {#if selected.mediaType === 'text/markdown'}
93
- <div class="agent-markdown">{@html parseMarkdown(selected.text)}</div>
121
+ <Markdown html={markdownHtml} />
94
122
  {:else if selected.mediaType === 'application/vnd.ant.code'}
95
- <pre class="agent-code"><code>{selected.text}</code></pre>
123
+ <pre class="overflow-auto whitespace-pre font-mono text-[13px] leading-relaxed"><code
124
+ >{selected.text}</code
125
+ ></pre>
96
126
  {:else}
97
- <!-- html, mermaid, svg, react: only ever inside the sandbox origin -->
98
- <iframe
99
- bind:this={frame}
100
- title={selected.title ?? 'Artifact'}
101
- src={`${sandboxOrigin}/frame`}
102
- sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms"
103
- referrerpolicy="no-referrer"
104
- onload={sendHtml}
105
- ></iframe>
127
+ <!-- html, mermaid, svg, react: only ever inside the sandbox origin. The sandbox page is
128
+ transparent and model HTML assumes a white page, so it gets a white surface. -->
129
+ <div class="h-full min-h-96 overflow-hidden rounded-md border border-border bg-white">
130
+ <SandboxFrame
131
+ {sandboxOrigin}
132
+ html={selected.mediaType === 'application/vnd.ant.mermaid'
133
+ ? mermaidPage(selected.text)
134
+ : selected.text}
135
+ uri={`artifact://${selected.artifactId}`}
136
+ title={selected.title ?? 'Artifact'}
137
+ style="height:100%"
138
+ />
139
+ </div>
106
140
  {/if}
107
- {#if selected.open}<div class="agent-artifacts__streaming">Writing…</div>{/if}
141
+ {#if selected.open}<div class="mt-2 text-xs text-muted-foreground">Writing…</div>{/if}
108
142
  </div>
109
143
  </aside>
110
144
  {/if}
111
-
112
- <style>
113
- .agent-artifacts {
114
- display: flex;
115
- flex-direction: column;
116
- height: 100%;
117
- border-left: 1px solid hsl(var(--border));
118
- background: hsl(var(--card));
119
- }
120
- .agent-artifacts__header {
121
- display: flex;
122
- align-items: center;
123
- gap: 0.5rem;
124
- padding: 0.5rem 0.75rem;
125
- border-bottom: 1px solid hsl(var(--border));
126
- }
127
- .agent-artifacts__select {
128
- flex: 1;
129
- min-width: 0;
130
- font: inherit;
131
- background: transparent;
132
- color: inherit;
133
- border: 0;
134
- }
135
- .agent-artifacts__actions {
136
- display: flex;
137
- gap: 0.125rem;
138
- }
139
- .agent-artifacts__body {
140
- flex: 1;
141
- min-height: 0;
142
- overflow: auto;
143
- padding: 0.75rem;
144
- }
145
- iframe {
146
- width: 100%;
147
- height: 100%;
148
- min-height: 24rem;
149
- border: 0;
150
- background: white;
151
- }
152
- .agent-code {
153
- white-space: pre;
154
- overflow: auto;
155
- font-size: 0.8125rem;
156
- }
157
- .agent-artifacts__streaming {
158
- font-size: 0.75rem;
159
- color: hsl(var(--muted-foreground));
160
- }
161
- </style>
@@ -4,6 +4,8 @@ interface Props {
4
4
  sandboxOrigin: string;
5
5
  selectedId?: string | null;
6
6
  onclose?: () => void;
7
+ class?: string;
8
+ headerClass?: string;
7
9
  }
8
10
  declare const ArtifactPanel: import("svelte").Component<Props, {}, "selectedId">;
9
11
  type ArtifactPanel = ReturnType<typeof ArtifactPanel>;
@@ -1 +1 @@
1
- {"version":3,"file":"ArtifactPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ArtifactPanel.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAK5C,UAAU,KAAK;IACb,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAiGH,QAAA,MAAM,aAAa,qDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"ArtifactPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ArtifactPanel.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAM5C,UAAU,KAAK;IACb,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAkHH,QAAA,MAAM,aAAa,qDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -0,0 +1,122 @@
1
+ <!--
2
+ @component
3
+ Header for a chat: conversation title, agent badge and the conversation menu (rename,
4
+ open full page, delete). Pass it to `AgentChat` through the `header` snippet. Actions only
5
+ show when their handler is set; rename and delete also need a `conversationId`.
6
+ -->
7
+ <script lang="ts">
8
+ import {
9
+ Badge,
10
+ Button,
11
+ cn,
12
+ DropdownMenu,
13
+ Icon,
14
+ type DropdownMenuItem,
15
+ } from '@classic-homes/theme-svelte';
16
+ import type { Snippet } from 'svelte';
17
+ import ConversationDialogs, { type ConversationRef } from './ConversationDialogs.svelte';
18
+
19
+ interface Props {
20
+ title?: string;
21
+ conversationId?: string | null;
22
+ /** Agent name shown as a badge, e.g. "Classic Agent". */
23
+ agentLabel?: string;
24
+ /** Opens the conversation list; the button only shows below `lg`. */
25
+ onopenrail?: () => void;
26
+ onnew?: () => void;
27
+ /** Opens this conversation as a full page (panel variant). */
28
+ onexpand?: () => void;
29
+ onrename?: (conversationId: string, title: string) => void | Promise<void>;
30
+ ondelete?: (conversationId: string) => void | Promise<void>;
31
+ onerror?: (message: string) => void;
32
+ /** Extra controls before the menu. */
33
+ actions?: Snippet;
34
+ class?: string;
35
+ }
36
+ let {
37
+ title,
38
+ conversationId = null,
39
+ agentLabel,
40
+ onopenrail,
41
+ onnew,
42
+ onexpand,
43
+ onrename,
44
+ ondelete,
45
+ onerror,
46
+ actions,
47
+ class: className,
48
+ }: Props = $props();
49
+
50
+ let renaming = $state<ConversationRef | null>(null);
51
+ let deleting = $state<ConversationRef | null>(null);
52
+ let headerEl = $state<HTMLElement | null>(null);
53
+ let returnFocus = $state<HTMLElement | null>(null);
54
+ /** Remember the menu trigger before a dialog takes focus from the closing menu. */
55
+ function open(set: () => void): void {
56
+ returnFocus = headerEl?.querySelector<HTMLElement>('[aria-haspopup="menu"]') ?? null;
57
+ set();
58
+ }
59
+ const displayTitle = $derived(title?.trim() || 'New chat');
60
+ const current = $derived(conversationId ? { conversationId, title: displayTitle } : null);
61
+
62
+ const items = $derived.by<DropdownMenuItem[]>(() => {
63
+ const list: DropdownMenuItem[] = [];
64
+ if (current && onrename)
65
+ list.push({
66
+ label: 'Rename',
67
+ icon: 'edit',
68
+ onSelect: () => open(() => (renaming = current)),
69
+ });
70
+ if (onexpand) list.push({ label: 'Open full page', icon: 'expand', onSelect: onexpand });
71
+ if (current && ondelete) {
72
+ if (list.length) list.push({ type: 'separator' });
73
+ list.push({
74
+ label: 'Delete',
75
+ icon: 'trash',
76
+ destructive: true,
77
+ onSelect: () => open(() => (deleting = current)),
78
+ });
79
+ }
80
+ return list;
81
+ });
82
+ </script>
83
+
84
+ <header
85
+ bind:this={headerEl}
86
+ class={cn('flex min-h-12 items-center gap-2 border-b border-border px-3 py-2', className)}
87
+ >
88
+ {#if onopenrail}
89
+ <Button
90
+ variant="ghost"
91
+ size="icon"
92
+ class="size-8 lg:hidden"
93
+ aria-label="Show conversations"
94
+ onclick={onopenrail}><Icon name="sidebar" size={18} /></Button
95
+ >
96
+ {/if}
97
+ <div class="flex min-w-0 flex-1 items-center gap-2">
98
+ <h2 class="truncate text-sm font-semibold" title={displayTitle}>{displayTitle}</h2>
99
+ {#if agentLabel}<Badge variant="outline" size="sm" class="shrink-0">{agentLabel}</Badge>{/if}
100
+ </div>
101
+ {#if actions}{@render actions()}{/if}
102
+ {#if onnew}
103
+ <Button variant="ghost" size="icon" class="size-8" aria-label="New chat" onclick={onnew}
104
+ ><Icon name="plus" size={18} /></Button
105
+ >
106
+ {/if}
107
+ {#if items.length}
108
+ <DropdownMenu
109
+ {items}
110
+ align="end"
111
+ triggerVariant="ghost"
112
+ triggerSize="icon"
113
+ triggerClass="size-8"
114
+ triggerLabel="Conversation options"
115
+ showChevron={false}
116
+ >
117
+ {#snippet trigger()}<Icon name="more-horizontal" size={18} />{/snippet}
118
+ </DropdownMenu>
119
+ {/if}
120
+ </header>
121
+
122
+ <ConversationDialogs bind:renaming bind:deleting {onrename} {ondelete} {onerror} {returnFocus} />
@@ -0,0 +1,27 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ title?: string;
4
+ conversationId?: string | null;
5
+ /** Agent name shown as a badge, e.g. "Classic Agent". */
6
+ agentLabel?: string;
7
+ /** Opens the conversation list; the button only shows below `lg`. */
8
+ onopenrail?: () => void;
9
+ onnew?: () => void;
10
+ /** Opens this conversation as a full page (panel variant). */
11
+ onexpand?: () => void;
12
+ onrename?: (conversationId: string, title: string) => void | Promise<void>;
13
+ ondelete?: (conversationId: string) => void | Promise<void>;
14
+ onerror?: (message: string) => void;
15
+ /** Extra controls before the menu. */
16
+ actions?: Snippet;
17
+ class?: string;
18
+ }
19
+ /**
20
+ * Header for a chat: conversation title, agent badge and the conversation menu (rename,
21
+ * open full page, delete). Pass it to `AgentChat` through the `header` snippet. Actions only
22
+ * show when their handler is set; rename and delete also need a `conversationId`.
23
+ */
24
+ declare const ChatHeader: import("svelte").Component<Props, {}, "">;
25
+ type ChatHeader = ReturnType<typeof ChatHeader>;
26
+ export default ChatHeader;
27
+ //# sourceMappingURL=ChatHeader.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatHeader.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ChatHeader.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIpC,UAAU,KAAK;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,sCAAsC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8EH;;;;GAIG;AACH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}