@classic-homes/agent-chat 0.10.0 → 0.12.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 (111) hide show
  1. package/README.md +57 -2
  2. package/dist/components/AgentChat.svelte +61 -22
  3. package/dist/components/AgentChat.svelte.d.ts +11 -1
  4. package/dist/components/AgentChat.svelte.d.ts.map +1 -1
  5. package/dist/components/ArtifactPanel.svelte +4 -2
  6. package/dist/components/AssistantMessage.svelte +14 -4
  7. package/dist/components/AssistantMessage.svelte.d.ts +2 -0
  8. package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
  9. package/dist/components/ChatHeader.svelte +4 -1
  10. package/dist/components/ChatHeader.svelte.d.ts +2 -0
  11. package/dist/components/ChatHeader.svelte.d.ts.map +1 -1
  12. package/dist/components/ChatLauncher.svelte +157 -0
  13. package/dist/components/ChatLauncher.svelte.d.ts +23 -0
  14. package/dist/components/ChatLauncher.svelte.d.ts.map +1 -0
  15. package/dist/components/Composer.svelte +7 -3
  16. package/dist/components/Composer.svelte.d.ts.map +1 -1
  17. package/dist/components/FloatingWindow.svelte +239 -0
  18. package/dist/components/FloatingWindow.svelte.d.ts +26 -0
  19. package/dist/components/FloatingWindow.svelte.d.ts.map +1 -0
  20. package/dist/components/Markdown.svelte +4 -0
  21. package/dist/components/Markdown.svelte.d.ts.map +1 -1
  22. package/dist/components/MessageFeedback.svelte +11 -4
  23. package/dist/components/MessageFeedback.svelte.d.ts.map +1 -1
  24. package/dist/components/MessageFiles.svelte +133 -40
  25. package/dist/components/MessageFiles.svelte.d.ts +6 -0
  26. package/dist/components/MessageFiles.svelte.d.ts.map +1 -1
  27. package/dist/components/MessageItem.svelte +2 -0
  28. package/dist/components/MessageItem.svelte.d.ts +2 -0
  29. package/dist/components/MessageItem.svelte.d.ts.map +1 -1
  30. package/dist/components/MessageList.svelte +40 -7
  31. package/dist/components/MessageList.svelte.d.ts +3 -1
  32. package/dist/components/MessageList.svelte.d.ts.map +1 -1
  33. package/dist/components/ShortcutsDialog.svelte +10 -3
  34. package/dist/components/ShortcutsDialog.svelte.d.ts +2 -0
  35. package/dist/components/ShortcutsDialog.svelte.d.ts.map +1 -1
  36. package/dist/components/UiFrame.svelte +12 -4
  37. package/dist/components/UiFrame.svelte.d.ts.map +1 -1
  38. package/dist/components/UserMessage.svelte +10 -1
  39. package/dist/components/UserMessage.svelte.d.ts.map +1 -1
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +4 -0
  43. package/dist/markdown.d.ts.map +1 -1
  44. package/dist/markdown.js +94 -61
  45. package/dist/reducer.d.ts +11 -0
  46. package/dist/reducer.d.ts.map +1 -1
  47. package/dist/reducer.js +18 -2
  48. package/dist/session.svelte.d.ts +46 -0
  49. package/dist/session.svelte.d.ts.map +1 -1
  50. package/dist/session.svelte.js +208 -13
  51. package/dist/shortcuts.d.ts +7 -0
  52. package/dist/shortcuts.d.ts.map +1 -1
  53. package/dist/shortcuts.js +9 -0
  54. package/dist/surface-geometry.d.ts +56 -0
  55. package/dist/surface-geometry.d.ts.map +1 -0
  56. package/dist/surface-geometry.js +99 -0
  57. package/dist/surface.svelte.d.ts +69 -0
  58. package/dist/surface.svelte.d.ts.map +1 -0
  59. package/dist/surface.svelte.js +156 -0
  60. package/dist/ui-frame-protocol.d.ts +6 -0
  61. package/dist/ui-frame-protocol.d.ts.map +1 -1
  62. package/dist/ui-frame-protocol.js +11 -0
  63. package/package.json +8 -7
  64. package/dist/artifact-pages.test.d.ts +0 -2
  65. package/dist/artifact-pages.test.d.ts.map +0 -1
  66. package/dist/artifact-pages.test.js +0 -102
  67. package/dist/components/a11y.svelte.test.d.ts +0 -2
  68. package/dist/components/a11y.svelte.test.d.ts.map +0 -1
  69. package/dist/components/a11y.svelte.test.js +0 -183
  70. package/dist/components/agent-picker.svelte.test.d.ts +0 -2
  71. package/dist/components/agent-picker.svelte.test.d.ts.map +0 -1
  72. package/dist/components/agent-picker.svelte.test.js +0 -84
  73. package/dist/components/composer.svelte.test.d.ts +0 -2
  74. package/dist/components/composer.svelte.test.d.ts.map +0 -1
  75. package/dist/components/composer.svelte.test.js +0 -162
  76. package/dist/components/conversation-chrome.svelte.test.d.ts +0 -2
  77. package/dist/components/conversation-chrome.svelte.test.d.ts.map +0 -1
  78. package/dist/components/conversation-chrome.svelte.test.js +0 -147
  79. package/dist/components/feedback.svelte.test.d.ts +0 -2
  80. package/dist/components/feedback.svelte.test.d.ts.map +0 -1
  81. package/dist/components/feedback.svelte.test.js +0 -99
  82. package/dist/components/live-fixes.svelte.test.d.ts +0 -2
  83. package/dist/components/live-fixes.svelte.test.d.ts.map +0 -1
  84. package/dist/components/live-fixes.svelte.test.js +0 -127
  85. package/dist/components/markdown-prose.svelte.test.d.ts +0 -2
  86. package/dist/components/markdown-prose.svelte.test.d.ts.map +0 -1
  87. package/dist/components/markdown-prose.svelte.test.js +0 -26
  88. package/dist/components/messages.svelte.test.d.ts +0 -2
  89. package/dist/components/messages.svelte.test.d.ts.map +0 -1
  90. package/dist/components/messages.svelte.test.js +0 -202
  91. package/dist/components/regenerate.svelte.test.d.ts +0 -2
  92. package/dist/components/regenerate.svelte.test.d.ts.map +0 -1
  93. package/dist/components/regenerate.svelte.test.js +0 -108
  94. package/dist/components/welcome.svelte.test.d.ts +0 -2
  95. package/dist/components/welcome.svelte.test.d.ts.map +0 -1
  96. package/dist/components/welcome.svelte.test.js +0 -119
  97. package/dist/composer-keys.test.d.ts +0 -2
  98. package/dist/composer-keys.test.d.ts.map +0 -1
  99. package/dist/composer-keys.test.js +0 -16
  100. package/dist/markdown.test.d.ts +0 -2
  101. package/dist/markdown.test.d.ts.map +0 -1
  102. package/dist/markdown.test.js +0 -35
  103. package/dist/reducer.test.d.ts +0 -2
  104. package/dist/reducer.test.d.ts.map +0 -1
  105. package/dist/reducer.test.js +0 -153
  106. package/dist/ui-frame-protocol.test.d.ts +0 -2
  107. package/dist/ui-frame-protocol.test.d.ts.map +0 -1
  108. package/dist/ui-frame-protocol.test.js +0 -48
  109. package/dist/ui-frame-relay.test.d.ts +0 -2
  110. package/dist/ui-frame-relay.test.d.ts.map +0 -1
  111. package/dist/ui-frame-relay.test.js +0 -36
