@classic-homes/agent-chat 0.5.0 → 0.6.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.
@@ -254,6 +254,8 @@
254
254
  ontool={(name, params) => ontoolrequest?.(name, params)}
255
255
  {onnotify}
256
256
  onretry={retry}
257
+ onregenerate={(requestId) => void session.regenerate(requestId)}
258
+ onedit={(requestId, text) => void session.edit(requestId, text)}
257
259
  oncontinue={() => void session.send('Continue.')}
258
260
  empty={emptyState}
259
261
  />
@@ -30,6 +30,8 @@
30
30
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
31
31
  onnotify?: (message: string) => void;
32
32
  onretry?: () => void;
33
+ /** Ask for a new answer to the same question (the latest finished answer). */
34
+ onregenerate?: () => void;
33
35
  /** Ask the agent to pick up a stopped or cut-short answer. */
34
36
  oncontinue?: () => void;
35
37
  }
@@ -46,6 +48,7 @@
46
48
  ontool,
47
49
  onnotify,
48
50
  onretry,
51
+ onregenerate,
49
52
  oncontinue,
50
53
  }: Props = $props();
51
54
 
@@ -179,6 +182,7 @@
179
182
  text={message.text}
180
183
  createdAt={message.createdAt}
181
184
  visible={last}
185
+ {onregenerate}
182
186
  class="-ml-1.5"
183
187
  />
184
188
  {/if}
