@classic-homes/agent-chat 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +40 -0
  2. package/dist/body-lock.d.ts +2 -0
  3. package/dist/body-lock.d.ts.map +1 -0
  4. package/dist/body-lock.js +27 -0
  5. package/dist/components/AgentChat.svelte +85 -47
  6. package/dist/components/AgentChat.svelte.d.ts +8 -0
  7. package/dist/components/AgentChat.svelte.d.ts.map +1 -1
  8. package/dist/components/ArtifactPanel.svelte +79 -96
  9. package/dist/components/ArtifactPanel.svelte.d.ts +2 -0
  10. package/dist/components/ArtifactPanel.svelte.d.ts.map +1 -1
  11. package/dist/components/ChatHeader.svelte +122 -0
  12. package/dist/components/ChatHeader.svelte.d.ts +27 -0
  13. package/dist/components/ChatHeader.svelte.d.ts.map +1 -0
  14. package/dist/components/Composer.svelte +78 -121
  15. package/dist/components/Composer.svelte.d.ts.map +1 -1
  16. package/dist/components/ConversationDialogs.svelte +154 -0
  17. package/dist/components/ConversationDialogs.svelte.d.ts +25 -0
  18. package/dist/components/ConversationDialogs.svelte.d.ts.map +1 -0
  19. package/dist/components/ConversationSidebar.svelte +135 -114
  20. package/dist/components/ConversationSidebar.svelte.d.ts +8 -2
  21. package/dist/components/ConversationSidebar.svelte.d.ts.map +1 -1
  22. package/dist/components/Markdown.svelte +49 -0
  23. package/dist/components/Markdown.svelte.d.ts +17 -0
  24. package/dist/components/Markdown.svelte.d.ts.map +1 -0
  25. package/dist/components/MessageItem.svelte +27 -116
  26. package/dist/components/MessageItem.svelte.d.ts.map +1 -1
  27. package/dist/components/MessageList.svelte +23 -34
  28. package/dist/components/ModeBanner.svelte +7 -13
  29. package/dist/components/ModeBanner.svelte.d.ts.map +1 -1
  30. package/dist/components/SandboxFrame.svelte +43 -0
  31. package/dist/components/SandboxFrame.svelte.d.ts +13 -0
  32. package/dist/components/SandboxFrame.svelte.d.ts.map +1 -0
  33. package/dist/components/ToolTimeline.svelte +17 -68
  34. package/dist/components/ToolTimeline.svelte.d.ts.map +1 -1
  35. package/dist/components/UiFrame.svelte +9 -55
  36. package/dist/components/UiFrame.svelte.d.ts.map +1 -1
  37. package/dist/components/conversation-chrome.svelte.test.d.ts +2 -0
  38. package/dist/components/conversation-chrome.svelte.test.d.ts.map +1 -0
  39. package/dist/components/conversation-chrome.svelte.test.js +127 -0
  40. package/dist/components/live-fixes.svelte.test.d.ts +2 -0
  41. package/dist/components/live-fixes.svelte.test.d.ts.map +1 -0
  42. package/dist/components/live-fixes.svelte.test.js +127 -0
  43. package/dist/components/markdown-prose.svelte.test.d.ts +2 -0
  44. package/dist/components/markdown-prose.svelte.test.d.ts.map +1 -0
  45. package/dist/components/markdown-prose.svelte.test.js +26 -0
  46. package/dist/composer-keys.d.ts +11 -0
  47. package/dist/composer-keys.d.ts.map +1 -0
  48. package/dist/composer-keys.js +10 -0
  49. package/dist/composer-keys.test.d.ts +2 -0
  50. package/dist/composer-keys.test.d.ts.map +1 -0
  51. package/dist/composer-keys.test.js +16 -0
  52. package/dist/index.d.ts +3 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +3 -0
  55. package/dist/reducer.d.ts +10 -0
  56. package/dist/reducer.d.ts.map +1 -1
  57. package/dist/reducer.js +19 -1
  58. package/dist/reducer.test.js +29 -1
  59. package/dist/session.svelte.d.ts +15 -0
  60. package/dist/session.svelte.d.ts.map +1 -1
  61. package/dist/session.svelte.js +21 -1
  62. package/dist/ui-frame-protocol.d.ts +2 -2
  63. package/dist/ui-frame-protocol.js +2 -2
  64. package/dist/ui-frame-relay.d.ts +16 -0
  65. package/dist/ui-frame-relay.d.ts.map +1 -0
  66. package/dist/ui-frame-relay.js +22 -0
  67. package/dist/ui-frame-relay.test.d.ts +2 -0
  68. package/dist/ui-frame-relay.test.d.ts.map +1 -0
  69. package/dist/ui-frame-relay.test.js +36 -0
  70. package/package.json +8 -5