package/README.md CHANGED
@@ -5,7 +5,7 @@ artifacts and MCP-UI sandbox frames. The Classic App's `/agent` page and agent p
5
5
 
6
6
  ## Requirements
7
7
 
8
- - `svelte` 5 and `@classic-homes/theme-svelte` 1.39 or later (peer dependencies).
8
+ - `svelte` `^5.31.0` and `@classic-homes/theme-svelte` `^1.42.2` (peer dependencies).
9
9
  - Tailwind with `@classic-homes/theme-tailwind-preset`. The components are styled with Tailwind
10
10
  utility classes and theme tokens, so the consumer's Tailwind build has to scan this package:
11
11
 
@@ -38,13 +38,68 @@ artifacts and MCP-UI sandbox frames. The Classic App's `/agent` page and agent p
38
38
  `client` is an `AgentApiClient` from `@classic-homes/agent-client`. `sandboxOrigin` is the
39
39
  agent-api origin that serves `/frame`; artifacts and MCP-UI resources only ever render there.
40
40
 
41
+ ### `conversationId`
42
+
43
+ - `null` (or absent): a new chat. When the first message starts a conversation,
44
+ `onconversationstarted(id)` reports its id; the host may pass that id back as
45
+ `conversationId` (the chat stays mounted and does not reload) or keep passing `null` (the
46
+ chat carries on in that conversation).
47
+ - Another id: the chat loads that transcript, re-attaching to a turn still running there. A
48
+ stream of the conversation shown before is dropped (not cancelled on the server; it finishes
49
+ there) and does not fire `onturnend`. A load that fails shows Try again; it is not retried
50
+ automatically for the same id.
51
+ - From an id back to `null` (the host's "New chat"): the transcript and the conversation are
52
+ cleared, so the next message starts a new conversation.
53
+
54
+ `ChatSession` (used by `AgentChat`, exported for other layouts) has the matching methods:
55
+ `load(id)`, `reset()` (forget the conversation and drop a running stream without a server
56
+ cancel), `send`, `cancel` (asks the runtime to stop and keeps reading until it confirms, at
57
+ most `cancelGraceMs`, default 3 s, so the text up to the stop is kept), `retry`, `regenerate`,
58
+ `edit` and `continue(requestId)`.
59
+
60
+ ### Continue
61
+
62
+ An answer cut short by the output limit (`finishReason: 'length'`) or stopped by the user
63
+ (`'cancelled'`) shows Continue: `ChatSession.continue` asks the runtime to carry on and streams
64
+ the rest into the same message. A connection that drops mid-answer is re-attached
65
+ automatically (twice); if that fails the answer shows a retryable error (Try again), not
66
+ Continue. When a continuation itself fails, the text so far stays and Continue is offered again.
67
+ Continue needs a runtime that supports `ChatRequest.continues`; an older one ignores it.
68
+
69
+ ### Files
70
+
71
+ The file endpoint needs the bearer, which `<img src>` and download links cannot send. `AgentChat`
72
+ loads produced files with `client.fetchFile` (blob URLs, revoked when the message goes away);
73
+ `MessageFiles` takes a `loadFile(fileId): Promise<Blob>` prop and links to `fileUrl` without one
74
+ (only for cookie-authenticated hosts).
75
+
76
+ ### Detached chat (launcher, floating window, dock)
77
+
78
+ A host can show the chat over its pages instead of on a route of its own:
79
+
80
+ - `createChatSurface({ storageKey })` holds which presentation is showing (`launcher`, `float`
81
+ or `dock`), the launcher's corner, the window's geometry and an `unread` flag; it persists to
82
+ `localStorage` under `storageKey` and keeps the window inside the viewport.
83
+ - `ChatLauncher` is the floating button (drag to another corner; shows answering, unseen answer,
84
+ error and a MOCK marker). `FloatingWindow` is a non-modal window around any content: drag by
85
+ the header, resize from the edges, drop on the right edge to dock, arrow keys on the move
86
+ handle, Escape to minimize. The dock itself is the host's (e.g. a theme `DataPanel`).
87
+ - Pass `AgentChat` a host-owned `session` (`new ChatSession({...})`) and `variant="float"` or
88
+ `"panel"`. The session then survives the view moving between presentations, including a
89
+ running answer. With `session` set, configure `mode`, `agent`, `clientKind`, `getContext` and
90
+ the turn callbacks on the session; those `AgentChat` props are ignored. Call
91
+ `surface.notifyTurnEnd()` from the session's `onTurnEnd` to flag an answer that finished while
92
+ collapsed.
93
+
41
94
  ## Components
42
95
 
43
96
  `AgentChat` composes the rest; each is also exported for other layouts:
44
97
 
45
98
  - `ChatHeader`, `ConversationSidebar`: title and conversation actions; the history rail.
46
99
  `ChatHeader`'s menu opens `ShortcutsDialog` (the chat's `CHAT_SHORTCUTS` plus the host's
47
- `shortcuts`, e.g. a panel toggle). Hosts implement their own app-level shortcuts.
100
+ `shortcuts`, e.g. a panel toggle). Hosts implement their own app-level shortcuts. Pass
101
+ `savedPrompts={false}` to `ChatHeader` when the chat has no `prompts`, so the `/` entry is
102
+ left out.
48
103
  - `MessageList`: transcript with day separators and a jump-to-latest button.