@@ -13,6 +13,8 @@ interface Props {
13
13
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
14
14
  onnotify?: (message: string) => void;
15
15
  onretry?: () => void;
16
+ /** Ask for a new answer to the same question (the latest finished answer). */
17
+ onregenerate?: () => void;
16
18
  /** Ask the agent to pick up a stopped or cut-short answer. */
17
19
  oncontinue?: () => void;
18
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AssistantMessage.svelte.ts"],"names":[],"mappings":"AAIA,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,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAwJH;;;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,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,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAyJH;;;GAGG;AACH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -1,11 +1,12 @@
1
1
  <!--
2
2
  @component
3
- Per-message action bar: copy the raw text and the time it was sent. Hidden until the message is
3
+ Per-message action bar: copy the raw text, regenerate or edit (latest turn only) and the time
4
+ it was sent. Hidden until the message is
4
5
  hovered or holds focus, except where `visible` (the last message) or on touch screens, which
5
6
  have no hover.
6
7
  -->
7
8
  <script lang="ts">
8
- import { cn, CopyButton, Tooltip } from '@classic-homes/theme-svelte';
9
+ import { Button, cn, CopyButton, Icon, Tooltip } from '@classic-homes/theme-svelte';
9
10
  import { formatDateTime, formatTime } from '../message-format.js';
10
11
 
11
12
  interface Props {
@@ -14,9 +15,21 @@
14
15
  /** Keep the bar visible without hover (the last message). */
15
16
  visible?: boolean;
16
17
  align?: 'start' | 'end';
18
+ /** Offer "Regenerate" (the latest answer). */
19
+ onregenerate?: () => void;
20
+ /** Offer "Edit" (the latest question). */
21
+ onedit?: () => void;
17
22
  class?: string;
18
23
  }
19
- let { text, createdAt, visible = false, align = 'start', class: className }: Props = $props();
24
+ let {
25
+ text,
26
+ createdAt,
27
+ visible = false,
28
+ align = 'start',
29
+ onregenerate,
30
+ onedit,
31
+ class: className,
32
+ }: Props = $props();
20
33
  </script>
21
34
 
22
35
  <div
@@ -39,6 +52,26 @@
39
52
  class="h-7 w-7"
40
53
  />
41
54
  {/if}
55
+ {#if onedit}
56
+ <Button
57
+ variant="ghost"
58
+ size="icon"
59
+ class="h-7 w-7"
60
+ aria-label="Edit message"
61
+ title="Edit"
62
+ onclick={onedit}><Icon name="edit" size={14} /></Button
63
+ >
64
+ {/if}
65
+ {#if onregenerate}
66
+ <Button
67
+ variant="ghost"
68
+ size="icon"
69
+ class="h-7 w-7"
70
+ aria-label="Regenerate response"
71
+ title="Regenerate"
72
+ onclick={onregenerate}><Icon name="refresh" size={14} /></Button
73
+ >
74
+ {/if}
42
75
  <Tooltip content={formatDateTime(createdAt)}>
43
76
  {#snippet children({ props })}
44
77
  <!-- Focusable so keyboard users can read the full date in the tooltip. -->
@@ -4,10 +4,15 @@ interface Props {
4
4
  /** Keep the bar visible without hover (the last message). */
5
5
  visible?: boolean;
6
6
  align?: 'start' | 'end';
7
+ /** Offer "Regenerate" (the latest answer). */
8
+ onregenerate?: () => void;
9
+ /** Offer "Edit" (the latest question). */
10
+ onedit?: () => void;
7
11
  class?: string;
8
12
  }
9
13
  /**
10
- * Per-message action bar: copy the raw text and the time it was sent. Hidden until the message is
14
+ * Per-message action bar: copy the raw text, regenerate or edit (latest turn only) and the time
15
+ * it was sent. Hidden until the message is
11
16
  * hovered or holds focus, except where `visible` (the last message) or on touch screens, which
12
17
  * have no hover.
13
18
  */
@@ -1 +1 @@
1
- {"version":3,"file":"MessageActions.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageActions.svelte.ts"],"names":[],"mappings":"AAOE,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAgCH;;;;GAIG;AACH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"MessageActions.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageActions.svelte.ts"],"names":[],"mappings":"AAOE,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACxB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8CH;;;;;GAKG;AACH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -20,13 +20,16 @@
20
20
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
21
21
  onnotify?: (message: string) => void;
22
22
  onretry?: () => void;
23
+ onregenerate?: () => void;
23
24
  oncontinue?: () => void;
25
+ /** User messages only: edit and resend. */
26
+ onedit?: (text: string) => void;
24
27
  }
25
- let { message, last = false, ...rest }: Props = $props();
28
+ let { message, last = false, onedit, ...rest }: Props = $props();
26
29
  </script>
27
30
 
28
31
  {#if message.role === 'user'}
29
- <UserMessage {message} {last} />
32
+ <UserMessage {message} {last} {onedit} />
30
33
  {:else}
31
34
  <AssistantMessage {message} {last} {...rest} />
32
35
  {/if}
@@ -12,7 +12,10 @@ interface Props {
12
12
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
13
13
  onnotify?: (message: string) => void;
14
14
  onretry?: () => void;
15
+ onregenerate?: () => void;
15
16
  oncontinue?: () => void;
17
+ /** User messages only: edit and resend. */
18
+ onedit?: (text: string) => void;
16
19
  }
17
20
  /** One transcript entry: a `UserMessage` or an `AssistantMessage`. */
18
21
  declare const MessageItem: import("svelte").Component<Props, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,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,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAkBH,sEAAsE;AACtE,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,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,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAkBH,sEAAsE;AACtE,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -17,6 +17,10 @@
17
17
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
18
18
  onnotify?: (message: string) => void;
19
19
  onretry?: () => void;
20
+ /** Regenerate the latest answer, by its request id. */
21
+ onregenerate?: (requestId: string) => void;
22
+ /** Edit the latest question, by its request id. */
23
+ onedit?: (requestId: string, text: string) => void;
20
24
  oncontinue?: () => void;
21
25
  empty?: import('svelte').Snippet;
22
26
  }
@@ -32,10 +36,19 @@
32
36
  ontool,
33
37
  onnotify,
34
38
  onretry,
39
+ onregenerate,
40
+ onedit,
35
41
  oncontinue,
36
42
  empty,
37
43
  }: Props = $props();
38
44
 
45
+ // The latest turn can be regenerated or edited once it has finished.
46
+ const latest = $derived.by(() => {
47
+ const last = messages[messages.length - 1];
48
+ if (streaming || last?.role !== 'assistant' || last.status === 'streaming') return undefined;
49
+ return messages[messages.length - 2]?.role === 'user' ? last.requestId : undefined;
50
+ });
51
+
39
52
  let container: HTMLDivElement | undefined = $state();
40
53
  /** Pinned to the bottom: new content scrolls into view. Off once the reader scrolls up. */
41
54
  let follow = $state(true);
@@ -143,6 +156,16 @@
143
156
  {onnotify}
144
157
  onretry={i === messages.length - 1 ? onretry : undefined}
145
158
  oncontinue={i === messages.length - 1 && !streaming ? oncontinue : undefined}
159
+ onregenerate={latest &&
160
+ onregenerate &&
161
+ i === messages.length - 1 &&
162
+ message.role === 'assistant' &&
163
+ message.status === 'done'
164
+ ? () => onregenerate(latest)
165
+ : undefined}
166
+ onedit={latest && onedit && i === messages.length - 2
167
+ ? (text: string) => onedit(latest, text)
168
+ : undefined}
146
169
  />
147
170
  {/each}
148
171
  </div>
@@ -11,6 +11,10 @@ interface Props {
11
11
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
12
12
  onnotify?: (message: string) => void;
13
13
  onretry?: () => void;
14
+ /** Regenerate the latest answer, by its request id. */
15
+ onregenerate?: (requestId: string) => void;
16
+ /** Edit the latest question, by its request id. */
17
+ onedit?: (requestId: string, text: string) => void;
14
18
  oncontinue?: () => void;
15
19
  empty?: import('svelte').Snippet;
16
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageList.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,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,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;AA4HH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"MessageList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageList.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,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,uDAAuD;IACvD,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,mDAAmD;IACnD,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;AA6IH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -1,9 +1,12 @@
1
1
  <!--
2
2
  @component
3
3
  The user's turn: attachments and context above a right-aligned muted bubble, then the action bar.
4
+ With `onedit` (the latest question) the bubble can be edited in place and sent again.
4
5
  -->
5
6
  <script lang="ts">
6
- import { Icon } from '@classic-homes/theme-svelte';
7
+ import { Button, Icon, Textarea } from '@classic-homes/theme-svelte';
8
+ import { LIMITS } from '@classic-homes/agent-contracts';
9
+ import { tick } from 'svelte';
7
10
  import type { UserMessage } from '../reducer.js';
8
11
  import { fileIcon, formatBytes } from '../message-format.js';
9
12
  import MessageActions from './MessageActions.svelte';
@@ -11,8 +14,37 @@
11
14
  interface Props {
12
15
  message: UserMessage;
13
16
  last?: boolean;
17
+ /** Replace this question with new text and answer it again. */
18
+ onedit?: (text: string) => void;
19
+ }
20
+ let { message, last = false, onedit }: Props = $props();
21
+
22
+ let editing = $state(false);
23
+ let draft = $state('');
24
+ let field = $state<HTMLTextAreaElement | null>(null);
25
+ async function startEdit(): Promise<void> {
26
+ draft = message.text;
27
+ editing = true;
28
+ await tick();
29
+ field?.focus();
30
+ field?.select();
31
+ }
32
+ function save(): void {
33
+ const text = draft.trim();
34
+ if (!text) return;
35
+ editing = false;
36
+ if (text !== message.text.trim()) onedit?.(text);
37
+ }
38
+ function onkeydown(event: KeyboardEvent): void {
39
+ if (event.isComposing || event.keyCode === 229) return;
40
+ if (event.key === 'Escape') {
41
+ event.preventDefault();
42
+ editing = false;
43
+ } else if (event.key === 'Enter' && !event.shiftKey) {
44
+ event.preventDefault();
45
+ save();
46
+ }
14
47
  }
15
- let { message, last = false }: Props = $props();
16
48
  </script>
17
49
 
18
50
  <div class="group/message flex flex-col items-end gap-1 px-4 py-2" data-role="user">
@@ -37,10 +69,45 @@
37
69
  {/each}
38
70
  </ul>
39
71
  {/if}
40
- <div
41
- class="max-w-[85%] whitespace-pre-wrap rounded-2xl rounded-br-md bg-muted px-4 py-2.5 text-foreground [overflow-wrap:anywhere]"
42
- >
43
- {message.text}
44
- </div>
45
- <MessageActions text={message.text} createdAt={message.createdAt} visible={last} align="end" />
72
+ {#if editing}
73
+ <form
74
+ class="flex w-full max-w-[85%] flex-col gap-2 rounded-2xl border border-input bg-background p-2 focus-within:ring-2 focus-within:ring-ring/20"
75
+ aria-label="Edit message"
76
+ onsubmit={(e) => {
77
+ e.preventDefault();
78
+ save();
79
+ }}
80
+ >
81
+ <Textarea
82
+ bind:ref={field}
83
+ bind:value={draft}
84
+ autosize
85
+ rows={1}
86
+ maxRows={10}
87
+ maxlength={LIMITS.messageChars}
88
+ aria-label="Edited message"
89
+ class="border-0 bg-transparent shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
90
+ {onkeydown}
91
+ />
92
+ <div class="flex justify-end gap-2">
93
+ <Button type="button" variant="ghost" size="sm" onclick={() => (editing = false)}
94
+ >Cancel</Button
95
+ >
96
+ <Button type="submit" size="sm" disabled={!draft.trim()}>Send</Button>
97
+ </div>
98
+ </form>
99
+ {:else}
100
+ <div
101
+ class="max-w-[85%] whitespace-pre-wrap rounded-2xl rounded-br-md bg-muted px-4 py-2.5 text-foreground [overflow-wrap:anywhere]"
102
+ >
103
+ {message.text}
104
+ </div>
105
+ <MessageActions
106
+ text={message.text}
107
+ createdAt={message.createdAt}
108
+ visible={last}
109
+ align="end"
110
+ onedit={onedit ? startEdit : undefined}
111
+ />
112
+ {/if}
46
113
  </div>
@@ -2,8 +2,13 @@ import type { UserMessage } from '../reducer.js';
2
2
  interface Props {
3
3
  message: UserMessage;
4
4
  last?: boolean;
5
+ /** Replace this question with new text and answer it again. */
6
+ onedit?: (text: string) => void;
5
7
  }
6
- /** The user's turn: attachments and context above a right-aligned muted bubble, then the action bar. */
8
+ /**
9
+ * The user's turn: attachments and context above a right-aligned muted bubble, then the action bar.
10
+ * With `onedit` (the latest question) the bubble can be edited in place and sent again.
11
+ */
7
12
  declare const UserMessage: import("svelte").Component<Props, {}, "">;
8
13
  type UserMessage = ReturnType<typeof UserMessage>;
9
14
  export default UserMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"UserMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/UserMessage.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAqCH,wGAAwG;AACxG,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"UserMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/UserMessage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,+DAA+D;IAC/D,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AA+EH;;;GAGG;AACH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=regenerate.svelte.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regenerate.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/regenerate.svelte.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,108 @@
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 { latestTurn, newAssistantMessage, newUserMessage } from '../reducer.js';
5
+ import { ChatSession } from '../session.svelte.js';
6
+ import AgentChat from './AgentChat.svelte';
7
+ afterEach(() => cleanup());
8
+ /** Answers each request with "Answer N" (N counts requests). */
9
+ function fakeClient() {
10
+ let n = 0;
11
+ return {
12
+ fileUrl: (id) => `#${id}`,
13
+ uploadFile: vi.fn(),
14
+ cancel: vi.fn(),
15
+ getConversation: vi.fn(),
16
+ resumeTurn: vi.fn(),
17
+ streamChat: vi.fn(async (req, opts) => {
18
+ n += 1;
19
+ [
20
+ { type: 'start', conversationId: 'c-1', mode: 'live' },
21
+ { type: 'delta', text: `Answer ${n}` },
22
+ { type: 'done', finishReason: 'stop' },
23
+ ].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
24
+ return { kind: 'done' };
25
+ }),
26
+ };
27
+ }
28
+ describe('latestTurn', () => {
29
+ it('finds the finished latest turn only', () => {
30
+ const done = { ...newAssistantMessage('r2'), status: 'done' };
31
+ const messages = [
32
+ newUserMessage('r1', 'one'),
33
+ { ...newAssistantMessage('r1'), status: 'done' },
34
+ newUserMessage('r2', 'two'),
35
+ done,
36
+ ];
37
+ expect(latestTurn(messages, 'r2')?.user.text).toBe('two');
38
+ expect(latestTurn(messages, 'r2')?.rest).toHaveLength(2);
39
+ expect(latestTurn(messages, 'r1')).toBeUndefined();
40
+ expect(latestTurn([...messages.slice(0, 3), newAssistantMessage('r2')], 'r2')).toBeUndefined();
41
+ });
42
+ });
43
+ describe('ChatSession regenerate and edit', () => {
44
+ it('replaces the latest turn instead of adding one', async () => {
45
+ const client = fakeClient();
46
+ const session = new ChatSession({ client: client });
47
+ await session.send('first');
48
+ const firstId = client.streamChat.mock.calls[0][0].requestId;
49
+ await session.regenerate(firstId);
50
+ const second = client.streamChat.mock.calls[1][0];
51
+ expect(second).toMatchObject({ message: 'first', replaces: firstId, conversationId: 'c-1' });
52
+ expect(session.messages.map((m) => m.text)).toEqual(['first', 'Answer 2']);
53
+ await session.edit(second.requestId, 'first, but better');
54
+ expect(client.streamChat.mock.calls[2][0]).toMatchObject({
55
+ message: 'first, but better',
56
+ replaces: second.requestId,
57
+ });
58
+ expect(session.messages.map((m) => m.text)).toEqual(['first, but better', 'Answer 3']);
59
+ });
60
+ it('sends a normal message without replaces', async () => {
61
+ const client = fakeClient();
62
+ const session = new ChatSession({ client: client });
63
+ await session.send('hi');
64
+ expect(client.streamChat.mock.calls[0][0]).not.toHaveProperty('replaces');
65
+ });
66
+ });
67
+ describe('AgentChat regenerate and edit', () => {
68
+ it('offers Regenerate on the latest answer and Edit on the latest question', async () => {
69
+ const user = userEvent.setup();
70
+ const client = fakeClient();
71
+ render(AgentChat, { props: { client: client, sandboxOrigin: 'https://frame.test' } });
72
+ await user.type(screen.getByRole('textbox', { name: 'Message' }), 'one{Enter}');
73
+ await screen.findAllByText('Answer 1');
74
+ await user.type(screen.getByRole('textbox', { name: 'Message' }), 'two{Enter}');
75
+ await screen.findAllByText('Answer 2');
76
+ // Only the latest turn.
77
+ expect(screen.getAllByRole('button', { name: 'Regenerate response' })).toHaveLength(1);
78
+ expect(screen.getAllByRole('button', { name: 'Edit message' })).toHaveLength(1);
79
+ await user.click(screen.getByRole('button', { name: 'Regenerate response' }));
80
+ await waitFor(() => expect(client.streamChat).toHaveBeenCalledTimes(3));
81
+ expect(client.streamChat.mock.calls[2][0]).toMatchObject({ message: 'two' });
82
+ expect((await screen.findAllByText('Answer 3')).length).toBeGreaterThan(0);
83
+ expect(screen.queryByText('Answer 2')).toBeNull();
84
+ await user.click(screen.getByRole('button', { name: 'Edit message' }));
85
+ const field = screen.getByRole('textbox', { name: 'Edited message' });
86
+ await user.clear(field);
87
+ await user.type(field, 'two, edited{Enter}');
88
+ await waitFor(() => expect(client.streamChat).toHaveBeenCalledTimes(4));
89
+ expect(client.streamChat.mock.calls[3][0]).toMatchObject({
90
+ message: 'two, edited',
91
+ replaces: client.streamChat.mock.calls[2][0].requestId,
92
+ });
93
+ expect(await screen.findByText('two, edited')).toBeTruthy();
94
+ expect(screen.queryByText(/^two$/)).toBeNull();
95
+ });
96
+ it('cancels an edit with Escape', async () => {
97
+ const user = userEvent.setup();
98
+ const client = fakeClient();
99
+ render(AgentChat, { props: { client: client, sandboxOrigin: 'https://frame.test' } });
100
+ await user.type(screen.getByRole('textbox', { name: 'Message' }), 'one{Enter}');
101
+ await screen.findAllByText('Answer 1');
102
+ await user.click(screen.getByRole('button', { name: 'Edit message' }));
103
+ await user.type(screen.getByRole('textbox', { name: 'Edited message' }), ' more{Escape}');
104
+ expect(screen.queryByRole('textbox', { name: 'Edited message' })).toBeNull();
105
+ expect(screen.getByText('one')).toBeTruthy();
106
+ expect(client.streamChat).toHaveBeenCalledOnce();
107
+ });
108
+ });
package/dist/reducer.d.ts CHANGED
@@ -100,6 +100,14 @@ export declare function turnForRetry(messages: ChatMessage[], requestId: string)
100
100
  user: UserMessage;
101
101
  rest: ChatMessage[];
102
102
  } | undefined;
103
+ /**
104
+ * The conversation's latest finished turn (any outcome), to regenerate or edit: its question
105
+ * and the messages before it. Undefined while it streams or when `requestId` is not the latest.
106
+ */
107
+ export declare function latestTurn(messages: ChatMessage[], requestId: string): {
108
+ user: UserMessage;
109
+ rest: ChatMessage[];
110
+ } | undefined;
103
111
  /** Human titles for client error codes (mirrors the task pane's wording). */
104
112
  export declare function errorTitle(code: ClientErrorCode): string;
105
113
  export type { ClientKind };
@@ -1 +1 @@
1
- {"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/lib/reducer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/lib/reducer.ts"],"names":[],"mappings":"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"}
package/dist/reducer.js CHANGED
@@ -182,6 +182,21 @@ export function turnForRetry(messages, requestId) {
182
182
  return undefined;
183
183
  return { user, rest: messages.slice(0, -2) };
184
184
  }
185
+ /**
186
+ * The conversation's latest finished turn (any outcome), to regenerate or edit: its question
187
+ * and the messages before it. Undefined while it streams or when `requestId` is not the latest.
188
+ */
189
+ export function latestTurn(messages, requestId) {
190
+ const last = messages[messages.length - 1];
191
+ if (last?.role !== 'assistant' || last.requestId !== requestId)
192
+ return undefined;
193
+ if (last.status === 'streaming')
194
+ return undefined;
195
+ const user = messages[messages.length - 2];
196
+ if (user?.role !== 'user')
197
+ return undefined;
198
+ return { user, rest: messages.slice(0, -2) };
199
+ }
185
200
  /** Human titles for client error codes (mirrors the task pane's wording). */
186
201
  export function errorTitle(code) {
187
202
  const titles = {
@@ -50,15 +50,23 @@ export declare class ChatSession {
50
50
  get current(): AssistantMessage | undefined;
51
51
  /** Load an existing conversation's transcript and re-attach to a running turn if any. */
52
52
  load(conversationId: string): Promise<void>;
53
+ /**
54
+ * Send a message. `replaces` names the latest turn this one replaces (regenerate, retry,
55
+ * edit); the runtime drops that turn's question and answer before answering.
56
+ */
53
57
  send(text: string, options?: {
54
58
  files?: FileRef[];
59
+ replaces?: string;
55
60
  }): Promise<void>;
56
61
  /**
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.)
62
+ * Resend the last turn after it failed, was stopped or was cut short. The turn is replaced,
63
+ * in the view and in the runtime's transcript, rather than duplicated.
60
64
  */
61
65
  retry(requestId: string): Promise<void>;
66
+ /** Ask again for a new answer to the latest question, replacing the current one. */
67
+ regenerate(requestId: string): Promise<void>;
68
+ /** Change the latest question and answer it again, replacing that turn. */
69
+ edit(requestId: string, text: string): Promise<void>;
62
70
  /** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
63
71
  resume(requestId: string): Promise<void>;
64
72
  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,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
+ {"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,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,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+BjD;;;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,+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, turnForRetry, } from './reducer.js';
2
+ import { applyEvent, newAssistantMessage, newUserMessage, latestTurn, 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.
@@ -58,6 +58,10 @@ export class ChatSession {
58
58
  this.loadError = err instanceof Error ? err.message : 'Could not load the conversation';
59
59
  }
60
60
  }
61
+ /**
62
+ * Send a message. `replaces` names the latest turn this one replaces (regenerate, retry,
63
+ * edit); the runtime drops that turn's question and answer before answering.
64
+ */
61
65
  async send(text, options = {}) {
62
66
  const message = text.trim();
63
67
  if (!message || this.status !== 'idle')
@@ -74,6 +78,7 @@ export class ChatSession {
74
78
  ...(context ? { context } : {}),
75
79
  ...(files.length ? { fileIds: files.map((f) => f.fileId) } : {}),
76
80
  ...(this.options.clientKind ? { client: this.options.clientKind } : {}),
81
+ ...(options.replaces ? { replaces: options.replaces } : {}),
77
82
  };
78
83
  this.messages = [
79
84
  ...this.messages,
@@ -83,9 +88,8 @@ export class ChatSession {
83
88
  await this.run(requestId, (signal, onEvent) => this.options.client.streamChat(request, { signal, onEvent }));
84
89
  }
85
90
  /**
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.)
91
+ * Resend the last turn after it failed, was stopped or was cut short. The turn is replaced,
92
+ * in the view and in the runtime's transcript, rather than duplicated.
89
93
  */
90
94
  async retry(requestId) {
91
95
  if (this.status !== 'idle')
@@ -94,7 +98,27 @@ export class ChatSession {
94
98
  if (!turn)
95
99
  return;
96
100
  this.messages = turn.rest;
97
- await this.send(turn.user.text, { files: turn.user.attachments });
101
+ await this.send(turn.user.text, { files: turn.user.attachments, replaces: requestId });
102
+ }
103
+ /** Ask again for a new answer to the latest question, replacing the current one. */
104
+ async regenerate(requestId) {
105
+ if (this.status !== 'idle')
106
+ return;
107
+ const turn = latestTurn(this.messages, requestId);
108
+ if (!turn)
109
+ return;
110
+ this.messages = turn.rest;
111
+ await this.send(turn.user.text, { files: turn.user.attachments, replaces: requestId });
112
+ }
113
+ /** Change the latest question and answer it again, replacing that turn. */
114
+ async edit(requestId, text) {
115
+ if (this.status !== 'idle' || !text.trim())
116
+ return;
117
+ const turn = latestTurn(this.messages, requestId);
118
+ if (!turn)
119
+ return;
120
+ this.messages = turn.rest;
121
+ await this.send(text, { files: turn.user.attachments, replaces: requestId });
98
122
  }
99
123
  /** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
100
124
  async resume(requestId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/agent-chat",
3
- "version": "0.5.0",
3
+ "version": "0.6.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.5.0",
21
- "@classic-homes/agent-contracts": "0.5.0",
20
+ "@classic-homes/agent-client": "0.6.0",
21
+ "@classic-homes/agent-contracts": "0.6.0",
22
22
  "dompurify": "3.4.17"
23
23
  },
24
24
  "peerDependencies": {