@@ -0,0 +1,26 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { cleanup, render } from '@testing-library/svelte';
3
+ import { parseMarkdown } from '../markdown.js';
4
+ import { errorTitle } from '../reducer.js';
5
+ import Markdown from './Markdown.svelte';
6
+ afterEach(cleanup);
7
+ describe('Markdown', () => {
8
+ // The Classic App's cascade-layers polyfill outranks plain component CSS, so the prose rules
9
+ // must be utilities on the container (classic-agent visual check, 2026-11-03).
10
+ it('styles prose with utility classes, not a stylesheet', () => {
11
+ const { container } = render(Markdown, {
12
+ props: { html: parseMarkdown('- one\n- two'), class: 'text-sm' },
13
+ });
14
+ const root = container.firstElementChild;
15
+ expect(root.className).toContain('[&_ul]:list-disc');
16
+ expect(root.className).toContain('[&_td]:border');
17
+ expect(root.className).toContain('text-sm');
18
+ expect(root.querySelectorAll('li')).toHaveLength(2);
19
+ expect(document.querySelectorAll('style')).toHaveLength(0);
20
+ });
21
+ });
22
+ describe('errorTitle', () => {
23
+ it('falls back for a code this client does not know', () => {
24
+ expect(errorTitle('upstream')).toBe('Something went wrong');
25
+ });
26
+ });
@@ -0,0 +1,11 @@
1
+ /** What a keydown in the composer textarea should do. Pure so it can be tested without a DOM. */
2
+ export type ComposerKeyAction = 'send' | 'cancel' | null;
3
+ export interface ComposerKey {
4
+ key: string;
5
+ shiftKey?: boolean;
6
+ isComposing?: boolean;
7
+ /** 229 is what browsers report for keys consumed by an input method editor. */
8
+ keyCode?: number;
9
+ }
10
+ export declare function composerKeyAction(event: ComposerKey, streaming: boolean): ComposerKeyAction;
11
+ //# sourceMappingURL=composer-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer-keys.d.ts","sourceRoot":"","sources":["../src/lib/composer-keys.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAM3F"}
@@ -0,0 +1,10 @@
1
+ export function composerKeyAction(event, streaming) {
2
+ // Enter that confirms an IME candidate (Japanese, Chinese, Korean input) must not send.
3
+ if (event.isComposing || event.keyCode === 229)
4
+ return null;
5
+ if (event.key === 'Escape')
6
+ return streaming ? 'cancel' : null;
7
+ if (event.key === 'Enter' && !event.shiftKey)
8
+ return 'send';
9
+ return null;
10
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=composer-keys.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer-keys.test.d.ts","sourceRoot":"","sources":["../src/lib/composer-keys.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { composerKeyAction } from './composer-keys.js';
3
+ describe('composerKeyAction', () => {
4
+ it('sends on Enter and keeps Shift+Enter for a newline', () => {
5
+ expect(composerKeyAction({ key: 'Enter' }, false)).toBe('send');
6
+ expect(composerKeyAction({ key: 'Enter', shiftKey: true }, false)).toBeNull();
7
+ });
8
+ it('ignores Enter that confirms an IME candidate', () => {
9
+ expect(composerKeyAction({ key: 'Enter', isComposing: true }, false)).toBeNull();
10
+ expect(composerKeyAction({ key: 'Enter', keyCode: 229 }, false)).toBeNull();
11
+ });
12
+ it('stops a running reply on Escape only while streaming', () => {
13
+ expect(composerKeyAction({ key: 'Escape' }, true)).toBe('cancel');
14
+ expect(composerKeyAction({ key: 'Escape' }, false)).toBeNull();
15
+ });
16
+ });
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export * from './body-lock.js';
2
+ export * from './composer-keys.js';
1
3
  export * from './markdown.js';
2
4
  export * from './reducer.js';
3
5
  export * from './session.svelte.js';
@@ -11,4 +13,5 @@ export { default as MessageList } from './components/MessageList.svelte';
11
13
  export { default as ModeBanner } from './components/ModeBanner.svelte';
12
14
  export { default as ToolTimeline } from './components/ToolTimeline.svelte';
13
15
  export { default as UiFrame } from './components/UiFrame.svelte';
16
+ export { default as ChatHeader } from './components/ChatHeader.svelte';
14
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ export * from './body-lock.js';
2
+ export * from './composer-keys.js';
1
3
  export * from './markdown.js';
2
4
  export * from './reducer.js';
3
5
  export * from './session.svelte.js';
@@ -11,3 +13,4 @@ export { default as MessageList } from './components/MessageList.svelte';
11
13
  export { default as ModeBanner } from './components/ModeBanner.svelte';
12
14
  export { default as ToolTimeline } from './components/ToolTimeline.svelte';
13
15
  export { default as UiFrame } from './components/UiFrame.svelte';
16
+ export { default as ChatHeader } from './components/ChatHeader.svelte';
package/dist/reducer.d.ts CHANGED
@@ -90,6 +90,16 @@ export declare function newUserMessage(requestId: string, text: string, options?
90
90
  export declare function describeContext(context: DocumentContext): string;
91
91
  /** Fold one stream event into the assistant message it belongs to. Returns a new object. */
92
92
  export declare function applyEvent(message: AssistantMessage, event: ChatEvent): AssistantMessage;
93
+ /**
94
+ * Prepare a retry of the turn `requestId`: returns the user message to resend and the transcript
95
+ * without that turn (its user message and the failed assistant reply), so the retried turn
96
+ * replaces the failed one instead of appending a duplicate. Only the last turn can be retried,
97
+ * and only when it did not finish normally.
98
+ */
99
+ export declare function turnForRetry(messages: ChatMessage[], requestId: string): {
100
+ user: UserMessage;
101
+ rest: ChatMessage[];
102
+ } | undefined;
93
103
  /** Human titles for client error codes (mirrors the task pane's wording). */
94
104
  export declare function errorTitle(code: ClientErrorCode): string;
95
105
  export type { ClientKind };
@@ -1 +1 @@
1
- {"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/lib/reducer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,UAAU,EACV,eAAe,EACf,OAAO,EACP,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEnE;;;GAGG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,GAAG,KAAK,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,YAAY,CAAC;AAE1F,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,EAAE,EAAE,eAAe,EAAE,CAAC;IACtB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAC/B,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,eAAe,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IACvE,YAAY,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;IACzD,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAEzD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,gBAAgB,CAgBzF;AAED,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,eAAe,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GACjF,WAAW,CASb;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAIhE;AAED,4FAA4F;AAC5F,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,SAAS,GAAG,gBAAgB,CAyIxF;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAiBxD;AAED,YAAY,EAAE,UAAU,EAAE,CAAC"}
1
+ {"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/lib/reducer.ts"],"names":[],"mappings":"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"}
package/dist/reducer.js CHANGED
@@ -165,6 +165,23 @@ export function applyEvent(message, event) {
165
165
  };
166
166
  }