49
104
  - `MessageItem` → `UserMessage` / `AssistantMessage`, with `MessageActions` (copy, time,
50
105
  `MessageFeedback` thumbs up/down with an optional note, Regenerate and Edit on the latest turn),
@@ -32,6 +32,14 @@
32
32
 
33
33
  interface Props {
34
34
  client: AgentApiClient;
35
+ /**
36
+ * A session the host owns, so it survives this component being unmounted and mounted
37
+ * elsewhere (launcher, floating window, dock) without dropping a running answer. The host
38
+ * then configures it itself: `mode`, `agent`, `clientKind`, `getContext`,
39
+ * `onconversationstarted`, `ontoolresult` and `onturnend` here are ignored. Without it the
40
+ * component creates its own, as before.
41
+ */
42
+ session?: ChatSession;
35
43
  /** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
36
44
  sandboxOrigin: string;
37
45
  conversationId?: string | null;
@@ -47,7 +55,8 @@
47
55
  /** The agent of this conversation changed (picked, or learned from a loaded conversation). */
48
56
  onagentchange?: (agent: AgentKey) => void;
49
57
  clientKind?: ClientKind;
50
- variant?: 'page' | 'panel';
58
+ /** `float` is the detached window: laid out like the panel. */
59
+ variant?: 'page' | 'panel' | 'float';
51
60
  getContext?: () => DocumentContext | null | undefined;
52
61
  onconversationstarted?: (conversationId: string) => void;
53
62
  ontoolresult?: (name: string, status: string) => void;
@@ -83,6 +92,7 @@
83
92
  }
