@classic-homes/agent-chat 0.9.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.
- package/README.md +2 -0
- package/dist/components/AgentChat.svelte +4 -1
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/AssistantMessage.svelte +2 -1
- package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
- package/dist/components/ChatHeader.svelte +27 -15
- package/dist/components/ChatHeader.svelte.d.ts +6 -2
- package/dist/components/ChatHeader.svelte.d.ts.map +1 -1
- package/dist/components/ShortcutsDialog.svelte +56 -0
- package/dist/components/ShortcutsDialog.svelte.d.ts +15 -0
- package/dist/components/ShortcutsDialog.svelte.d.ts.map +1 -0
- package/dist/components/a11y.svelte.test.js +11 -0
- package/dist/components/continue.svelte.test.d.ts +2 -0
- package/dist/components/continue.svelte.test.d.ts.map +1 -0
- package/dist/components/continue.svelte.test.js +109 -0
- package/dist/components/conversation-chrome.svelte.test.js +22 -2
- package/dist/components/messages.svelte.test.js +15 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/session.svelte.d.ts +8 -0
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +45 -3
- package/dist/shortcuts.d.ts +11 -0
- package/dist/shortcuts.d.ts.map +1 -0
- package/dist/shortcuts.js +8 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ agent-api origin that serves `/frame`; artifacts and MCP-UI resources only ever
|
|
|
43
43
|
`AgentChat` composes the rest; each is also exported for other layouts:
|
|
44
44
|
|
|
45
45
|
- `ChatHeader`, `ConversationSidebar`: title and conversation actions; the history rail.
|
|
46
|
+
`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.
|
|
46
48
|
- `MessageList`: transcript with day separators and a jump-to-latest button.
|
|
47
49
|
- `MessageItem` → `UserMessage` / `AssistantMessage`, with `MessageActions` (copy, time,
|
|
48
50
|
`MessageFeedback` thumbs up/down with an optional note, Regenerate and Edit on the latest turn),
|
|
@@ -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={() =>
|
|
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;
|
|
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
|
-
|
|
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;
|
|
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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
3
|
Header for a chat: conversation title, agent badge and the conversation menu (rename,
|
|
4
|
-
open full page, delete). Pass it to `AgentChat` through the `header`
|
|
5
|
-
show when their handler is set; rename and delete also need a
|
|
4
|
+
open full page, delete, keyboard shortcuts). Pass it to `AgentChat` through the `header`
|
|
5
|
+
snippet. Actions only show when their handler is set; rename and delete also need a
|
|
6
|
+
`conversationId`. "Keyboard shortcuts" always shows and lists the host's `shortcuts` first.
|
|
6
7
|
-->
|
|
7
8
|
<script lang="ts">
|
|
8
9
|
import {
|
|
@@ -14,7 +15,9 @@
|
|
|
14
15
|
type DropdownMenuItem,
|
|
15
16
|
} from '@classic-homes/theme-svelte';
|
|
16
17
|
import type { Snippet } from 'svelte';
|
|
18
|
+
import type { ChatShortcut } from '../shortcuts.js';
|
|
17
19
|
import ConversationDialogs, { type ConversationRef } from './ConversationDialogs.svelte';
|
|
20
|
+
import ShortcutsDialog from './ShortcutsDialog.svelte';
|
|
18
21
|
|
|
19
22
|
interface Props {
|
|
20
23
|
title?: string;
|
|
@@ -29,6 +32,8 @@
|
|
|
29
32
|
onrename?: (conversationId: string, title: string) => void | Promise<void>;
|
|
30
33
|
ondelete?: (conversationId: string) => void | Promise<void>;
|
|
31
34
|
onerror?: (message: string) => void;
|
|
35
|
+
/** Host shortcuts for the "Keyboard shortcuts" dialog, e.g. opening the panel. */
|
|
36
|
+
shortcuts?: ChatShortcut[];
|
|
32
37
|
/** Extra controls before the menu. */
|
|
33
38
|
actions?: Snippet;
|
|
34
39
|
class?: string;
|
|
@@ -43,12 +48,14 @@
|
|
|
43
48
|
onrename,
|
|
44
49
|
ondelete,
|
|
45
50
|
onerror,
|
|
51
|
+
shortcuts,
|
|
46
52
|
actions,
|
|
47
53
|
class: className,
|
|
48
54
|
}: Props = $props();
|
|
49
55
|
|
|
50
56
|
let renaming = $state<ConversationRef | null>(null);
|
|
51
57
|
let deleting = $state<ConversationRef | null>(null);
|
|
58
|
+
let showingShortcuts = $state(false);
|
|
52
59
|
let headerEl = $state<HTMLElement | null>(null);
|
|
53
60
|
let returnFocus = $state<HTMLElement | null>(null);
|
|
54
61
|
/** Remember the menu trigger before a dialog takes focus from the closing menu. */
|
|
@@ -77,6 +84,12 @@
|
|
|
77
84
|
onSelect: () => open(() => (deleting = current)),
|
|
78
85
|
});
|
|
79
86
|
}
|
|
87
|
+
if (list.length) list.push({ type: 'separator' });
|
|
88
|
+
list.push({
|
|
89
|
+
label: 'Keyboard shortcuts',
|
|
90
|
+
icon: 'keyboard',
|
|
91
|
+
onSelect: () => open(() => (showingShortcuts = true)),
|
|
92
|
+
});
|
|
80
93
|
return list;
|
|
81
94
|
});
|
|
82
95
|
</script>
|
|
@@ -104,19 +117,18 @@
|
|
|
104
117
|
><Icon name="plus" size={18} /></Button
|
|
105
118
|
>
|
|
106
119
|
{/if}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</DropdownMenu>
|
|
119
|
-
{/if}
|
|
120
|
+
<DropdownMenu
|
|
121
|
+
{items}
|
|
122
|
+
align="end"
|
|
123
|
+
triggerVariant="ghost"
|
|
124
|
+
triggerSize="icon"
|
|
125
|
+
triggerClass="size-8"
|
|
126
|
+
triggerLabel="Conversation options"
|
|
127
|
+
showChevron={false}
|
|
128
|
+
>
|
|
129
|
+
{#snippet trigger()}<Icon name="more-horizontal" size={18} />{/snippet}
|
|
130
|
+
</DropdownMenu>
|
|
120
131
|
</header>
|
|
121
132
|
|
|
122
133
|
<ConversationDialogs bind:renaming bind:deleting {onrename} {ondelete} {onerror} {returnFocus} />
|
|
134
|
+
<ShortcutsDialog bind:open={showingShortcuts} {shortcuts} {returnFocus} />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ChatShortcut } from '../shortcuts.js';
|
|
2
3
|
interface Props {
|
|
3
4
|
title?: string;
|
|
4
5
|
conversationId?: string | null;
|
|
@@ -12,14 +13,17 @@ interface Props {
|
|
|
12
13
|
onrename?: (conversationId: string, title: string) => void | Promise<void>;
|
|
13
14
|
ondelete?: (conversationId: string) => void | Promise<void>;
|
|
14
15
|
onerror?: (message: string) => void;
|
|
16
|
+
/** Host shortcuts for the "Keyboard shortcuts" dialog, e.g. opening the panel. */
|
|
17
|
+
shortcuts?: ChatShortcut[];
|
|
15
18
|
/** Extra controls before the menu. */
|
|
16
19
|
actions?: Snippet;
|
|
17
20
|
class?: string;
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
23
|
* Header for a chat: conversation title, agent badge and the conversation menu (rename,
|
|
21
|
-
* open full page, delete). Pass it to `AgentChat` through the `header`
|
|
22
|
-
* show when their handler is set; rename and delete also need a
|
|
24
|
+
* open full page, delete, keyboard shortcuts). Pass it to `AgentChat` through the `header`
|
|
25
|
+
* snippet. Actions only show when their handler is set; rename and delete also need a
|
|
26
|
+
* `conversationId`. "Keyboard shortcuts" always shows and lists the host's `shortcuts` first.
|
|
23
27
|
*/
|
|
24
28
|
declare const ChatHeader: import("svelte").Component<Props, {}, "">;
|
|
25
29
|
type ChatHeader = ReturnType<typeof ChatHeader>;
|
|
@@ -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;
|
|
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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Lists the chat's keyboard shortcuts (`CHAT_SHORTCUTS`) and the host's own (`shortcuts`, shown
|
|
4
|
+
first). Opened from the `ChatHeader` menu; focus returns to `returnFocus` on close.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { Dialog, Kbd } from '@classic-homes/theme-svelte';
|
|
8
|
+
import { releaseStuckBodyLock } from '../body-lock.js';
|
|
9
|
+
import { CHAT_SHORTCUTS, type ChatShortcut } from '../shortcuts.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
open?: boolean;
|
|
13
|
+
/** Host shortcuts, e.g. opening the agent panel or starting a new chat. */
|
|
14
|
+
shortcuts?: ChatShortcut[];
|
|
15
|
+
returnFocus?: HTMLElement | null;
|
|
16
|
+
}
|
|
17
|
+
let { open = $bindable(false), shortcuts = [], returnFocus = null }: Props = $props();
|
|
18
|
+
|
|
19
|
+
const groups = $derived(
|
|
20
|
+
[
|
|
21
|
+
{ title: 'App', items: shortcuts },
|
|
22
|
+
{ title: 'Chat', items: [...CHAT_SHORTCUTS] },
|
|
23
|
+
].filter((g) => g.items.length),
|
|
24
|
+
);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<Dialog
|
|
28
|
+
bind:open
|
|
29
|
+
title="Keyboard shortcuts"
|
|
30
|
+
size="sm"
|
|
31
|
+
onCloseAutoFocus={(event: Event) => {
|
|
32
|
+
if (returnFocus?.isConnected) {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
returnFocus.focus();
|
|
35
|
+
}
|
|
36
|
+
releaseStuckBodyLock();
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<div class="grid gap-4">
|
|
40
|
+
{#each groups as group (group.title)}
|
|
41
|
+
<section>
|
|
42
|
+
<h3 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
43
|
+
{group.title}
|
|
44
|
+
</h3>
|
|
45
|
+
<dl class="divide-y divide-border text-sm">
|
|
46
|
+
{#each group.items as s (s.label)}
|
|
47
|
+
<div class="flex items-center justify-between gap-4 py-1.5">
|
|
48
|
+
<dt>{s.label}</dt>
|
|
49
|
+
<dd class="shrink-0"><Kbd keys={s.keys} size="sm" /></dd>
|
|
50
|
+
</div>
|
|
51
|
+
{/each}
|
|
52
|
+
</dl>
|
|
53
|
+
</section>
|
|
54
|
+
{/each}
|
|
55
|
+
</div>
|
|
56
|
+
</Dialog>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ChatShortcut } from '../shortcuts.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
/** Host shortcuts, e.g. opening the agent panel or starting a new chat. */
|
|
5
|
+
shortcuts?: ChatShortcut[];
|
|
6
|
+
returnFocus?: HTMLElement | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Lists the chat's keyboard shortcuts (`CHAT_SHORTCUTS`) and the host's own (`shortcuts`, shown
|
|
10
|
+
* first). Opened from the `ChatHeader` menu; focus returns to `returnFocus` on close.
|
|
11
|
+
*/
|
|
12
|
+
declare const ShortcutsDialog: import("svelte").Component<Props, {}, "open">;
|
|
13
|
+
type ShortcutsDialog = ReturnType<typeof ShortcutsDialog>;
|
|
14
|
+
export default ShortcutsDialog;
|
|
15
|
+
//# sourceMappingURL=ShortcutsDialog.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShortcutsDialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ShortcutsDialog.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGlE,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2EAA2E;IAC3E,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAClC;AA6CH;;;GAGG;AACH,QAAA,MAAM,eAAe,+CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -3,6 +3,7 @@ import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
|
|
|
3
3
|
import axe from 'axe-core';
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
5
|
import AgentChat from './AgentChat.svelte';
|
|
6
|
+
import ChatHeader from './ChatHeader.svelte';
|
|
6
7
|
import ConversationSidebar from './ConversationSidebar.svelte';
|
|
7
8
|
afterEach(() => {
|
|
8
9
|
cleanup();
|
|
@@ -100,6 +101,16 @@ describe('accessibility (axe)', () => {
|
|
|
100
101
|
await user.keyboard('{Escape}');
|
|
101
102
|
expect(await violations()).toEqual([]);
|
|
102
103
|
});
|
|
104
|
+
it('has no violations in the keyboard shortcuts dialog', async () => {
|
|
105
|
+
const user = userEvent.setup();
|
|
106
|
+
render(ChatHeader, {
|
|
107
|
+
props: { title: 'x', shortcuts: [{ keys: ['Mod', 'J'], label: 'Open the agent panel' }] },
|
|
108
|
+
});
|
|
109
|
+
await user.click(screen.getByRole('button', { name: 'Conversation options' }));
|
|
110
|
+
await user.click(await screen.findByRole('menuitem', { name: /Keyboard shortcuts/ }));
|
|
111
|
+
await screen.findByRole('dialog', { name: 'Keyboard shortcuts' });
|
|
112
|
+
expect(await violations()).toEqual([]);
|
|
113
|
+
});
|
|
103
114
|
it('has no violations in the conversation rail', async () => {
|
|
104
115
|
const now = new Date().toISOString();
|
|
105
116
|
render(ConversationSidebar, {
|
|
@@ -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
|
+
});
|
|
@@ -94,11 +94,31 @@ describe('ConversationSidebar', () => {
|
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
96
|
describe('ChatHeader', () => {
|
|
97
|
-
it('shows the title, falls back to "New chat" and hides actions without handlers', () => {
|
|
97
|
+
it('shows the title, falls back to "New chat" and hides actions without handlers', async () => {
|
|
98
|
+
const user = userEvent.setup();
|
|
98
99
|
render(ChatHeader, { props: { title: ' ' } });
|
|
99
100
|
expect(screen.getByRole('heading', { name: 'New chat' })).toBeTruthy();
|
|
100
|
-
expect(screen.queryByRole('button', { name: 'Conversation options' })).toBeNull();
|
|
101
101
|
expect(screen.queryByRole('button', { name: 'Show conversations' })).toBeNull();
|
|
102
|
+
// Only the shortcuts entry is left in the menu.
|
|
103
|
+
await user.click(screen.getByRole('button', { name: 'Conversation options' }));
|
|
104
|
+
const items = await screen.findAllByRole('menuitem');
|
|
105
|
+
expect(items.map((i) => i.textContent?.trim())).toEqual(['Keyboard shortcuts']);
|
|
106
|
+
});
|
|
107
|
+
it('lists host and chat shortcuts, then returns focus to the menu button', async () => {
|
|
108
|
+
const user = userEvent.setup();
|
|
109
|
+
render(ChatHeader, {
|
|
110
|
+
props: { title: 'x', shortcuts: [{ keys: ['Mod', 'Shift', 'O'], label: 'New chat' }] },
|
|
111
|
+
});
|
|
112
|
+
const trigger = screen.getByRole('button', { name: 'Conversation options' });
|
|
113
|
+
await user.click(trigger);
|
|
114
|
+
await user.click(await screen.findByRole('menuitem', { name: /Keyboard shortcuts/ }));
|
|
115
|
+
const dialog = await screen.findByRole('dialog', { name: 'Keyboard shortcuts' });
|
|
116
|
+
expect(dialog.textContent).toContain('New chat');
|
|
117
|
+
expect(dialog.textContent).toContain('Stop the reply');
|
|
118
|
+
expect(dialog.textContent).toContain('Insert a saved prompt');
|
|
119
|
+
await user.keyboard('{Escape}');
|
|
120
|
+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
121
|
+
await waitFor(() => expect(document.activeElement).toBe(trigger));
|
|
102
122
|
});
|
|
103
123
|
const handlers = () => ({ title: 'Lot 12 budget', onrename: vi.fn(), ondelete: vi.fn() });
|
|
104
124
|
it('offers no rename or delete before the conversation is saved', async () => {
|
|
@@ -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: {
|
|
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();
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './markdown.js';
|
|
|
5
5
|
export * from './message-format.js';
|
|
6
6
|
export * from './reducer.js';
|
|
7
7
|
export * from './session.svelte.js';
|
|
8
|
+
export * from './shortcuts.js';
|
|
8
9
|
export * from './ui-frame-protocol.js';
|
|
9
10
|
export * from './welcome.js';
|
|
10
11
|
export { default as AgentChat } from './components/AgentChat.svelte';
|
|
@@ -24,4 +25,5 @@ export { default as ArtifactChip } from './components/ArtifactChip.svelte';
|
|
|
24
25
|
export { default as MessageFiles } from './components/MessageFiles.svelte';
|
|
25
26
|
export { default as ProposalCard } from './components/ProposalCard.svelte';
|
|
26
27
|
export { default as Welcome } from './components/Welcome.svelte';
|
|
28
|
+
export { default as ShortcutsDialog } from './components/ShortcutsDialog.svelte';
|
|
27
29
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,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;AACvE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,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,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,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;AACvE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,qCAAqC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from './markdown.js';
|
|
|
5
5
|
export * from './message-format.js';
|
|
6
6
|
export * from './reducer.js';
|
|
7
7
|
export * from './session.svelte.js';
|
|
8
|
+
export * from './shortcuts.js';
|
|
8
9
|
export * from './ui-frame-protocol.js';
|
|
9
10
|
export * from './welcome.js';
|
|
10
11
|
export { default as AgentChat } from './components/AgentChat.svelte';
|
|
@@ -24,3 +25,4 @@ export { default as ArtifactChip } from './components/ArtifactChip.svelte';
|
|
|
24
25
|
export { default as MessageFiles } from './components/MessageFiles.svelte';
|
|
25
26
|
export { default as ProposalCard } from './components/ProposalCard.svelte';
|
|
26
27
|
export { default as Welcome } from './components/Welcome.svelte';
|
|
28
|
+
export { default as ShortcutsDialog } from './components/ShortcutsDialog.svelte';
|
package/dist/session.svelte.d.ts
CHANGED
|
@@ -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":"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,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;
|
|
1
|
+
{"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"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"}
|
package/dist/session.svelte.js
CHANGED
|
@@ -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
|
-
|
|
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?.({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcuts listed in the chat's "Keyboard shortcuts" dialog. `keys` use the theme
|
|
3
|
+
* `Kbd` names ("Mod" shows as ⌘ on Apple devices and Ctrl elsewhere).
|
|
4
|
+
*/
|
|
5
|
+
export interface ChatShortcut {
|
|
6
|
+
keys: string[];
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
/** What the chat itself handles, in the composer. Hosts add their own (e.g. a panel toggle). */
|
|
10
|
+
export declare const CHAT_SHORTCUTS: readonly ChatShortcut[];
|
|
11
|
+
//# sourceMappingURL=shortcuts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shortcuts.d.ts","sourceRoot":"","sources":["../src/lib/shortcuts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gGAAgG;AAChG,eAAO,MAAM,cAAc,EAAE,SAAS,YAAY,EAMjD,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** What the chat itself handles, in the composer. Hosts add their own (e.g. a panel toggle). */
|
|
2
|
+
export const CHAT_SHORTCUTS = [
|
|
3
|
+
{ keys: ['Enter'], label: 'Send the message' },
|
|
4
|
+
{ keys: ['Shift', 'Enter'], label: 'New line' },
|
|
5
|
+
{ keys: ['Esc'], label: 'Stop the reply' },
|
|
6
|
+
{ keys: ['/'], label: 'Insert a saved prompt' },
|
|
7
|
+
{ keys: ['Esc'], label: 'Cancel editing a message' },
|
|
8
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/agent-chat",
|
|
3
|
-
"version": "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.
|
|
21
|
-
"@classic-homes/agent-contracts": "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": {
|