167
167
  }
168
+ /**
169
+ * Prepare a retry of the turn `requestId`: returns the user message to resend and the transcript
170
+ * without that turn (its user message and the failed assistant reply), so the retried turn
171
+ * replaces the failed one instead of appending a duplicate. Only the last turn can be retried,
172
+ * and only when it did not finish normally.
173
+ */
174
+ export function turnForRetry(messages, requestId) {
175
+ const last = messages[messages.length - 1];
176
+ if (last?.role !== 'assistant' || last.requestId !== requestId)
177
+ return undefined;
178
+ if (last.status === 'streaming' || last.status === 'done')
179
+ return undefined;
180
+ const user = messages[messages.length - 2];
181
+ if (user?.role !== 'user')
182
+ return undefined;
183
+ return { user, rest: messages.slice(0, -2) };
184
+ }
168
185
  /** Human titles for client error codes (mirrors the task pane's wording). */
169
186
  export function errorTitle(code) {
170
187
  const titles = {
@@ -182,5 +199,6 @@ export function errorTitle(code) {
182
199
  network: 'Cannot reach Classic Agent',
183
200
  protocol: 'Unexpected response from Classic Agent',
184
201
  };
185
- return titles[code];
202
+ // A newer runtime may send a code this client doesn't know yet.
203
+ return titles[code] ?? 'Something went wrong';
186
204
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { applyEvent, errorTitle, newAssistantMessage, newUserMessage } from './reducer.js';
2
+ import { applyEvent, errorTitle, newAssistantMessage, newUserMessage, turnForRetry, } from './reducer.js';
3
3
  const requestId = '11111111-1111-4111-8111-111111111111';
4
4
  const scope = { requestId, agent: 'classic-agent', runId: 'r1' };
5
5
  const ev = (e, seq) => ({ ...e, seq });
@@ -123,3 +123,31 @@ describe('applyEvent', () => {
123
123
  expect(user.contextLabel).toBe('Lot 12');
124
124
  });
125
125
  });