84
93
  let {
85
94
  client,
95
+ session: hostSession,
86
96
  sandboxOrigin,
87
97
  conversationId = null,
88
98
  mode,
@@ -111,29 +121,54 @@
111
121
  class: className,
112
122
  }: Props = $props();
113
123
 
114
- const session = new ChatSession({
115
- client,
116
- conversationId,
117
- mode,
118
- agent,
119
- clientKind,
120
- getContext,
121
- onConversationStarted: (id) => onconversationstarted?.(id),
122
- onToolResult: (name, status) => ontoolresult?.(name, status),
123
- onTurnEnd: (info) => onturnend?.(info),
124
- // Open the pane when the runtime starts an artifact, not by watching state later: a fast
125
- // stream can open and close it before any effect sees `open`.
126
- onEvent: (event) => {
127
- if (event.type === 'artifact' && event.op === 'open') {
124
+ // One session for the component's lifetime: the host's, or our own.
125
+ // svelte-ignore state_referenced_locally
126
+ const session =
127
+ hostSession ??
128
+ new ChatSession({
129
+ client,
130
+ conversationId,
131
+ mode,
132
+ agent,
133
+ clientKind,
134
+ getContext,
135
+ onConversationStarted: (id) => onconversationstarted?.(id),
136
+ onToolResult: (name, status) => ontoolresult?.(name, status),
137
+ onTurnEnd: (info) => onturnend?.(info),
138
+ });
139
+ // Open the pane when the runtime starts an artifact, not by watching state later: a fast
140
+ // stream can open and close it before any effect sees `open`. Only beside the chat: a sheet
141
+ // (narrow screens, the panel, the floating window) would cover the answer mid-stream, so the
142
+ // chip opens it there.
143
+ $effect(() =>
144
+ session.subscribe((event) => {
145
+ if (event.type === 'artifact' && event.op === 'open' && sidePane) {
128
146
  selectedArtifact = event.artifactId;
129
147
  artifactsOpen = true;
130
148
  }
131
- },
132
- });
149
+ }),
150
+ );
151
+ const compact = $derived(variant !== 'page');
133
152
  let attemptedId: string | null = null;
153
+ /** The prop as last seen, to tell the host's "New chat" (id → null) from a chat started here. */
154
+ let previousId: string | null = null;
134
155
  // The constructor already knows `conversationId`; what decides a fetch is whether the
135
- // transcript for it has been loaded (or started) in this session.
156
+ // transcript for it has been loaded (or started) in this session. `load` stops a stream of
157
+ // the conversation shown before.
136
158
  $effect(() => {
159
+ const before = previousId;
160
+ previousId = conversationId;
161
+ if (!conversationId) {
162
+ // The host left a conversation for a new chat: forget it, or the next message would go
163
+ // to it. A chat started here while the prop stayed null keeps going (null → null).
164
+ if (before && session.conversationId) {
165
+ session.reset();
166
+ attemptedId = null;
167
+ selectedArtifact = null;
168
+ artifactsOpen = false;
169
+ }
170
+ return;
171
+ }
137
172
  if (
138
173
  conversationId &&
139
174
  conversationId !== session.loadedConversationId &&
@@ -146,7 +181,7 @@
146
181
  }
147
182
  });
148
183
  $effect(() => {
149
- if (mode && !session.mode) session.mode = mode;
184
+ if (mode && !hostSession && !session.mode) session.mode = mode;
150
185
  });
151
186
 
152
187
  let composer: Composer | undefined = $state();
@@ -174,7 +209,7 @@
174
209
  }
175
210
  function retry(): void {
176
211
  const last = session.messages[session.messages.length - 1];
177
- if (last?.role === 'assistant') void session.retry(last.requestId);
212
+ if (last?.role === 'assistant') thenFocus(session.retry(last.requestId));
178
213
  }
179
214
  const agentName = $derived(agents?.find((a) => a.key === session.agent)?.name);
