@classic-homes/agent-chat 0.3.0 → 0.4.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 +5 -1
- package/dist/components/AgentChat.svelte +9 -0
- package/dist/components/AgentChat.svelte.d.ts +5 -0
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/AssistantMessage.svelte +8 -1
- package/dist/components/AssistantMessage.svelte.d.ts +1 -0
- package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
- package/dist/components/Composer.svelte +221 -30
- package/dist/components/Composer.svelte.d.ts +8 -0
- package/dist/components/Composer.svelte.d.ts.map +1 -1
- package/dist/components/MessageItem.svelte +1 -0
- package/dist/components/MessageItem.svelte.d.ts +1 -0
- package/dist/components/MessageItem.svelte.d.ts.map +1 -1
- package/dist/components/MessageList.svelte +3 -0
- package/dist/components/MessageList.svelte.d.ts +1 -0
- package/dist/components/MessageList.svelte.d.ts.map +1 -1
- package/dist/components/ToolTimeline.svelte +138 -54
- package/dist/components/ToolTimeline.svelte.d.ts +11 -3
- package/dist/components/ToolTimeline.svelte.d.ts.map +1 -1
- package/dist/components/composer.svelte.test.d.ts +2 -0
- package/dist/components/composer.svelte.test.d.ts.map +1 -0
- package/dist/components/composer.svelte.test.js +162 -0
- package/dist/composer-keys.d.ts +3 -0
- package/dist/composer-keys.d.ts.map +1 -1
- package/dist/composer-keys.js +18 -0
- package/dist/composer-prompts.d.ts +16 -0
- package/dist/composer-prompts.d.ts.map +1 -0
- package/dist/composer-prompts.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/message-format.d.ts +28 -0
- package/dist/message-format.d.ts.map +1 -1
- package/dist/message-format.js +31 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -47,6 +47,10 @@ agent-api origin that serves `/frame`; artifacts and MCP-UI resources only ever
|
|
|
47
47
|
- `MessageItem` → `UserMessage` / `AssistantMessage`, with `MessageActions` (copy, time),
|
|
48
48
|
`ArtifactChip`, `MessageFiles` (image lightbox, downloads) and `ProposalCard` (read-only
|
|
49
49
|
document edits; applying stays in the Office add-in).
|
|
50
|
-
- `Composer
|
|
50
|
+
- `Composer`: attach by button, drop or paste; `prompts` turns on the `/` saved-prompt menu
|
|
51
|
+
(`ComposerPrompt.text` may be a loader, e.g. fetching the template).
|
|
52
|
+
- `ToolTimeline`: tool calls with status and duration, delegations nested; `toolLabels` maps
|
|
53
|
+
tool names to readable ones (`AgentChat` passes both through).
|
|
54
|
+
- `ArtifactPanel`, `UiFrame`, `ModeBanner`.
|
|
51
55
|
|
|
52
56
|
Publishing: `docs/operations/package-publishing.md`.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { cn, ResizablePanes, Sheet } from '@classic-homes/theme-svelte';
|
|
11
11
|
import { MediaQuery } from 'svelte/reactivity';
|
|
12
12
|
import { releaseStuckBodyLock } from '../body-lock.js';
|
|
13
|
+
import type { ComposerPrompt } from '../composer-prompts.js';
|
|
13
14
|
import { ChatSession } from '../session.svelte.js';
|
|
14
15
|
import ArtifactPanel from './ArtifactPanel.svelte';
|
|
15
16
|
import Composer from './Composer.svelte';
|
|
@@ -40,6 +41,10 @@
|
|
|
40
41
|
/** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
|
|
41
42
|
ontoolrequest?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
42
43
|
placeholder?: string;
|
|
44
|
+
/** Saved prompts offered by the composer's `/` menu. */
|
|
45
|
+
prompts?: ComposerPrompt[];
|
|
46
|
+
/** Readable tool names for the timeline, by tool name (`server/name` or `name`). */
|
|
47
|
+
toolLabels?: Record<string, string>;
|
|
43
48
|
/** Rendered above the transcript, e.g. a `ChatHeader`. */
|
|
44
49
|
header?: import('svelte').Snippet;
|
|
45
50
|
context?: import('svelte').Snippet;
|
|
@@ -62,6 +67,8 @@
|
|
|
62
67
|
onnotify,
|
|
63
68
|
ontoolrequest,
|
|
64
69
|
placeholder,
|
|
70
|
+
prompts,
|
|
71
|
+
toolLabels,
|
|
65
72
|
header,
|
|
66
73
|
context,
|
|
67
74
|
empty,
|
|
@@ -146,6 +153,7 @@
|
|
|
146
153
|
streaming={session.streaming}
|
|
147
154
|
{sandboxOrigin}
|
|
148
155
|
fileUrl={(id) => client.fileUrl(id)}
|
|
156
|
+
{toolLabels}
|
|
149
157
|
{onnavigate}
|
|
150
158
|
onopenartifact={(id) => {
|
|
151
159
|
selectedArtifact = id;
|
|
@@ -165,6 +173,7 @@
|
|
|
165
173
|
disabled={session.status === 'loading'}
|
|
166
174
|
streaming={session.streaming}
|
|
167
175
|
{placeholder}
|
|
176
|
+
{prompts}
|
|
168
177
|
upload={(file) => client.uploadFile(file, file.name)}
|
|
169
178
|
onsend={send}
|
|
170
179
|
oncancel={() => session.cancel()}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentApiClient } from '@classic-homes/agent-client';
|
|
2
2
|
import type { AgentKey, ClientKind, DocumentContext } from '@classic-homes/agent-contracts';
|
|
3
3
|
import type { BackendMode } from '@classic-homes/agent-contracts';
|
|
4
|
+
import type { ComposerPrompt } from '../composer-prompts.js';
|
|
4
5
|
interface Props {
|
|
5
6
|
client: AgentApiClient;
|
|
6
7
|
/** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
|
|
@@ -25,6 +26,10 @@ interface Props {
|
|
|
25
26
|
/** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
|
|
26
27
|
ontoolrequest?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
27
28
|
placeholder?: string;
|
|
29
|
+
/** Saved prompts offered by the composer's `/` menu. */
|
|
30
|
+
prompts?: ComposerPrompt[];
|
|
31
|
+
/** Readable tool names for the timeline, by tool name (`server/name` or `name`). */
|
|
32
|
+
toolLabels?: Record<string, string>;
|
|
28
33
|
/** Rendered above the transcript, e.g. a `ChatHeader`. */
|
|
29
34
|
header?: import('svelte').Snippet;
|
|
30
35
|
context?: import('svelte').Snippet;
|
|
@@ -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,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,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,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAIlE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAQ3D,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,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,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,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAgJH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
last?: boolean;
|
|
24
24
|
sandboxOrigin: string;
|
|
25
25
|
fileUrl: (fileId: string) => string;
|
|
26
|
+
toolLabels?: Record<string, string>;
|
|
26
27
|
onnavigate?: (href: string) => void;
|
|
27
28
|
onopenartifact?: (artifactId: string) => void;
|
|
28
29
|
onprompt?: (text: string) => void;
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
last = false,
|
|
39
40
|
sandboxOrigin,
|
|
40
41
|
fileUrl,
|
|
42
|
+
toolLabels,
|
|
41
43
|
onnavigate,
|
|
42
44
|
onopenartifact,
|
|
43
45
|
onprompt,
|
|
@@ -101,7 +103,12 @@
|
|
|
101
103
|
</span>
|
|
102
104
|
<div class="flex min-w-0 flex-1 flex-col gap-2">
|
|
103
105
|
{#if message.tools.length || message.delegations.length}
|
|
104
|
-
<ToolTimeline
|
|
106
|
+
<ToolTimeline
|
|
107
|
+
tools={message.tools}
|
|
108
|
+
delegations={message.delegations}
|
|
109
|
+
{streaming}
|
|
110
|
+
{toolLabels}
|
|
111
|
+
/>
|
|
105
112
|
{/if}
|
|
106
113
|
{#if html}<Markdown {html} streaming={streaming && !!message.text} onclick={handleClick} />{/if}
|
|
107
114
|
{#if thinking}
|
|
@@ -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,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;
|
|
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,8 +1,16 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The message box: an autosizing textarea in a card with attach, the host's context chip and
|
|
4
|
+
Send/Stop. Files can also be dropped on it or pasted. Typing `/` at the start opens the saved
|
|
5
|
+
prompt menu when the host passes `prompts`.
|
|
6
|
+
-->
|
|
1
7
|
<script lang="ts">
|
|
2
|
-
import { Button, Icon, Textarea } from '@classic-homes/theme-svelte';
|
|
8
|
+
import { cn, Button, Icon, Kbd, Popover, Spinner, Textarea } from '@classic-homes/theme-svelte';
|
|
3
9
|
import type { FileRef } from '@classic-homes/agent-contracts';
|
|
4
10
|
import { LIMITS } from '@classic-homes/agent-contracts';
|
|
5
|
-
import { composerKeyAction } from '../composer-keys.js';
|
|
11
|
+
import { composerKeyAction, promptMenuKeyAction } from '../composer-keys.js';
|
|
12
|
+
import { filterPrompts, promptQuery, type ComposerPrompt } from '../composer-prompts.js';
|
|
13
|
+
import { fileIcon, formatBytes } from '../message-format.js';
|
|
6
14
|
|
|
7
15
|
interface Props {
|
|
8
16
|
disabled?: boolean;
|
|
@@ -12,6 +20,8 @@
|
|
|
12
20
|
placeholder?: string;
|
|
13
21
|
/** Upload a file; the composer shows it as pending until the message is sent. */
|
|
14
22
|
upload?: (file: File) => Promise<FileRef>;
|
|
23
|
+
/** Saved prompts for the `/` menu; the menu is off without them. */
|
|
24
|
+
prompts?: ComposerPrompt[];
|
|
15
25
|
onsend?: (text: string) => void;
|
|
16
26
|
oncancel?: () => void;
|
|
17
27
|
onremovefile?: (fileId: string) => void;
|
|
@@ -25,28 +35,76 @@
|
|
|
25
35
|
files = $bindable([]),
|
|
26
36
|
placeholder = 'Ask Classic Agent…',
|
|
27
37
|
upload,
|
|
38
|
+
prompts,
|
|
28
39
|
onsend,
|
|
29
40
|
oncancel,
|
|
30
41
|
onremovefile,
|
|
31
42
|
context,
|
|
32
43
|
}: Props = $props();
|
|
33
44
|
|
|
45
|
+
const uid = $props.id();
|
|
34
46
|
let textarea = $state<HTMLTextAreaElement | null>(null);
|
|
47
|
+
let card: HTMLDivElement | undefined = $state();
|
|
35
48
|
let fileInput: HTMLInputElement | undefined = $state();
|
|
36
|
-
|
|
49
|
+
/** Files being uploaded right now, shown as chips with a spinner. */
|
|
50
|
+
let pending = $state<{ key: string; name: string; bytes: number; mimeType: string }[]>([]);
|
|
37
51
|
let uploadError = $state<string | undefined>();
|
|
52
|
+
let dragging = $state(false);
|
|
53
|
+
let dragDepth = 0;
|
|
38
54
|
|
|
39
55
|
export function focus(): void {
|
|
40
56
|
textarea?.focus({ preventScroll: true });
|
|
41
57
|
}
|
|
42
58
|
|
|
59
|
+
// `/` prompt menu
|
|
60
|
+
const query = $derived(prompts?.length ? promptQuery(value) : null);
|
|
61
|
+
const matches = $derived(query === null ? [] : filterPrompts(prompts ?? [], query));
|
|
62
|
+
let dismissedFor = $state<string | null>(null);
|
|
63
|
+
const menuOpen = $derived(query !== null && matches.length > 0 && dismissedFor !== value);
|
|
64
|
+
let active = $state(0);
|
|
65
|
+
let loadingPrompt = $state(false);
|
|
66
|
+
$effect(() => {
|
|
67
|
+
void query;
|
|
68
|
+
active = 0;
|
|
69
|
+
});
|
|
70
|
+
async function choose(prompt: ComposerPrompt | undefined): Promise<void> {
|
|
71
|
+
if (!prompt) return;
|
|
72
|
+
if (typeof prompt.text === 'string') value = prompt.text;
|
|
73
|
+
else {
|
|
74
|
+
loadingPrompt = true;
|
|
75
|
+
try {
|
|
76
|
+
value = await prompt.text();
|
|
77
|
+
} catch {
|
|
78
|
+
uploadError = `Could not load “${prompt.label}”`;
|
|
79
|
+
} finally {
|
|
80
|
+
loadingPrompt = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
focus();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const chars = $derived(value.length);
|
|
87
|
+
const showCount = $derived(chars > LIMITS.messageChars * 0.8);
|
|
88
|
+
const busy = $derived(pending.length > 0 || loadingPrompt);
|
|
89
|
+
|
|
43
90
|
function submit(): void {
|
|
44
91
|
const text = value.trim();
|
|
45
|
-
if (!text || disabled || streaming ||
|
|
92
|
+
if (!text || disabled || streaming || busy) return;
|
|
46
93
|
value = '';
|
|
47
94
|
onsend?.(text);
|
|
48
95
|
}
|
|
49
96
|
function onkeydown(event: KeyboardEvent): void {
|
|
97
|
+
if (menuOpen) {
|
|
98
|
+
const menu = promptMenuKeyAction(event);
|
|
99
|
+
if (menu) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
if (menu === 'next') active = (active + 1) % matches.length;
|
|
102
|
+
else if (menu === 'previous') active = (active - 1 + matches.length) % matches.length;
|
|
103
|
+
else if (menu === 'choose') void choose(matches[active]);
|
|
104
|
+
else dismissedFor = value;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
50
108
|
const action = composerKeyAction(event, streaming);
|
|
51
109
|
if (action === 'send') {
|
|
52
110
|
event.preventDefault();
|
|
@@ -56,31 +114,70 @@
|
|
|
56
114
|
oncancel?.();
|
|
57
115
|
}
|
|
58
116
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const chosen = [...(input.files ?? [])];
|
|
62
|
-
input.value = '';
|
|
117
|
+
|
|
118
|
+
async function addFiles(chosen: File[]): Promise<void> {
|
|
63
119
|
if (!upload || chosen.length === 0) return;
|
|
64
120
|
uploadError = undefined;
|
|
65
121
|
for (const file of chosen) {
|
|
122
|
+
if (files.length + pending.length >= LIMITS.requestFiles) {
|
|
123
|
+
uploadError = `Attach up to ${LIMITS.requestFiles} files per message`;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
66
126
|
if (file.size > LIMITS.fileBytes) {
|
|
67
127
|
uploadError = `${file.name} is larger than ${Math.round(LIMITS.fileBytes / 1048576)} MB`;
|
|
68
128
|
continue;
|
|
69
129
|
}
|
|
70
|
-
|
|
130
|
+
const key = `${file.name}-${file.size}-${Math.random().toString(36).slice(2)}`;
|
|
131
|
+
pending = [...pending, { key, name: file.name, bytes: file.size, mimeType: file.type }];
|
|
71
132
|
try {
|
|
72
133
|
files = [...files, await upload(file)];
|
|
73
134
|
} catch (err) {
|
|
74
135
|
uploadError = err instanceof Error ? err.message : `Could not upload ${file.name}`;
|
|
75
136
|
} finally {
|
|
76
|
-
|
|
137
|
+
pending = pending.filter((p) => p.key !== key);
|
|
77
138
|
}
|
|
78
139
|
}
|
|
79
140
|
}
|
|
141
|
+
function onfiles(event: Event): void {
|
|
142
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
143
|
+
const chosen = [...(input.files ?? [])];
|
|
144
|
+
input.value = '';
|
|
145
|
+
void addFiles(chosen);
|
|
146
|
+
}
|
|
147
|
+
function onpaste(event: ClipboardEvent): void {
|
|
148
|
+
const pasted = [...(event.clipboardData?.files ?? [])];
|
|
149
|
+
if (!upload || pasted.length === 0) return;
|
|
150
|
+
// Pasting a screenshot or a copied file attaches it; plain text pastes as usual.
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
void addFiles(pasted);
|
|
153
|
+
}
|
|
154
|
+
const hasFiles = (event: DragEvent) => event.dataTransfer?.types.includes('Files') ?? false;
|
|
155
|
+
function ondragenter(event: DragEvent): void {
|
|
156
|
+
if (!upload || disabled || !hasFiles(event)) return;
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
dragDepth += 1;
|
|
159
|
+
dragging = true;
|
|
160
|
+
}
|
|
161
|
+
function ondragover(event: DragEvent): void {
|
|
162
|
+
if (!upload || disabled || !hasFiles(event)) return;
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
|
|
165
|
+
}
|
|
166
|
+
function ondragleave(): void {
|
|
167
|
+
dragDepth = Math.max(0, dragDepth - 1);
|
|
168
|
+
if (dragDepth === 0) dragging = false;
|
|
169
|
+
}
|
|
170
|
+
function ondrop(event: DragEvent): void {
|
|
171
|
+
if (!upload || disabled || !hasFiles(event)) return;
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
dragDepth = 0;
|
|
174
|
+
dragging = false;
|
|
175
|
+
void addFiles([...(event.dataTransfer?.files ?? [])]);
|
|
176
|
+
}
|
|
80
177
|
</script>
|
|
81
178
|
|
|
82
179
|
<form
|
|
83
|
-
class="bg-background px-4 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-2"
|
|
180
|
+
class="group/composer bg-background px-4 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-2"
|
|
84
181
|
onsubmit={(e) => {
|
|
85
182
|
e.preventDefault();
|
|
86
183
|
submit();
|
|
@@ -88,29 +185,58 @@
|
|
|
88
185
|
aria-label="Send a message"
|
|
89
186
|
>
|
|
90
187
|
<div
|
|
91
|
-
|
|
188
|
+
bind:this={card}
|
|
189
|
+
class={cn(
|
|
190
|
+
'relative mx-auto w-full max-w-3xl rounded-xl border border-input bg-background shadow-sm transition-shadow focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/20',
|
|
191
|
+
dragging && 'border-primary ring-2 ring-primary/30',
|
|
192
|
+
)}
|
|
193
|
+
role="group"
|
|
194
|
+
aria-label="Message composer"
|
|
195
|
+
{ondragenter}
|
|
196
|
+
{ondragover}
|
|
197
|
+
{ondragleave}
|
|
198
|
+
{ondrop}
|
|
92
199
|
>
|
|
93
|
-
{#if
|
|
94
|
-
<div
|
|
200
|
+
{#if dragging}
|
|
201
|
+
<div
|
|
202
|
+
class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center gap-2 rounded-xl bg-background/90 text-sm font-medium text-primary"
|
|
203
|
+
>
|
|
204
|
+
<Icon name="paperclip" size={16} /> Drop files to attach
|
|
205
|
+
</div>
|
|
206
|
+
{/if}
|
|
207
|
+
{#if files.length || pending.length || uploadError}
|
|
208
|
+
<ul class="flex flex-wrap items-center gap-1.5 px-3 pt-3 text-xs" aria-label="Attachments">
|
|
95
209
|
{#each files as file (file.fileId)}
|
|
96
|
-
<
|
|
97
|
-
class="inline-flex max-w-
|
|
210
|
+
<li
|
|
211
|
+
class="inline-flex max-w-60 items-center gap-1.5 rounded-md border border-border bg-background py-1 pl-2 pr-0.5"
|
|
98
212
|
>
|
|
99
|
-
<Icon name=
|
|
213
|
+
<Icon name={fileIcon(file.mimeType)} size={14} class="shrink-0 text-muted-foreground" />
|
|
100
214
|
<span class="truncate">{file.name}</span>
|
|
101
|
-
<
|
|
215
|
+
<span class="shrink-0 text-muted-foreground">{formatBytes(file.bytes)}</span>
|
|
216
|
+
<Button
|
|
102
217
|
type="button"
|
|
103
|
-
|
|
218
|
+
variant="ghost"
|
|
219
|
+
size="icon"
|
|
220
|
+
class="size-6 shrink-0 text-muted-foreground"
|
|
104
221
|
aria-label={`Remove ${file.name}`}
|
|
105
222
|
onclick={() => {
|
|
106
223
|
files = files.filter((f) => f.fileId !== file.fileId);
|
|
107
224
|
onremovefile?.(file.fileId);
|
|
108
|
-
}}><Icon name="close" size={12} /></
|
|
225
|
+
}}><Icon name="close" size={12} /></Button
|
|
109
226
|
>
|
|
110
|
-
</
|
|
227
|
+
</li>
|
|
111
228
|
{/each}
|
|
112
|
-
{#
|
|
113
|
-
|
|
229
|
+
{#each pending as file (file.key)}
|
|
230
|
+
<li
|
|
231
|
+
class="inline-flex max-w-60 items-center gap-1.5 rounded-md border border-dashed border-border py-1 pl-2 pr-2 text-muted-foreground"
|
|
232
|
+
>
|
|
233
|
+
<Spinner size="sm" label={`Uploading ${file.name}`} class="shrink-0" />
|
|
234
|
+
<span class="truncate">{file.name}</span>
|
|
235
|
+
<span class="shrink-0">{formatBytes(file.bytes)}</span>
|
|
236
|
+
</li>
|
|
237
|
+
{/each}
|
|
238
|
+
{#if uploadError}<li class="text-destructive" role="alert">{uploadError}</li>{/if}
|
|
239
|
+
</ul>
|
|
114
240
|
{/if}
|
|
115
241
|
<Textarea
|
|
116
242
|
bind:ref={textarea}
|
|
@@ -122,8 +248,14 @@
|
|
|
122
248
|
{placeholder}
|
|
123
249
|
aria-label="Message"
|
|
124
250
|
maxlength={LIMITS.messageChars}
|
|
251
|
+
role={prompts?.length ? 'combobox' : undefined}
|
|
252
|
+
aria-autocomplete={prompts?.length ? 'list' : undefined}
|
|
253
|
+
aria-expanded={prompts?.length ? menuOpen : undefined}
|
|
254
|
+
aria-controls={menuOpen ? `${uid}-prompts` : undefined}
|
|
255
|
+
aria-activedescendant={menuOpen ? `${uid}-prompt-${active}` : undefined}
|
|
125
256
|
{disabled}
|
|
126
257
|
{onkeydown}
|
|
258
|
+
{onpaste}
|
|
127
259
|
/>
|
|
128
260
|
<div class="flex items-center gap-1 px-2 pb-2">
|
|
129
261
|
{#if upload}
|
|
@@ -134,22 +266,31 @@
|
|
|
134
266
|
size="icon"
|
|
135
267
|
class="size-8 text-muted-foreground"
|
|
136
268
|
aria-label="Attach files"
|
|
137
|
-
disabled={disabled ||
|
|
138
|
-
loading={uploading > 0}
|
|
269
|
+
disabled={disabled || files.length >= LIMITS.requestFiles}
|
|
139
270
|
onclick={() => fileInput?.click()}
|
|
140
271
|
>
|
|
141
|
-
|
|
272
|
+
<Icon name="paperclip" size={18} />
|
|
142
273
|
</Button>
|
|
143
274
|
{/if}
|
|
144
275
|
{#if context}<div class="min-w-0">{@render context()}</div>{/if}
|
|
145
|
-
<div class="ml-auto">
|
|
276
|
+
<div class="ml-auto flex items-center gap-2">
|
|
277
|
+
{#if showCount}
|
|
278
|
+
<span
|
|
279
|
+
class={cn(
|
|
280
|
+
'text-xs tabular-nums text-muted-foreground',
|
|
281
|
+
chars >= LIMITS.messageChars && 'text-destructive',
|
|
282
|
+
)}
|
|
283
|
+
aria-live="polite"
|
|
284
|
+
>{chars.toLocaleString()} / {LIMITS.messageChars.toLocaleString()}</span
|
|
285
|
+
>
|
|
286
|
+
{/if}
|
|
146
287
|
{#if streaming}
|
|
147
288
|
<Button
|
|
148
289
|
type="button"
|
|
149
290
|
variant="outline"
|
|
150
291
|
size="icon"
|
|
151
292
|
class="size-8"
|
|
152
|
-
aria-label="Stop"
|
|
293
|
+
aria-label="Stop (Esc)"
|
|
153
294
|
onclick={() => oncancel?.()}><Icon name="stop" size={14} /></Button
|
|
154
295
|
>
|
|
155
296
|
{:else}
|
|
@@ -158,11 +299,61 @@
|
|
|
158
299
|
size="icon"
|
|
159
300
|
class="size-8"
|
|
160
301
|
aria-label="Send"
|
|
161
|
-
disabled={disabled || !value.trim() ||
|
|
162
|
-
><Icon name="send" size={16} /></Button
|
|
302
|
+
disabled={disabled || !value.trim() || busy}><Icon name="send" size={16} /></Button
|
|
163
303
|
>
|
|
164
304
|
{/if}
|
|
165
305
|
</div>
|
|
166
306
|
</div>
|
|
167
307
|
</div>
|
|
308
|
+
<p
|
|
309
|
+
class="mx-auto mt-1.5 hidden max-w-3xl items-center gap-1.5 px-1 text-[11px] text-muted-foreground opacity-0 transition-opacity group-focus-within/composer:opacity-100 sm:flex"
|
|
310
|
+
aria-hidden="true"
|
|
311
|
+
>
|
|
312
|
+
{#if streaming}
|
|
313
|
+
<Kbd size="sm">Esc</Kbd> to stop
|
|
314
|
+
{:else}
|
|
315
|
+
<Kbd size="sm">Enter</Kbd> to send · <Kbd keys={['Shift', 'Enter']} size="sm" /> for a new line
|
|
316
|
+
{#if prompts?.length}· <Kbd size="sm">/</Kbd> for saved prompts{/if}
|
|
317
|
+
{/if}
|
|
318
|
+
</p>
|
|
168
319
|
</form>
|
|
320
|
+
|
|
321
|
+
{#if prompts?.length}
|
|
322
|
+
<Popover
|
|
323
|
+
open={menuOpen}
|
|
324
|
+
onOpenChange={(next) => {
|
|
325
|
+
if (!next) dismissedFor = value;
|
|
326
|
+
}}
|
|
327
|
+
anchor={card ?? null}
|
|
328
|
+
side="top"
|
|
329
|
+
align="start"
|
|
330
|
+
sideOffset={6}
|
|
331
|
+
autoFocus={false}
|
|
332
|
+
aria-label="Saved prompts"
|
|
333
|
+
class="w-[min(28rem,calc(100vw-2rem))] p-1"
|
|
334
|
+
>
|
|
335
|
+
<ul id={`${uid}-prompts`} role="listbox" aria-label="Saved prompts" class="grid gap-0.5">
|
|
336
|
+
{#each matches as prompt, i (prompt.id)}
|
|
337
|
+
<!-- Keyboard choice happens in the textarea (combobox + aria-activedescendant). -->
|
|
338
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
339
|
+
<li
|
|
340
|
+
id={`${uid}-prompt-${i}`}
|
|
341
|
+
role="option"
|
|
342
|
+
aria-selected={i === active}
|
|
343
|
+
class={cn(
|
|
344
|
+
'cursor-pointer rounded-md px-2.5 py-1.5 text-sm',
|
|
345
|
+
i === active ? 'bg-muted text-foreground' : 'text-foreground hover:bg-muted/60',
|
|
346
|
+
)}
|
|
347
|
+
onpointerdown={(e) => e.preventDefault()}
|
|
348
|
+
onclick={() => void choose(prompt)}
|
|
349
|
+
onpointermove={() => (active = i)}
|
|
350
|
+
>
|
|
351
|
+
<span class="block truncate font-medium">{prompt.label}</span>
|
|
352
|
+
{#if prompt.description}
|
|
353
|
+
<span class="block truncate text-xs text-muted-foreground">{prompt.description}</span>
|
|
354
|
+
{/if}
|
|
355
|
+
</li>
|
|
356
|
+
{/each}
|
|
357
|
+
</ul>
|
|
358
|
+
</Popover>
|
|
359
|
+
{/if}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FileRef } from '@classic-homes/agent-contracts';
|
|
2
|
+
import { type ComposerPrompt } from '../composer-prompts.js';
|
|
2
3
|
interface Props {
|
|
3
4
|
disabled?: boolean;
|
|
4
5
|
streaming?: boolean;
|
|
@@ -7,12 +8,19 @@ interface Props {
|
|
|
7
8
|
placeholder?: string;
|
|
8
9
|
/** Upload a file; the composer shows it as pending until the message is sent. */
|
|
9
10
|
upload?: (file: File) => Promise<FileRef>;
|
|
11
|
+
/** Saved prompts for the `/` menu; the menu is off without them. */
|
|
12
|
+
prompts?: ComposerPrompt[];
|
|
10
13
|
onsend?: (text: string) => void;
|
|
11
14
|
oncancel?: () => void;
|
|
12
15
|
onremovefile?: (fileId: string) => void;
|
|
13
16
|
/** Slot for a host-provided context chip (page context toggle). */
|
|
14
17
|
context?: import('svelte').Snippet;
|
|
15
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* The message box: an autosizing textarea in a card with attach, the host's context chip and
|
|
21
|
+
* Send/Stop. Files can also be dropped on it or pasted. Typing `/` at the start opens the saved
|
|
22
|
+
* prompt menu when the host passes `prompts`.
|
|
23
|
+
*/
|
|
16
24
|
declare const Composer: import("svelte").Component<Props, {
|
|
17
25
|
focus: () => void;
|
|
18
26
|
}, "value" | "files">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAIvF,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,oEAAoE;IACpE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CACpC;AA2PH;;;;GAIG;AACH,QAAA,MAAM,QAAQ;iBA/NO,IAAI;qBA+N6B,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -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,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,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"}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
streaming: boolean;
|
|
11
11
|
sandboxOrigin: string;
|
|
12
12
|
fileUrl: (fileId: string) => string;
|
|
13
|
+
toolLabels?: Record<string, string>;
|
|
13
14
|
onnavigate?: (href: string) => void;
|
|
14
15
|
onopenartifact?: (artifactId: string) => void;
|
|
15
16
|
onprompt?: (text: string) => void;
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
streaming,
|
|
25
26
|
sandboxOrigin,
|
|
26
27
|
fileUrl,
|
|
28
|
+
toolLabels,
|
|
27
29
|
onnavigate,
|
|
28
30
|
onopenartifact,
|
|
29
31
|
onprompt,
|
|
@@ -133,6 +135,7 @@
|
|
|
133
135
|
streaming={streaming && i === messages.length - 1 && message.role === 'assistant'}
|
|
134
136
|
{sandboxOrigin}
|
|
135
137
|
{fileUrl}
|
|
138
|
+
{toolLabels}
|
|
136
139
|
{onnavigate}
|
|
137
140
|
{onopenartifact}
|
|
138
141
|
{onprompt}
|
|
@@ -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,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;
|
|
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,62 +1,146 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
What the agent did in a turn: tool calls (status icon, readable name, duration) and the
|
|
4
|
+
specialists it delegated to, with their own calls nested. Open while the turn runs and folded
|
|
5
|
+
away when it finishes, unless the reader toggled it.
|
|
6
|
+
-->
|
|
1
7
|
<script lang="ts">
|
|
2
|
-
import { cn, Icon } from '@classic-homes/theme-svelte';
|
|
8
|
+
import { cn, Collapsible, Icon, Spinner, type IconName } from '@classic-homes/theme-svelte';
|
|
3
9
|
import type { DelegationState, ToolCallState } from '../reducer.js';
|
|
10
|
+
import { formatDuration, groupToolRuns, humanizeToolName } from '../message-format.js';
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
tools
|
|
7
|
-
delegations
|
|
8
|
-
streaming
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
interface Props {
|
|
13
|
+
tools: ToolCallState[];
|
|
14
|
+
delegations: DelegationState[];
|
|
15
|
+
streaming?: boolean;
|
|
16
|
+
/** Readable names by tool name (with or without the `server/` prefix). */
|
|
17
|
+
toolLabels?: Record<string, string>;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
let { tools, delegations, streaming = false, toolLabels, class: className }: Props = $props();
|
|
21
|
+
|
|
22
|
+
/** The reader's choice; until they make one the timeline follows the turn. */
|
|
23
|
+
let toggled = $state<boolean | null>(null);
|
|
24
|
+
const open = $derived(toggled ?? streaming);
|
|
25
|
+
let details = $state<Record<string, boolean>>({});
|
|
26
|
+
|
|
27
|
+
const running = $derived(tools.filter((t) => !t.status));
|
|
28
|
+
const failed = $derived(
|
|
29
|
+
tools.filter((t) => t.status === 'error' || t.status === 'unauthenticated').length,
|
|
30
|
+
);
|
|
31
|
+
const refused = $derived(tools.filter((t) => t.status === 'refused').length);
|
|
32
|
+
const groups = $derived(groupToolRuns(tools, delegations));
|
|
33
|
+
|
|
34
|
+
function labelFor(tool: ToolCallState): string {
|
|
35
|
+
const full = tool.server ? `${tool.server}/${tool.name}` : tool.name;
|
|
36
|
+
return toolLabels?.[full] ?? toolLabels?.[tool.name] ?? humanizeToolName(tool.name);
|
|
37
|
+
}
|
|
38
|
+
function statusIcon(tool: ToolCallState): { icon: IconName; tone: string; label: string } {
|
|
39
|
+
switch (tool.status) {
|
|
40
|
+
case 'ok':
|
|
41
|
+
return { icon: 'check-circle', tone: 'text-success', label: 'Done' };
|
|
42
|
+
case 'error':
|
|
43
|
+
return { icon: 'x-circle', tone: 'text-destructive', label: 'Failed' };
|
|
44
|
+
case 'refused':
|
|
45
|
+
return { icon: 'alert-triangle', tone: 'text-warning', label: 'Refused' };
|
|
46
|
+
case 'unauthenticated':
|
|
47
|
+
return { icon: 'lock', tone: 'text-warning', label: 'Needs sign-in' };
|
|
48
|
+
default:
|
|
49
|
+
return { icon: 'circle', tone: 'text-muted-foreground', label: 'Interrupted' };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const summary = $derived.by(() => {
|
|
54
|
+
if (streaming && running.length) return `${labelFor(running[running.length - 1]!)}…`;
|
|
55
|
+
const parts: string[] = [];
|
|
56
|
+
if (tools.length) parts.push(`${tools.length} tool call${tools.length === 1 ? '' : 's'}`);
|
|
57
|
+
const children = [...new Set(delegations.map((d) => d.child))];
|
|
58
|
+
if (children.length) parts.push(`via ${children.join(', ')}`);
|
|
59
|
+
if (failed) parts.push(`${failed} failed`);
|
|
60
|
+
if (refused) parts.push(`${refused} refused`);
|
|
61
|
+
return parts.join(' · ') || 'Working…';
|
|
22
62
|
});
|
|
23
63
|
</script>
|
|
24
64
|
|
|
25
|
-
{#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{tool.summary}
|
|
56
|
-
</div>{/if}
|
|
57
|
-
</li>
|
|
58
|
-
{/each}
|
|
59
|
-
</ol>
|
|
65
|
+
{#snippet toolRow(tool: ToolCallState)}
|
|
66
|
+
{@const s = statusIcon(tool)}
|
|
67
|
+
{@const preview = [tool.argsPreview, tool.summary].filter(Boolean).join('\n→ ')}
|
|
68
|
+
<li class="min-w-0">
|
|
69
|
+
<div class="flex items-center gap-2">
|
|
70
|
+
{#if !tool.status && streaming}
|
|
71
|
+
<Spinner size="sm" label="Running" class="shrink-0" />
|
|
72
|
+
{:else}
|
|
73
|
+
<span class={cn('shrink-0', s.tone)} title={s.label}>
|
|
74
|
+
<Icon name={s.icon} size={14} />
|
|
75
|
+
<span class="sr-only">{s.label}</span>
|
|
76
|
+
</span>
|
|
77
|
+
{/if}
|
|
78
|
+
<span class="min-w-0 truncate text-foreground">{labelFor(tool)}</span>
|
|
79
|
+
{#if tool.server}<span class="hidden shrink-0 font-mono sm:inline">{tool.server}</span>{/if}
|
|
80
|
+
{#if tool.durationMs !== undefined}
|
|
81
|
+
<span class="ml-auto shrink-0 tabular-nums">{formatDuration(tool.durationMs)}</span>
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
84
|
+
{#if preview}
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
class={cn(
|
|
88
|
+
'ml-[22px] mt-0.5 block max-w-full whitespace-pre-wrap rounded text-left font-mono text-[11px] leading-snug [overflow-wrap:anywhere] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
89
|
+
!details[tool.callId] && 'line-clamp-2',
|
|
90
|
+
)}
|
|
91
|
+
aria-expanded={!!details[tool.callId]}
|
|
92
|
+
aria-label={details[tool.callId] ? 'Show less' : 'Show the full call'}
|
|
93
|
+
onclick={() => (details[tool.callId] = !details[tool.callId])}>{preview}</button
|
|
94
|
+
>
|
|
60
95
|
{/if}
|
|
61
|
-
</
|
|
96
|
+
</li>
|
|
97
|
+
{/snippet}
|
|
98
|
+
|
|
99
|
+
{#if tools.length > 0 || delegations.length > 0}
|
|
100
|
+
<Collapsible
|
|
101
|
+
{open}
|
|
102
|
+
onOpenChange={(next) => (toggled = next)}
|
|
103
|
+
class={cn('text-xs text-muted-foreground', className)}
|
|
104
|
+
triggerClass="gap-1.5 py-0.5 text-xs text-muted-foreground hover:text-foreground"
|
|
105
|
+
contentClass="pt-1.5"
|
|
106
|
+
>
|
|
107
|
+
{#snippet trigger()}
|
|
108
|
+
<span class="inline-flex items-center gap-1.5">
|
|
109
|
+
{#if streaming && running.length}
|
|
110
|
+
<Spinner size="sm" label="Working" />
|
|
111
|
+
{:else}
|
|
112
|
+
<Icon name="tool" size={12} />
|
|
113
|
+
{/if}
|
|
114
|
+
<span>{summary}</span>
|
|
115
|
+
</span>
|
|
116
|
+
{/snippet}
|
|
117
|
+
<div class="grid gap-2 border-l border-border pl-3">
|
|
118
|
+
{#each groups as group, gi (group.delegation?.childRunId ?? `top-${gi}`)}
|
|
119
|
+
{#if group.delegation}
|
|
120
|
+
<div class="grid gap-1">
|
|
121
|
+
<div class="flex items-center gap-2 text-foreground">
|
|
122
|
+
{#if group.delegation.status === 'start' && streaming}
|
|
123
|
+
<Spinner size="sm" label="Working" class="shrink-0" />
|
|
124
|
+
{:else}
|
|
125
|
+
<Icon name="corner-down-right" size={14} class="shrink-0 text-muted-foreground" />
|
|
126
|
+
{/if}
|
|
127
|
+
<span>Delegated to <span class="font-medium">{group.delegation.child}</span></span>
|
|
128
|
+
</div>
|
|
129
|
+
{#if group.delegation.summary}
|
|
130
|
+
<p class="ml-[22px] [overflow-wrap:anywhere]">{group.delegation.summary}</p>
|
|
131
|
+
{/if}
|
|
132
|
+
{#if group.tools.length}
|
|
133
|
+
<ol class="ml-[22px] grid gap-1">
|
|
134
|
+
{#each group.tools as tool (tool.callId)}{@render toolRow(tool)}{/each}
|
|
135
|
+
</ol>
|
|
136
|
+
{/if}
|
|
137
|
+
</div>
|
|
138
|
+
{:else}
|
|
139
|
+
<ol class="grid gap-1">
|
|
140
|
+
{#each group.tools as tool (tool.callId)}{@render toolRow(tool)}{/each}
|
|
141
|
+
</ol>
|
|
142
|
+
{/if}
|
|
143
|
+
{/each}
|
|
144
|
+
</div>
|
|
145
|
+
</Collapsible>
|
|
62
146
|
{/if}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { DelegationState, ToolCallState } from '../reducer.js';
|
|
2
|
-
|
|
2
|
+
interface Props {
|
|
3
3
|
tools: ToolCallState[];
|
|
4
4
|
delegations: DelegationState[];
|
|
5
5
|
streaming?: boolean;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/** Readable names by tool name (with or without the `server/` prefix). */
|
|
7
|
+
toolLabels?: Record<string, string>;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* What the agent did in a turn: tool calls (status icon, readable name, duration) and the
|
|
12
|
+
* specialists it delegated to, with their own calls nested. Open while the turn runs and folded
|
|
13
|
+
* away when it finishes, unless the reader toggled it.
|
|
14
|
+
*/
|
|
15
|
+
declare const ToolTimeline: import("svelte").Component<Props, {}, "">;
|
|
8
16
|
type ToolTimeline = ReturnType<typeof ToolTimeline>;
|
|
9
17
|
export default ToolTimeline;
|
|
10
18
|
//# sourceMappingURL=ToolTimeline.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolTimeline.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ToolTimeline.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"ToolTimeline.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ToolTimeline.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAIlE,UAAU,KAAK;IACb,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6HH;;;;GAIG;AACH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/composer.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { LIMITS } from '@classic-homes/agent-contracts';
|
|
5
|
+
import { promptMenuKeyAction } from '../composer-keys.js';
|
|
6
|
+
import { filterPrompts, promptQuery } from '../composer-prompts.js';
|
|
7
|
+
import { formatDuration, groupToolRuns, humanizeToolName } from '../message-format.js';
|
|
8
|
+
import Composer from './Composer.svelte';
|
|
9
|
+
import ToolTimeline from './ToolTimeline.svelte';
|
|
10
|
+
afterEach(() => cleanup());
|
|
11
|
+
const prompts = [
|
|
12
|
+
{
|
|
13
|
+
id: '1',
|
|
14
|
+
label: 'Weekly recap',
|
|
15
|
+
description: 'Summary for a community',
|
|
16
|
+
text: 'Recap {{community}}',
|
|
17
|
+
},
|
|
18
|
+
{ id: '2', label: 'Lot report', text: async () => 'Report on lot {{lot}}' },
|
|
19
|
+
{ id: '3', label: 'Draft email', description: 'Weekly buyer update', text: 'Email' },
|
|
20
|
+
];
|
|
21
|
+
describe('prompt menu helpers', () => {
|
|
22
|
+
it('reads the query only from a bare /command', () => {
|
|
23
|
+
expect(promptQuery('/')).toBe('');
|
|
24
|
+
expect(promptQuery('/wee')).toBe('wee');
|
|
25
|
+
expect(promptQuery('/weekly recap')).toBeNull();
|
|
26
|
+
expect(promptQuery('hello /x')).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
it('ranks label matches over description matches', () => {
|
|
29
|
+
expect(filterPrompts(prompts, 'weekly').map((p) => p.id)).toEqual(['1', '3']);
|
|
30
|
+
expect(filterPrompts(prompts, 'lot').map((p) => p.id)).toEqual(['2']);
|
|
31
|
+
expect(filterPrompts(prompts, '')).toHaveLength(3);
|
|
32
|
+
});
|
|
33
|
+
it('maps keys while the menu is open', () => {
|
|
34
|
+
expect(promptMenuKeyAction({ key: 'ArrowDown' })).toBe('next');
|
|
35
|
+
expect(promptMenuKeyAction({ key: 'Enter' })).toBe('choose');
|
|
36
|
+
expect(promptMenuKeyAction({ key: 'Enter', shiftKey: true })).toBeNull();
|
|
37
|
+
expect(promptMenuKeyAction({ key: 'Enter', isComposing: true })).toBeNull();
|
|
38
|
+
expect(promptMenuKeyAction({ key: 'Escape' })).toBe('close');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('tool helpers', () => {
|
|
42
|
+
it('humanizes names and durations', () => {
|
|
43
|
+
expect(humanizeToolName('hotsheet/list_plans')).toBe('List plans');
|
|
44
|
+
expect(humanizeToolName('getLotDetails')).toBe('Get lot details');
|
|
45
|
+
expect(formatDuration(640)).toBe('640 ms');
|
|
46
|
+
expect(formatDuration(1234)).toBe('1.2 s');
|
|
47
|
+
expect(formatDuration(75_000)).toBe('1 min 15 s');
|
|
48
|
+
});
|
|
49
|
+
it('groups calls under their delegation', () => {
|
|
50
|
+
const t = (callId, runId) => ({ callId, runId });
|
|
51
|
+
const groups = groupToolRuns([t('a', 'root'), t('b', 'child-1'), t('c', 'root')], [{ child: 'research', childRunId: 'child-1', status: 'end' }]);
|
|
52
|
+
expect(groups.map((g) => [g.delegation?.child, g.tools.map((x) => x.callId)])).toEqual([
|
|
53
|
+
[undefined, ['a', 'c']],
|
|
54
|
+
['research', ['b']],
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('Composer', () => {
|
|
59
|
+
it('inserts a saved prompt from the / menu with the keyboard', async () => {
|
|
60
|
+
const user = userEvent.setup();
|
|
61
|
+
render(Composer, { props: { prompts } });
|
|
62
|
+
const box = screen.getByRole('combobox', { name: 'Message' });
|
|
63
|
+
await user.type(box, '/lot');
|
|
64
|
+
const listbox = await screen.findByRole('listbox', { name: 'Saved prompts' });
|
|
65
|
+
expect(listbox.textContent).toContain('Lot report');
|
|
66
|
+
await user.keyboard('{Enter}');
|
|
67
|
+
await waitFor(() => expect(box.value).toBe('Report on lot {{lot}}'));
|
|
68
|
+
expect(screen.queryByRole('listbox')).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
it('closes the menu on Escape and keeps the draft', async () => {
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
render(Composer, { props: { prompts } });
|
|
73
|
+
const box = screen.getByRole('combobox', { name: 'Message' });
|
|
74
|
+
await user.type(box, '/');
|
|
75
|
+
await screen.findByRole('listbox');
|
|
76
|
+
await user.keyboard('{ArrowDown}{Escape}');
|
|
77
|
+
await waitFor(() => expect(screen.queryByRole('listbox')).toBeNull());
|
|
78
|
+
expect(box.value).toBe('/');
|
|
79
|
+
});
|
|
80
|
+
it('uploads pasted and dropped files and shows them while uploading', async () => {
|
|
81
|
+
let finish = () => { };
|
|
82
|
+
const upload = vi.fn((file) => new Promise((resolve) => {
|
|
83
|
+
finish = resolve;
|
|
84
|
+
void file;
|
|
85
|
+
}));
|
|
86
|
+
render(Composer, { props: { upload } });
|
|
87
|
+
const box = screen.getByRole('textbox', { name: 'Message' });
|
|
88
|
+
const file = new File(['x'], 'shot.png', { type: 'image/png' });
|
|
89
|
+
await fireEvent.paste(box, { clipboardData: { files: [file], types: ['Files'] } });
|
|
90
|
+
expect(upload).toHaveBeenCalledWith(file);
|
|
91
|
+
expect(await screen.findByText('shot.png')).toBeTruthy();
|
|
92
|
+
expect(screen.getByRole('button', { name: 'Send' }).hasAttribute('disabled')).toBe(true);
|
|
93
|
+
finish({ fileId: 'f1', name: 'shot.png', mimeType: 'image/png', bytes: 1, status: 'uploaded' });
|
|
94
|
+
expect(await screen.findByRole('button', { name: 'Remove shot.png' })).toBeTruthy();
|
|
95
|
+
const card = screen.getByRole('group', { name: 'Message composer' });
|
|
96
|
+
const dropped = new File(['y'], 'plan.pdf', { type: 'application/pdf' });
|
|
97
|
+
const dataTransfer = { types: ['Files'], files: [dropped], dropEffect: 'none' };
|
|
98
|
+
await fireEvent.dragEnter(card, { dataTransfer });
|
|
99
|
+
expect(screen.getByText('Drop files to attach')).toBeTruthy();
|
|
100
|
+
await fireEvent.drop(card, { dataTransfer });
|
|
101
|
+
expect(upload).toHaveBeenLastCalledWith(dropped);
|
|
102
|
+
expect(screen.queryByText('Drop files to attach')).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
it('counts characters near the limit', async () => {
|
|
105
|
+
const { rerender } = render(Composer, { props: { value: 'short' } });
|
|
106
|
+
expect(screen.queryByText(/\/ 8,000/)).toBeNull();
|
|
107
|
+
await rerender({ value: 'x'.repeat(Math.ceil(LIMITS.messageChars * 0.85)) });
|
|
108
|
+
expect(screen.getByText(/\/ 8,000/)).toBeTruthy();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('ToolTimeline', () => {
|
|
112
|
+
const tool = (patch) => ({
|
|
113
|
+
callId: 'c1',
|
|
114
|
+
name: 'list_plans',
|
|
115
|
+
server: 'hotsheet',
|
|
116
|
+
argsPreview: '{"community":"Wolf Ranch"}',
|
|
117
|
+
agent: 'classic-agent',
|
|
118
|
+
runId: 'root',
|
|
119
|
+
...patch,
|
|
120
|
+
});
|
|
121
|
+
it('is open with a spinner while running and folds when the turn ends', async () => {
|
|
122
|
+
const { rerender } = render(ToolTimeline, {
|
|
123
|
+
props: { tools: [tool({})], delegations: [], streaming: true },
|
|
124
|
+
});
|
|
125
|
+
expect(screen.getAllByText('List plans…').length).toBeGreaterThan(0);
|
|
126
|
+
expect(screen.getByText('{"community":"Wolf Ranch"}')).toBeTruthy();
|
|
127
|
+
await rerender({
|
|
128
|
+
tools: [tool({ status: 'ok', durationMs: 1234 })],
|
|
129
|
+
delegations: [],
|
|
130
|
+
streaming: false,
|
|
131
|
+
});
|
|
132
|
+
const trigger = screen.getByRole('button', { name: /1 tool call/ });
|
|
133
|
+
expect(trigger.getAttribute('aria-expanded')).toBe('false');
|
|
134
|
+
await userEvent.click(trigger);
|
|
135
|
+
expect(screen.getByText('1.2 s')).toBeTruthy();
|
|
136
|
+
});
|
|
137
|
+
it('uses host labels, nests delegations and counts failures', async () => {
|
|
138
|
+
render(ToolTimeline, {
|
|
139
|
+
props: {
|
|
140
|
+
streaming: false,
|
|
141
|
+
toolLabels: { 'hotsheet/list_plans': 'Look up plans' },
|
|
142
|
+
tools: [
|
|
143
|
+
tool({ status: 'ok' }),
|
|
144
|
+
tool({
|
|
145
|
+
callId: 'c2',
|
|
146
|
+
name: 'web_search',
|
|
147
|
+
server: undefined,
|
|
148
|
+
runId: 'child',
|
|
149
|
+
status: 'error',
|
|
150
|
+
}),
|
|
151
|
+
],
|
|
152
|
+
delegations: [{ child: 'research', childRunId: 'child', status: 'end' }],
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const trigger = screen.getByRole('button', { name: /2 tool calls · via research · 1 failed/ });
|
|
156
|
+
await userEvent.click(trigger);
|
|
157
|
+
expect(screen.getByText('Look up plans')).toBeTruthy();
|
|
158
|
+
expect(screen.getByText('research')).toBeTruthy();
|
|
159
|
+
expect(screen.getByText('Web search')).toBeTruthy();
|
|
160
|
+
expect(screen.getByText('Failed')).toBeTruthy();
|
|
161
|
+
});
|
|
162
|
+
});
|
package/dist/composer-keys.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** What a keydown in the composer textarea should do. Pure so it can be tested without a DOM. */
|
|
2
2
|
export type ComposerKeyAction = 'send' | 'cancel' | null;
|
|
3
|
+
/** What a keydown does while the `/` prompt menu is open. */
|
|
4
|
+
export type PromptMenuKeyAction = 'next' | 'previous' | 'choose' | 'close' | null;
|
|
3
5
|
export interface ComposerKey {
|
|
4
6
|
key: string;
|
|
5
7
|
shiftKey?: boolean;
|
|
@@ -8,4 +10,5 @@ export interface ComposerKey {
|
|
|
8
10
|
keyCode?: number;
|
|
9
11
|
}
|
|
10
12
|
export declare function composerKeyAction(event: ComposerKey, streaming: boolean): ComposerKeyAction;
|
|
13
|
+
export declare function promptMenuKeyAction(event: ComposerKey): PromptMenuKeyAction;
|
|
11
14
|
//# sourceMappingURL=composer-keys.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composer-keys.d.ts","sourceRoot":"","sources":["../src/lib/composer-keys.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAM3F"}
|
|
1
|
+
{"version":3,"file":"composer-keys.d.ts","sourceRoot":"","sources":["../src/lib/composer-keys.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEzD,6DAA6D;AAC7D,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;AAElF,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAM3F;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,mBAAmB,CAgB3E"}
|
package/dist/composer-keys.js
CHANGED
|
@@ -8,3 +8,21 @@ export function composerKeyAction(event, streaming) {
|
|
|
8
8
|
return 'send';
|
|
9
9
|
return null;
|
|
10
10
|
}
|
|
11
|
+
export function promptMenuKeyAction(event) {
|
|
12
|
+
if (event.isComposing || event.keyCode === 229)
|
|
13
|
+
return null;
|
|
14
|
+
switch (event.key) {
|
|
15
|
+
case 'ArrowDown':
|
|
16
|
+
return 'next';
|
|
17
|
+
case 'ArrowUp':
|
|
18
|
+
return 'previous';
|
|
19
|
+
case 'Enter':
|
|
20
|
+
return event.shiftKey ? null : 'choose';
|
|
21
|
+
case 'Tab':
|
|
22
|
+
return 'choose';
|
|
23
|
+
case 'Escape':
|
|
24
|
+
return 'close';
|
|
25
|
+
default:
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composer's `/` prompt menu: which query the draft asks for and which prompts match. Pure,
|
|
3
|
+
* so the matching can be tested without a DOM.
|
|
4
|
+
*/
|
|
5
|
+
export interface ComposerPrompt {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
/** The text to insert, or a loader for it (e.g. fetch the saved template). */
|
|
10
|
+
text: string | (() => Promise<string>);
|
|
11
|
+
}
|
|
12
|
+
/** The filter text when the draft is a bare `/command` (no spaces or newlines yet), else null. */
|
|
13
|
+
export declare function promptQuery(draft: string): string | null;
|
|
14
|
+
/** Prompts whose label or description contains every word of the query, labels first. */
|
|
15
|
+
export declare function filterPrompts<T extends Pick<ComposerPrompt, 'label' | 'description'>>(prompts: T[], query: string, limit?: number): T[];
|
|
16
|
+
//# sourceMappingURL=composer-prompts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer-prompts.d.ts","sourceRoot":"","sources":["../src/lib/composer-prompts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;CACxC;AAED,kGAAkG;AAClG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGxD;AAED,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,CAAC,SAAS,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,aAAa,CAAC,EACnF,OAAO,EAAE,CAAC,EAAE,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,SAAI,GACR,CAAC,EAAE,CAkBL"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The filter text when the draft is a bare `/command` (no spaces or newlines yet), else null. */
|
|
2
|
+
export function promptQuery(draft) {
|
|
3
|
+
const match = /^\/(\S*)$/.exec(draft);
|
|
4
|
+
return match ? match[1] : null;
|
|
5
|
+
}
|
|
6
|
+
/** Prompts whose label or description contains every word of the query, labels first. */
|
|
7
|
+
export function filterPrompts(prompts, query, limit = 8) {
|
|
8
|
+
const words = query
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.split(/[-_\s]+/)
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
const score = (p) => {
|
|
13
|
+
const label = p.label.toLowerCase();
|
|
14
|
+
const haystack = `${label} ${p.description?.toLowerCase() ?? ''}`;
|
|
15
|
+
if (!words.every((w) => haystack.includes(w)))
|
|
16
|
+
return -1;
|
|
17
|
+
if (!words.length)
|
|
18
|
+
return 1;
|
|
19
|
+
return words.every((w) => label.includes(w)) ? (label.startsWith(words[0]) ? 3 : 2) : 1;
|
|
20
|
+
};
|
|
21
|
+
return prompts
|
|
22
|
+
.map((p, i) => ({ p, i, s: score(p) }))
|
|
23
|
+
.filter((x) => x.s >= 0)
|
|
24
|
+
.sort((a, b) => b.s - a.s || a.i - b.i)
|
|
25
|
+
.slice(0, limit)
|
|
26
|
+
.map((x) => x.p);
|
|
27
|
+
}
|
package/dist/index.d.ts
CHANGED
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,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;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"}
|
|
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,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"}
|
package/dist/index.js
CHANGED
package/dist/message-format.d.ts
CHANGED
|
@@ -27,4 +27,32 @@ export interface ProposalDescription {
|
|
|
27
27
|
}
|
|
28
28
|
/** Read-only description of a proposed document edit; applying happens in the Office add-in. */
|
|
29
29
|
export declare function describeProposal(op: ProposedOperation): ProposalDescription;
|
|
30
|
+
/** "list_plans" / "listPlans" / "hotsheet/list-plans" → "List plans". */
|
|
31
|
+
export declare function humanizeToolName(name: string): string;
|
|
32
|
+
/** 640 → "640 ms", 1234 → "1.2 s", 75000 → "1 min 15 s". */
|
|
33
|
+
export declare function formatDuration(ms: number): string;
|
|
34
|
+
export interface ToolRunGroup<T extends {
|
|
35
|
+
runId: string;
|
|
36
|
+
}> {
|
|
37
|
+
/** The delegation this group ran under; undefined for the top-level agent. */
|
|
38
|
+
delegation?: {
|
|
39
|
+
child: string;
|
|
40
|
+
childRunId: string;
|
|
41
|
+
status: 'start' | 'end';
|
|
42
|
+
summary?: string;
|
|
43
|
+
};
|
|
44
|
+
tools: T[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Group tool calls by the run that made them: the top-level agent's own calls first, then one
|
|
48
|
+
* group per delegation (in the order the delegations started) with the child's calls.
|
|
49
|
+
*/
|
|
50
|
+
export declare function groupToolRuns<T extends {
|
|
51
|
+
runId: string;
|
|
52
|
+
}>(tools: T[], delegations: {
|
|
53
|
+
child: string;
|
|
54
|
+
childRunId: string;
|
|
55
|
+
status: 'start' | 'end';
|
|
56
|
+
summary?: string;
|
|
57
|
+
}[]): ToolRunGroup<T>[];
|
|
30
58
|
//# sourceMappingURL=message-format.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-format.d.ts","sourceRoot":"","sources":["../src/lib/message-format.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAC3F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI7C;AAED,wCAAwC;AACxC,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,wCAAwC;AACxC,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CASlE;AAQD,2FAA2F;AAC3F,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,SAAa,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAW/E;AAED,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CASrF;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,QAAQ,CAcnE;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,MAAM,CAejE;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAQnD;AAED,MAAM,WAAW,mBAAmB;IAClC,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,iBAAiB,GAAG,mBAAmB,CAgB3E"}
|
|
1
|
+
{"version":3,"file":"message-format.d.ts","sourceRoot":"","sources":["../src/lib/message-format.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAC3F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI7C;AAED,wCAAwC;AACxC,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,wCAAwC;AACxC,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CASlE;AAQD,2FAA2F;AAC3F,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,SAAa,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAW/E;AAED,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CASrF;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,QAAQ,CAcnE;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,MAAM,CAejE;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAQnD;AAED,MAAM,WAAW,mBAAmB;IAClC,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,iBAAiB,GAAG,mBAAmB,CAgB3E;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQrD;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE;IACvD,8EAA8E;IAC9E,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,GAAG,KAAK,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9F,KAAK,EAAE,CAAC,EAAE,CAAC;CACZ;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EACvD,KAAK,EAAE,CAAC,EAAE,EACV,WAAW,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,GAAG,KAAK,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAC9F,YAAY,CAAC,CAAC,CAAC,EAAE,CAMnB"}
|
package/dist/message-format.js
CHANGED
|
@@ -116,3 +116,34 @@ export function describeProposal(op) {
|
|
|
116
116
|
return { action: 'Replace selection', host: 'Word' };
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
/** "list_plans" / "listPlans" / "hotsheet/list-plans" → "List plans". */
|
|
120
|
+
export function humanizeToolName(name) {
|
|
121
|
+
const bare = name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name;
|
|
122
|
+
const words = bare
|
|
123
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
124
|
+
.replace(/[_\-.]+/g, ' ')
|
|
125
|
+
.trim()
|
|
126
|
+
.toLowerCase();
|
|
127
|
+
return words ? words[0].toUpperCase() + words.slice(1) : name;
|
|
128
|
+
}
|
|
129
|
+
/** 640 → "640 ms", 1234 → "1.2 s", 75000 → "1 min 15 s". */
|
|
130
|
+
export function formatDuration(ms) {
|
|
131
|
+
if (ms < 1000)
|
|
132
|
+
return `${Math.round(ms)} ms`;
|
|
133
|
+
if (ms < 60_000)
|
|
134
|
+
return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)} s`;
|
|
135
|
+
const min = Math.floor(ms / 60_000);
|
|
136
|
+
const s = Math.round((ms % 60_000) / 1000);
|
|
137
|
+
return s ? `${min} min ${s} s` : `${min} min`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Group tool calls by the run that made them: the top-level agent's own calls first, then one
|
|
141
|
+
* group per delegation (in the order the delegations started) with the child's calls.
|
|
142
|
+
*/
|
|
143
|
+
export function groupToolRuns(tools, delegations) {
|
|
144
|
+
const childRuns = new Set(delegations.map((d) => d.childRunId));
|
|
145
|
+
const groups = [{ tools: tools.filter((t) => !childRuns.has(t.runId)) }];
|
|
146
|
+
for (const delegation of delegations)
|
|
147
|
+
groups.push({ delegation, tools: tools.filter((t) => t.runId === delegation.childRunId) });
|
|
148
|
+
return groups.filter((g) => g.delegation || g.tools.length);
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/agent-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.0",
|
|
21
|
+
"@classic-homes/agent-contracts": "0.4.0",
|
|
22
22
|
"dompurify": "3.4.17"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|