126
+ describe('turnForRetry', () => {
127
+ const failed = (id, status) => ({
128
+ ...newAssistantMessage(id, 2),
129
+ status,
130
+ });
131
+ const idA = '22222222-2222-4222-8222-222222222222';
132
+ it('drops the failed turn so the retry replaces it', () => {
133
+ const earlier = [newUserMessage(idA, 'first', { now: 1 }), failed(idA, 'done')];
134
+ const messages = [
135
+ ...earlier,
136
+ newUserMessage(requestId, 'again', { now: 3 }),
137
+ failed(requestId, 'error'),
138
+ ];
139
+ const turn = turnForRetry(messages, requestId);
140
+ expect(turn?.user.text).toBe('again');
141
+ expect(turn?.rest).toEqual(earlier);
142
+ });
143
+ it('retries stopped turns but not finished, running or earlier ones', () => {
144
+ const turn = (status) => [
145
+ newUserMessage(requestId, 'q', { now: 1 }),
146
+ failed(requestId, status),
147
+ ];
148
+ expect(turnForRetry(turn('cancelled'), requestId)).toBeDefined();
149
+ expect(turnForRetry(turn('done'), requestId)).toBeUndefined();
150
+ expect(turnForRetry(turn('streaming'), requestId)).toBeUndefined();
151
+ expect(turnForRetry(turn('error'), idA)).toBeUndefined();
152
+ });
153
+ });
@@ -13,6 +13,15 @@ export interface ChatSessionOptions {
13
13
  onConversationStarted?: (conversationId: string) => void;
14
14
  onToolResult?: (name: string, status: string) => void;
15
15
  onEvent?: (event: ChatEvent) => void;
16
+ /**
17
+ * A turn stopped streaming (done, error, cancelled or cut short), with its final status.
18
+ * Hosts refresh what the turn may have changed, e.g. the conversation list's running flag.
19
+ */
20
+ onTurnEnd?: (info: {
21
+ requestId: string;
22
+ conversationId: string | null;
23
+ status: string;
24
+ }) => void;
16
25
  }
