@classic-homes/agent-chat 0.10.0 → 0.11.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.
@@ -310,7 +310,10 @@
310
310
  .rate(requestId, rating, comment)
311
311
  .catch(() => onnotify?.('Could not save your feedback. Try again.'))
312
312
  : undefined}
313
- oncontinue={() => void session.send('Continue.')}
313
+ oncontinue={() => {
314
+ const answer = session.current;
315
+ if (answer) void session.continue(answer.requestId);
316
+ }}
314
317
  empty={emptyState}
315
318
  />
316
319
  <Composer
@@ -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":"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;AAqPH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -98,7 +98,8 @@
98
98
  !message.tools.some((t) => !t.status) &&
99
99
  !message.artifacts.some((a) => a.open),
100
100
  );
101
- const canContinue = $derived(last && !!oncontinue);
101
+ // Continue carries on the text so far; a reply stopped before any text is retried instead.
102
+ const canContinue = $derived(last && !!oncontinue && !!message.text);
102
103
  </script>
103
104
 
104
105
  <div class="group/message flex gap-3 px-4 py-2" data-role="assistant" data-status={message.status}>
@@ -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,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;AA4JH;;;GAGG;AACH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=continue.svelte.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continue.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/continue.svelte.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,109 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { ChatSession } from '../session.svelte.js';
5
+ import AgentChat from './AgentChat.svelte';
6
+ afterEach(() => cleanup());
7
+ const conversation = {
8
+ conversation: { conversationId: 'c-1', agent: 'classic-agent' },
9
+ messages: [
10
+ {
11
+ id: 'u-r1',
12
+ seq: 0,
13
+ role: 'user',
14
+ content: [{ type: 'text', text: 'Write a memo' }],
15
+ createdAt: 1,
16
+ },
17
+ {
18
+ id: 'a-r1',
19
+ seq: 1,
20
+ role: 'assistant',
21
+ content: [{ type: 'text', text: 'The memo begins' }],
22
+ createdAt: 2,
23
+ finish: 'length',
24
+ },
25
+ ],
26
+ files: [],
27
+ };
28
+ function fakeClient(events) {
29
+ return {
30
+ fileUrl: (id) => `#${id}`,
31
+ uploadFile: vi.fn(),
32
+ cancel: vi.fn(),
33
+ resumeTurn: vi.fn(),
34
+ sendFeedback: vi.fn(),
35
+ getConversation: vi.fn().mockResolvedValue(conversation),
36
+ streamChat: vi.fn(async (req, opts) => {
37
+ events.forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
38
+ return { kind: 'done' };
39
+ }),
40
+ };
41
+ }
42
+ const continued = [
43
+ { type: 'start', conversationId: 'c-1', mode: 'live' },
44
+ { type: 'delta', text: ' and ends here.' },
45
+ { type: 'done', finishReason: 'stop' },
46
+ ];
47
+ describe('ChatSession.continue', () => {
48
+ it('keeps Continue after a reload and streams the rest into the same answer', async () => {
49
+ const client = fakeClient(continued);
50
+ const session = new ChatSession({ client: client, conversationId: 'c-1' });
51
+ await session.load('c-1');
52
+ expect(session.current).toMatchObject({ status: 'incomplete', finishReason: 'length' });
53
+ await session.continue('r1');
54
+ const request = client.streamChat.mock.calls[0][0];
55
+ expect(request).toMatchObject({ conversationId: 'c-1', continues: 'r1' });
56
+ expect(request.requestId).not.toBe('r1');
57
+ expect(session.messages).toHaveLength(2);
58
+ expect(session.current).toMatchObject({
59
+ requestId: 'r1',
60
+ text: 'The memo begins and ends here.',
61
+ status: 'done',
62
+ });
63
+ });
64
+ it('cancels the continuation request, not the original answer', async () => {
65
+ const client = fakeClient([]);
66
+ let release = () => { };
67
+ client.streamChat.mockImplementation(((_req, opts) => new Promise((resolve) => {
68
+ release = () => resolve({ kind: 'aborted' });
69
+ opts.signal.addEventListener('abort', () => release());
70
+ })));
71
+ const session = new ChatSession({ client: client, conversationId: 'c-1' });
72
+ await session.load('c-1');
73
+ const running = session.continue('r1');
74
+ session.cancel();
75
+ await running;
76
+ const streamed = client.streamChat.mock.calls[0][0].requestId;
77
+ expect(client.cancel).toHaveBeenCalledWith(streamed);
78
+ expect(session.current).toMatchObject({ requestId: 'r1', status: 'cancelled' });
79
+ });
80
+ it('does nothing for an answer that finished normally', async () => {
81
+ const client = fakeClient(continued);
82
+ const session = new ChatSession({ client: client, conversationId: 'c-1' });
83
+ client.getConversation.mockResolvedValueOnce({
84
+ ...conversation,
85
+ messages: conversation.messages.map((m) => ({ ...m, finish: undefined })),
86
+ });
87
+ await session.load('c-1');
88
+ await session.continue('r1');
89
+ expect(client.streamChat).not.toHaveBeenCalled();
90
+ });
91
+ });
92
+ describe('AgentChat Continue', () => {
93
+ it('continues the cut-short answer in place without a new question', async () => {
94
+ const user = userEvent.setup();
95
+ const client = fakeClient(continued);
96
+ render(AgentChat, {
97
+ props: {
98
+ sandboxOrigin: 'https://frame.test',
99
+ client: client,
100
+ conversationId: 'c-1',
101
+ },
102
+ });
103
+ await user.click(await screen.findByRole('button', { name: 'Continue' }));
104
+ await screen.findAllByText(/and ends here\./);
105
+ expect(screen.queryByText('Continue')).toBeNull();
106
+ expect(screen.getAllByText('Write a memo')).toHaveLength(1);
107
+ await waitFor(() => expect(client.streamChat.mock.calls[0][0]).toMatchObject({ continues: 'r1' }));
108
+ });
109
+ });
@@ -92,7 +92,12 @@ describe('AssistantMessage', () => {
92
92
  it('offers Continue on a cut-short or stopped last answer only', async () => {
93
93
  const oncontinue = vi.fn();
94
94
  const { rerender } = render(AssistantMessageView, {
95
- props: { ...base, last: true, oncontinue, message: assistant({ status: 'incomplete' }) },
95
+ props: {
96
+ ...base,
97
+ last: true,
98
+ oncontinue,
99
+ message: assistant({ status: 'incomplete', text: 'Partial' }),
100
+ },
96
101
  });
97
102
  expect(screen.getByText('The answer was cut short.')).toBeTruthy();
98
103
  await userEvent.click(screen.getByRole('button', { name: 'Continue' }));
@@ -101,7 +106,7 @@ describe('AssistantMessage', () => {
101
106
  ...base,
102
107
  last: true,
103
108
  oncontinue,
104
- message: assistant({ status: 'cancelled' }),
109
+ message: assistant({ status: 'cancelled', text: 'Partial' }),
105
110
  });
106
111
  expect(screen.getByText('Stopped')).toBeTruthy();
107
112
  expect(screen.getByRole('button', { name: 'Continue' })).toBeTruthy();
@@ -109,6 +114,14 @@ describe('AssistantMessage', () => {
109
114
  ...base,
110
115
  last: false,
111
116
  oncontinue,
117
+ message: assistant({ status: 'cancelled', text: 'Partial' }),
118
+ });
119
+ expect(screen.queryByRole('button', { name: 'Continue' })).toBeNull();
120
+ // Stopped before any text: nothing to continue (Regenerate asks again).
121
+ await rerender({
122
+ ...base,
123
+ last: true,
124
+ oncontinue,
112
125
  message: assistant({ status: 'cancelled' }),
113
126
  });
114
127
  expect(screen.queryByRole('button', { name: 'Continue' })).toBeNull();
@@ -46,6 +46,8 @@ export declare class ChatSession {
46
46
  status: "uploaded" | "indexed" | "failed";
47
47
  }[];
48
48
  private controller;
49
+ /** The request being streamed; differs from the shown answer's id while continuing it. */
50
+ private streamRequestId;
49
51
  private readonly options;
50
52
  constructor(options: ChatSessionOptions);
51
53
  get streaming(): boolean;
@@ -69,6 +71,12 @@ export declare class ChatSession {
69
71
  regenerate(requestId: string): Promise<void>;
70
72
  /** Change the latest question and answer it again, replacing that turn. */
71
73
  edit(requestId: string, text: string): Promise<void>;
74
+ /**
75
+ * Carry on the latest answer where it was cut short or stopped. The runtime asks the agent to
76
+ * continue and appends the new text to that answer, so the view streams into the same message
77
+ * (no new question shows).
78
+ */
79
+ continue(requestId: string): Promise<void>;
72
80
  /** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
73
81
  resume(requestId: string): Promise<void>;
74
82
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,SAAS,EAET,UAAU,EACV,eAAe,EACf,OAAO,EACP,UAAU,EACX,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAML,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,4FAA4F;IAC5F,KAAK,SAAqC;IAC1C,SAAS,qBAAyC;IAClD,YAAY;;;;;;QAAyB;IAErC,OAAO,CAAC,UAAU,CAA8B;IAChD,0FAA0F;IAC1F,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE,kBAAkB;IAOvC,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;IA0CjD;;;OAGG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA2B/F;;;OAGG;IACG,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ7C,oFAAoF;IAC9E,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQlD,2EAA2E;IACrE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ1D;;;;OAIG;IACG,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BhD,+FAA+F;IACzF,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C;;;OAGG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBzF,MAAM,IAAI,IAAI;YAOA,GAAG;IA+DjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,MAAM;CAKf;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAE1E"}
@@ -16,6 +16,8 @@ export class ChatSession {
16
16
  loadError = $state(undefined);
17
17
  pendingFiles = $state([]);
18
18
  controller;
19
+ /** The request being streamed; differs from the shown answer's id while continuing it. */
20
+ streamRequestId;
19
21
  options;
20
22
  constructor(options) {
21
23
  this.options = options;
@@ -49,7 +51,13 @@ export class ChatSession {
49
51
  : {
50
52
  ...newAssistantMessage(m.id.replace(/^a-/, ''), m.createdAt),
51
53
  text: m.content.map((c) => c.text).join('\n'),
52
- status: 'done',
54
+ // An answer that ended early keeps its Continue after a reload.
55
+ status: m.finish === 'length'
56
+ ? 'incomplete'
57
+ : m.finish === 'cancelled'
58
+ ? 'cancelled'
59
+ : 'done',
60
+ ...(m.finish ? { finishReason: m.finish } : {}),
53
61
  ...(detail.feedback?.[m.id.replace(/^a-/, '')]
54
62
  ? { feedback: detail.feedback[m.id.replace(/^a-/, '')] }
55
63
  : {}),
@@ -127,6 +135,36 @@ export class ChatSession {
127
135
  this.messages = turn.rest;
128
136
  await this.send(text, { files: turn.user.attachments, replaces: requestId });
129
137
  }
138
+ /**
139
+ * Carry on the latest answer where it was cut short or stopped. The runtime asks the agent to
140
+ * continue and appends the new text to that answer, so the view streams into the same message
141
+ * (no new question shows).
142
+ */
143
+ async continue(requestId) {
144
+ const conversationId = this.conversationId;
145
+ if (!conversationId || this.status !== 'idle')
146
+ return;
147
+ const last = this.current;
148
+ if (last?.requestId !== requestId ||
149
+ (last.status !== 'incomplete' && last.status !== 'cancelled') ||
150
+ !last.text)
151
+ return;
152
+ const request = {
153
+ requestId: crypto.randomUUID(),
154
+ conversationId,
155
+ agent: this.agent,
156
+ message: 'Continue',
157
+ continues: requestId,
158
+ ...(this.options.clientKind ? { client: this.options.clientKind } : {}),
159
+ };
160
+ this.update(requestId, (m) => {
161
+ const next = { ...m, status: 'streaming' };
162
+ delete next.finishReason;
163
+ delete next.error;
164
+ return next;
165
+ });
166
+ await this.run(requestId, (signal, onEvent) => this.options.client.streamChat(request, { signal, onEvent }), request.requestId);
167
+ }
130
168
  /** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
131
169
  async resume(requestId) {
132
170
  if (!this.conversationId || this.status !== 'idle')
@@ -174,11 +212,14 @@ export class ChatSession {
174
212
  if (!this.controller || !current)
175
213
  return;
176
214
  this.controller.abort();
177
- void this.options.client.cancel(current.requestId);
215
+ void this.options.client.cancel(this.streamRequestId ?? current.requestId);
178
216
  }
179
- async run(requestId, start) {
217
+ async run(requestId, start,
218
+ /** The request streamed, when it is not `requestId` (a continuation of that answer). */
219
+ streamRequestId = requestId) {
180
220
  this.status = 'streaming';
181
221
  this.controller = new AbortController();
222
+ this.streamRequestId = streamRequestId;
182
223
  const onEvent = (event) => {
183
224
  if (event.type === 'start') {
184
225
  this.mode = event.mode;
@@ -215,6 +256,7 @@ export class ChatSession {
215
256
  }
216
257
  finally {
217
258
  this.controller = undefined;
259
+ this.streamRequestId = undefined;
218
260
  this.status = 'idle';
219
261
  const final = this.messages.find((m) => m.role === 'assistant' && m.requestId === requestId);
220
262
  this.options.onTurnEnd?.({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/agent-chat",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Svelte 5 chat UI for Classic Agent: session state, message list, composer, tool timeline, artifacts and MCP-UI sandbox frames. Consumed by the Classic App.",
@@ -17,8 +17,8 @@
17
17
  "dist"
18
18
  ],
19
19
  "dependencies": {
20
- "@classic-homes/agent-client": "0.10.0",
21
- "@classic-homes/agent-contracts": "0.10.0",
20
+ "@classic-homes/agent-client": "0.11.0",
21
+ "@classic-homes/agent-contracts": "0.11.0",
22
22
  "dompurify": "3.4.17"
23
23
  },
24
24
  "peerDependencies": {