@classic-homes/agent-chat 0.1.0 → 0.1.2
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/components/AgentChat.svelte +15 -1
- package/dist/components/AgentChat.svelte.d.ts +3 -0
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/UiFrame.svelte +2 -1
- package/dist/components/UiFrame.svelte.d.ts.map +1 -1
- package/dist/session.svelte.d.ts +5 -1
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +5 -0
- package/dist/ui-frame-protocol.d.ts +13 -1
- package/dist/ui-frame-protocol.d.ts.map +1 -1
- package/dist/ui-frame-protocol.js +16 -2
- package/dist/ui-frame-protocol.test.js +15 -1
- package/package.json +3 -3
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
DocumentContext,
|
|
7
7
|
FileRef,
|
|
8
8
|
} from '@classic-homes/agent-contracts';
|
|
9
|
+
import type { BackendMode } from '@classic-homes/agent-contracts';
|
|
9
10
|
import { ChatSession } from '../session.svelte.js';
|
|
10
11
|
import ArtifactPanel from './ArtifactPanel.svelte';
|
|
11
12
|
import Composer from './Composer.svelte';
|
|
@@ -17,6 +18,8 @@
|
|
|
17
18
|
/** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
|
|
18
19
|
sandboxOrigin: string;
|
|
19
20
|
conversationId?: string | null;
|
|
21
|
+
/** Backend mode from `/api/me`; shows the mock banner before any turn runs. */
|
|
22
|
+
mode?: BackendMode;
|
|
20
23
|
agent?: AgentKey;
|
|
21
24
|
clientKind?: ClientKind;
|
|
22
25
|
variant?: 'page' | 'panel';
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
client,
|
|
37
40
|
sandboxOrigin,
|
|
38
41
|
conversationId = null,
|
|
42
|
+
mode,
|
|
39
43
|
agent = 'classic-agent',
|
|
40
44
|
clientKind,
|
|
41
45
|
variant = 'page',
|
|
@@ -54,16 +58,26 @@
|
|
|
54
58
|
const session = new ChatSession({
|
|
55
59
|
client,
|
|
56
60
|
conversationId,
|
|
61
|
+
mode,
|
|
57
62
|
agent,
|
|
58
63
|
clientKind,
|
|
59
64
|
getContext,
|
|
60
65
|
onConversationStarted: (id) => onconversationstarted?.(id),
|
|
61
66
|
onToolResult: (name, status) => ontoolresult?.(name, status),
|
|
62
67
|
});
|
|
68
|
+
// The constructor already knows `conversationId`; what decides a fetch is whether the
|
|
69
|
+
// transcript for it has been loaded (or started) in this session.
|
|
63
70
|
$effect(() => {
|
|
64
|
-
if (
|
|
71
|
+
if (
|
|
72
|
+
conversationId &&
|
|
73
|
+
conversationId !== session.loadedConversationId &&
|
|
74
|
+
session.status !== 'loading'
|
|
75
|
+
)
|
|
65
76
|
void session.load(conversationId);
|
|
66
77
|
});
|
|
78
|
+
$effect(() => {
|
|
79
|
+
if (mode && !session.mode) session.mode = mode;
|
|
80
|
+
});
|
|
67
81
|
|
|
68
82
|
let composer: Composer | undefined = $state();
|
|
69
83
|
let draft = $state('');
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { AgentApiClient } from '@classic-homes/agent-client';
|
|
2
2
|
import type { AgentKey, ClientKind, DocumentContext } from '@classic-homes/agent-contracts';
|
|
3
|
+
import type { BackendMode } from '@classic-homes/agent-contracts';
|
|
3
4
|
interface Props {
|
|
4
5
|
client: AgentApiClient;
|
|
5
6
|
/** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
|
|
6
7
|
sandboxOrigin: string;
|
|
7
8
|
conversationId?: string | null;
|
|
9
|
+
/** Backend mode from `/api/me`; shows the mock banner before any turn runs. */
|
|
10
|
+
mode?: BackendMode;
|
|
8
11
|
agent?: AgentKey;
|
|
9
12
|
clientKind?: ClientKind;
|
|
10
13
|
variant?: 'page' | 'panel';
|
|
@@ -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;
|
|
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;AAQhE,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,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,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+GH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
import type { UiResourceState } from '../reducer.js';
|
|
4
4
|
import {
|
|
5
|
+
FRAME_TARGET_ORIGIN,
|
|
5
6
|
MIN_FRAME_HEIGHT,
|
|
6
7
|
UI_FRAME_RENDER,
|
|
7
8
|
composerTextFor,
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
function send(): void {
|
|
28
29
|
frame?.contentWindow?.postMessage(
|
|
29
30
|
{ type: UI_FRAME_RENDER, html: resource.html, uri: resource.uri },
|
|
30
|
-
|
|
31
|
+
FRAME_TARGET_ORIGIN,
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UiFrame.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/UiFrame.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"UiFrame.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/UiFrame.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAYnD,UAAU,KAAK;IACb,QAAQ,EAAE,eAAe,CAAC;IAC1B,gGAAgG;IAChG,aAAa,EAAE,MAAM,CAAC;IACtB,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;CACtC;AA8DH,QAAA,MAAM,OAAO,2CAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
|
package/dist/session.svelte.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { AgentApiClient } from '@classic-homes/agent-client';
|
|
2
|
-
import type { AgentKey, ChatEvent, ClientKind, DocumentContext, FileRef } from '@classic-homes/agent-contracts';
|
|
2
|
+
import type { AgentKey, BackendMode, ChatEvent, ClientKind, DocumentContext, FileRef } from '@classic-homes/agent-contracts';
|
|
3
3
|
import { type AssistantMessage, type ChatMessage } from './reducer.js';
|
|
4
4
|
export interface ChatSessionOptions {
|
|
5
5
|
client: AgentApiClient;
|
|
6
6
|
conversationId?: string | null;
|
|
7
|
+
/** Backend mode known before the first turn (from `/api/me`), so the mock banner shows at once. */
|
|
8
|
+
mode?: BackendMode;
|
|
7
9
|
agent?: AgentKey;
|
|
8
10
|
clientKind?: ClientKind;
|
|
9
11
|
/** Context attached to each turn when the host's toggle is on. */
|
|
@@ -21,6 +23,8 @@ export declare class ChatSession {
|
|
|
21
23
|
messages: ChatMessage[];
|
|
22
24
|
status: SessionStatus;
|
|
23
25
|
conversationId: string | null;
|
|
26
|
+
/** The conversation whose transcript `messages` holds (loaded or started here); null before. */
|
|
27
|
+
loadedConversationId: string | null;
|
|
24
28
|
mode: "mock" | "live" | undefined;
|
|
25
29
|
loadError: string | undefined;
|
|
26
30
|
pendingFiles: {
|
|
@@ -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,
|
|
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,EAIL,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;CACtC;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,+FAA+F;IACzF,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C,MAAM,IAAI,IAAI;YAOA,GAAG;IAmDjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,MAAM;CAKf;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAE1E"}
|
package/dist/session.svelte.js
CHANGED
|
@@ -8,6 +8,8 @@ export class ChatSession {
|
|
|
8
8
|
messages = $state([]);
|
|
9
9
|
status = $state('idle');
|
|
10
10
|
conversationId = $state(null);
|
|
11
|
+
/** The conversation whose transcript `messages` holds (loaded or started here); null before. */
|
|
12
|
+
loadedConversationId = $state(null);
|
|
11
13
|
mode = $state(undefined);
|
|
12
14
|
loadError = $state(undefined);
|
|
13
15
|
pendingFiles = $state([]);
|
|
@@ -16,6 +18,7 @@ export class ChatSession {
|
|
|
16
18
|
constructor(options) {
|
|
17
19
|
this.options = options;
|
|
18
20
|
this.conversationId = options.conversationId ?? null;
|
|
21
|
+
this.mode = options.mode;
|
|
19
22
|
}
|
|
20
23
|
get streaming() {
|
|
21
24
|
return this.status === 'streaming';
|
|
@@ -31,6 +34,7 @@ export class ChatSession {
|
|
|
31
34
|
try {
|
|
32
35
|
const detail = await this.options.client.getConversation(conversationId);
|
|
33
36
|
this.conversationId = conversationId;
|
|
37
|
+
this.loadedConversationId = conversationId;
|
|
34
38
|
this.messages = detail.messages.map((m) => m.role === 'user'
|
|
35
39
|
? {
|
|
36
40
|
id: m.id,
|
|
@@ -106,6 +110,7 @@ export class ChatSession {
|
|
|
106
110
|
this.mode = event.mode;
|
|
107
111
|
if (this.conversationId !== event.conversationId) {
|
|
108
112
|
this.conversationId = event.conversationId;
|
|
113
|
+
this.loadedConversationId = event.conversationId;
|
|
109
114
|
this.options.onConversationStarted?.(event.conversationId);
|
|
110
115
|
}
|
|
111
116
|
}
|
|
@@ -33,8 +33,20 @@ export declare const MIN_FRAME_HEIGHT = 120;
|
|
|
33
33
|
export declare const MAX_FRAME_HEIGHT = 1200;
|
|
34
34
|
/** Parse a message from the sandbox into a FrameAction, or null when it is not one. */
|
|
35
35
|
export declare function parseFrameMessage(data: unknown): FrameAction | null;
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* True when a message event came from our sandbox frame and nowhere else. The frame runs with
|
|
38
|
+
* `sandbox` and no `allow-same-origin`, so its origin is opaque and the browser reports it as
|
|
39
|
+
* the string "null"; the window identity is the check that matters. The page's own origin is
|
|
40
|
+
* accepted too for a frame that is not sandboxed (tests, future same-origin hosting).
|
|
41
|
+
*/
|
|
37
42
|
export declare function isFromFrame(event: MessageEvent, frame: HTMLIFrameElement | null, sandboxOrigin: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Target origin for messages to the sandbox frame. An opaque origin matches nothing but "*",
|
|
45
|
+
* so a message addressed to the sandbox's https origin would be dropped silently. The window
|
|
46
|
+
* reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries is
|
|
47
|
+
* model output that is untrusted anyway.
|
|
48
|
+
*/
|
|
49
|
+
export declare const FRAME_TARGET_ORIGIN = "*";
|
|
38
50
|
/** Text an intent or prompt action becomes in the composer. */
|
|
39
51
|
export declare function composerTextFor(action: Extract<FrameAction, {
|
|
40
52
|
type: 'prompt' | 'intent';
|
|
@@ -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
|
|
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"}
|
|
@@ -60,10 +60,24 @@ function asRecord(value) {
|
|
|
60
60
|
? value
|
|
61
61
|
: undefined;
|
|
62
62
|
}
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* True when a message event came from our sandbox frame and nowhere else. The frame runs with
|
|
65
|
+
* `sandbox` and no `allow-same-origin`, so its origin is opaque and the browser reports it as
|
|
66
|
+
* the string "null"; the window identity is the check that matters. The page's own origin is
|
|
67
|
+
* accepted too for a frame that is not sandboxed (tests, future same-origin hosting).
|
|
68
|
+
*/
|
|
64
69
|
export function isFromFrame(event, frame, sandboxOrigin) {
|
|
65
|
-
|
|
70
|
+
if (!frame || !frame.contentWindow || event.source !== frame.contentWindow)
|
|
71
|
+
return false;
|
|
72
|
+
return event.origin === 'null' || event.origin === sandboxOrigin;
|
|
66
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Target origin for messages to the sandbox frame. An opaque origin matches nothing but "*",
|
|
76
|
+
* so a message addressed to the sandbox's https origin would be dropped silently. The window
|
|
77
|
+
* reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries is
|
|
78
|
+
* model output that is untrusted anyway.
|
|
79
|
+
*/
|
|
80
|
+
export const FRAME_TARGET_ORIGIN = '*';
|
|
67
81
|
/** Text an intent or prompt action becomes in the composer. */
|
|
68
82
|
export function composerTextFor(action) {
|
|
69
83
|
if (action.type === 'prompt')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { composerTextFor, parseFrameMessage } from './ui-frame-protocol.js';
|
|
2
|
+
import { composerTextFor, isFromFrame, parseFrameMessage } from './ui-frame-protocol.js';
|
|
3
3
|
describe('parseFrameMessage', () => {
|
|
4
4
|
it('accepts the MCP-UI action set and clamps sizes', () => {
|
|
5
5
|
expect(parseFrameMessage({ type: 'ui-lifecycle-iframe-ready' })).toEqual({ type: 'ready' });
|
|
@@ -32,3 +32,17 @@ describe('parseFrameMessage', () => {
|
|
|
32
32
|
expect(parseFrameMessage({ type: 'eval', payload: {} })).toBeNull();
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
+
describe('isFromFrame', () => {
|
|
36
|
+
const win = {};
|
|
37
|
+
const frame = { contentWindow: win };
|
|
38
|
+
const event = (source, origin) => ({ source, origin });
|
|
39
|
+
it('accepts the sandbox window with an opaque ("null") or the sandbox origin', () => {
|
|
40
|
+
expect(isFromFrame(event(win, 'null'), frame, 'https://sandbox.example')).toBe(true);
|
|
41
|
+
expect(isFromFrame(event(win, 'https://sandbox.example'), frame, 'https://sandbox.example')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it('rejects other windows, other origins and a missing frame', () => {
|
|
44
|
+
expect(isFromFrame(event({}, 'null'), frame, 'https://sandbox.example')).toBe(false);
|
|
45
|
+
expect(isFromFrame(event(win, 'https://evil.example'), frame, 'https://sandbox.example')).toBe(false);
|
|
46
|
+
expect(isFromFrame(event(win, 'null'), null, 'https://sandbox.example')).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/agent-chat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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.1.
|
|
21
|
-
"@classic-homes/agent-contracts": "0.1.
|
|
20
|
+
"@classic-homes/agent-client": "0.1.2",
|
|
21
|
+
"@classic-homes/agent-contracts": "0.1.2",
|
|
22
22
|
"dompurify": "3.4.17"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|