@moxxy/plugin-channel-web 0.27.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/LICENSE +21 -0
- package/dist/channel.d.ts +152 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +595 -0
- package/dist/channel.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +171 -0
- package/dist/index.js.map +1 -0
- package/dist/projector.d.ts +17 -0
- package/dist/projector.d.ts.map +1 -0
- package/dist/projector.js +96 -0
- package/dist/projector.js.map +1 -0
- package/dist/protocol.d.ts +145 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +33 -0
- package/dist/protocol.js.map +1 -0
- package/dist/public/app.js +54 -0
- package/dist/public/index.html +178 -0
- package/dist/tunnel-settings.d.ts +17 -0
- package/dist/tunnel-settings.d.ts.map +1 -0
- package/dist/tunnel-settings.js +42 -0
- package/dist/tunnel-settings.js.map +1 -0
- package/package.json +71 -0
- package/src/channel.test.ts +514 -0
- package/src/channel.ts +669 -0
- package/src/discovery.test.ts +40 -0
- package/src/frontend/chat.test.ts +47 -0
- package/src/frontend/chat.tsx +138 -0
- package/src/frontend/index.html +178 -0
- package/src/frontend/main.tsx +87 -0
- package/src/frontend/render-diff.test.ts +86 -0
- package/src/frontend/render-diff.tsx +66 -0
- package/src/frontend/render.test.ts +166 -0
- package/src/frontend/render.tsx +274 -0
- package/src/frontend/socket.ts +212 -0
- package/src/frontend/url-safety.test.ts +0 -0
- package/src/frontend/url-safety.ts +33 -0
- package/src/frontend/view-store.test.ts +104 -0
- package/src/frontend/view-store.ts +120 -0
- package/src/index.ts +212 -0
- package/src/projector.test.ts +172 -0
- package/src/projector.ts +95 -0
- package/src/protocol.test.ts +59 -0
- package/src/protocol.ts +105 -0
- package/src/tunnel-settings.test.ts +64 -0
- package/src/tunnel-settings.ts +57 -0
- package/src/tunnel-tools.test.ts +120 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Plugin, type TunnelProviderDef } from '@moxxy/sdk';
|
|
2
|
+
import { type WebSurfaceControls } from './channel.js';
|
|
3
|
+
export { WebChannel, type WebChannelOptions, type WebStartOpts, type WebSurfaceControls } from './channel.js';
|
|
4
|
+
export { EventProjector } from './projector.js';
|
|
5
|
+
export { readTunnelSetting, writeTunnelSetting, normalizeTunnelName } from './tunnel-settings.js';
|
|
6
|
+
export { actionPrompt, type ServerFrame, type ClientFrame } from './protocol.js';
|
|
7
|
+
/** Live access to the session's tunnel registry (injected by the CLI builder). */
|
|
8
|
+
export interface TunnelControls {
|
|
9
|
+
list(): string[];
|
|
10
|
+
active(): string | null;
|
|
11
|
+
setActive(name: string): void;
|
|
12
|
+
isAvailable(name: string): Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
export interface BuildWebChannelOptions {
|
|
15
|
+
readonly getTunnel?: () => TunnelProviderDef | null;
|
|
16
|
+
readonly publishSurface?: (surface: {
|
|
17
|
+
url: string;
|
|
18
|
+
nextViewId: () => string;
|
|
19
|
+
} | null) => void;
|
|
20
|
+
readonly publishControls?: (controls: WebSurfaceControls | null) => void;
|
|
21
|
+
/** Read the live surface controls (for live tunnel switching from the tool). */
|
|
22
|
+
readonly getControls?: () => WebSurfaceControls | null;
|
|
23
|
+
/** Live tunnel registry access for the web_set_tunnel / web_tunnel_status tools. */
|
|
24
|
+
readonly tunnels?: TunnelControls;
|
|
25
|
+
/** Static default tunnel (from config.channels.web.tunnel) when no persisted setting. */
|
|
26
|
+
readonly defaultTunnel?: string;
|
|
27
|
+
/** Override the persisted settings file path (tests). Defaults to ~/.moxxy/web.json. */
|
|
28
|
+
readonly settingsFile?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the web channel plugin. The CLI passes closures over the concrete
|
|
32
|
+
* Session (mirroring the Telegram plugin's vault injection). Contributes:
|
|
33
|
+
* - the `web` channel (surface + action round-trip),
|
|
34
|
+
* - the self-hosted `proxy` tunnel provider (registered; `localhost` is the
|
|
35
|
+
* core-seeded default),
|
|
36
|
+
* - `web_set_tunnel` / `web_tunnel_status` tools so the user/agent can switch
|
|
37
|
+
* the tunnel at runtime — persisted to ~/.moxxy/web.json,
|
|
38
|
+
* - an onInit hook that applies the persisted (or configured) tunnel on boot.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildWebChannelPlugin(opts?: BuildWebChannelOptions): Plugin;
|
|
41
|
+
/**
|
|
42
|
+
* Discovery-loadable default export. Resolves the tunnel registry
|
|
43
|
+
* (`'tunnelProviders'`), the shared web-surface ref (`'viewSurface'`, read by the
|
|
44
|
+
* view plugin) + web-controls ref (`'webControls'`) and the configured default
|
|
45
|
+
* tunnel (`'webDefaultTunnel'`) from the inter-plugin service registry in
|
|
46
|
+
* `onInit`, instead of the host `{ getTunnel, publishSurface, … }` closure. A
|
|
47
|
+
* lazy `tunnels` object keeps the `web_set_tunnel`/`web_tunnel_status` tools +
|
|
48
|
+
* the boot tunnel-apply hook present (built before onInit), deferring every
|
|
49
|
+
* registry call to the resolved instance.
|
|
50
|
+
*/
|
|
51
|
+
export declare const webChannelPlugin: Plugin;
|
|
52
|
+
export default webChannelPlugin;
|
|
53
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,KAAK,MAAM,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE7G,OAAO,EAAc,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGnE,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC9G,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjF,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,IAAI,IAAI,MAAM,EAAE,CAAC;IACjB,MAAM,IAAI,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAC;IACpD,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9F,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,CAAC;IACzE,gFAAgF;IAChF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAC;IACvD,oFAAoF;IACpF,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,yFAAyF;IACzF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,wFAAwF;IACxF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,sBAA2B,GAAG,MAAM,CA8F/E;AAcD;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAiD3B,CAAC;AAEL,eAAe,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { defineChannel, definePlugin, defineTool, z } from '@moxxy/sdk';
|
|
2
|
+
import { proxyTunnel } from '@moxxy/plugin-tunnel-proxy';
|
|
3
|
+
import { WebChannel } from './channel.js';
|
|
4
|
+
import { normalizeTunnelName, readTunnelSetting, writeTunnelSetting } from './tunnel-settings.js';
|
|
5
|
+
export { WebChannel } from './channel.js';
|
|
6
|
+
export { EventProjector } from './projector.js';
|
|
7
|
+
export { readTunnelSetting, writeTunnelSetting, normalizeTunnelName } from './tunnel-settings.js';
|
|
8
|
+
export { actionPrompt } from './protocol.js';
|
|
9
|
+
/**
|
|
10
|
+
* Build the web channel plugin. The CLI passes closures over the concrete
|
|
11
|
+
* Session (mirroring the Telegram plugin's vault injection). Contributes:
|
|
12
|
+
* - the `web` channel (surface + action round-trip),
|
|
13
|
+
* - the self-hosted `proxy` tunnel provider (registered; `localhost` is the
|
|
14
|
+
* core-seeded default),
|
|
15
|
+
* - `web_set_tunnel` / `web_tunnel_status` tools so the user/agent can switch
|
|
16
|
+
* the tunnel at runtime — persisted to ~/.moxxy/web.json,
|
|
17
|
+
* - an onInit hook that applies the persisted (or configured) tunnel on boot.
|
|
18
|
+
*/
|
|
19
|
+
export function buildWebChannelPlugin(opts = {}) {
|
|
20
|
+
const def = defineChannel({
|
|
21
|
+
name: 'web',
|
|
22
|
+
description: 'Browser surface that renders agent-authored view-spec UIs over a WebSocket and routes form/button actions back into the session. Exposes itself via the self-hosted proxy relay so users on other channels can open it.',
|
|
23
|
+
create: (deps) => {
|
|
24
|
+
const o = deps.options ?? {};
|
|
25
|
+
return new WebChannel({
|
|
26
|
+
...(typeof o.port === 'number' ? { port: o.port } : {}),
|
|
27
|
+
...(typeof o.host === 'string' ? { host: o.host } : {}),
|
|
28
|
+
...(typeof o.authToken === 'string'
|
|
29
|
+
? { authToken: o.authToken }
|
|
30
|
+
: process.env.MOXXY_WEB_TOKEN
|
|
31
|
+
? { authToken: process.env.MOXXY_WEB_TOKEN }
|
|
32
|
+
: {}),
|
|
33
|
+
...(Array.isArray(o.allowedTools) ? { allowedTools: o.allowedTools } : {}),
|
|
34
|
+
...(opts.getTunnel ? { getTunnel: opts.getTunnel } : {}),
|
|
35
|
+
...(opts.publishSurface ? { publishSurface: opts.publishSurface } : {}),
|
|
36
|
+
...(opts.publishControls ? { publishControls: opts.publishControls } : {}),
|
|
37
|
+
logger: deps.logger,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const tunnels = opts.tunnels;
|
|
42
|
+
const tools = tunnels
|
|
43
|
+
? [
|
|
44
|
+
defineTool({
|
|
45
|
+
name: 'web_set_tunnel',
|
|
46
|
+
description: 'Choose how the web app surface is exposed: "proxy" (public URL via the self-hosted relay, for remote channels like Telegram) or "none"/"localhost" (loopback only). Persisted to ~/.moxxy/web.json and applied immediately if a surface is live. Use when the user asks to change or disable the tunnel.',
|
|
47
|
+
inputSchema: z.object({ provider: z.string().min(1) }),
|
|
48
|
+
permission: { action: 'prompt' },
|
|
49
|
+
// Persists the choice, then (re)establishes the tunnel through the
|
|
50
|
+
// live surface controls — the relay host is config-dependent, so
|
|
51
|
+
// the net surface is genuinely dynamic.
|
|
52
|
+
isolation: {
|
|
53
|
+
capabilities: {
|
|
54
|
+
fs: {
|
|
55
|
+
read: [opts.settingsFile ?? '~/.moxxy/web.json'],
|
|
56
|
+
write: [opts.settingsFile ?? '~/.moxxy/web.json'],
|
|
57
|
+
},
|
|
58
|
+
net: { mode: 'any' },
|
|
59
|
+
timeMs: 60_000,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
handler: async ({ provider }) => {
|
|
63
|
+
const name = normalizeTunnelName(provider);
|
|
64
|
+
if (!tunnels.list().includes(name)) {
|
|
65
|
+
return { ok: false, error: `unknown tunnel "${provider}"`, available: ['none', ...tunnels.list()] };
|
|
66
|
+
}
|
|
67
|
+
const available = await tunnels.isAvailable(name);
|
|
68
|
+
if (!available) {
|
|
69
|
+
return { ok: false, error: `${name} is not available` };
|
|
70
|
+
}
|
|
71
|
+
tunnels.setActive(name);
|
|
72
|
+
await writeTunnelSetting(name, opts.settingsFile);
|
|
73
|
+
const url = (await opts.getControls?.()?.retunnel()) ?? null;
|
|
74
|
+
return { ok: true, active: name, ...(url ? { url } : { note: 'applies when the web surface (re)starts' }) };
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
defineTool({
|
|
78
|
+
name: 'web_tunnel_status',
|
|
79
|
+
description: 'Report the active web tunnel provider and the available options.',
|
|
80
|
+
inputSchema: z.object({}),
|
|
81
|
+
// Pure registry view — no fs / net / subprocess.
|
|
82
|
+
isolation: { capabilities: { net: { mode: 'none' }, timeMs: 10_000 } },
|
|
83
|
+
handler: () => ({ active: tunnels.active(), available: ['none', ...tunnels.list()] }),
|
|
84
|
+
}),
|
|
85
|
+
]
|
|
86
|
+
: [];
|
|
87
|
+
return definePlugin({
|
|
88
|
+
name: '@moxxy/plugin-channel-web',
|
|
89
|
+
version: '0.0.0',
|
|
90
|
+
channels: [def],
|
|
91
|
+
tunnelProviders: [proxyTunnel],
|
|
92
|
+
tools,
|
|
93
|
+
hooks: tunnels
|
|
94
|
+
? {
|
|
95
|
+
onInit: () => {
|
|
96
|
+
// Apply the persisted choice (or the configured default) on boot.
|
|
97
|
+
const chosen = readTunnelSetting(opts.settingsFile) ?? (opts.defaultTunnel ? normalizeTunnelName(opts.defaultTunnel) : undefined);
|
|
98
|
+
if (chosen && tunnels.list().includes(chosen)) {
|
|
99
|
+
try {
|
|
100
|
+
tunnels.setActive(chosen);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* keep the seeded default */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
: {},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Discovery-loadable default export. Resolves the tunnel registry
|
|
113
|
+
* (`'tunnelProviders'`), the shared web-surface ref (`'viewSurface'`, read by the
|
|
114
|
+
* view plugin) + web-controls ref (`'webControls'`) and the configured default
|
|
115
|
+
* tunnel (`'webDefaultTunnel'`) from the inter-plugin service registry in
|
|
116
|
+
* `onInit`, instead of the host `{ getTunnel, publishSurface, … }` closure. A
|
|
117
|
+
* lazy `tunnels` object keeps the `web_set_tunnel`/`web_tunnel_status` tools +
|
|
118
|
+
* the boot tunnel-apply hook present (built before onInit), deferring every
|
|
119
|
+
* registry call to the resolved instance.
|
|
120
|
+
*/
|
|
121
|
+
export const webChannelPlugin = (() => {
|
|
122
|
+
let tp = null;
|
|
123
|
+
let surfaceRef = null;
|
|
124
|
+
let controlsRef = null;
|
|
125
|
+
let defaultTunnel;
|
|
126
|
+
const tunnels = {
|
|
127
|
+
list: () => tp?.list().map((p) => p.name) ?? [],
|
|
128
|
+
active: () => tp?.getActive()?.name ?? null,
|
|
129
|
+
setActive: (n) => {
|
|
130
|
+
tp?.setActive(n);
|
|
131
|
+
},
|
|
132
|
+
isAvailable: async (n) => {
|
|
133
|
+
const p = tp?.list().find((x) => x.name === n);
|
|
134
|
+
return p?.isAvailable ? p.isAvailable() : true;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const opts = {
|
|
138
|
+
getTunnel: () => tp?.getActive() ?? null,
|
|
139
|
+
publishSurface: (s) => {
|
|
140
|
+
if (surfaceRef)
|
|
141
|
+
surfaceRef.current = s;
|
|
142
|
+
},
|
|
143
|
+
publishControls: (c) => {
|
|
144
|
+
if (controlsRef)
|
|
145
|
+
controlsRef.current = c;
|
|
146
|
+
},
|
|
147
|
+
getControls: () => controlsRef?.current ?? null,
|
|
148
|
+
tunnels,
|
|
149
|
+
get defaultTunnel() {
|
|
150
|
+
return defaultTunnel;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const plugin = buildWebChannelPlugin(opts);
|
|
154
|
+
const innerOnInit = plugin.hooks?.onInit;
|
|
155
|
+
return definePlugin({
|
|
156
|
+
...plugin,
|
|
157
|
+
hooks: {
|
|
158
|
+
...plugin.hooks,
|
|
159
|
+
onInit: (ctx) => {
|
|
160
|
+
tp = ctx.services.get('tunnelProviders') ?? null;
|
|
161
|
+
surfaceRef = ctx.services.get('viewSurface') ?? null;
|
|
162
|
+
controlsRef = ctx.services.get('webControls') ?? null;
|
|
163
|
+
defaultTunnel = ctx.services.get('webDefaultTunnel') ?? undefined;
|
|
164
|
+
// Now the refs are resolved → apply the persisted/default tunnel on boot.
|
|
165
|
+
return innerOnInit?.(ctx);
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
})();
|
|
170
|
+
export default webChannelPlugin;
|
|
171
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type MoxxyEvent } from '@moxxy/sdk';
|
|
2
|
+
import type { ServerFrame } from './protocol.js';
|
|
3
|
+
/**
|
|
4
|
+
* Folds the session's event stream into {@link ServerFrame}s for the browser.
|
|
5
|
+
* Stateful: it correlates a `present_view` `tool_call_requested` with its
|
|
6
|
+
* `tool_result` (to read the validated AST) and tracks the last view id so each
|
|
7
|
+
* new view `replaces` the prior one. One projector per channel (single-user
|
|
8
|
+
* surface); it renders ANY turn on the shared log, including ones the surface
|
|
9
|
+
* didn't initiate — that is what gives mirror-to-both for free.
|
|
10
|
+
*/
|
|
11
|
+
export declare class EventProjector {
|
|
12
|
+
private readonly presentCalls;
|
|
13
|
+
private lastViewId;
|
|
14
|
+
private viewSeq;
|
|
15
|
+
project(event: MoxxyEvent): ServerFrame[];
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=projector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projector.d.ts","sourceRoot":"","sources":["../src/projector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,UAAU,EAAgB,MAAM,YAAY,CAAC;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAUjD;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgD;IAC7E,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,OAAO,CAAK;IAEpB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,EAAE;CAsE1C"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isFileDiffDisplay } from '@moxxy/sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Cap on in-flight present_view calls awaiting their tool_result. A normal turn
|
|
4
|
+
* has at most a handful; entries only linger when a turn is aborted/errors before
|
|
5
|
+
* the tool resolves (no matching tool_result ever arrives). Bound it so an
|
|
6
|
+
* accumulation of aborts over a very long session can't leak unboundedly.
|
|
7
|
+
*/
|
|
8
|
+
const MAX_PENDING_CALLS = 64;
|
|
9
|
+
/**
|
|
10
|
+
* Folds the session's event stream into {@link ServerFrame}s for the browser.
|
|
11
|
+
* Stateful: it correlates a `present_view` `tool_call_requested` with its
|
|
12
|
+
* `tool_result` (to read the validated AST) and tracks the last view id so each
|
|
13
|
+
* new view `replaces` the prior one. One projector per channel (single-user
|
|
14
|
+
* surface); it renders ANY turn on the shared log, including ones the surface
|
|
15
|
+
* didn't initiate — that is what gives mirror-to-both for free.
|
|
16
|
+
*/
|
|
17
|
+
export class EventProjector {
|
|
18
|
+
presentCalls = new Map();
|
|
19
|
+
lastViewId = null;
|
|
20
|
+
viewSeq = 0;
|
|
21
|
+
project(event) {
|
|
22
|
+
switch (event.type) {
|
|
23
|
+
case 'user_prompt': {
|
|
24
|
+
// Hide the synthesized ui-action turns; show real user prompts.
|
|
25
|
+
if (!event.text.trim() || event.text.startsWith('[ui-action]'))
|
|
26
|
+
return [];
|
|
27
|
+
return [{ kind: 'message', turnId: String(event.turnId), role: 'user', text: event.text }];
|
|
28
|
+
}
|
|
29
|
+
case 'tool_call_requested': {
|
|
30
|
+
if (event.name === 'present_view') {
|
|
31
|
+
const input = event.input;
|
|
32
|
+
this.presentCalls.set(String(event.callId), { fallbackText: input?.fallbackText });
|
|
33
|
+
// Evict the oldest pending call(s) if a long run of aborted/errored
|
|
34
|
+
// turns left their tool_result-less entries stranded.
|
|
35
|
+
while (this.presentCalls.size > MAX_PENDING_CALLS) {
|
|
36
|
+
const oldest = this.presentCalls.keys().next().value;
|
|
37
|
+
if (oldest === undefined)
|
|
38
|
+
break;
|
|
39
|
+
this.presentCalls.delete(oldest);
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return [{ kind: 'status', turnId: String(event.turnId), phase: 'tool', text: `${event.name}…` }];
|
|
44
|
+
}
|
|
45
|
+
case 'tool_result': {
|
|
46
|
+
// Rich tool results (Write/Edit) carry a structured `display`; render
|
|
47
|
+
// the diff in the chat stream, independent of present_view correlation.
|
|
48
|
+
const display = event.output?.display;
|
|
49
|
+
if (event.ok && isFileDiffDisplay(display)) {
|
|
50
|
+
return [{ kind: 'file-diff', turnId: String(event.turnId), display }];
|
|
51
|
+
}
|
|
52
|
+
const pending = this.presentCalls.get(String(event.callId));
|
|
53
|
+
if (!pending)
|
|
54
|
+
return [];
|
|
55
|
+
this.presentCalls.delete(String(event.callId));
|
|
56
|
+
if (!event.ok) {
|
|
57
|
+
return [{ kind: 'status', turnId: String(event.turnId), phase: 'error', text: 'view failed to render' }];
|
|
58
|
+
}
|
|
59
|
+
const doc = event.output?.ast;
|
|
60
|
+
if (!doc)
|
|
61
|
+
return [];
|
|
62
|
+
const viewId = `v_${String(event.turnId)}_${++this.viewSeq}`;
|
|
63
|
+
const replaces = this.lastViewId;
|
|
64
|
+
this.lastViewId = viewId;
|
|
65
|
+
const name = doc.root.kind === 'element' && typeof doc.root.props.name === 'string' ? doc.root.props.name : undefined;
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
kind: 'view',
|
|
69
|
+
viewId,
|
|
70
|
+
turnId: String(event.turnId),
|
|
71
|
+
replaces,
|
|
72
|
+
...(name ? { name } : {}),
|
|
73
|
+
doc,
|
|
74
|
+
...(pending.fallbackText ? { fallbackText: pending.fallbackText } : {}),
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
case 'assistant_message': {
|
|
79
|
+
const frames = [];
|
|
80
|
+
if (event.content.trim()) {
|
|
81
|
+
frames.push({ kind: 'message', turnId: String(event.turnId), role: 'assistant', text: event.content });
|
|
82
|
+
}
|
|
83
|
+
if (event.stopReason !== 'tool_use') {
|
|
84
|
+
frames.push({ kind: 'status', turnId: String(event.turnId), phase: 'done', text: '' });
|
|
85
|
+
}
|
|
86
|
+
return frames;
|
|
87
|
+
}
|
|
88
|
+
case 'error': {
|
|
89
|
+
return [{ kind: 'status', turnId: String(event.turnId), phase: 'error', text: event.message }];
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=projector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projector.js","sourceRoot":"","sources":["../src/projector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { FileDiffDisplay, ViewDoc } from '@moxxy/sdk';
|
|
3
|
+
/**
|
|
4
|
+
* Wire protocol between the web surface server and the browser. Plain JSON over
|
|
5
|
+
* a WebSocket. Shared by the channel (Node) and the frontend (browser); keep it
|
|
6
|
+
* DOM-free so esbuild can bundle it for the browser. (The frontend imports
|
|
7
|
+
* ONLY types from this module, so the zod runtime never reaches the browser
|
|
8
|
+
* bundle.)
|
|
9
|
+
*/
|
|
10
|
+
/** Server → browser. */
|
|
11
|
+
export type ServerFrame = {
|
|
12
|
+
readonly kind: 'hello';
|
|
13
|
+
readonly sessionId?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A new view to render.
|
|
17
|
+
*
|
|
18
|
+
* INVARIANT: the in-place-vs-push decision is driven SOLELY by the logical
|
|
19
|
+
* `name` (a re-render of the screen already on top updates in place; a
|
|
20
|
+
* different name pushes a history entry — see `applyView`). `replaces`
|
|
21
|
+
* carries the prior `viewId` purely as provenance/diagnostic context (the
|
|
22
|
+
* single-user projector emits the id of the view this one supersedes); it
|
|
23
|
+
* is NOT a navigation directive and the frontend reducer does not branch on
|
|
24
|
+
* it. Do not reintroduce `replaces`-based in-place logic without reconciling
|
|
25
|
+
* it with the name-keyed history/back model in view-store.ts.
|
|
26
|
+
*/
|
|
27
|
+
| {
|
|
28
|
+
readonly kind: 'view';
|
|
29
|
+
readonly viewId: string;
|
|
30
|
+
readonly turnId: string;
|
|
31
|
+
readonly replaces: string | null;
|
|
32
|
+
/** Logical screen name (from `<view name>`), for client-side navigation/caching. */
|
|
33
|
+
readonly name?: string;
|
|
34
|
+
readonly doc: ViewDoc;
|
|
35
|
+
readonly fallbackText?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Lightweight turn status (thinking / running a tool / done / error). */
|
|
38
|
+
| {
|
|
39
|
+
readonly kind: 'status';
|
|
40
|
+
readonly turnId: string;
|
|
41
|
+
readonly phase: 'thinking' | 'tool' | 'done' | 'error';
|
|
42
|
+
readonly text: string;
|
|
43
|
+
}
|
|
44
|
+
/** Assistant or user prose, mirrored into the transcript. */
|
|
45
|
+
| {
|
|
46
|
+
readonly kind: 'message';
|
|
47
|
+
readonly turnId: string;
|
|
48
|
+
readonly role: 'assistant' | 'user';
|
|
49
|
+
readonly text: string;
|
|
50
|
+
}
|
|
51
|
+
/** A structured file diff (from a Write/Edit tool result), rendered in the chat stream. */
|
|
52
|
+
| {
|
|
53
|
+
readonly kind: 'file-diff';
|
|
54
|
+
readonly turnId: string;
|
|
55
|
+
readonly display: FileDiffDisplay;
|
|
56
|
+
}
|
|
57
|
+
/** Acknowledge an inbound action. */
|
|
58
|
+
| {
|
|
59
|
+
readonly kind: 'ack';
|
|
60
|
+
readonly actionId: string;
|
|
61
|
+
readonly accepted: boolean;
|
|
62
|
+
readonly reason?: string;
|
|
63
|
+
};
|
|
64
|
+
/** Browser → server. */
|
|
65
|
+
export type ClientFrame =
|
|
66
|
+
/** Free-text prompt typed into the surface's input box. */
|
|
67
|
+
{
|
|
68
|
+
readonly kind: 'prompt';
|
|
69
|
+
readonly text: string;
|
|
70
|
+
}
|
|
71
|
+
/** A form submission / button click from a rendered view. */
|
|
72
|
+
| {
|
|
73
|
+
readonly kind: 'action';
|
|
74
|
+
readonly actionId: string;
|
|
75
|
+
readonly viewId: string | null;
|
|
76
|
+
readonly action: {
|
|
77
|
+
readonly name: string;
|
|
78
|
+
readonly params?: Record<string, unknown>;
|
|
79
|
+
};
|
|
80
|
+
readonly formValues: Record<string, string>;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Runtime validator for browser → server frames. The WS endpoint is
|
|
84
|
+
* internet-exposed via tunnels, so every inbound frame MUST be validated
|
|
85
|
+
* before any field access — a cast after JSON.parse let `{"kind":"prompt"}`
|
|
86
|
+
* throw inside the ws 'message' listener and take the whole process down.
|
|
87
|
+
* Mirrors {@link ClientFrame}; the drift guard below keeps them in sync.
|
|
88
|
+
*/
|
|
89
|
+
export declare const clientFrameSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
90
|
+
kind: z.ZodLiteral<"prompt">;
|
|
91
|
+
text: z.ZodString;
|
|
92
|
+
}, "strip", z.ZodTypeAny, {
|
|
93
|
+
kind: "prompt";
|
|
94
|
+
text: string;
|
|
95
|
+
}, {
|
|
96
|
+
kind: "prompt";
|
|
97
|
+
text: string;
|
|
98
|
+
}>, z.ZodObject<{
|
|
99
|
+
kind: z.ZodLiteral<"action">;
|
|
100
|
+
actionId: z.ZodString;
|
|
101
|
+
viewId: z.ZodNullable<z.ZodString>;
|
|
102
|
+
action: z.ZodObject<{
|
|
103
|
+
name: z.ZodString;
|
|
104
|
+
params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
105
|
+
}, "strip", z.ZodTypeAny, {
|
|
106
|
+
name: string;
|
|
107
|
+
params?: Record<string, unknown> | undefined;
|
|
108
|
+
}, {
|
|
109
|
+
name: string;
|
|
110
|
+
params?: Record<string, unknown> | undefined;
|
|
111
|
+
}>;
|
|
112
|
+
formValues: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
113
|
+
}, "strip", z.ZodTypeAny, {
|
|
114
|
+
action: {
|
|
115
|
+
name: string;
|
|
116
|
+
params?: Record<string, unknown> | undefined;
|
|
117
|
+
};
|
|
118
|
+
kind: "action";
|
|
119
|
+
actionId: string;
|
|
120
|
+
viewId: string | null;
|
|
121
|
+
formValues: Record<string, string>;
|
|
122
|
+
}, {
|
|
123
|
+
action: {
|
|
124
|
+
name: string;
|
|
125
|
+
params?: Record<string, unknown> | undefined;
|
|
126
|
+
};
|
|
127
|
+
kind: "action";
|
|
128
|
+
actionId: string;
|
|
129
|
+
viewId: string | null;
|
|
130
|
+
formValues: Record<string, string>;
|
|
131
|
+
}>]>;
|
|
132
|
+
/** Compile-time drift guard: schema output and ClientFrame must stay mutually assignable. */
|
|
133
|
+
type AssertTrue<T extends true> = T;
|
|
134
|
+
export type _ClientFrameSchemaInSync = AssertTrue<z.infer<typeof clientFrameSchema> extends ClientFrame ? ClientFrame extends z.infer<typeof clientFrameSchema> ? true : false : false>;
|
|
135
|
+
/**
|
|
136
|
+
* Synthesize the user-turn prompt for a view action. The agent is taught (via
|
|
137
|
+
* the build-view skill) to read `[ui-action]` blocks and respond by calling
|
|
138
|
+
* present_view again. Kept here so the channel and any test share one format.
|
|
139
|
+
*/
|
|
140
|
+
export declare function actionPrompt(action: {
|
|
141
|
+
name: string;
|
|
142
|
+
params?: Record<string, unknown>;
|
|
143
|
+
}, formValues: Record<string, string>): string;
|
|
144
|
+
export {};
|
|
145
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Runtime validator for browser → server frames. The WS endpoint is
|
|
4
|
+
* internet-exposed via tunnels, so every inbound frame MUST be validated
|
|
5
|
+
* before any field access — a cast after JSON.parse let `{"kind":"prompt"}`
|
|
6
|
+
* throw inside the ws 'message' listener and take the whole process down.
|
|
7
|
+
* Mirrors {@link ClientFrame}; the drift guard below keeps them in sync.
|
|
8
|
+
*/
|
|
9
|
+
export const clientFrameSchema = z.discriminatedUnion('kind', [
|
|
10
|
+
z.object({ kind: z.literal('prompt'), text: z.string() }),
|
|
11
|
+
z.object({
|
|
12
|
+
kind: z.literal('action'),
|
|
13
|
+
actionId: z.string(),
|
|
14
|
+
viewId: z.string().nullable(),
|
|
15
|
+
action: z.object({ name: z.string(), params: z.record(z.unknown()).optional() }),
|
|
16
|
+
formValues: z.record(z.string()),
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Synthesize the user-turn prompt for a view action. The agent is taught (via
|
|
21
|
+
* the build-view skill) to read `[ui-action]` blocks and respond by calling
|
|
22
|
+
* present_view again. Kept here so the channel and any test share one format.
|
|
23
|
+
*/
|
|
24
|
+
export function actionPrompt(action, formValues) {
|
|
25
|
+
const payload = JSON.stringify({ action: action.name, ...(action.params ? { params: action.params } : {}), values: formValues });
|
|
26
|
+
return [
|
|
27
|
+
'[ui-action] The user interacted with the presented view.',
|
|
28
|
+
'```json',
|
|
29
|
+
payload,
|
|
30
|
+
'```',
|
|
31
|
+
].join('\n');
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA+DxB;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE;IAC5D,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;IACzD,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;QACzB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC7B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;QAChF,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;KACjC,CAAC;CACH,CAAC,CAAC;AAYH;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAA0D,EAAE,UAAkC;IACzH,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IACjI,OAAO;QACL,0DAA0D;QAC1D,SAAS;QACT,OAAO;QACP,KAAK;KACN,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|