@classic-homes/agent-chat 0.11.0 → 0.12.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 +57 -2
- package/dist/components/AgentChat.svelte +58 -22
- package/dist/components/AgentChat.svelte.d.ts +11 -1
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/ArtifactPanel.svelte +4 -2
- package/dist/components/AssistantMessage.svelte +14 -5
- package/dist/components/AssistantMessage.svelte.d.ts +2 -0
- package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
- package/dist/components/ChatHeader.svelte +4 -1
- package/dist/components/ChatHeader.svelte.d.ts +2 -0
- package/dist/components/ChatHeader.svelte.d.ts.map +1 -1
- package/dist/components/ChatLauncher.svelte +157 -0
- package/dist/components/ChatLauncher.svelte.d.ts +23 -0
- package/dist/components/ChatLauncher.svelte.d.ts.map +1 -0
- package/dist/components/Composer.svelte +7 -3
- package/dist/components/Composer.svelte.d.ts.map +1 -1
- package/dist/components/FloatingWindow.svelte +239 -0
- package/dist/components/FloatingWindow.svelte.d.ts +26 -0
- package/dist/components/FloatingWindow.svelte.d.ts.map +1 -0
- package/dist/components/Markdown.svelte +4 -0
- package/dist/components/Markdown.svelte.d.ts.map +1 -1
- package/dist/components/MessageFeedback.svelte +11 -4
- package/dist/components/MessageFeedback.svelte.d.ts.map +1 -1
- package/dist/components/MessageFiles.svelte +133 -40
- package/dist/components/MessageFiles.svelte.d.ts +6 -0
- package/dist/components/MessageFiles.svelte.d.ts.map +1 -1
- package/dist/components/MessageItem.svelte +2 -0
- package/dist/components/MessageItem.svelte.d.ts +2 -0
- package/dist/components/MessageItem.svelte.d.ts.map +1 -1
- package/dist/components/MessageList.svelte +40 -7
- package/dist/components/MessageList.svelte.d.ts +3 -1
- package/dist/components/MessageList.svelte.d.ts.map +1 -1
- package/dist/components/ShortcutsDialog.svelte +10 -3
- package/dist/components/ShortcutsDialog.svelte.d.ts +2 -0
- package/dist/components/ShortcutsDialog.svelte.d.ts.map +1 -1
- package/dist/components/UiFrame.svelte +12 -4
- package/dist/components/UiFrame.svelte.d.ts.map +1 -1
- package/dist/components/UserMessage.svelte +10 -1
- package/dist/components/UserMessage.svelte.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +94 -61
- package/dist/reducer.d.ts +11 -0
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +18 -2
- package/dist/session.svelte.d.ts +38 -0
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +176 -23
- package/dist/shortcuts.d.ts +7 -0
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +9 -0
- package/dist/surface-geometry.d.ts +56 -0
- package/dist/surface-geometry.d.ts.map +1 -0
- package/dist/surface-geometry.js +99 -0
- package/dist/surface.svelte.d.ts +69 -0
- package/dist/surface.svelte.d.ts.map +1 -0
- package/dist/surface.svelte.js +156 -0
- package/dist/ui-frame-protocol.d.ts +6 -0
- package/dist/ui-frame-protocol.d.ts.map +1 -1
- package/dist/ui-frame-protocol.js +11 -0
- package/package.json +8 -7
- package/dist/artifact-pages.test.d.ts +0 -2
- package/dist/artifact-pages.test.d.ts.map +0 -1
- package/dist/artifact-pages.test.js +0 -102
- package/dist/components/a11y.svelte.test.d.ts +0 -2
- package/dist/components/a11y.svelte.test.d.ts.map +0 -1
- package/dist/components/a11y.svelte.test.js +0 -183
- package/dist/components/agent-picker.svelte.test.d.ts +0 -2
- package/dist/components/agent-picker.svelte.test.d.ts.map +0 -1
- package/dist/components/agent-picker.svelte.test.js +0 -84
- package/dist/components/composer.svelte.test.d.ts +0 -2
- package/dist/components/composer.svelte.test.d.ts.map +0 -1
- package/dist/components/composer.svelte.test.js +0 -162
- package/dist/components/continue.svelte.test.d.ts +0 -2
- package/dist/components/continue.svelte.test.d.ts.map +0 -1
- package/dist/components/continue.svelte.test.js +0 -109
- package/dist/components/conversation-chrome.svelte.test.d.ts +0 -2
- package/dist/components/conversation-chrome.svelte.test.d.ts.map +0 -1
- package/dist/components/conversation-chrome.svelte.test.js +0 -147
- package/dist/components/feedback.svelte.test.d.ts +0 -2
- package/dist/components/feedback.svelte.test.d.ts.map +0 -1
- package/dist/components/feedback.svelte.test.js +0 -99
- package/dist/components/live-fixes.svelte.test.d.ts +0 -2
- package/dist/components/live-fixes.svelte.test.d.ts.map +0 -1
- package/dist/components/live-fixes.svelte.test.js +0 -127
- package/dist/components/markdown-prose.svelte.test.d.ts +0 -2
- package/dist/components/markdown-prose.svelte.test.d.ts.map +0 -1
- package/dist/components/markdown-prose.svelte.test.js +0 -26
- package/dist/components/messages.svelte.test.d.ts +0 -2
- package/dist/components/messages.svelte.test.d.ts.map +0 -1
- package/dist/components/messages.svelte.test.js +0 -215
- package/dist/components/regenerate.svelte.test.d.ts +0 -2
- package/dist/components/regenerate.svelte.test.d.ts.map +0 -1
- package/dist/components/regenerate.svelte.test.js +0 -108
- package/dist/components/welcome.svelte.test.d.ts +0 -2
- package/dist/components/welcome.svelte.test.d.ts.map +0 -1
- package/dist/components/welcome.svelte.test.js +0 -119
- package/dist/composer-keys.test.d.ts +0 -2
- package/dist/composer-keys.test.d.ts.map +0 -1
- package/dist/composer-keys.test.js +0 -16
- package/dist/markdown.test.d.ts +0 -2
- package/dist/markdown.test.d.ts.map +0 -1
- package/dist/markdown.test.js +0 -35
- package/dist/reducer.test.d.ts +0 -2
- package/dist/reducer.test.d.ts.map +0 -1
- package/dist/reducer.test.js +0 -153
- package/dist/ui-frame-protocol.test.d.ts +0 -2
- package/dist/ui-frame-protocol.test.d.ts.map +0 -1
- package/dist/ui-frame-protocol.test.js +0 -48
- package/dist/ui-frame-relay.test.d.ts +0 -2
- package/dist/ui-frame-relay.test.d.ts.map +0 -1
- package/dist/ui-frame-relay.test.js +0 -36
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Corner, type Rect, type ResizeDirection, type Size } from './surface-geometry.js';
|
|
2
|
+
/**
|
|
3
|
+
* How the chat is presented over a host page: collapsed to the launcher button, a floating
|
|
4
|
+
* window, or docked to the side. The full page is a host route, not a mode here; a host on that
|
|
5
|
+
* route simply renders none of these.
|
|
6
|
+
*/
|
|
7
|
+
export type SurfaceMode = 'launcher' | 'float' | 'dock';
|
|
8
|
+
export type OpenSurfaceMode = Exclude<SurfaceMode, 'launcher'>;
|
|
9
|
+
/** What the collapsed launcher shows about the chat behind it. */
|
|
10
|
+
export type LauncherStatus = 'idle' | 'streaming' | 'unread' | 'error';
|
|
11
|
+
export interface ChatSurfaceOptions {
|
|
12
|
+
/** Where layout is remembered, e.g. `classic-agent:surface:<userId>`. Not persisted without it. */
|
|
13
|
+
storageKey?: string;
|
|
14
|
+
/** Defaults to `localStorage` when available. */
|
|
15
|
+
storage?: Pick<Storage, 'getItem' | 'setItem'>;
|
|
16
|
+
/** Defaults to the window's inner size. */
|
|
17
|
+
viewport?: () => Size;
|
|
18
|
+
/** What the launcher opens the first time. */
|
|
19
|
+
defaultOpen?: OpenSurfaceMode;
|
|
20
|
+
corner?: Corner;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reactive state of the detached chat (Svelte 5 runes class): which presentation is showing,
|
|
24
|
+
* the launcher's corner, the floating window's geometry and whether an answer arrived unseen.
|
|
25
|
+
* Layout survives reloads through `storageKey`; a saved window that no longer fits the viewport
|
|
26
|
+
* is pulled back inside.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ChatSurface {
|
|
29
|
+
mode: SurfaceMode;
|
|
30
|
+
/** The open presentation the launcher returns to. */
|
|
31
|
+
lastOpen: OpenSurfaceMode;
|
|
32
|
+
corner: Corner;
|
|
33
|
+
/** An answer finished while the chat was collapsed. Cleared on open. */
|
|
34
|
+
unread: boolean;
|
|
35
|
+
/** True while the user drags or resizes the window. */
|
|
36
|
+
moving: boolean;
|
|
37
|
+
private saved;
|
|
38
|
+
private readonly options;
|
|
39
|
+
constructor(options?: ChatSurfaceOptions);
|
|
40
|
+
get isOpen(): boolean;
|
|
41
|
+
/** The floating window's rect; a default beside the launcher until the user moves it. */
|
|
42
|
+
get rect(): Rect;
|
|
43
|
+
open(mode?: OpenSurfaceMode): void;
|
|
44
|
+
minimize(): void;
|
|
45
|
+
toggle(): void;
|
|
46
|
+
dock(): void;
|
|
47
|
+
float(): void;
|
|
48
|
+
/** A turn ended; flag it when the user cannot see the answer. */
|
|
49
|
+
notifyTurnEnd(): void;
|
|
50
|
+
setCorner(corner: Corner): void;
|
|
51
|
+
/** Move the window by a pointer or key delta. Call `commit` when the gesture ends. */
|
|
52
|
+
moveBy(dx: number, dy: number): void;
|
|
53
|
+
resizeBy(direction: ResizeDirection, dx: number, dy: number): void;
|
|
54
|
+
/** End of a move or resize: remember the result. */
|
|
55
|
+
commit(): void;
|
|
56
|
+
/** Forget the user's geometry and return the window to its default place. */
|
|
57
|
+
resetRect(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Keep the window inside the viewport as it changes size. Returns the cleanup; call from an
|
|
60
|
+
* effect while the floating window is mounted.
|
|
61
|
+
*/
|
|
62
|
+
attach(target?: Pick<Window, 'addEventListener' | 'removeEventListener'>): () => void;
|
|
63
|
+
private viewport;
|
|
64
|
+
private storage;
|
|
65
|
+
private restore;
|
|
66
|
+
private persist;
|
|
67
|
+
}
|
|
68
|
+
export declare function createChatSurface(options?: ChatSurfaceOptions): ChatSurface;
|
|
69
|
+
//# sourceMappingURL=surface.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"surface.svelte.d.ts","sourceRoot":"","sources":["../src/lib/surface.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,MAAM,EACX,KAAK,IAAI,EACT,KAAK,eAAe,EACpB,KAAK,IAAI,EACV,MAAM,uBAAuB,CAAC;AAE/B;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;AAE/D,kEAAkE;AAClE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEvE,MAAM,WAAW,kBAAkB;IACjC,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;IAC/C,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAiBD;;;;;GAKG;AACH,qBAAa,WAAW;IACtB,IAAI,cAAmC;IACvC,qDAAqD;IACrD,QAAQ,kBAAoC;IAC5C,MAAM,SAAkC;IACxC,wEAAwE;IACxE,MAAM,UAAiB;IACvB,uDAAuD;IACvD,MAAM,UAAiB;IACvB,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,GAAE,kBAAuB;IAO5C,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,yFAAyF;IACzF,IAAI,IAAI,IAAI,IAAI,CAGf;IAED,IAAI,CAAC,IAAI,GAAE,eAA+B,GAAG,IAAI;IAMjD,QAAQ,IAAI,IAAI;IAIhB,MAAM,IAAI,IAAI;IAId,IAAI,IAAI,IAAI;IAGZ,KAAK,IAAI,IAAI;IAIb,iEAAiE;IACjE,aAAa,IAAI,IAAI;IAIrB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK/B,sFAAsF;IACtF,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAIpC,QAAQ,CAAC,SAAS,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAGlE,oDAAoD;IACpD,MAAM,IAAI,IAAI;IAId,6EAA6E;IAC7E,SAAS,IAAI,IAAI;IAKjB;;;OAGG;IACH,MAAM,CAAC,MAAM,GAAE,IAAI,CAAC,MAAM,EAAE,kBAAkB,GAAG,qBAAqB,CAAU,GAAG,MAAM,IAAI;IAS7F,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,OAAO;CAchB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAE/E"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { CORNERS, clampRect, defaultRect, resizeRect, } from './surface-geometry.js';
|
|
2
|
+
const isRect = (value) => typeof value === 'object' &&
|
|
3
|
+
value !== null &&
|
|
4
|
+
['x', 'y', 'width', 'height'].every((key) => Number.isFinite(value[key]));
|
|
5
|
+
/**
|
|
6
|
+
* Reactive state of the detached chat (Svelte 5 runes class): which presentation is showing,
|
|
7
|
+
* the launcher's corner, the floating window's geometry and whether an answer arrived unseen.
|
|
8
|
+
* Layout survives reloads through `storageKey`; a saved window that no longer fits the viewport
|
|
9
|
+
* is pulled back inside.
|
|
10
|
+
*/
|
|
11
|
+
export class ChatSurface {
|
|
12
|
+
mode = $state('launcher');
|
|
13
|
+
/** The open presentation the launcher returns to. */
|
|
14
|
+
lastOpen = $state('float');
|
|
15
|
+
corner = $state('bottom-right');
|
|
16
|
+
/** An answer finished while the chat was collapsed. Cleared on open. */
|
|
17
|
+
unread = $state(false);
|
|
18
|
+
/** True while the user drags or resizes the window. */
|
|
19
|
+
moving = $state(false);
|
|
20
|
+
saved = $state(null);
|
|
21
|
+
options;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.lastOpen = options.defaultOpen ?? 'float';
|
|
25
|
+
this.corner = options.corner ?? 'bottom-right';
|
|
26
|
+
this.restore();
|
|
27
|
+
}
|
|
28
|
+
get isOpen() {
|
|
29
|
+
return this.mode !== 'launcher';
|
|
30
|
+
}
|
|
31
|
+
/** The floating window's rect; a default beside the launcher until the user moves it. */
|
|
32
|
+
get rect() {
|
|
33
|
+
const viewport = this.viewport();
|
|
34
|
+
return this.saved ? clampRect(this.saved, viewport) : defaultRect(viewport, this.corner);
|
|
35
|
+
}
|
|
36
|
+
open(mode = this.lastOpen) {
|
|
37
|
+
this.mode = mode;
|
|
38
|
+
this.lastOpen = mode;
|
|
39
|
+
this.unread = false;
|
|
40
|
+
this.persist();
|
|
41
|
+
}
|
|
42
|
+
minimize() {
|
|
43
|
+
this.mode = 'launcher';
|
|
44
|
+
this.persist();
|
|
45
|
+
}
|
|
46
|
+
toggle() {
|
|
47
|
+
if (this.isOpen)
|
|
48
|
+
this.minimize();
|
|
49
|
+
else
|
|
50
|
+
this.open();
|
|
51
|
+
}
|
|
52
|
+
dock() {
|
|
53
|
+
this.open('dock');
|
|
54
|
+
}
|
|
55
|
+
float() {
|
|
56
|
+
this.open('float');
|
|
57
|
+
}
|
|
58
|
+
/** A turn ended; flag it when the user cannot see the answer. */
|
|
59
|
+
notifyTurnEnd() {
|
|
60
|
+
if (!this.isOpen)
|
|
61
|
+
this.unread = true;
|
|
62
|
+
}
|
|
63
|
+
setCorner(corner) {
|
|
64
|
+
this.corner = corner;
|
|
65
|
+
this.persist();
|
|
66
|
+
}
|
|
67
|
+
/** Move the window by a pointer or key delta. Call `commit` when the gesture ends. */
|
|
68
|
+
moveBy(dx, dy) {
|
|
69
|
+
const rect = this.rect;
|
|
70
|
+
this.saved = clampRect({ ...rect, x: rect.x + dx, y: rect.y + dy }, this.viewport());
|
|
71
|
+
}
|
|
72
|
+
resizeBy(direction, dx, dy) {
|
|
73
|
+
this.saved = resizeRect(this.rect, direction, dx, dy, this.viewport());
|
|
74
|
+
}
|
|
75
|
+
/** End of a move or resize: remember the result. */
|
|
76
|
+
commit() {
|
|
77
|
+
this.moving = false;
|
|
78
|
+
this.persist();
|
|
79
|
+
}
|
|
80
|
+
/** Forget the user's geometry and return the window to its default place. */
|
|
81
|
+
resetRect() {
|
|
82
|
+
this.saved = null;
|
|
83
|
+
this.persist();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Keep the window inside the viewport as it changes size. Returns the cleanup; call from an
|
|
87
|
+
* effect while the floating window is mounted.
|
|
88
|
+
*/
|
|
89
|
+
attach(target = window) {
|
|
90
|
+
const fit = () => {
|
|
91
|
+
if (this.saved)
|
|
92
|
+
this.saved = clampRect(this.saved, this.viewport());
|
|
93
|
+
};
|
|
94
|
+
fit();
|
|
95
|
+
target.addEventListener('resize', fit);
|
|
96
|
+
return () => target.removeEventListener('resize', fit);
|
|
97
|
+
}
|
|
98
|
+
viewport() {
|
|
99
|
+
if (this.options.viewport)
|
|
100
|
+
return this.options.viewport();
|
|
101
|
+
if (typeof window === 'undefined')
|
|
102
|
+
return { width: 1280, height: 800 };
|
|
103
|
+
return { width: window.innerWidth, height: window.innerHeight };
|
|
104
|
+
}
|
|
105
|
+
storage() {
|
|
106
|
+
if (!this.options.storageKey)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (this.options.storage)
|
|
109
|
+
return this.options.storage;
|
|
110
|
+
try {
|
|
111
|
+
return typeof localStorage === 'undefined' ? undefined : localStorage;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
restore() {
|
|
118
|
+
try {
|
|
119
|
+
const raw = this.storage()?.getItem(this.options.storageKey);
|
|
120
|
+
if (!raw)
|
|
121
|
+
return;
|
|
122
|
+
const data = JSON.parse(raw);
|
|
123
|
+
if (data.v !== 1)
|
|
124
|
+
return;
|
|
125
|
+
if (data.mode === 'launcher' || data.mode === 'float' || data.mode === 'dock')
|
|
126
|
+
this.mode = data.mode;
|
|
127
|
+
if (data.lastOpen === 'float' || data.lastOpen === 'dock')
|
|
128
|
+
this.lastOpen = data.lastOpen;
|
|
129
|
+
if (data.corner && CORNERS.includes(data.corner))
|
|
130
|
+
this.corner = data.corner;
|
|
131
|
+
if (isRect(data.rect))
|
|
132
|
+
this.saved = clampRect(data.rect, this.viewport());
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Unreadable or blocked storage: start from the defaults.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
persist() {
|
|
139
|
+
try {
|
|
140
|
+
const data = {
|
|
141
|
+
v: 1,
|
|
142
|
+
mode: this.mode,
|
|
143
|
+
lastOpen: this.lastOpen,
|
|
144
|
+
corner: this.corner,
|
|
145
|
+
rect: this.saved,
|
|
146
|
+
};
|
|
147
|
+
this.storage()?.setItem(this.options.storageKey, JSON.stringify(data));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Storage full or blocked: layout just is not remembered.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export function createChatSurface(options = {}) {
|
|
155
|
+
return new ChatSurface(options);
|
|
156
|
+
}
|
|
@@ -31,6 +31,12 @@ export type FrameAction = {
|
|
|
31
31
|
};
|
|
32
32
|
export declare const MIN_FRAME_HEIGHT = 120;
|
|
33
33
|
export declare const MAX_FRAME_HEIGHT = 1200;
|
|
34
|
+
/**
|
|
35
|
+
* A UI resource's preferred height (`preferredFrameSize[1]`, server-supplied) in px: only a plain
|
|
36
|
+
* pixel number (`480` or `480px`) counts, clamped to the frame limits; anything else is the
|
|
37
|
+
* minimum. Never interpolate the raw value into a style.
|
|
38
|
+
*/
|
|
39
|
+
export declare function initialFrameHeight(value: string | undefined): number;
|
|
34
40
|
/** Parse a message from the sandbox into a FrameAction, or null when it is not one. */
|
|
35
41
|
export declare function parseFrameMessage(data: unknown): FrameAction | null;
|
|
36
42
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-frame-protocol.d.ts","sourceRoot":"","sources":["../src/lib/ui-frame-protocol.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,cAAc,8BAA8B,CAAC;AAC1D,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAEjD,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CA0CnE;AAQD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,iBAAiB,GAAG,IAAI,EAC/B,aAAa,EAAE,MAAM,GACpB,OAAO,CAGT;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,+DAA+D;AAC/D,wBAAgB,eAAe,CAC7B,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAAE,CAAC,GAC1D,MAAM,CAIR"}
|
|
1
|
+
{"version":3,"file":"ui-frame-protocol.d.ts","sourceRoot":"","sources":["../src/lib/ui-frame-protocol.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,cAAc,8BAA8B,CAAC;AAC1D,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAEjD,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAIpE;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CA0CnE;AAQD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,iBAAiB,GAAG,IAAI,EAC/B,aAAa,EAAE,MAAM,GACpB,OAAO,CAGT;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,+DAA+D;AAC/D,wBAAgB,eAAe,CAC7B,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAAE,CAAC,GAC1D,MAAM,CAIR"}
|
|
@@ -8,6 +8,17 @@ export const UI_FRAME_READY = 'ui-lifecycle-iframe-ready';
|
|
|
8
8
|
export const UI_FRAME_RENDER = 'agent-ui:render';
|
|
9
9
|
export const MIN_FRAME_HEIGHT = 120;
|
|
10
10
|
export const MAX_FRAME_HEIGHT = 1200;
|
|
11
|
+
/**
|
|
12
|
+
* A UI resource's preferred height (`preferredFrameSize[1]`, server-supplied) in px: only a plain
|
|
13
|
+
* pixel number (`480` or `480px`) counts, clamped to the frame limits; anything else is the
|
|
14
|
+
* minimum. Never interpolate the raw value into a style.
|
|
15
|
+
*/
|
|
16
|
+
export function initialFrameHeight(value) {
|
|
17
|
+
const match = value === undefined ? null : /^(\d{1,4})(px)?$/.exec(value.trim());
|
|
18
|
+
if (!match)
|
|
19
|
+
return MIN_FRAME_HEIGHT;
|
|
20
|
+
return Math.min(MAX_FRAME_HEIGHT, Math.max(MIN_FRAME_HEIGHT, Number(match[1])));
|
|
21
|
+
}
|
|
11
22
|
/** Parse a message from the sandbox into a FrameAction, or null when it is not one. */
|
|
12
23
|
export function parseFrameMessage(data) {
|
|
13
24
|
if (!data || typeof data !== 'object')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/agent-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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.",
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"!dist/**/*.test.*"
|
|
18
19
|
],
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@classic-homes/agent-client": "0.
|
|
21
|
-
"@classic-homes/agent-contracts": "0.
|
|
21
|
+
"@classic-homes/agent-client": "0.12.0",
|
|
22
|
+
"@classic-homes/agent-contracts": "0.12.0",
|
|
22
23
|
"dompurify": "3.4.17"
|
|
23
24
|
},
|
|
24
25
|
"peerDependencies": {
|
|
@@ -30,11 +31,11 @@
|
|
|
30
31
|
"@sveltejs/package": "3.0.2",
|
|
31
32
|
"@sveltejs/vite-plugin-svelte": "7.3.1",
|
|
32
33
|
"@testing-library/svelte": "5.4.2",
|
|
33
|
-
"@testing-library/user-event": "14.6.
|
|
34
|
-
"axe-core": "4.
|
|
34
|
+
"@testing-library/user-event": "14.6.8",
|
|
35
|
+
"axe-core": "4.14.0",
|
|
35
36
|
"jsdom": "30.1.3",
|
|
36
37
|
"svelte": "5.57.3",
|
|
37
|
-
"svelte-check": "4.7.
|
|
38
|
+
"svelte-check": "4.7.8",
|
|
38
39
|
"vite": "8.3.5"
|
|
39
40
|
},
|
|
40
41
|
"license": "UNLICENSED",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"artifact-pages.test.d.ts","sourceRoot":"","sources":["../src/lib/artifact-pages.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { runInThisContext, Script } from 'node:vm';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { inlineScriptLiteral, MERMAID_LOAD_TIMEOUT_MS, MERMAID_MODULE_URL, mermaidPage, mermaidScript, } from './artifact-pages.js';
|
|
6
|
-
const BREAKOUT = 'graph TD\n A["</script><script>alert(1)</script>"] --> B[<!-- x & y >]\u2028';
|
|
7
|
-
function scripts(html) {
|
|
8
|
-
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
9
|
-
return [...doc.querySelectorAll('script')].map((s) => ({
|
|
10
|
-
attrs: s
|
|
11
|
-
.getAttributeNames()
|
|
12
|
-
.map((n) => `${n}=${s.getAttribute(n)}`)
|
|
13
|
-
.join(' '),
|
|
14
|
-
body: s.textContent ?? '',
|
|
15
|
-
}));
|
|
16
|
-
}
|
|
17
|
-
/** Run the page's init script against the current (jsdom) document with a stand-in `import()`. */
|
|
18
|
-
function run(source, load) {
|
|
19
|
-
document.body.innerHTML = '<div id="diagram" aria-busy="true"></div>';
|
|
20
|
-
const body = mermaidScript(source).replace('import(', '__load(');
|
|
21
|
-
const init = runInThisContext(`(function (__load) {\n${body}\n})`);
|
|
22
|
-
init(load);
|
|
23
|
-
}
|
|
24
|
-
describe('mermaidPage', () => {
|
|
25
|
-
it('initializes the renderer from a module script', () => {
|
|
26
|
-
const found = scripts(mermaidPage('graph TD; A-->B'));
|
|
27
|
-
expect(found).toHaveLength(1);
|
|
28
|
-
expect(found[0].attrs).toBe('type=module');
|
|
29
|
-
expect(found[0].body).toContain(`import(${JSON.stringify(MERMAID_MODULE_URL)})`);
|
|
30
|
-
});
|
|
31
|
-
it('emits a script that parses as a module and as a classic script', async () => {
|
|
32
|
-
const body = mermaidScript(BREAKOUT);
|
|
33
|
-
expect(() => new Script(body)).not.toThrow();
|
|
34
|
-
// Module goal: strict mode, `import`/`await` reserved. vm.SourceTextModule is behind a
|
|
35
|
-
// flag, so check the module-only constraints the classic parse cannot see.
|
|
36
|
-
expect(() => new Script(`'use strict';${body}`)).not.toThrow();
|
|
37
|
-
expect(body).not.toMatch(/^\s*import\s[^(]/m);
|
|
38
|
-
expect(body).not.toMatch(/^\s*export\s/m);
|
|
39
|
-
});
|
|
40
|
-
it('cannot be broken out of by the diagram source', () => {
|
|
41
|
-
const html = mermaidPage(BREAKOUT);
|
|
42
|
-
const found = scripts(html);
|
|
43
|
-
expect(found).toHaveLength(1);
|
|
44
|
-
expect(html.match(/<\/script/gi)).toHaveLength(1);
|
|
45
|
-
expect(found[0].body).not.toMatch(/<\/?script|<!--|\u2028|\u2029/i);
|
|
46
|
-
expect(found[0].body).toContain(inlineScriptLiteral(BREAKOUT));
|
|
47
|
-
// The literal round-trips to the exact source.
|
|
48
|
-
expect(JSON.parse(inlineScriptLiteral(BREAKOUT))).toBe(BREAKOUT);
|
|
49
|
-
expect(runInThisContext(`(${inlineScriptLiteral(BREAKOUT)})`)).toBe(BREAKOUT);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
describe('mermaid init script', () => {
|
|
53
|
-
beforeEach(() => vi.useFakeTimers());
|
|
54
|
-
afterEach(() => vi.useRealTimers());
|
|
55
|
-
it('renders the SVG and passes the exact source', async () => {
|
|
56
|
-
const render = vi.fn(async () => ({ svg: '<svg id="ok"></svg>' }));
|
|
57
|
-
const initialize = vi.fn();
|
|
58
|
-
const load = vi.fn(async () => ({ default: { initialize, render } }));
|
|
59
|
-
run(BREAKOUT, load);
|
|
60
|
-
await vi.runAllTimersAsync();
|
|
61
|
-
expect(load).toHaveBeenCalledWith(MERMAID_MODULE_URL);
|
|
62
|
-
expect(initialize).toHaveBeenCalledWith({
|
|
63
|
-
startOnLoad: false,
|
|
64
|
-
securityLevel: 'strict',
|
|
65
|
-
suppressErrorRendering: true,
|
|
66
|
-
});
|
|
67
|
-
expect(render).toHaveBeenCalledWith('agent-mermaid-svg', BREAKOUT);
|
|
68
|
-
expect(document.querySelector('#diagram svg#ok')).not.toBeNull();
|
|
69
|
-
expect(document.querySelector('[role="alert"]')).toBeNull();
|
|
70
|
-
expect(document.getElementById('diagram').hasAttribute('aria-busy')).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
it('shows an error and the source when the renderer fails to load', async () => {
|
|
73
|
-
run('graph TD; A-->B', () => Promise.reject(new TypeError('Failed to fetch module')));
|
|
74
|
-
await vi.runAllTimersAsync();
|
|
75
|
-
expect(document.querySelector('[role="alert"]')?.textContent).toBe('The diagram renderer could not be loaded: Failed to fetch module');
|
|
76
|
-
expect(document.querySelector('#diagram pre')?.textContent).toBe('graph TD; A-->B');
|
|
77
|
-
});
|
|
78
|
-
it('shows an error when the diagram does not parse', async () => {
|
|
79
|
-
const render = () => Promise.reject(new Error('Parse error on line 1'));
|
|
80
|
-
run('graph ??', async () => ({ default: { initialize() { }, render } }));
|
|
81
|
-
await vi.runAllTimersAsync();
|
|
82
|
-
expect(document.querySelector('[role="alert"]')?.textContent).toBe('The diagram could not be rendered: Parse error on line 1');
|
|
83
|
-
});
|
|
84
|
-
it('shows an error when the renderer never loads', async () => {
|
|
85
|
-
run('graph TD; A-->B', () => new Promise(() => { }));
|
|
86
|
-
await vi.advanceTimersByTimeAsync(MERMAID_LOAD_TIMEOUT_MS);
|
|
87
|
-
expect(document.querySelector('[role="alert"]')?.textContent).toBe('The diagram renderer did not load in time.');
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
describe('svelte-package script-type stripping (R18)', () => {
|
|
91
|
-
// svelte-package removes `type="…"` from every `<script` in a .svelte file, string literals
|
|
92
|
-
// included. A .svelte file may hold only its own top-level <script> tags.
|
|
93
|
-
it('no component builds <script> markup in a string', () => {
|
|
94
|
-
const dir = join(import.meta.dirname, 'components');
|
|
95
|
-
for (const file of readdirSync(dir).filter((f) => f.endsWith('.svelte'))) {
|
|
96
|
-
const text = readFileSync(join(dir, file), 'utf8');
|
|
97
|
-
const tags = text.match(/<script\b[^>]*>/g) ?? [];
|
|
98
|
-
const topLevel = text.match(/^<script\b[^>]*>/gm) ?? [];
|
|
99
|
-
expect(tags, file).toEqual(topLevel);
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"a11y.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/a11y.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
-
import axe from 'axe-core';
|
|
4
|
-
import userEvent from '@testing-library/user-event';
|
|
5
|
-
import AgentChat from './AgentChat.svelte';
|
|
6
|
-
import ChatHeader from './ChatHeader.svelte';
|
|
7
|
-
import ConversationSidebar from './ConversationSidebar.svelte';
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
cleanup();
|
|
10
|
-
document.body.removeAttribute('style');
|
|
11
|
-
});
|
|
12
|
-
/** axe in jsdom: no layout, so colour contrast is checked separately (tokens), not here. */
|
|
13
|
-
async function violations(root = document.body) {
|
|
14
|
-
const result = await axe.run(root, {
|
|
15
|
-
rules: { 'color-contrast': { enabled: false }, region: { enabled: false } },
|
|
16
|
-
});
|
|
17
|
-
return result.violations.map((v) => `${v.id}: ${v.nodes.map((n) => n.html).join(' | ')}`);
|
|
18
|
-
}
|
|
19
|
-
const scope = { agent: 'classic-agent', runId: 'run-1' };
|
|
20
|
-
function fakeClient() {
|
|
21
|
-
return {
|
|
22
|
-
fileUrl: (id) => `#${id}`,
|
|
23
|
-
uploadFile: vi.fn(),
|
|
24
|
-
cancel: vi.fn(),
|
|
25
|
-
resumeTurn: vi.fn(),
|
|
26
|
-
sendFeedback: vi.fn().mockResolvedValue(undefined),
|
|
27
|
-
getConversation: vi.fn(),
|
|
28
|
-
streamChat: vi.fn(async (req, opts) => {
|
|
29
|
-
[
|
|
30
|
-
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
31
|
-
{ type: 'tool_call', callId: 't1', name: 'hotsheet/list_communities', argsPreview: '{}' },
|
|
32
|
-
{ type: 'tool_result', callId: 't1', status: 'ok', durationMs: 640, summary: '24 rows' },
|
|
33
|
-
{ type: 'delta', text: '| A | B |\n|---|---|\n| 1 | 2 |\n\n```ts\nconst a = 1;\n```\n' },
|
|
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
|
-
].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
46
|
-
return { kind: 'done' };
|
|
47
|
-
}),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
const base = { sandboxOrigin: 'https://frame.test' };
|
|
51
|
-
describe('accessibility (axe)', () => {
|
|
52
|
-
it('has no violations on the welcome screen with the agent picker', async () => {
|
|
53
|
-
render(AgentChat, {
|
|
54
|
-
props: {
|
|
55
|
-
...base,
|
|
56
|
-
client: fakeClient(),
|
|
57
|
-
userName: 'Pat',
|
|
58
|
-
suggestions: [{ label: 'Open lots', description: 'By community', icon: 'home' }],
|
|
59
|
-
agents: [
|
|
60
|
-
{ key: 'classic-agent', name: 'Classic Agent' },
|
|
61
|
-
{ key: 'elevation-studio', name: 'Elevation Studio' },
|
|
62
|
-
],
|
|
63
|
-
prompts: [{ id: 'p1', label: 'Weekly recap', text: 'Recap' }],
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
expect(await violations()).toEqual([]);
|
|
67
|
-
// With the saved-prompt menu open.
|
|
68
|
-
await userEvent.type(screen.getByRole('textbox', { name: 'Message' }), '/');
|
|
69
|
-
await screen.findByRole('listbox', { name: 'Saved prompts' });
|
|
70
|
-
expect(await violations()).toEqual([]);
|
|
71
|
-
});
|
|
72
|
-
it('has no violations on a failed turn with Try again, and while editing', async () => {
|
|
73
|
-
const user = userEvent.setup();
|
|
74
|
-
const client = fakeClient();
|
|
75
|
-
client.streamChat.mockImplementationOnce(async (req, opts) => {
|
|
76
|
-
[
|
|
77
|
-
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
78
|
-
{ type: 'error', code: 'upstream_error', message: 'Model failed.', retryable: true },
|
|
79
|
-
].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
80
|
-
return { kind: 'done' };
|
|
81
|
-
});
|
|
82
|
-
render(AgentChat, { props: { ...base, client: client } });
|
|
83
|
-
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'hi{Enter}');
|
|
84
|
-
await screen.findByRole('button', { name: /Try again/ });
|
|
85
|
-
expect(await violations()).toEqual([]);
|
|
86
|
-
await user.click(screen.getByRole('button', { name: /Try again/ }));
|
|
87
|
-
// The retried answer opens its artifact in a (modal) sheet; close it first.
|
|
88
|
-
await screen.findByRole('dialog', { name: 'Memo' });
|
|
89
|
-
await user.keyboard('{Escape}');
|
|
90
|
-
await waitFor(() => expect(document.body.style.pointerEvents).not.toBe('none'));
|
|
91
|
-
await user.click(await screen.findByRole('button', { name: 'Edit message' }));
|
|
92
|
-
await screen.findByRole('textbox', { name: 'Edited message' });
|
|
93
|
-
expect(await violations()).toEqual([]);
|
|
94
|
-
});
|
|
95
|
-
it('has no violations on a finished turn with tools, code, a table and an artifact', async () => {
|
|
96
|
-
const user = userEvent.setup();
|
|
97
|
-
render(AgentChat, { props: { ...base, client: fakeClient(), variant: 'panel' } });
|
|
98
|
-
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'hi{Enter}');
|
|
99
|
-
await screen.findByRole('button', { name: /Open Memo|Memo/ });
|
|
100
|
-
// The artifact opened in a sheet; close it to check the transcript itself.
|
|
101
|
-
await user.keyboard('{Escape}');
|
|
102
|
-
expect(await violations()).toEqual([]);
|
|
103
|
-
});
|
|
104
|
-
it('has no violations in the keyboard shortcuts dialog', async () => {
|
|
105
|
-
const user = userEvent.setup();
|
|
106
|
-
render(ChatHeader, {
|
|
107
|
-
props: { title: 'x', shortcuts: [{ keys: ['Mod', 'J'], label: 'Open the agent panel' }] },
|
|
108
|
-
});
|
|
109
|
-
await user.click(screen.getByRole('button', { name: 'Conversation options' }));
|
|
110
|
-
await user.click(await screen.findByRole('menuitem', { name: /Keyboard shortcuts/ }));
|
|
111
|
-
await screen.findByRole('dialog', { name: 'Keyboard shortcuts' });
|
|
112
|
-
expect(await violations()).toEqual([]);
|
|
113
|
-
});
|
|
114
|
-
it('has no violations in the conversation rail', async () => {
|
|
115
|
-
const now = new Date().toISOString();
|
|
116
|
-
render(ConversationSidebar, {
|
|
117
|
-
props: {
|
|
118
|
-
conversations: [
|
|
119
|
-
{
|
|
120
|
-
conversationId: 'c1',
|
|
121
|
-
agent: 'classic-agent',
|
|
122
|
-
title: 'Lot 12',
|
|
123
|
-
createdAt: now,
|
|
124
|
-
updatedAt: now,
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
conversationId: 'c2',
|
|
128
|
-
agent: 'classic-agent',
|
|
129
|
-
title: 'Running',
|
|
130
|
-
createdAt: now,
|
|
131
|
-
updatedAt: now,
|
|
132
|
-
activeRequestId: '11111111-1111-4111-8111-111111111111',
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
selectedId: 'c1',
|
|
136
|
-
onrename: vi.fn(),
|
|
137
|
-
ondelete: vi.fn(),
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
expect(await violations()).toEqual([]);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
/** Answers "ok" with no artifact (an artifact opens a modal sheet, which takes focus). */
|
|
144
|
-
function plainAnswers(client) {
|
|
145
|
-
client.streamChat.mockImplementation(async (req, opts) => {
|
|
146
|
-
[
|
|
147
|
-
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
148
|
-
{ type: 'delta', text: 'ok' },
|
|
149
|
-
{ type: 'done', finishReason: 'stop' },
|
|
150
|
-
].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
151
|
-
return { kind: 'done' };
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
describe('focus', () => {
|
|
155
|
-
it('starts a new chat in the composer and returns there after a suggestion', async () => {
|
|
156
|
-
const user = userEvent.setup();
|
|
157
|
-
const client = fakeClient();
|
|
158
|
-
plainAnswers(client);
|
|
159
|
-
render(AgentChat, {
|
|
160
|
-
props: { ...base, client: client, suggestions: [{ label: 'Open lots' }] },
|
|
161
|
-
});
|
|
162
|
-
const box = screen.getByRole('textbox', { name: 'Message' });
|
|
163
|
-
await waitFor(() => expect(document.activeElement).toBe(box));
|
|
164
|
-
await user.click(screen.getByRole('button', { name: 'Open lots' }));
|
|
165
|
-
await waitFor(() => expect(document.activeElement).toBe(box));
|
|
166
|
-
});
|
|
167
|
-
it('returns to the Edit button when an edit is cancelled', async () => {
|
|
168
|
-
const user = userEvent.setup();
|
|
169
|
-
const client = fakeClient();
|
|
170
|
-
plainAnswers(client);
|
|
171
|
-
render(AgentChat, { props: { ...base, client: client } });
|
|
172
|
-
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'hi{Enter}');
|
|
173
|
-
await user.click(await screen.findByRole('button', { name: 'Edit message' }));
|
|
174
|
-
await user.keyboard('{Escape}');
|
|
175
|
-
await waitFor(() => expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Edit message' })));
|
|
176
|
-
});
|
|
177
|
-
it('does not steal focus when opening an existing conversation', () => {
|
|
178
|
-
const client = fakeClient();
|
|
179
|
-
client.getConversation.mockReturnValue(new Promise(() => { }));
|
|
180
|
-
render(AgentChat, { props: { ...base, client: client, conversationId: 'c-9' } });
|
|
181
|
-
expect(document.activeElement).toBe(document.body);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"agent-picker.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/agent-picker.svelte.test.ts"],"names":[],"mappings":""}
|