@classic-homes/agent-chat 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/body-lock.d.ts +2 -0
- package/dist/body-lock.d.ts.map +1 -0
- package/dist/body-lock.js +27 -0
- package/dist/components/AgentChat.svelte +21 -9
- package/dist/components/AgentChat.svelte.d.ts +6 -0
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/ConversationDialogs.svelte +23 -3
- package/dist/components/ConversationDialogs.svelte.d.ts.map +1 -1
- package/dist/components/Markdown.svelte +3 -2
- package/dist/components/Markdown.svelte.d.ts +2 -1
- package/dist/components/Markdown.svelte.d.ts.map +1 -1
- package/dist/components/live-fixes.svelte.test.d.ts +2 -0
- package/dist/components/live-fixes.svelte.test.d.ts.map +1 -0
- package/dist/components/live-fixes.svelte.test.js +127 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/session.svelte.d.ts +9 -0
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +6 -0
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body-lock.d.ts","sourceRoot":"","sources":["../src/lib/body-lock.ts"],"names":[],"mappings":"AAYA,wBAAgB,oBAAoB,CAAC,QAAQ,SAAY,GAAG,IAAI,CAY/D"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard for a bits-ui 1.x body-lock race (theme-svelte Sheet/Dialog): when one modal closes and
|
|
3
|
+
* another opens in the same tick, the closing one's deferred restore saves the opening one's
|
|
4
|
+
* locked style as the "original", so closing the second leaves `<body>` at
|
|
5
|
+
* `pointer-events: none; overflow: hidden` and the page stops taking clicks.
|
|
6
|
+
*
|
|
7
|
+
* Call after a sheet or dialog closes. Once the close animation is over, if no modal is open
|
|
8
|
+
* but the body is still locked, the lock is cleared. With a modal open nothing is touched.
|
|
9
|
+
*/
|
|
10
|
+
const SETTLE_MS = 400;
|
|
11
|
+
const OPEN_MODAL = '[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"]';
|
|
12
|
+
export function releaseStuckBodyLock(settleMs = SETTLE_MS) {
|
|
13
|
+
if (typeof document === 'undefined')
|
|
14
|
+
return;
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
if (document.querySelector(OPEN_MODAL))
|
|
17
|
+
return;
|
|
18
|
+
const body = document.body;
|
|
19
|
+
if (body.style.pointerEvents !== 'none')
|
|
20
|
+
return;
|
|
21
|
+
body.style.removeProperty('pointer-events');
|
|
22
|
+
body.style.removeProperty('overflow');
|
|
23
|
+
body.style.removeProperty('padding-right');
|
|
24
|
+
body.style.removeProperty('margin-right');
|
|
25
|
+
body.style.removeProperty('--scrollbar-width');
|
|
26
|
+
}, settleMs);
|
|
27
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { BackendMode } from '@classic-homes/agent-contracts';
|
|
10
10
|
import { cn, ResizablePanes, Sheet } from '@classic-homes/theme-svelte';
|
|
11
11
|
import { MediaQuery } from 'svelte/reactivity';
|
|
12
|
+
import { releaseStuckBodyLock } from '../body-lock.js';
|
|
12
13
|
import { ChatSession } from '../session.svelte.js';
|
|
13
14
|
import ArtifactPanel from './ArtifactPanel.svelte';
|
|
14
15
|
import Composer from './Composer.svelte';
|
|
@@ -28,6 +29,12 @@
|
|
|
28
29
|
getContext?: () => DocumentContext | null | undefined;
|
|
29
30
|
onconversationstarted?: (conversationId: string) => void;
|
|
30
31
|
ontoolresult?: (name: string, status: string) => void;
|
|
32
|
+
/** A turn finished streaming (any outcome); refresh conversation lists here. */
|
|
33
|
+
onturnend?: (info: {
|
|
34
|
+
requestId: string;
|
|
35
|
+
conversationId: string | null;
|
|
36
|
+
status: string;
|
|
37
|
+
}) => void;
|
|
31
38
|
onnavigate?: (href: string) => void;
|
|
32
39
|
onnotify?: (message: string) => void;
|
|
33
40
|
/** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
|
|
@@ -50,6 +57,7 @@
|
|
|
50
57
|
getContext,
|
|
51
58
|
onconversationstarted,
|
|
52
59
|
ontoolresult,
|
|
60
|
+
onturnend,
|
|
53
61
|
onnavigate,
|
|
54
62
|
onnotify,
|
|
55
63
|
ontoolrequest,
|
|
@@ -69,6 +77,15 @@
|
|
|
69
77
|
getContext,
|
|
70
78
|
onConversationStarted: (id) => onconversationstarted?.(id),
|
|
71
79
|
onToolResult: (name, status) => ontoolresult?.(name, status),
|
|
80
|
+
onTurnEnd: (info) => onturnend?.(info),
|
|
81
|
+
// Open the pane when the runtime starts an artifact, not by watching state later: a fast
|
|
82
|
+
// stream can open and close it before any effect sees `open`.
|
|
83
|
+
onEvent: (event) => {
|
|
84
|
+
if (event.type === 'artifact' && event.op === 'open') {
|
|
85
|
+
selectedArtifact = event.artifactId;
|
|
86
|
+
artifactsOpen = true;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
72
89
|
});
|
|
73
90
|
// The constructor already knows `conversationId`; what decides a fetch is whether the
|
|
74
91
|
// transcript for it has been loaded (or started) in this session.
|
|
@@ -92,14 +109,6 @@
|
|
|
92
109
|
const artifacts = $derived(
|
|
93
110
|
session.messages.flatMap((m) => (m.role === 'assistant' ? m.artifacts : [])),
|
|
94
111
|
);
|
|
95
|
-
$effect(() => {
|
|
96
|
-
// A newly opened artifact pops the panel.
|
|
97
|
-
const last = artifacts[artifacts.length - 1];
|
|
98
|
-
if (last && last.open && selectedArtifact !== last.artifactId) {
|
|
99
|
-
selectedArtifact = last.artifactId;
|
|
100
|
-
artifactsOpen = true;
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
112
|
|
|
104
113
|
// The page shows artifacts beside the chat on wide screens; narrow screens and the panel use a
|
|
105
114
|
// sheet so the transcript keeps its width.
|
|
@@ -196,7 +205,10 @@
|
|
|
196
205
|
{@render chat()}
|
|
197
206
|
<Sheet
|
|
198
207
|
open={showArtifacts}
|
|
199
|
-
onOpenChange={(open) =>
|
|
208
|
+
onOpenChange={(open) => {
|
|
209
|
+
artifactsOpen = open;
|
|
210
|
+
if (!open) releaseStuckBodyLock();
|
|
211
|
+
}}
|
|
200
212
|
side="right"
|
|
201
213
|
size="lg"
|
|
202
214
|
title={selectedTitle}
|
|
@@ -14,6 +14,12 @@ interface Props {
|
|
|
14
14
|
getContext?: () => DocumentContext | null | undefined;
|
|
15
15
|
onconversationstarted?: (conversationId: string) => void;
|
|
16
16
|
ontoolresult?: (name: string, status: string) => void;
|
|
17
|
+
/** A turn finished streaming (any outcome); refresh conversation lists here. */
|
|
18
|
+
onturnend?: (info: {
|
|
19
|
+
requestId: string;
|
|
20
|
+
conversationId: string | null;
|
|
21
|
+
status: string;
|
|
22
|
+
}) => void;
|
|
17
23
|
onnavigate?: (href: string) => void;
|
|
18
24
|
onnotify?: (message: string) => void;
|
|
19
25
|
/** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
|
|
@@ -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;AAWhE,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,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;AA6IH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
<script lang="ts">
|
|
18
18
|
import { AlertDialog, Button, Dialog, Input } from '@classic-homes/theme-svelte';
|
|
19
|
+
import { tick } from 'svelte';
|
|
20
|
+
import { releaseStuckBodyLock } from '../body-lock.js';
|
|
19
21
|
|
|
20
22
|
interface Props {
|
|
21
23
|
renaming?: ConversationRef | null;
|
|
@@ -75,6 +77,17 @@
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
let renameForm = $state<HTMLFormElement | null>(null);
|
|
81
|
+
/** Start in the title field with the text selected, not on the dialog's close button. */
|
|
82
|
+
function focusTitle(event: Event): void {
|
|
83
|
+
const field = renameForm?.querySelector('input');
|
|
84
|
+
if (!field) return;
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
field.focus();
|
|
87
|
+
// The title is written into the field by an effect after this runs; select once it's there.
|
|
88
|
+
void tick().then(() => field.select());
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
function restoreFocus(event: Event): void {
|
|
79
92
|
if (!returnFocus?.isConnected) return;
|
|
80
93
|
event.preventDefault();
|
|
@@ -100,9 +113,13 @@
|
|
|
100
113
|
}}
|
|
101
114
|
title="Rename conversation"
|
|
102
115
|
size="sm"
|
|
103
|
-
|
|
116
|
+
onOpenAutoFocus={focusTitle}
|
|
117
|
+
onCloseAutoFocus={(event) => {
|
|
118
|
+
restoreFocus(event);
|
|
119
|
+
releaseStuckBodyLock();
|
|
120
|
+
}}
|
|
104
121
|
>
|
|
105
|
-
<form id={formId} class="grid gap-2" onsubmit={saveRename}>
|
|
122
|
+
<form bind:this={renameForm} id={formId} class="grid gap-2" onsubmit={saveRename}>
|
|
106
123
|
<Input
|
|
107
124
|
bind:value={draft}
|
|
108
125
|
aria-label="Conversation title"
|
|
@@ -130,5 +147,8 @@
|
|
|
130
147
|
confirmText="Delete"
|
|
131
148
|
confirmVariant="destructive"
|
|
132
149
|
onConfirm={() => void confirmDelete()}
|
|
133
|
-
onCloseAutoFocus={
|
|
150
|
+
onCloseAutoFocus={(event) => {
|
|
151
|
+
restoreFocus(event);
|
|
152
|
+
releaseStuckBodyLock();
|
|
153
|
+
}}
|
|
134
154
|
/>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConversationDialogs.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConversationDialogs.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"ConversationDialogs.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConversationDialogs.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AAQD,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,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,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAClC;AAkHH;;;;;;;GAOG;AACH,QAAA,MAAM,mBAAmB,gEAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Renders sanitized markdown HTML from `parseMarkdown`, with the chat prose rules. The rules are
|
|
4
4
|
Tailwind arbitrary variants on the container rather than a stylesheet: the Classic App's
|
|
5
5
|
cascade-layers polyfill raises the specificity of its base styles above any plain component
|
|
6
|
-
CSS, while utilities still win. Values follow theme-docs `.markdown-content`, sized for chat.
|
|
6
|
+
CSS, while utilities still win. Values follow theme-docs `.markdown-content`, sized for chat. Tables don't break words (the
|
|
7
|
+
column would squeeze "$1,075,000" to two lines); they scroll sideways instead.
|
|
7
8
|
-->
|
|
8
9
|
<script lang="ts" module>
|
|
9
10
|
const PROSE = [
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
'[&_pre_code]:rounded-none [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-[length:inherit]',
|
|
24
25
|
'[&_blockquote]:mb-3 [&_blockquote]:border-l-4 [&_blockquote]:border-primary [&_blockquote]:bg-muted/50 [&_blockquote]:px-4 [&_blockquote]:py-2 [&_blockquote]:text-muted-foreground',
|
|
25
26
|
'[&_hr]:my-5 [&_hr]:border-border',
|
|
26
|
-
'[&_table]:mb-3 [&_table]:block [&_table]:max-w-full [&_table]:border-collapse [&_table]:overflow-x-auto [&_table]:text-[0.875em]',
|
|
27
|
+
'[&_table]:mb-3 [&_table]:block [&_table]:max-w-full [&_table]:border-collapse [&_table]:overflow-x-auto [&_table]:text-[0.875em] [&_table]:[overflow-wrap:normal]',
|
|
27
28
|
'[&_th]:border [&_th]:border-border [&_th]:px-3 [&_th]:py-1.5 [&_th]:text-left [&_th]:align-top',
|
|
28
29
|
'[&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-1.5 [&_td]:text-left [&_td]:align-top',
|
|
29
30
|
'[&_th]:bg-muted [&_th]:font-semibold',
|
|
@@ -8,7 +8,8 @@ interface Props {
|
|
|
8
8
|
* Renders sanitized markdown HTML from `parseMarkdown`, with the chat prose rules. The rules are
|
|
9
9
|
* Tailwind arbitrary variants on the container rather than a stylesheet: the Classic App's
|
|
10
10
|
* cascade-layers polyfill raises the specificity of its base styles above any plain component
|
|
11
|
-
* CSS, while utilities still win. Values follow theme-docs `.markdown-content`, sized for chat.
|
|
11
|
+
* CSS, while utilities still win. Values follow theme-docs `.markdown-content`, sized for chat. Tables don't break words (the
|
|
12
|
+
* column would squeeze "$1,075,000" to two lines); they scroll sideways instead.
|
|
12
13
|
*/
|
|
13
14
|
declare const Markdown: import("svelte").Component<Props, {}, "">;
|
|
14
15
|
type Markdown = ReturnType<typeof Markdown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Markdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Markdown.svelte.ts"],"names":[],"mappings":"AA+BE,UAAU,KAAK;IACb,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAgBH
|
|
1
|
+
{"version":3,"file":"Markdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Markdown.svelte.ts"],"names":[],"mappings":"AA+BE,UAAU,KAAK;IACb,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAgBH;;;;;;GAMG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-fixes.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/live-fixes.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,127 @@
|
|
|
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 TwoSheets from '../../test-fixtures/TwoSheets.svelte';
|
|
5
|
+
import { releaseStuckBodyLock } from '../body-lock.js';
|
|
6
|
+
import { parseMarkdown } from '../markdown.js';
|
|
7
|
+
import { ChatSession } from '../session.svelte.js';
|
|
8
|
+
import AgentChat from './AgentChat.svelte';
|
|
9
|
+
import ConversationSidebar from './ConversationSidebar.svelte';
|
|
10
|
+
import Markdown from './Markdown.svelte';
|
|
11
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
document.body.removeAttribute('style');
|
|
15
|
+
});
|
|
16
|
+
/** A client whose streamChat replays `events` (all in one tick) and reports done. */
|
|
17
|
+
function fakeClient(events) {
|
|
18
|
+
return {
|
|
19
|
+
fileUrl: (id) => `#${id}`,
|
|
20
|
+
uploadFile: vi.fn(),
|
|
21
|
+
cancel: vi.fn(),
|
|
22
|
+
getConversation: vi.fn(),
|
|
23
|
+
resumeTurn: vi.fn(),
|
|
24
|
+
streamChat: vi.fn(async (req, opts) => {
|
|
25
|
+
events(req.requestId).forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
26
|
+
return { kind: 'done' };
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const scope = { agent: 'classic-agent', runId: 'run-1' };
|
|
31
|
+
const turn = (requestId) => [
|
|
32
|
+
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
33
|
+
{ type: 'delta', text: 'Here it is.' },
|
|
34
|
+
{
|
|
35
|
+
type: 'artifact',
|
|
36
|
+
...scope,
|
|
37
|
+
artifactId: 'a1',
|
|
38
|
+
op: 'open',
|
|
39
|
+
mediaType: 'text/markdown',
|
|
40
|
+
title: 'Memo',
|
|
41
|
+
},
|
|
42
|
+
{ type: 'artifact', ...scope, artifactId: 'a1', op: 'delta', text: '# Memo' },
|
|
43
|
+
{ type: 'artifact', ...scope, artifactId: 'a1', op: 'close' },
|
|
44
|
+
{ type: 'done', finishReason: 'stop' },
|
|
45
|
+
].map((e) => ({ ...e, requestId }));
|
|
46
|
+
describe('body lock guard', () => {
|
|
47
|
+
it('clears the lock the bits-ui race leaves behind once no modal is open', async () => {
|
|
48
|
+
// Reproduces the race: A closes and B opens in the same tick, then B closes.
|
|
49
|
+
const { rerender } = render(TwoSheets, { props: { a: true, b: false } });
|
|
50
|
+
await wait(100);
|
|
51
|
+
await rerender({ a: false, b: false });
|
|
52
|
+
await wait(0);
|
|
53
|
+
await rerender({ a: false, b: true });
|
|
54
|
+
await wait(300);
|
|
55
|
+
await rerender({ a: false, b: false });
|
|
56
|
+
await wait(600);
|
|
57
|
+
expect(document.body.style.pointerEvents).toBe('none');
|
|
58
|
+
releaseStuckBodyLock(0);
|
|
59
|
+
await wait(10);
|
|
60
|
+
expect(document.body.style.pointerEvents).toBe('');
|
|
61
|
+
expect(document.body.style.overflow).toBe('');
|
|
62
|
+
});
|
|
63
|
+
it('leaves the lock alone while a modal is open', async () => {
|
|
64
|
+
render(TwoSheets, { props: { a: true } });
|
|
65
|
+
await wait(300);
|
|
66
|
+
releaseStuckBodyLock(0);
|
|
67
|
+
await wait(10);
|
|
68
|
+
expect(document.body.style.pointerEvents).toBe('none');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('ChatSession onTurnEnd', () => {
|
|
72
|
+
it('reports the finished turn with its conversation and status', async () => {
|
|
73
|
+
const onTurnEnd = vi.fn();
|
|
74
|
+
const client = fakeClient(turn);
|
|
75
|
+
const session = new ChatSession({ client: client, onTurnEnd });
|
|
76
|
+
await session.send('hi');
|
|
77
|
+
expect(onTurnEnd).toHaveBeenCalledOnce();
|
|
78
|
+
expect(onTurnEnd.mock.calls[0][0]).toMatchObject({ conversationId: 'c-1', status: 'done' });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('AgentChat artifacts', () => {
|
|
82
|
+
it('opens an artifact that starts and finishes within one tick', async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
const onturnend = vi.fn();
|
|
85
|
+
render(AgentChat, {
|
|
86
|
+
props: { client: fakeClient(turn), sandboxOrigin: 'https://frame.test', onturnend },
|
|
87
|
+
});
|
|
88
|
+
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'memo{Enter}');
|
|
89
|
+
// jsdom has no wide viewport, so the artifact opens in the sheet.
|
|
90
|
+
expect(await screen.findByRole('dialog', { name: 'Memo' })).toBeTruthy();
|
|
91
|
+
await waitFor(() => expect(onturnend).toHaveBeenCalledOnce());
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('rename dialog', () => {
|
|
95
|
+
it('starts in the title field with the title selected', async () => {
|
|
96
|
+
const user = userEvent.setup();
|
|
97
|
+
const now = new Date().toISOString();
|
|
98
|
+
render(ConversationSidebar, {
|
|
99
|
+
props: {
|
|
100
|
+
conversations: [
|
|
101
|
+
{
|
|
102
|
+
conversationId: 'c1',
|
|
103
|
+
agent: 'classic-agent',
|
|
104
|
+
title: 'Lot 12',
|
|
105
|
+
createdAt: now,
|
|
106
|
+
updatedAt: now,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
onrename: vi.fn(),
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
await user.click(screen.getByRole('button', { name: 'Options for Lot 12' }));
|
|
113
|
+
await user.click(await screen.findByRole('menuitem', { name: /Rename/ }));
|
|
114
|
+
const field = await screen.findByRole('textbox', { name: 'Conversation title' });
|
|
115
|
+
await waitFor(() => expect(document.activeElement).toBe(field));
|
|
116
|
+
const input = field;
|
|
117
|
+
expect([input.selectionStart, input.selectionEnd]).toEqual([0, 'Lot 12'.length]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('Markdown tables', () => {
|
|
121
|
+
it('keep words whole so wide tables scroll instead of breaking numbers', () => {
|
|
122
|
+
const { container } = render(Markdown, {
|
|
123
|
+
props: { html: parseMarkdown('| a | b |\n| - | - |\n| $1,075,000 | x |') },
|
|
124
|
+
});
|
|
125
|
+
expect(container.firstElementChild.className).toContain('[&_table]:[overflow-wrap:normal]');
|
|
126
|
+
});
|
|
127
|
+
});
|
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,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/session.svelte.d.ts
CHANGED
|
@@ -13,6 +13,15 @@ export interface ChatSessionOptions {
|
|
|
13
13
|
onConversationStarted?: (conversationId: string) => void;
|
|
14
14
|
onToolResult?: (name: string, status: string) => void;
|
|
15
15
|
onEvent?: (event: ChatEvent) => void;
|
|
16
|
+
/**
|
|
17
|
+
* A turn stopped streaming (done, error, cancelled or cut short), with its final status.
|
|
18
|
+
* Hosts refresh what the turn may have changed, e.g. the conversation list's running flag.
|
|
19
|
+
*/
|
|
20
|
+
onTurnEnd?: (info: {
|
|
21
|
+
requestId: string;
|
|
22
|
+
conversationId: string | null;
|
|
23
|
+
status: string;
|
|
24
|
+
}) => void;
|
|
16
25
|
}
|
|
17
26
|
export type SessionStatus = 'idle' | 'loading' | 'streaming';
|
|
18
27
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,SAAS,EAET,UAAU,EACV,eAAe,EACf,OAAO,EACR,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAKL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,qBAAqB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,SAAS,EAET,UAAU,EACV,eAAe,EACf,OAAO,EACR,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAKL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,qBAAqB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAClG;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7D;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,gBAA6B;IACrC,MAAM,gBAAiC;IACvC,cAAc,gBAA+B;IAC7C,gGAAgG;IAChG,oBAAoB,gBAA+B;IACnD,IAAI,8BAA8C;IAClD,SAAS,qBAAyC;IAClD,YAAY;;;;;;QAAyB;IAErC,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE,kBAAkB;IAMvC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,gBAAgB,GAAG,SAAS,CAG1C;IAED,yFAAyF;IACnF,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+B3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B5E;;;;OAIG;IACG,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ7C,+FAA+F;IACzF,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C,MAAM,IAAI,IAAI;YAOA,GAAG;IA2DjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,MAAM;CAKf;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAE1E"}
|
package/dist/session.svelte.js
CHANGED
|
@@ -156,6 +156,12 @@ export class ChatSession {
|
|
|
156
156
|
finally {
|
|
157
157
|
this.controller = undefined;
|
|
158
158
|
this.status = 'idle';
|
|
159
|
+
const final = this.messages.find((m) => m.role === 'assistant' && m.requestId === requestId);
|
|
160
|
+
this.options.onTurnEnd?.({
|
|
161
|
+
requestId,
|
|
162
|
+
conversationId: this.conversationId,
|
|
163
|
+
status: final?.status ?? 'done',
|
|
164
|
+
});
|
|
159
165
|
}
|
|
160
166
|
}
|
|
161
167
|
toolName(requestId, callId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/agent-chat",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Svelte 5 chat UI for Classic Agent: session state, message list, composer, tool timeline, artifacts and MCP-UI sandbox frames. Consumed by the Classic App.",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"dist"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@classic-homes/agent-client": "0.2.
|
|
21
|
-
"@classic-homes/agent-contracts": "0.2.
|
|
20
|
+
"@classic-homes/agent-client": "0.2.1",
|
|
21
|
+
"@classic-homes/agent-contracts": "0.2.1",
|
|
22
22
|
"dompurify": "3.4.17"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|