17
26
  export type SessionStatus = 'idle' | 'loading' | 'streaming';
18
27
  /**
@@ -44,6 +53,12 @@ export declare class ChatSession {
44
53
  send(text: string, options?: {
45
54
  files?: FileRef[];
46
55
  }): Promise<void>;
56
+ /**
57
+ * Resend the last turn after it failed, was stopped or was cut short. The failed turn is
58
+ * replaced in the view rather than duplicated. (The runtime may still hold the first user
59
+ * message when it had accepted the turn; a server-side regenerate is a separate change.)
60
+ */
61
+ retry(requestId: string): Promise<void>;
47
62
  /** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
48
63
  resume(requestId: string): Promise<void>;
49
64
  cancel(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,SAAS,EAET,UAAU,EACV,eAAe,EACf,OAAO,EACR,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kEAAkE;IAClE,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,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7D;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,gBAA6B;IACrC,MAAM,gBAAiC;IACvC,cAAc,gBAA+B;IAC7C,gGAAgG;IAChG,oBAAoB,gBAA+B;IACnD,IAAI,8BAA8C;IAClD,SAAS,qBAAyC;IAClD,YAAY;;;;;;QAAyB;IAErC,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE,kBAAkB;IAMvC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,gBAAgB,GAAG,SAAS,CAG1C;IAED,yFAAyF;IACnF,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+B3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B5E,+FAA+F;IACzF,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C,MAAM,IAAI,IAAI;YAOA,GAAG;IAmDjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,MAAM;CAKf;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAE1E"}
1
+ {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,SAAS,EAET,UAAU,EACV,eAAe,EACf,OAAO,EACR,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAKL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kEAAkE;IAClE,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,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAClG;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7D;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,gBAA6B;IACrC,MAAM,gBAAiC;IACvC,cAAc,gBAA+B;IAC7C,gGAAgG;IAChG,oBAAoB,gBAA+B;IACnD,IAAI,8BAA8C;IAClD,SAAS,qBAAyC;IAClD,YAAY;;;;;;QAAyB;IAErC,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE,kBAAkB;IAMvC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,gBAAgB,GAAG,SAAS,CAG1C;IAED,yFAAyF;IACnF,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+B3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B5E;;;;OAIG;IACG,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ7C,+FAA+F;IACzF,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C,MAAM,IAAI,IAAI;YAOA,GAAG;IA2DjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,MAAM;CAKf;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAE1E"}
@@ -1,5 +1,5 @@
1
1
  import { ApiRequestError } from '@classic-homes/agent-client';
2
- import { applyEvent, newAssistantMessage, newUserMessage, } from './reducer.js';
2
+ import { applyEvent, newAssistantMessage, newUserMessage, turnForRetry, } from './reducer.js';
3
3
  /**
4
4
  * Reactive conversation state (Svelte 5 runes class). Owns the stream lifecycle: one turn at a
5
5
  * time, cancel, resume after reload, history load. Rendering is left to the components.
@@ -82,6 +82,20 @@ export class ChatSession {
82
82
  ];
83
83
  await this.run(requestId, (signal, onEvent) => this.options.client.streamChat(request, { signal, onEvent }));
84
84
  }
85
+ /**
86
+ * Resend the last turn after it failed, was stopped or was cut short. The failed turn is
87
+ * replaced in the view rather than duplicated. (The runtime may still hold the first user
88
+ * message when it had accepted the turn; a server-side regenerate is a separate change.)
89
+ */
90
+ async retry(requestId) {
91
+ if (this.status !== 'idle')
92
+ return;
93
+ const turn = turnForRetry(this.messages, requestId);
94
+ if (!turn)
95
+ return;
96
+ this.messages = turn.rest;
97
+ await this.send(turn.user.text, { files: turn.user.attachments });
98
+ }
85
99
  /** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
86
100
  async resume(requestId) {
87
101
  if (!this.conversationId || this.status !== 'idle')
@@ -142,6 +156,12 @@ export class ChatSession {
142
156
  finally {
143
157
  this.controller = undefined;
144
158
  this.status = 'idle';
159
+ const final = this.messages.find((m) => m.role === 'assistant' && m.requestId === requestId);
160
+ this.options.onTurnEnd?.({
161
+ requestId,
162
+ conversationId: this.conversationId,
163
+ status: final?.status ?? 'done',
164
+ });
145
165
  }
146
166
  }
147
167
  toolName(requestId, callId) {
@@ -43,8 +43,8 @@ export declare function isFromFrame(event: MessageEvent, frame: HTMLIFrameElemen
43
43
  /**
44
44
  * Target origin for messages to the sandbox frame. An opaque origin matches nothing but "*",
45
45
  * so a message addressed to the sandbox's https origin would be dropped silently. The window
46
- * reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries is
47
- * model output that is untrusted anyway.
46
+ * reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries may contain private data. Keep the outer frame
47
+ * on the trusted sandbox page; never add navigation controlled by rendered content.
48
48
  */