180
215
  const pickAgent = $derived(
@@ -259,7 +294,7 @@
259
294
  title={welcomeTitle}
260
295
  description={welcomeDescription}
261
296
  {suggestions}
262
- compact={variant === 'panel'}
297
+ {compact}
263
298
  disabled={session.status !== 'idle'}
264
299
  onselect={(text) => thenFocus(send(text))}
265
300
  />
@@ -292,6 +327,7 @@
292
327
  streaming={session.streaming}
293
328
  {sandboxOrigin}
294
329
  fileUrl={(id) => client.fileUrl(id)}
330
+ loadFile={client.fetchFile ? (id) => client.fetchFile!(id) : undefined}
295
331
  {toolLabels}
296
332
  {onnavigate}
297
333
  onopenartifact={(id) => {
@@ -310,7 +346,10 @@
310
346
  .rate(requestId, rating, comment)
311
347
  .catch(() => onnotify?.('Could not save your feedback. Try again.'))
312
348
  : undefined}
313
- oncontinue={() => void session.send('Continue.')}
349
+ oncontinue={() => {
350
+ const answer = session.current;
351
+ if (answer) thenFocus(session.continue(answer.requestId));
352
+ }}
314
353
  empty={emptyState}
315
354
  />
316
355
  <Composer
@@ -2,9 +2,18 @@ import type { AgentApiClient } from '@classic-homes/agent-client';
2
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
+ import { ChatSession } from '../session.svelte.js';
5
6
  import type { WelcomeSuggestion } from '../welcome.js';
6
7
  interface Props {
7
8
  client: AgentApiClient;
9
+ /**
10
+ * A session the host owns, so it survives this component being unmounted and mounted
11
+ * elsewhere (launcher, floating window, dock) without dropping a running answer. The host
12
+ * then configures it itself: `mode`, `agent`, `clientKind`, `getContext`,
13
+ * `onconversationstarted`, `ontoolresult` and `onturnend` here are ignored. Without it the
14
+ * component creates its own, as before.
15
+ */
16
+ session?: ChatSession;
8
17
  /** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
9
18
  sandboxOrigin: string;
10
19
  conversationId?: string | null;
@@ -20,7 +29,8 @@ interface Props {
20
29
  /** The agent of this conversation changed (picked, or learned from a loaded conversation). */
21
30
  onagentchange?: (agent: AgentKey) => void;
22
31
  clientKind?: ClientKind;
23
- variant?: 'page' | 'panel';
32
+ /** `float` is the detached window: laid out like the panel. */
33
+ variant?: 'page' | 'panel' | 'float';
24
34
  getContext?: () => DocumentContext | null | undefined;
25
35
  onconversationstarted?: (conversationId: string) => void;
26
36
  ontoolresult?: (name: string, status: string) => void;
@@ -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,YAAY,EACZ,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAclE,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;AAkPH,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":"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"}
@@ -115,8 +115,10 @@
115
115
  <div class="min-h-0 flex-1 overflow-auto p-3">
116
116
  {#if selected.mediaType === 'text/markdown'}
117
117
  <Markdown html={markdownHtml} />
118
- {:else if selected.mediaType === 'application/vnd.ant.code' || (selected.mediaType === 'application/vnd.ant.mermaid' && selected.open)}
119
- <!-- Mermaid renders once the artifact is complete; a partial diagram is a parse error. -->
118
+ {:else if selected.mediaType === 'application/vnd.ant.code' || selected.open}
119
+ <!-- Framed artifacts (html, svg, react, mermaid) render once complete: the source shows
120
+ while it streams, so the sandbox is not reloaded on every delta (and a partial
121
+ diagram is a parse error). -->
120
122
  <pre class="overflow-auto whitespace-pre font-mono text-[13px] leading-relaxed"><code
121
123
  >{selected.text}</code
122
124
  ></pre>
@@ -7,7 +7,7 @@
7
7
  import { Alert, Button, Icon } from '@classic-homes/theme-svelte';
8
8
  import type { TurnRating } from '@classic-homes/agent-contracts';
9
9
  import type { AssistantMessage } from '../reducer.js';
10
- import { errorTitle } from '../reducer.js';
10
+ import { errorTitle, isContinuable } from '../reducer.js';
11
11
  import { parseMarkdown } from '../markdown.js';
12
12
  import ArtifactChip from './ArtifactChip.svelte';
13
13
  import Markdown from './Markdown.svelte';
@@ -24,6 +24,8 @@
24
24
  last?: boolean;
25
25
  sandboxOrigin: string;
26
26
  fileUrl: (fileId: string) => string;
27
+ /** Fetch a file with the host's credentials (blob URLs); without it files link to `fileUrl`. */
28
+ loadFile?: (fileId: string) => Promise<Blob>;
27
29
  toolLabels?: Record<string, string>;
28
30
  onnavigate?: (href: string) => void;
29
31
  onopenartifact?: (artifactId: string) => void;
@@ -44,6 +46,7 @@
44
46
  last = false,
45
47
  sandboxOrigin,
46
48
  fileUrl,
49
+ loadFile,
47
50
  toolLabels,
48
51
  onnavigate,
49
52
  onopenartifact,
@@ -98,7 +101,9 @@
98
101
  !message.tools.some((t) => !t.status) &&
99
102
  !message.artifacts.some((a) => a.open),
100
103
  );
101
- const canContinue = $derived(last && !!oncontinue);
104
+ // Continue carries on the text so far of an answer cut short or stopped; a reply stopped before
105
+ // any text, or one that failed or lost its connection, is retried instead.
106
+ const canContinue = $derived(last && !!oncontinue && isContinuable(message));
102
107
  </script>
103
108
 
104
109
  <div class="group/message flex gap-3 px-4 py-2" data-role="assistant" data-status={message.status}>
@@ -142,7 +147,7 @@
142
147
  {#each message.ui as resource (resource.callId + resource.uri)}
143
148
  <UiFrame {resource} {sandboxOrigin} {onprompt} {ontool} {onnotify} />
144
149
  {/each}
145
- {#if message.files.length}<MessageFiles files={message.files} {fileUrl} />{/if}
150
+ {#if message.files.length}<MessageFiles files={message.files} {fileUrl} {loadFile} />{/if}
146
151
  {#if message.proposals.length}
147
152
  <div class="flex flex-col gap-1.5">
148
153
  {#each message.proposals as operation (operation.operationId)}
@@ -157,7 +162,10 @@
157
162
  <strong class="font-semibold">{errorTitle(message.error.code)}.</strong>
158
163
  {message.error.message}
159
164
  </p>
160
- {#if message.error.retryable && onretry}
165
+ {#if canContinue}
166
+ <!-- A continuation failed: the text so far is kept and can still be continued. -->
167
+ <Button variant="outline" size="sm" onclick={oncontinue}>Continue</Button>
168
+ {:else if message.error.retryable && onretry}
161
169
  <Button variant="outline" size="sm" onclick={onretry}>
162
170
  <Icon name="refresh" size={14} /> Try again
163
171
  </Button>
@@ -169,6 +177,8 @@
169
177
  <Icon name="stop" size={14} /> Stopped
170
178
  {#if canContinue}
171
179
  <Button variant="link" size="sm" class="h-auto p-0" onclick={oncontinue}>Continue</Button>
180
+ {:else if last && !message.text && onretry}
181
+ <Button variant="link" size="sm" class="h-auto p-0" onclick={onretry}>Try again</Button>
172
182
  {/if}
173
183
  </div>
174
184
  {:else if message.status === 'incomplete'}
@@ -7,6 +7,8 @@ interface Props {
7
7
  last?: boolean;
8
8
  sandboxOrigin: string;
9
9
  fileUrl: (fileId: string) => string;
10
+ /** Fetch a file with the host's credentials (blob URLs); without it files link to `fileUrl`. */
11
+ loadFile?: (fileId: string) => Promise<Blob>;
10
12
  toolLabels?: Record<string, string>;
11
13
  onnavigate?: (href: string) => void;
12
14
  onopenartifact?: (artifactId: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AssistantMessage.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAYpD,UAAU,KAAK;IACb,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,iDAAiD;IACjD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AA2JH;;;GAGG;AACH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"AssistantMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AssistantMessage.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAYpD,UAAU,KAAK;IACb,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,gGAAgG;IAChG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,iDAAiD;IACjD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAmKH;;;GAGG;AACH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -34,6 +34,8 @@
34
34
  onerror?: (message: string) => void;
35
35
  /** Host shortcuts for the "Keyboard shortcuts" dialog, e.g. opening the panel. */
36
36
  shortcuts?: ChatShortcut[];
37
+ /** Whether the chat offers saved prompts (`AgentChat`'s `prompts`); false hides the `/` shortcut. */
38
+ savedPrompts?: boolean;
37
39
  /** Extra controls before the menu. */
38
40
  actions?: Snippet;
39
41
  class?: string;
@@ -49,6 +51,7 @@
49
51
  ondelete,
50
52
  onerror,
51
53
  shortcuts,
54
+ savedPrompts = true,
52
55
  actions,
53
56
  class: className,
54
57
  }: Props = $props();
@@ -131,4 +134,4 @@
131
134
  </header>
132
135
 
133
136
  <ConversationDialogs bind:renaming bind:deleting {onrename} {ondelete} {onerror} {returnFocus} />
134
- <ShortcutsDialog bind:open={showingShortcuts} {shortcuts} {returnFocus} />
137
+ <ShortcutsDialog bind:open={showingShortcuts} {shortcuts} {returnFocus} {savedPrompts} />
@@ -15,6 +15,8 @@ interface Props {
15
15
  onerror?: (message: string) => void;
16
16
  /** Host shortcuts for the "Keyboard shortcuts" dialog, e.g. opening the panel. */
17
17
  shortcuts?: ChatShortcut[];
18
+ /** Whether the chat offers saved prompts (`AgentChat`'s `prompts`); false hides the `/` shortcut. */
19
+ savedPrompts?: boolean;
18
20
  /** Extra controls before the menu. */
19
21
  actions?: Snippet;
20
22
  class?: string;
@@ -1 +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;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKlD,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,kFAAkF;IAClF,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,sCAAsC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuFH;;;;;GAKG;AACH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
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;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKlD,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,kFAAkF;IAClF,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,qGAAqG;IACrG,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sCAAsC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAwFH;;;;;GAKG;AACH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The floating launcher: a round button pinned to a viewport corner that opens the chat. The
4
+ * user can drag it; on release it snaps to the nearest corner. It also reports what the chat
5
+ * is doing while collapsed (answering, an unseen answer, an error) and keeps the mock backend
6
+ * visible (AGENTS.md: mock must always be visible).
7
+ */
8
+ import { cn, Icon } from '@classic-homes/theme-svelte';
9
+ import type { LauncherStatus } from '../surface.svelte.js';
10
+ import { DRAG_THRESHOLD, nearestCorner, type Corner, type Point } from '../surface-geometry.js';
11
+
12
+ interface Props {
13
+ corner?: Corner;
14
+ status?: LauncherStatus;
15
+ /** The backend is the mock: show the marker on the button. */
16
+ mock?: boolean;
17
+ label?: string;
18
+ /** Id of the surface this button opens, for `aria-controls`. */
19
+ controls?: string;
20
+ expanded?: boolean;
21
+ draggable?: boolean;
22
+ onclick?: () => void;
23
+ /** The user dropped the button in another corner. */
24
+ oncornerchange?: (corner: Corner) => void;
25
+ class?: string;
26
+ }
27
+ let {
28
+ corner = 'bottom-right',
29
+ status = 'idle',
30
+ mock = false,
31
+ label = 'Classic Agent',
32
+ controls,
33
+ expanded = false,
34
+ draggable = true,
35
+ onclick,
36
+ oncornerchange,
37
+ class: className,
38
+ }: Props = $props();
39
+
40
+ let button: HTMLButtonElement | undefined = $state();
41
+ export function focus(): void {
42
+ button?.focus();
43
+ }
44
+
45
+ const STATUS_TEXT: Record<LauncherStatus, string> = {
46
+ idle: '',
47
+ streaming: 'answering',
48
+ unread: 'new answer',
49
+ error: 'something went wrong',
50
+ };
51
+ const name = $derived(
52
+ [label, STATUS_TEXT[status], mock ? 'mock backend' : ''].filter(Boolean).join(', '),
53
+ );
54
+
55
+ const CORNER_CLASS: Record<Corner, string> = {
56
+ 'bottom-right': 'bottom-5 right-5',
57
+ 'bottom-left': 'bottom-5 left-5',
58
+ 'top-right': 'top-5 right-5',
59
+ 'top-left': 'top-5 left-5',
60
+ };
61
+
62
+ let start: Point | null = null;
63
+ let offset = $state<Point>({ x: 0, y: 0 });
64
+ let dragging = $state(false);
65
+ // A drag ends in a click event on the same button; that click must not open the chat.
66
+ let suppressClick = false;
67
+
68
+ function onpointerdown(event: PointerEvent): void {
69
+ if (!draggable || event.button !== 0) return;
70
+ start = { x: event.clientX, y: event.clientY };
71
+ button?.setPointerCapture?.(event.pointerId);
72
+ }
73
+ function onpointermove(event: PointerEvent): void {
74
+ if (!start) return;
75
+ const dx = event.clientX - start.x;
76
+ const dy = event.clientY - start.y;
77
+ if (!dragging && Math.abs(dx) <= DRAG_THRESHOLD && Math.abs(dy) <= DRAG_THRESHOLD) return;
78
+ dragging = true;
79
+ offset = { x: dx, y: dy };
80
+ }
81
+ function onpointerup(event: PointerEvent): void {
82
+ if (!start) return;
83
+ start = null;
84
+ button?.releasePointerCapture?.(event.pointerId);
85
+ if (!dragging) return;
86
+ dragging = false;
87
+ suppressClick = true;
88
+ offset = { x: 0, y: 0 };
89
+ const next = nearestCorner(
90
+ { x: event.clientX, y: event.clientY },
91
+ { width: window.innerWidth, height: window.innerHeight },
92
+ );
93
+ if (next !== corner) oncornerchange?.(next);
94
+ }
95
+ function onpointercancel(): void {
96
+ start = null;
97
+ dragging = false;
98
+ offset = { x: 0, y: 0 };
99
+ }
100
+ function handleClick(): void {
101
+ if (suppressClick) {
102
+ suppressClick = false;
103
+ return;
104
+ }
105
+ onclick?.();
106
+ }
107
+ </script>
108
+
109
+ <button
110
+ bind:this={button}
111
+ type="button"
112
+ class={cn(
113
+ 'fixed z-40 flex size-14 touch-none select-none items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg outline-none ring-offset-background transition-shadow hover:shadow-xl focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
114
+ CORNER_CLASS[corner],
115
+ dragging ? 'cursor-grabbing opacity-80' : 'cursor-pointer',
116
+ className,
117
+ )}
118
+ style:transform={dragging ? `translate(${offset.x}px, ${offset.y}px)` : undefined}
119
+ aria-label={name}
120
+ aria-expanded={expanded}
121
+ aria-controls={controls}
122
+ data-agent-launcher
123
+ data-corner={corner}
124
+ data-status={status}
125
+ onclick={handleClick}
126
+ {onpointerdown}
127
+ {onpointermove}
128
+ {onpointerup}
129
+ {onpointercancel}
130
+ >
131
+ {#if status === 'streaming'}
132
+ <span
133
+ class="pointer-events-none absolute inset-0 animate-ping rounded-full bg-primary opacity-30 motion-reduce:hidden"
134
+ aria-hidden="true"
135
+ ></span>
136
+ {/if}
137
+ <Icon name="sparkles" size={24} />
138
+ {#if status === 'unread' || status === 'error'}
139
+ <span
140
+ class={cn(
141
+ 'pointer-events-none absolute right-0.5 top-0.5 size-3.5 rounded-full border-2 border-background',
142
+ status === 'error' ? 'bg-destructive' : 'bg-success',
143
+ )}
144
+ aria-hidden="true"
145
+ data-testid="launcher-dot"
146
+ ></span>
147
+ {/if}
148
+ {#if mock}
149
+ <span
150
+ class="pointer-events-none absolute -bottom-1 left-1/2 -translate-x-1/2 rounded bg-warning px-1 text-[10px] font-bold leading-4 tracking-wide text-warning-foreground"
151
+ aria-hidden="true"
152
+ data-testid="launcher-mock"
153
+ >
154
+ MOCK
155
+ </span>
156
+ {/if}
157
+ </button>
@@ -0,0 +1,23 @@
1
+ import type { LauncherStatus } from '../surface.svelte.js';
2
+ import { type Corner } from '../surface-geometry.js';
3
+ interface Props {
4
+ corner?: Corner;
5
+ status?: LauncherStatus;
6
+ /** The backend is the mock: show the marker on the button. */
7
+ mock?: boolean;
8
+ label?: string;
9
+ /** Id of the surface this button opens, for `aria-controls`. */
10
+ controls?: string;
11
+ expanded?: boolean;
12
+ draggable?: boolean;
13
+ onclick?: () => void;
14
+ /** The user dropped the button in another corner. */
15
+ oncornerchange?: (corner: Corner) => void;
16
+ class?: string;
17
+ }
18
+ declare const ChatLauncher: import("svelte").Component<Props, {
19
+ focus: () => void;
20
+ }, "">;
21
+ type ChatLauncher = ReturnType<typeof ChatLauncher>;
22
+ export default ChatLauncher;
23
+ //# sourceMappingURL=ChatLauncher.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatLauncher.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ChatLauncher.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAiC,KAAK,MAAM,EAAc,MAAM,wBAAwB,CAAC;AAG9F,UAAU,KAAK;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qDAAqD;IACrD,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkHH,QAAA,MAAM,YAAY;iBA7FG,IAAI;MA6FiC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -150,6 +150,8 @@
150
150
  function onpaste(event: ClipboardEvent): void {
151
151
  const pasted = [...(event.clipboardData?.files ?? [])];
152
152
  if (!upload || pasted.length === 0) return;
153
+ // Excel and Word put both text and a picture of the cells on the clipboard: paste the text.
154
+ if (event.clipboardData?.getData?.('text/plain')) return;
153
155
  // Pasting a screenshot or a copied file attaches it; plain text pastes as usual.
154
156
  event.preventDefault();
155
157
  void addFiles(pasted);
@@ -281,9 +283,11 @@
281
283
  class={cn(
282
284
  'text-xs tabular-nums text-muted-foreground',
283
285
  chars >= LIMITS.messageChars && 'text-destructive',
284
- )}
285
- aria-live="polite"
286
- >{chars.toLocaleString()} / {LIMITS.messageChars.toLocaleString()}</span
286
+ )}>{chars.toLocaleString()} / {LIMITS.messageChars.toLocaleString()}</span
287
+ >
288
+ <!-- Announced once when the limit is crossed, not on every keystroke. -->
289
+ <span class="sr-only" aria-live="polite"
290
+ >{chars > LIMITS.messageChars ? 'Message is over the length limit' : ''}</span
287
291
  >
288
292
  {/if}
289
293
  {#if streaming}
@@ -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;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"}
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;AAiQH;;;;GAIG;AACH,QAAA,MAAM,QAAQ;iBApOO,IAAI;qBAoO6B,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}