49
49
  export declare const FRAME_TARGET_ORIGIN = "*";
50
50
  /** Text an intent or prompt action becomes in the composer. */
@@ -74,8 +74,8 @@ export function isFromFrame(event, frame, sandboxOrigin) {
74
74
  /**
75
75
  * Target origin for messages to the sandbox frame. An opaque origin matches nothing but "*",
76
76
  * so a message addressed to the sandbox's https origin would be dropped silently. The window
77
- * reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries is
78
- * model output that is untrusted anyway.
77
+ * reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries may contain private data. Keep the outer frame
78
+ * on the trusted sandbox page; never add navigation controlled by rendered content.
79
79
  */
80
80
  export const FRAME_TARGET_ORIGIN = '*';
81
81
  /** Text an intent or prompt action becomes in the composer. */
@@ -0,0 +1,16 @@
1
+ import { type FrameAction } from './ui-frame-protocol.js';
2
+ export interface FrameRelay {
3
+ send(): void;
4
+ destroy(): void;
5
+ }
6
+ /** Shared lifecycle for artifacts and MCP-UI; the outer frame URL is host-controlled. */
7
+ export declare function createFrameRelay(options: {
8
+ frame: () => HTMLIFrameElement | null;
9
+ origin: () => string;
10
+ payload: () => {
11
+ html: string;
12
+ uri: string;
13
+ };
14
+ onaction: (action: FrameAction) => void;
15
+ }): FrameRelay;
16
+ //# sourceMappingURL=ui-frame-relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-frame-relay.d.ts","sourceRoot":"","sources":["../src/lib/ui-frame-relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,IAAI,IAAI,IAAI,CAAC;IACb,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IACxC,KAAK,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAC;IACtC,MAAM,EAAE,MAAM,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CACzC,GAAG,UAAU,CAkBb"}
@@ -0,0 +1,22 @@
1
+ import { FRAME_TARGET_ORIGIN, UI_FRAME_RENDER, isFromFrame, parseFrameMessage, } from './ui-frame-protocol.js';
2
+ /** Shared lifecycle for artifacts and MCP-UI; the outer frame URL is host-controlled. */
3
+ export function createFrameRelay(options) {
4
+ const send = () => {
5
+ options
6
+ .frame()
7
+ ?.contentWindow?.postMessage({ type: UI_FRAME_RENDER, ...options.payload() }, FRAME_TARGET_ORIGIN);
8
+ };
9
+ const listener = (event) => {
10
+ if (!isFromFrame(event, options.frame(), options.origin()))
11
+ return;
12
+ const action = parseFrameMessage(event.data);
13
+ if (!action)
14
+ return;
15
+ if (action.type === 'ready')
16
+ send();
17
+ else
18
+ options.onaction(action);
19
+ };
20
+ window.addEventListener('message', listener);
21
+ return { send, destroy: () => window.removeEventListener('message', listener) };
22
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ui-frame-relay.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-frame-relay.test.d.ts","sourceRoot":"","sources":["../src/lib/ui-frame-relay.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ import { expect, it, vi } from 'vitest';
2
+ import { createFrameRelay } from './ui-frame-relay.js';
3
+ it('scopes ready/actions to its own frame, sends current HTML, and removes the listener', () => {
4
+ const postMessage = vi.fn();
5
+ const source = { postMessage };
6
+ const frame = { contentWindow: source };
7
+ const onaction = vi.fn();
8
+ let html = '<p>first</p>';
9
+ const relay = createFrameRelay({
10
+ frame: () => frame,
11
+ origin: () => 'https://sandbox.example',
12
+ payload: () => ({ html, uri: 'ui://test' }),
13
+ onaction,
14
+ });
15
+ const dispatch = (sender, data, origin = 'null') => window.dispatchEvent(new MessageEvent('message', { source: sender, origin, data }));
16
+ try {
17
+ dispatch(window, { type: 'ui-lifecycle-iframe-ready' });
18
+ expect(postMessage).not.toHaveBeenCalled();
19
+ dispatch(source, { type: 'ui-lifecycle-iframe-ready' });
20
+ expect(postMessage).toHaveBeenLastCalledWith({ type: 'agent-ui:render', html, uri: 'ui://test' }, '*');
21
+ html = '<p>updated artifact</p>';
22
+ relay.send();
23
+ expect(postMessage).toHaveBeenLastCalledWith({ type: 'agent-ui:render', html, uri: 'ui://test' }, '*');
24
+ dispatch(source, { type: 'tool', payload: { toolName: 'test' } });
25
+ expect(onaction).toHaveBeenCalledTimes(1);
26
+ expect(onaction).toHaveBeenCalledWith({ type: 'tool', toolName: 'test', params: undefined });
27
+ dispatch(source, { type: 'link', payload: { url: 'javascript:alert(1)' } });
28
+ dispatch(source, { type: 'notify', payload: { message: 'spoof' } }, 'https://other.example');
29
+ expect(onaction).toHaveBeenCalledTimes(1);
30
+ }
31
+ finally {
32
+ relay.destroy();
33
+ }
34
+ dispatch(source, { type: 'ui-lifecycle-iframe-ready' });
35
+ expect(postMessage).toHaveBeenCalledTimes(2);
36
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/agent-chat",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
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,18 +17,21 @@
17
17
  "dist"
18
18
  ],
19
19
  "dependencies": {
20
- "@classic-homes/agent-client": "0.1.3",
21
- "@classic-homes/agent-contracts": "0.1.3",
20
+ "@classic-homes/agent-client": "0.2.1",
21
+ "@classic-homes/agent-contracts": "0.2.1",
22
22
  "dompurify": "3.4.17"
23
23
  },
24
24
  "peerDependencies": {
25
- "@classic-homes/theme-svelte": "^1.34.0",
25
+ "@classic-homes/theme-svelte": "^1.39.0",
26
26
  "svelte": "^5.0.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@classic-homes/theme-svelte": "1.34.0",
29
+ "@classic-homes/theme-svelte": "1.39.0",
30
30
  "@sveltejs/package": "3.0.2",
31
31
  "@sveltejs/vite-plugin-svelte": "7.3.1",
32
+ "@testing-library/svelte": "5.4.2",
33
+ "@testing-library/user-event": "14.6.1",
34
+ "jsdom": "30.1.3",
32
35
  "svelte": "5.57.3",
33
36
  "svelte-check": "4.7.7",
34
37
  "vite": "8.3.5"