@agimon-ai/doompi-web-contracts 0.0.1-alpha.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/LICENSE +22 -0
- package/README.md +28 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +1 -0
- package/dist/services/define.cjs +2 -0
- package/dist/services/define.cjs.map +1 -0
- package/dist/services/define.d.cts +20 -0
- package/dist/services/define.d.cts.map +1 -0
- package/dist/services/define.d.mts +20 -0
- package/dist/services/define.d.mts.map +1 -0
- package/dist/services/define.mjs +2 -0
- package/dist/services/define.mjs.map +1 -0
- package/dist/services/sessionStore.cjs +2 -0
- package/dist/services/sessionStore.cjs.map +1 -0
- package/dist/services/sessionStore.d.cts +15 -0
- package/dist/services/sessionStore.d.cts.map +1 -0
- package/dist/services/sessionStore.d.mts +15 -0
- package/dist/services/sessionStore.d.mts.map +1 -0
- package/dist/services/sessionStore.mjs +2 -0
- package/dist/services/sessionStore.mjs.map +1 -0
- package/dist/services/testing/channels.cjs +2 -0
- package/dist/services/testing/channels.cjs.map +1 -0
- package/dist/services/testing/channels.d.cts +52 -0
- package/dist/services/testing/channels.d.cts.map +1 -0
- package/dist/services/testing/channels.d.mts +52 -0
- package/dist/services/testing/channels.d.mts.map +1 -0
- package/dist/services/testing/channels.mjs +2 -0
- package/dist/services/testing/channels.mjs.map +1 -0
- package/dist/services/testing/render.cjs +2 -0
- package/dist/services/testing/render.cjs.map +1 -0
- package/dist/services/testing/render.d.cts +32 -0
- package/dist/services/testing/render.d.cts.map +1 -0
- package/dist/services/testing/render.d.mts +32 -0
- package/dist/services/testing/render.d.mts.map +1 -0
- package/dist/services/testing/render.mjs +2 -0
- package/dist/services/testing/render.mjs.map +1 -0
- package/dist/services/testing/slotProps.cjs +2 -0
- package/dist/services/testing/slotProps.cjs.map +1 -0
- package/dist/services/testing/slotProps.d.cts +57 -0
- package/dist/services/testing/slotProps.d.cts.map +1 -0
- package/dist/services/testing/slotProps.d.mts +57 -0
- package/dist/services/testing/slotProps.d.mts.map +1 -0
- package/dist/services/testing/slotProps.mjs +2 -0
- package/dist/services/testing/slotProps.mjs.map +1 -0
- package/dist/services/toolResult.cjs +7 -0
- package/dist/services/toolResult.cjs.map +1 -0
- package/dist/services/toolResult.d.cts +15 -0
- package/dist/services/toolResult.d.cts.map +1 -0
- package/dist/services/toolResult.d.mts +15 -0
- package/dist/services/toolResult.d.mts.map +1 -0
- package/dist/services/toolResult.mjs +7 -0
- package/dist/services/toolResult.mjs.map +1 -0
- package/dist/testing.cjs +1 -0
- package/dist/testing.d.cts +4 -0
- package/dist/testing.d.mts +4 -0
- package/dist/testing.mjs +1 -0
- package/dist/types/webHub.d.cts +56 -0
- package/dist/types/webHub.d.cts.map +1 -0
- package/dist/types/webHub.d.mts +56 -0
- package/dist/types/webHub.d.mts.map +1 -0
- package/dist/types/webPlugin.d.cts +343 -0
- package/dist/types/webPlugin.d.cts.map +1 -0
- package/dist/types/webPlugin.d.mts +343 -0
- package/dist/types/webPlugin.d.mts.map +1 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vuong Ngo
|
|
4
|
+
Copyright (c) 2026 pulseaiclub
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @agimon-ai/doompi-web-contracts
|
|
2
|
+
|
|
3
|
+
Web cockpit plugin contracts for DoomPi.
|
|
4
|
+
|
|
5
|
+
Part of the [DoomPi distribution](https://www.npmjs.com/package/@agimon-ai/doompi).
|
|
6
|
+
|
|
7
|
+
A DoomPi web plugin has two halves that share these types:
|
|
8
|
+
|
|
9
|
+
- A client entry exporting `webPlugin: WebPluginDefinition`, compiled into the cockpit bundle by
|
|
10
|
+
the host package's build. It can contribute tabs with panels and badges, session data channels,
|
|
11
|
+
overlays, Leader Space key bindings, command palette commands, rail, selection bar, and
|
|
12
|
+
activity dock sections, and tool renderers: one `message` component per claimed tool (by name,
|
|
13
|
+
or by a `matches` predicate for tools named at runtime) that owns the tool call's whole timeline
|
|
14
|
+
item, composed from the shared components package's `MessageItem` so it looks like every other
|
|
15
|
+
item; `toolResultText` and `toolResultTextLines` read a result's text blocks the same way.
|
|
16
|
+
- An optional hub entry exporting `webHubChannels: readonly WebHubChannel[]`, loaded by the cockpit
|
|
17
|
+
server at startup. A channel watches some data source and answers per-session payloads that reach
|
|
18
|
+
the page as `ChannelFrame` messages whose frame type is the channel name.
|
|
19
|
+
|
|
20
|
+
Plugin packages declare both entries in a `doompiWeb` block in their package.json; the cockpit's
|
|
21
|
+
build scans the workspace and generates its registration modules from those blocks.
|
|
22
|
+
|
|
23
|
+
Use `defineWebPlugin` and `defineSessionChannel` for checked literals. Server code should import
|
|
24
|
+
these contracts type-only.
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/define.cjs"),t=require("./services/sessionStore.cjs"),n=require("./services/toolResult.cjs");exports.defineSessionChannel=e.defineSessionChannel,exports.defineSessionStore=t.defineSessionStore,exports.defineSlot=e.defineSlot,exports.defineWebPlugin=e.defineWebPlugin,exports.toolResultText=n.toolResultText,exports.toolResultTextLines=n.toolResultTextLines;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ActivityGroupContribution, LeaderBindingContribution, LeaderKeyContribution, MinorModeContribution, PaletteCommandContext, PaletteCommandContribution, SelectionAxisContribution, SessionChannelContribution, SessionFrameSender, SessionRecords, SessionStore, SessionStoreChannel, SlotDataFill, SlotDeclaration, SlotFillContribution, SurfaceContribution, TabContribution, ToolMessageRenderProps, ToolRendererContribution, ToolResultView, TransientTab, WebPluginDefinition, WebPluginRuntime, WebPluginSlotProps } from "./types/webPlugin.cjs";
|
|
2
|
+
import { defineSessionChannel, defineSlot, defineWebPlugin } from "./services/define.cjs";
|
|
3
|
+
import { defineSessionStore } from "./services/sessionStore.cjs";
|
|
4
|
+
import { toolResultText, toolResultTextLines } from "./services/toolResult.cjs";
|
|
5
|
+
import { ChannelFrame, HubChannelHost, HubChannelSource, HubSessionScope, WebHubChannel } from "./types/webHub.cjs";
|
|
6
|
+
export { type ActivityGroupContribution, type ChannelFrame, type HubChannelHost, type HubChannelSource, type HubSessionScope, type LeaderBindingContribution, type LeaderKeyContribution, type MinorModeContribution, type PaletteCommandContext, type PaletteCommandContribution, type SelectionAxisContribution, type SessionChannelContribution, type SessionFrameSender, type SessionRecords, type SessionStore, type SessionStoreChannel, type SlotDataFill, type SlotDeclaration, type SlotFillContribution, type SurfaceContribution, type TabContribution, type ToolMessageRenderProps, type ToolRendererContribution, type ToolResultView, type TransientTab, type WebHubChannel, type WebPluginDefinition, type WebPluginRuntime, type WebPluginSlotProps, defineSessionChannel, defineSessionStore, defineSlot, defineWebPlugin, toolResultText, toolResultTextLines };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ActivityGroupContribution, LeaderBindingContribution, LeaderKeyContribution, MinorModeContribution, PaletteCommandContext, PaletteCommandContribution, SelectionAxisContribution, SessionChannelContribution, SessionFrameSender, SessionRecords, SessionStore, SessionStoreChannel, SlotDataFill, SlotDeclaration, SlotFillContribution, SurfaceContribution, TabContribution, ToolMessageRenderProps, ToolRendererContribution, ToolResultView, TransientTab, WebPluginDefinition, WebPluginRuntime, WebPluginSlotProps } from "./types/webPlugin.mjs";
|
|
2
|
+
import { defineSessionChannel, defineSlot, defineWebPlugin } from "./services/define.mjs";
|
|
3
|
+
import { defineSessionStore } from "./services/sessionStore.mjs";
|
|
4
|
+
import { toolResultText, toolResultTextLines } from "./services/toolResult.mjs";
|
|
5
|
+
import { ChannelFrame, HubChannelHost, HubChannelSource, HubSessionScope, WebHubChannel } from "./types/webHub.mjs";
|
|
6
|
+
export { type ActivityGroupContribution, type ChannelFrame, type HubChannelHost, type HubChannelSource, type HubSessionScope, type LeaderBindingContribution, type LeaderKeyContribution, type MinorModeContribution, type PaletteCommandContext, type PaletteCommandContribution, type SelectionAxisContribution, type SessionChannelContribution, type SessionFrameSender, type SessionRecords, type SessionStore, type SessionStoreChannel, type SlotDataFill, type SlotDeclaration, type SlotFillContribution, type SurfaceContribution, type TabContribution, type ToolMessageRenderProps, type ToolRendererContribution, type ToolResultView, type TransientTab, type WebHubChannel, type WebPluginDefinition, type WebPluginRuntime, type WebPluginSlotProps, defineSessionChannel, defineSessionStore, defineSlot, defineWebPlugin, toolResultText, toolResultTextLines };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineSessionChannel as e,defineSlot as t,defineWebPlugin as n}from"./services/define.mjs";import{defineSessionStore as r}from"./services/sessionStore.mjs";import{toolResultText as i,toolResultTextLines as a}from"./services/toolResult.mjs";export{e as defineSessionChannel,r as defineSessionStore,t as defineSlot,n as defineWebPlugin,i as toolResultText,a as toolResultTextLines};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.cjs","names":[],"sources":["../../src/services/define.ts"],"sourcesContent":["import type { SessionChannelContribution, SlotDeclaration, WebPluginDefinition } from '../types/webPlugin.ts';\n\n/** Identity helper so plugin modules get checked literals without a type annotation. */\nexport function defineWebPlugin(plugin: WebPluginDefinition): WebPluginDefinition {\n return plugin;\n}\n\n/**\n * The one audited erasure from Payload to unknown. Channels are stored\n * untyped in the host registry; the payload type lives entirely inside the\n * plugin, behind its own parse gate.\n */\nexport function defineSessionChannel<Payload>(\n channel: SessionChannelContribution<Payload>,\n): SessionChannelContribution {\n return channel as SessionChannelContribution;\n}\n\n/**\n * Identity helper for a typed slot handle: the owner keeps the\n * SlotDeclaration<Data> to read its fills back through slotData, and the\n * plugin's `slots` array holds it erased. No cast is needed because Data only\n * appears in the parse gate's return position.\n */\nexport function defineSlot<Data>(slot: SlotDeclaration<Data>): SlotDeclaration<Data> {\n return slot;\n}\n"],"mappings":"AAGA,SAAgB,EAAgB,EAAkD,CAChF,OAAO,CACT,CAOA,SAAgB,EACd,EAC4B,CAC5B,OAAO,CACT,CAQA,SAAgB,EAAiB,EAAoD,CACnF,OAAO,CACT"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SessionChannelContribution, SlotDeclaration, WebPluginDefinition } from "../types/webPlugin.cjs";
|
|
2
|
+
//#region src/services/define.d.ts
|
|
3
|
+
/** Identity helper so plugin modules get checked literals without a type annotation. */
|
|
4
|
+
declare function defineWebPlugin(plugin: WebPluginDefinition): WebPluginDefinition;
|
|
5
|
+
/**
|
|
6
|
+
* The one audited erasure from Payload to unknown. Channels are stored
|
|
7
|
+
* untyped in the host registry; the payload type lives entirely inside the
|
|
8
|
+
* plugin, behind its own parse gate.
|
|
9
|
+
*/
|
|
10
|
+
declare function defineSessionChannel<Payload>(channel: SessionChannelContribution<Payload>): SessionChannelContribution;
|
|
11
|
+
/**
|
|
12
|
+
* Identity helper for a typed slot handle: the owner keeps the
|
|
13
|
+
* SlotDeclaration<Data> to read its fills back through slotData, and the
|
|
14
|
+
* plugin's `slots` array holds it erased. No cast is needed because Data only
|
|
15
|
+
* appears in the parse gate's return position.
|
|
16
|
+
*/
|
|
17
|
+
declare function defineSlot<Data>(slot: SlotDeclaration<Data>): SlotDeclaration<Data>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { defineSessionChannel, defineSlot, defineWebPlugin };
|
|
20
|
+
//# sourceMappingURL=define.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.cts","names":[],"sources":["../../src/services/define.ts"],"mappings":";;;iBAGgB,gBAAgB,QAAQ,sBAAsB;;;;;;iBAS9C,qBAAqB,SACnC,SAAS,2BAA2B,WACnC;;;;;;;iBAUa,WAAW,MAAM,MAAM,gBAAgB,QAAQ,gBAAgB"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SessionChannelContribution, SlotDeclaration, WebPluginDefinition } from "../types/webPlugin.mjs";
|
|
2
|
+
//#region src/services/define.d.ts
|
|
3
|
+
/** Identity helper so plugin modules get checked literals without a type annotation. */
|
|
4
|
+
declare function defineWebPlugin(plugin: WebPluginDefinition): WebPluginDefinition;
|
|
5
|
+
/**
|
|
6
|
+
* The one audited erasure from Payload to unknown. Channels are stored
|
|
7
|
+
* untyped in the host registry; the payload type lives entirely inside the
|
|
8
|
+
* plugin, behind its own parse gate.
|
|
9
|
+
*/
|
|
10
|
+
declare function defineSessionChannel<Payload>(channel: SessionChannelContribution<Payload>): SessionChannelContribution;
|
|
11
|
+
/**
|
|
12
|
+
* Identity helper for a typed slot handle: the owner keeps the
|
|
13
|
+
* SlotDeclaration<Data> to read its fills back through slotData, and the
|
|
14
|
+
* plugin's `slots` array holds it erased. No cast is needed because Data only
|
|
15
|
+
* appears in the parse gate's return position.
|
|
16
|
+
*/
|
|
17
|
+
declare function defineSlot<Data>(slot: SlotDeclaration<Data>): SlotDeclaration<Data>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { defineSessionChannel, defineSlot, defineWebPlugin };
|
|
20
|
+
//# sourceMappingURL=define.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.mts","names":[],"sources":["../../src/services/define.ts"],"mappings":";;;iBAGgB,gBAAgB,QAAQ,sBAAsB;;;;;;iBAS9C,qBAAqB,SACnC,SAAS,2BAA2B,WACnC;;;;;;;iBAUa,WAAW,MAAM,MAAM,gBAAgB,QAAQ,gBAAgB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.mjs","names":[],"sources":["../../src/services/define.ts"],"sourcesContent":["import type { SessionChannelContribution, SlotDeclaration, WebPluginDefinition } from '../types/webPlugin.ts';\n\n/** Identity helper so plugin modules get checked literals without a type annotation. */\nexport function defineWebPlugin(plugin: WebPluginDefinition): WebPluginDefinition {\n return plugin;\n}\n\n/**\n * The one audited erasure from Payload to unknown. Channels are stored\n * untyped in the host registry; the payload type lives entirely inside the\n * plugin, behind its own parse gate.\n */\nexport function defineSessionChannel<Payload>(\n channel: SessionChannelContribution<Payload>,\n): SessionChannelContribution {\n return channel as SessionChannelContribution;\n}\n\n/**\n * Identity helper for a typed slot handle: the owner keeps the\n * SlotDeclaration<Data> to read its fills back through slotData, and the\n * plugin's `slots` array holds it erased. No cast is needed because Data only\n * appears in the parse gate's return position.\n */\nexport function defineSlot<Data>(slot: SlotDeclaration<Data>): SlotDeclaration<Data> {\n return slot;\n}\n"],"mappings":"AAGA,SAAgB,EAAgB,EAAkD,CAChF,OAAO,CACT,CAOA,SAAgB,EACd,EAC4B,CAC5B,OAAO,CACT,CAQA,SAAgB,EAAiB,EAAoD,CACnF,OAAO,CACT"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./define.cjs");let t=require("@tanstack/store");function n(n){let r=new t.Store({}),i=(e,t)=>t===null?n:e[t]??n,a=(e,t)=>{let n=i(r.state,e),a=t(n);a!==n&&r.setState(t=>({...t,[e]:a}))},o=e=>{r.setState(t=>{if(!(e in t))return t;let n={...t};return delete n[e],n})};return{store:r,select:i,update:a,drop:o,reset:()=>{r.setState(()=>({}))},channel:t=>e.defineSessionChannel({channel:t.channel,parse:e=>t.parse(e),apply(e,n){a(e,e=>t.reduce(e,n))},drop:o})}}exports.defineSessionStore=n;
|
|
2
|
+
//# sourceMappingURL=sessionStore.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionStore.cjs","names":["Store","defineSessionChannel"],"sources":["../../src/services/sessionStore.ts"],"sourcesContent":["import { Store } from '@tanstack/store';\nimport type {\n SessionChannelContribution,\n SessionRecords,\n SessionStore,\n SessionStoreChannel,\n} from '../types/webPlugin.ts';\nimport { defineSessionChannel } from './define.ts';\n\n/**\n * Per-session plugin state with the drop and reset bookkeeping built in.\n *\n * One TanStack store holds a record per session id. `empty` is handed back by\n * reference for every session that has not reported, which is what keeps a\n * useStore selector stable: the hook compares by identity, so a fresh object\n * per read would re-render forever. Records are therefore immutable values,\n * and updaters and reducers return new ones.\n */\nexport function defineSessionStore<T>(empty: T): SessionStore<T> {\n const store = new Store<SessionRecords<T>>({});\n\n const select = (state: SessionRecords<T>, sessionId: string | null): T =>\n sessionId === null ? empty : (state[sessionId] ?? empty);\n\n const update = (sessionId: string, updater: (current: T) => T): void => {\n const current = select(store.state, sessionId);\n const next = updater(current);\n if (next === current) return;\n store.setState((state) => ({ ...state, [sessionId]: next }));\n };\n\n const drop = (sessionId: string): void => {\n store.setState((state) => {\n if (!(sessionId in state)) return state;\n const rest = { ...state };\n delete rest[sessionId];\n return rest;\n });\n };\n\n const reset = (): void => {\n store.setState(() => ({}));\n };\n\n const channel = <Payload>(options: SessionStoreChannel<T, Payload>): SessionChannelContribution =>\n defineSessionChannel<Payload>({\n channel: options.channel,\n parse: (input) => options.parse(input),\n apply(sessionId, payload) {\n update(sessionId, (current) => options.reduce(current, payload));\n },\n drop,\n });\n\n return { store, select, update, drop, reset, channel };\n}\n"],"mappings":"iEAkBA,SAAgB,EAAsB,EAA2B,CAC/D,IAAM,EAAQ,IAAIA,EAAAA,MAAyB,CAAC,CAAC,EAEvC,GAAU,EAA0B,IACxC,IAAc,KAAO,EAAS,EAAM,IAAc,EAE9C,GAAU,EAAmB,IAAqC,CACtE,IAAM,EAAU,EAAO,EAAM,MAAO,CAAS,EACvC,EAAO,EAAQ,CAAO,EACxB,IAAS,GACb,EAAM,SAAU,IAAW,CAAE,GAAG,GAAQ,GAAY,CAAK,EAAE,CAC7D,EAEM,EAAQ,GAA4B,CACxC,EAAM,SAAU,GAAU,CACxB,GAAI,EAAE,KAAa,GAAQ,OAAO,EAClC,IAAM,EAAO,CAAE,GAAG,CAAM,EAExB,OADA,OAAO,EAAK,GACL,CACT,CAAC,CACH,EAgBA,MAAO,CAAE,QAAO,SAAQ,SAAQ,OAAM,UAdZ,CACxB,EAAM,cAAgB,CAAC,EAAE,CAC3B,EAY6C,QAVnB,GACxBC,EAAAA,qBAA8B,CAC5B,QAAS,EAAQ,QACjB,MAAQ,GAAU,EAAQ,MAAM,CAAK,EACrC,MAAM,EAAW,EAAS,CACxB,EAAO,EAAY,GAAY,EAAQ,OAAO,EAAS,CAAO,CAAC,CACjE,EACA,MACF,CAAC,CAEkD,CACvD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SessionStore } from "../types/webPlugin.cjs";
|
|
2
|
+
//#region src/services/sessionStore.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Per-session plugin state with the drop and reset bookkeeping built in.
|
|
5
|
+
*
|
|
6
|
+
* One TanStack store holds a record per session id. `empty` is handed back by
|
|
7
|
+
* reference for every session that has not reported, which is what keeps a
|
|
8
|
+
* useStore selector stable: the hook compares by identity, so a fresh object
|
|
9
|
+
* per read would re-render forever. Records are therefore immutable values,
|
|
10
|
+
* and updaters and reducers return new ones.
|
|
11
|
+
*/
|
|
12
|
+
declare function defineSessionStore<T>(empty: T): SessionStore<T>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { defineSessionStore };
|
|
15
|
+
//# sourceMappingURL=sessionStore.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionStore.d.cts","names":[],"sources":["../../src/services/sessionStore.ts"],"mappings":";;;;;;;;;;;iBAkBgB,mBAAmB,GAAG,OAAO,IAAI,aAAa"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SessionStore } from "../types/webPlugin.mjs";
|
|
2
|
+
//#region src/services/sessionStore.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Per-session plugin state with the drop and reset bookkeeping built in.
|
|
5
|
+
*
|
|
6
|
+
* One TanStack store holds a record per session id. `empty` is handed back by
|
|
7
|
+
* reference for every session that has not reported, which is what keeps a
|
|
8
|
+
* useStore selector stable: the hook compares by identity, so a fresh object
|
|
9
|
+
* per read would re-render forever. Records are therefore immutable values,
|
|
10
|
+
* and updaters and reducers return new ones.
|
|
11
|
+
*/
|
|
12
|
+
declare function defineSessionStore<T>(empty: T): SessionStore<T>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { defineSessionStore };
|
|
15
|
+
//# sourceMappingURL=sessionStore.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionStore.d.mts","names":[],"sources":["../../src/services/sessionStore.ts"],"mappings":";;;;;;;;;;;iBAkBgB,mBAAmB,GAAG,OAAO,IAAI,aAAa"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{defineSessionChannel as e}from"./define.mjs";import{Store as t}from"@tanstack/store";function n(n){let r=new t({}),i=(e,t)=>t===null?n:e[t]??n,a=(e,t)=>{let n=i(r.state,e),a=t(n);a!==n&&r.setState(t=>({...t,[e]:a}))},o=e=>{r.setState(t=>{if(!(e in t))return t;let n={...t};return delete n[e],n})};return{store:r,select:i,update:a,drop:o,reset:()=>{r.setState(()=>({}))},channel:t=>e({channel:t.channel,parse:e=>t.parse(e),apply(e,n){a(e,e=>t.reduce(e,n))},drop:o})}}export{n as defineSessionStore};
|
|
2
|
+
//# sourceMappingURL=sessionStore.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionStore.mjs","names":[],"sources":["../../src/services/sessionStore.ts"],"sourcesContent":["import { Store } from '@tanstack/store';\nimport type {\n SessionChannelContribution,\n SessionRecords,\n SessionStore,\n SessionStoreChannel,\n} from '../types/webPlugin.ts';\nimport { defineSessionChannel } from './define.ts';\n\n/**\n * Per-session plugin state with the drop and reset bookkeeping built in.\n *\n * One TanStack store holds a record per session id. `empty` is handed back by\n * reference for every session that has not reported, which is what keeps a\n * useStore selector stable: the hook compares by identity, so a fresh object\n * per read would re-render forever. Records are therefore immutable values,\n * and updaters and reducers return new ones.\n */\nexport function defineSessionStore<T>(empty: T): SessionStore<T> {\n const store = new Store<SessionRecords<T>>({});\n\n const select = (state: SessionRecords<T>, sessionId: string | null): T =>\n sessionId === null ? empty : (state[sessionId] ?? empty);\n\n const update = (sessionId: string, updater: (current: T) => T): void => {\n const current = select(store.state, sessionId);\n const next = updater(current);\n if (next === current) return;\n store.setState((state) => ({ ...state, [sessionId]: next }));\n };\n\n const drop = (sessionId: string): void => {\n store.setState((state) => {\n if (!(sessionId in state)) return state;\n const rest = { ...state };\n delete rest[sessionId];\n return rest;\n });\n };\n\n const reset = (): void => {\n store.setState(() => ({}));\n };\n\n const channel = <Payload>(options: SessionStoreChannel<T, Payload>): SessionChannelContribution =>\n defineSessionChannel<Payload>({\n channel: options.channel,\n parse: (input) => options.parse(input),\n apply(sessionId, payload) {\n update(sessionId, (current) => options.reduce(current, payload));\n },\n drop,\n });\n\n return { store, select, update, drop, reset, channel };\n}\n"],"mappings":"4FAkBA,SAAgB,EAAsB,EAA2B,CAC/D,IAAM,EAAQ,IAAI,EAAyB,CAAC,CAAC,EAEvC,GAAU,EAA0B,IACxC,IAAc,KAAO,EAAS,EAAM,IAAc,EAE9C,GAAU,EAAmB,IAAqC,CACtE,IAAM,EAAU,EAAO,EAAM,MAAO,CAAS,EACvC,EAAO,EAAQ,CAAO,EACxB,IAAS,GACb,EAAM,SAAU,IAAW,CAAE,GAAG,GAAQ,GAAY,CAAK,EAAE,CAC7D,EAEM,EAAQ,GAA4B,CACxC,EAAM,SAAU,GAAU,CACxB,GAAI,EAAE,KAAa,GAAQ,OAAO,EAClC,IAAM,EAAO,CAAE,GAAG,CAAM,EAExB,OADA,OAAO,EAAK,GACL,CACT,CAAC,CACH,EAgBA,MAAO,CAAE,QAAO,SAAQ,SAAQ,OAAM,UAdZ,CACxB,EAAM,cAAgB,CAAC,EAAE,CAC3B,EAY6C,QAVnB,GACxB,EAA8B,CAC5B,QAAS,EAAQ,QACjB,MAAQ,GAAU,EAAQ,MAAM,CAAK,EACrC,MAAM,EAAW,EAAS,CACxB,EAAO,EAAY,GAAY,EAAQ,OAAO,EAAS,CAAO,CAAC,CACjE,EACA,MACF,CAAC,CAEkD,CACvD"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e,t,n){let r=e.parse(n);return r===null?{accepted:!1}:(e.apply(t,r),{accepted:!0})}const t={sessionId:`s1`,cwd:`/repo`};function n(e,n={}){let r=[...n.sessions??[t]],i=[],a=[],o=e.start({sessions:()=>r,publish:(t,n)=>{i.push({type:e.frameType,sessionId:t,payload:n})},onNotice:e=>a.push(e)});return{source:o,published:i,notices:a,snapshot(e){let n=r.find(t=>t.sessionId===e)??r[0]??t;return o.payloadFor(n)},addSession(e){r.push(e),o.sessionAdded?.(e)},removeSession(e){let t=r.findIndex(t=>t.sessionId===e);t>=0&&r.splice(t,1),o.sessionRemoved?.(e)},close:()=>o.close()}}exports.driveChannel=e,exports.hubChannelHarness=n;
|
|
2
|
+
//# sourceMappingURL=channels.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.cjs","names":[],"sources":["../../../src/services/testing/channels.ts"],"sourcesContent":["import type {\n ChannelFrame,\n HubChannelHost,\n HubChannelSource,\n HubSessionScope,\n WebHubChannel,\n} from '../../types/webHub.ts';\nimport type { SessionChannelContribution } from '../../types/webPlugin.ts';\n\n/**\n * The two halves of a plugin's data channel, driven the way the hub drives them.\n *\n * A channel is the one place a plugin parses input it did not produce, so its\n * parse gate is the boundary that matters, and a test that calls `apply`\n * directly steps straight over it. `driveChannel` sends a wire payload the way\n * the page does: through `parse` first, and nothing applied when it is\n * rejected.\n */\n\nexport interface ChannelDelivery {\n /** Whether the parse gate accepted the payload. */\n accepted: boolean;\n}\n\n/**\n * Sends one wire payload to a session channel.\n *\n * Returns whether the gate accepted it, so a test can assert a rejection\n * without knowing what the plugin's parsed shape looks like.\n */\nexport function driveChannel(\n channel: SessionChannelContribution,\n sessionId: string,\n payload: unknown,\n): ChannelDelivery {\n const parsed: unknown = channel.parse(payload);\n if (parsed === null) return { accepted: false };\n channel.apply(sessionId, parsed);\n return { accepted: true };\n}\n\nexport interface HubChannelHarness {\n /** The running source, as the hub holds it. */\n source: HubChannelSource;\n /** Everything the source pushed, in order. */\n readonly published: readonly ChannelFrame[];\n /** What the source told the hub. */\n readonly notices: readonly string[];\n /** The subscribe-time snapshot one session would receive; undefined means no frame. */\n snapshot(sessionId?: string): unknown;\n /** Registers a session with the source, as the hub does when one appears. */\n addSession(scope: HubSessionScope): void;\n removeSession(sessionId: string): void;\n close(): void;\n}\n\nexport interface HubChannelHarnessOptions {\n /** Sessions the hub already manages when the channel starts. */\n sessions?: readonly HubSessionScope[];\n}\n\nconst DEFAULT_SESSION: HubSessionScope = { sessionId: 's1', cwd: '/repo' };\n\n/**\n * A plugin's hub channel, started against a real host.\n *\n * The hub calls `payloadFor` once per subscriber and pushes through `publish`\n * afterwards, and a channel that only answers one of those looks correct in a\n * test that exercises the other.\n */\nexport function hubChannelHarness(channel: WebHubChannel, options: HubChannelHarnessOptions = {}): HubChannelHarness {\n const sessions = [...(options.sessions ?? [DEFAULT_SESSION])];\n const published: ChannelFrame[] = [];\n const notices: string[] = [];\n const host: HubChannelHost = {\n sessions: () => sessions,\n publish: (sessionId, payload) => {\n published.push({ type: channel.frameType, sessionId, payload });\n },\n onNotice: (message) => notices.push(message),\n };\n const source = channel.start(host);\n\n return {\n source,\n published,\n notices,\n snapshot(sessionId) {\n const scope = sessions.find((candidate) => candidate.sessionId === sessionId) ?? sessions[0] ?? DEFAULT_SESSION;\n return source.payloadFor(scope);\n },\n addSession(scope) {\n sessions.push(scope);\n source.sessionAdded?.(scope);\n },\n removeSession(sessionId) {\n const index = sessions.findIndex((candidate) => candidate.sessionId === sessionId);\n if (index >= 0) sessions.splice(index, 1);\n source.sessionRemoved?.(sessionId);\n },\n close: () => source.close(),\n };\n}\n"],"mappings":"AA8BA,SAAgB,EACd,EACA,EACA,EACiB,CACjB,IAAM,EAAkB,EAAQ,MAAM,CAAO,EAG7C,OAFI,IAAW,KAAa,CAAE,SAAU,EAAM,GAC9C,EAAQ,MAAM,EAAW,CAAM,EACxB,CAAE,SAAU,EAAK,EAC1B,CAsBA,MAAM,EAAmC,CAAE,UAAW,KAAM,IAAK,OAAQ,EASzE,SAAgB,EAAkB,EAAwB,EAAoC,CAAC,EAAsB,CACnH,IAAM,EAAW,CAAC,GAAI,EAAQ,UAAY,CAAC,CAAe,CAAE,EACtD,EAA4B,CAAC,EAC7B,EAAoB,CAAC,EAQrB,EAAS,EAAQ,MAAM,CAN3B,aAAgB,EAChB,SAAU,EAAW,IAAY,CAC/B,EAAU,KAAK,CAAE,KAAM,EAAQ,UAAW,YAAW,SAAQ,CAAC,CAChE,EACA,SAAW,GAAY,EAAQ,KAAK,CAAO,CAEb,CAAC,EAEjC,MAAO,CACL,SACA,YACA,UACA,SAAS,EAAW,CAClB,IAAM,EAAQ,EAAS,KAAM,GAAc,EAAU,YAAc,CAAS,GAAK,EAAS,IAAM,EAChG,OAAO,EAAO,WAAW,CAAK,CAChC,EACA,WAAW,EAAO,CAChB,EAAS,KAAK,CAAK,EACnB,EAAO,eAAe,CAAK,CAC7B,EACA,cAAc,EAAW,CACvB,IAAM,EAAQ,EAAS,UAAW,GAAc,EAAU,YAAc,CAAS,EAC7E,GAAS,GAAG,EAAS,OAAO,EAAO,CAAC,EACxC,EAAO,iBAAiB,CAAS,CACnC,EACA,UAAa,EAAO,MAAM,CAC5B,CACF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SessionChannelContribution } from "../../types/webPlugin.cjs";
|
|
2
|
+
import { ChannelFrame, HubChannelSource, HubSessionScope, WebHubChannel } from "../../types/webHub.cjs";
|
|
3
|
+
//#region src/services/testing/channels.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The two halves of a plugin's data channel, driven the way the hub drives them.
|
|
6
|
+
*
|
|
7
|
+
* A channel is the one place a plugin parses input it did not produce, so its
|
|
8
|
+
* parse gate is the boundary that matters, and a test that calls `apply`
|
|
9
|
+
* directly steps straight over it. `driveChannel` sends a wire payload the way
|
|
10
|
+
* the page does: through `parse` first, and nothing applied when it is
|
|
11
|
+
* rejected.
|
|
12
|
+
*/
|
|
13
|
+
interface ChannelDelivery {
|
|
14
|
+
/** Whether the parse gate accepted the payload. */
|
|
15
|
+
accepted: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sends one wire payload to a session channel.
|
|
19
|
+
*
|
|
20
|
+
* Returns whether the gate accepted it, so a test can assert a rejection
|
|
21
|
+
* without knowing what the plugin's parsed shape looks like.
|
|
22
|
+
*/
|
|
23
|
+
declare function driveChannel(channel: SessionChannelContribution, sessionId: string, payload: unknown): ChannelDelivery;
|
|
24
|
+
interface HubChannelHarness {
|
|
25
|
+
/** The running source, as the hub holds it. */
|
|
26
|
+
source: HubChannelSource;
|
|
27
|
+
/** Everything the source pushed, in order. */
|
|
28
|
+
readonly published: readonly ChannelFrame[];
|
|
29
|
+
/** What the source told the hub. */
|
|
30
|
+
readonly notices: readonly string[];
|
|
31
|
+
/** The subscribe-time snapshot one session would receive; undefined means no frame. */
|
|
32
|
+
snapshot(sessionId?: string): unknown;
|
|
33
|
+
/** Registers a session with the source, as the hub does when one appears. */
|
|
34
|
+
addSession(scope: HubSessionScope): void;
|
|
35
|
+
removeSession(sessionId: string): void;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
interface HubChannelHarnessOptions {
|
|
39
|
+
/** Sessions the hub already manages when the channel starts. */
|
|
40
|
+
sessions?: readonly HubSessionScope[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A plugin's hub channel, started against a real host.
|
|
44
|
+
*
|
|
45
|
+
* The hub calls `payloadFor` once per subscriber and pushes through `publish`
|
|
46
|
+
* afterwards, and a channel that only answers one of those looks correct in a
|
|
47
|
+
* test that exercises the other.
|
|
48
|
+
*/
|
|
49
|
+
declare function hubChannelHarness(channel: WebHubChannel, options?: HubChannelHarnessOptions): HubChannelHarness;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { ChannelDelivery, HubChannelHarness, HubChannelHarnessOptions, driveChannel, hubChannelHarness };
|
|
52
|
+
//# sourceMappingURL=channels.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.d.cts","names":[],"sources":["../../../src/services/testing/channels.ts"],"mappings":";;;;;;;;;;;;UAmBiB;;EAEf;;;;;;;;iBASc,aACd,SAAS,4BACT,mBACA,mBACC;UAOc;;EAEf,QAAQ;;WAEC,oBAAoB;;WAEpB;;EAET,SAAS;;EAET,WAAW,OAAO;EAClB,cAAc;EACd;;UAGe;;EAEf,oBAAoB;;;;;;;;;iBAYN,kBAAkB,SAAS,eAAe,UAAS,2BAAgC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SessionChannelContribution } from "../../types/webPlugin.mjs";
|
|
2
|
+
import { ChannelFrame, HubChannelSource, HubSessionScope, WebHubChannel } from "../../types/webHub.mjs";
|
|
3
|
+
//#region src/services/testing/channels.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The two halves of a plugin's data channel, driven the way the hub drives them.
|
|
6
|
+
*
|
|
7
|
+
* A channel is the one place a plugin parses input it did not produce, so its
|
|
8
|
+
* parse gate is the boundary that matters, and a test that calls `apply`
|
|
9
|
+
* directly steps straight over it. `driveChannel` sends a wire payload the way
|
|
10
|
+
* the page does: through `parse` first, and nothing applied when it is
|
|
11
|
+
* rejected.
|
|
12
|
+
*/
|
|
13
|
+
interface ChannelDelivery {
|
|
14
|
+
/** Whether the parse gate accepted the payload. */
|
|
15
|
+
accepted: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sends one wire payload to a session channel.
|
|
19
|
+
*
|
|
20
|
+
* Returns whether the gate accepted it, so a test can assert a rejection
|
|
21
|
+
* without knowing what the plugin's parsed shape looks like.
|
|
22
|
+
*/
|
|
23
|
+
declare function driveChannel(channel: SessionChannelContribution, sessionId: string, payload: unknown): ChannelDelivery;
|
|
24
|
+
interface HubChannelHarness {
|
|
25
|
+
/** The running source, as the hub holds it. */
|
|
26
|
+
source: HubChannelSource;
|
|
27
|
+
/** Everything the source pushed, in order. */
|
|
28
|
+
readonly published: readonly ChannelFrame[];
|
|
29
|
+
/** What the source told the hub. */
|
|
30
|
+
readonly notices: readonly string[];
|
|
31
|
+
/** The subscribe-time snapshot one session would receive; undefined means no frame. */
|
|
32
|
+
snapshot(sessionId?: string): unknown;
|
|
33
|
+
/** Registers a session with the source, as the hub does when one appears. */
|
|
34
|
+
addSession(scope: HubSessionScope): void;
|
|
35
|
+
removeSession(sessionId: string): void;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
interface HubChannelHarnessOptions {
|
|
39
|
+
/** Sessions the hub already manages when the channel starts. */
|
|
40
|
+
sessions?: readonly HubSessionScope[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A plugin's hub channel, started against a real host.
|
|
44
|
+
*
|
|
45
|
+
* The hub calls `payloadFor` once per subscriber and pushes through `publish`
|
|
46
|
+
* afterwards, and a channel that only answers one of those looks correct in a
|
|
47
|
+
* test that exercises the other.
|
|
48
|
+
*/
|
|
49
|
+
declare function hubChannelHarness(channel: WebHubChannel, options?: HubChannelHarnessOptions): HubChannelHarness;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { ChannelDelivery, HubChannelHarness, HubChannelHarnessOptions, driveChannel, hubChannelHarness };
|
|
52
|
+
//# sourceMappingURL=channels.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.d.mts","names":[],"sources":["../../../src/services/testing/channels.ts"],"mappings":";;;;;;;;;;;;UAmBiB;;EAEf;;;;;;;;iBASc,aACd,SAAS,4BACT,mBACA,mBACC;UAOc;;EAEf,QAAQ;;WAEC,oBAAoB;;WAEpB;;EAET,SAAS;;EAET,WAAW,OAAO;EAClB,cAAc;EACd;;UAGe;;EAEf,oBAAoB;;;;;;;;;iBAYN,kBAAkB,SAAS,eAAe,UAAS,2BAAgC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e,t,n){let r=e.parse(n);return r===null?{accepted:!1}:(e.apply(t,r),{accepted:!0})}const t={sessionId:`s1`,cwd:`/repo`};function n(e,n={}){let r=[...n.sessions??[t]],i=[],a=[],o=e.start({sessions:()=>r,publish:(t,n)=>{i.push({type:e.frameType,sessionId:t,payload:n})},onNotice:e=>a.push(e)});return{source:o,published:i,notices:a,snapshot(e){let n=r.find(t=>t.sessionId===e)??r[0]??t;return o.payloadFor(n)},addSession(e){r.push(e),o.sessionAdded?.(e)},removeSession(e){let t=r.findIndex(t=>t.sessionId===e);t>=0&&r.splice(t,1),o.sessionRemoved?.(e)},close:()=>o.close()}}export{e as driveChannel,n as hubChannelHarness};
|
|
2
|
+
//# sourceMappingURL=channels.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.mjs","names":[],"sources":["../../../src/services/testing/channels.ts"],"sourcesContent":["import type {\n ChannelFrame,\n HubChannelHost,\n HubChannelSource,\n HubSessionScope,\n WebHubChannel,\n} from '../../types/webHub.ts';\nimport type { SessionChannelContribution } from '../../types/webPlugin.ts';\n\n/**\n * The two halves of a plugin's data channel, driven the way the hub drives them.\n *\n * A channel is the one place a plugin parses input it did not produce, so its\n * parse gate is the boundary that matters, and a test that calls `apply`\n * directly steps straight over it. `driveChannel` sends a wire payload the way\n * the page does: through `parse` first, and nothing applied when it is\n * rejected.\n */\n\nexport interface ChannelDelivery {\n /** Whether the parse gate accepted the payload. */\n accepted: boolean;\n}\n\n/**\n * Sends one wire payload to a session channel.\n *\n * Returns whether the gate accepted it, so a test can assert a rejection\n * without knowing what the plugin's parsed shape looks like.\n */\nexport function driveChannel(\n channel: SessionChannelContribution,\n sessionId: string,\n payload: unknown,\n): ChannelDelivery {\n const parsed: unknown = channel.parse(payload);\n if (parsed === null) return { accepted: false };\n channel.apply(sessionId, parsed);\n return { accepted: true };\n}\n\nexport interface HubChannelHarness {\n /** The running source, as the hub holds it. */\n source: HubChannelSource;\n /** Everything the source pushed, in order. */\n readonly published: readonly ChannelFrame[];\n /** What the source told the hub. */\n readonly notices: readonly string[];\n /** The subscribe-time snapshot one session would receive; undefined means no frame. */\n snapshot(sessionId?: string): unknown;\n /** Registers a session with the source, as the hub does when one appears. */\n addSession(scope: HubSessionScope): void;\n removeSession(sessionId: string): void;\n close(): void;\n}\n\nexport interface HubChannelHarnessOptions {\n /** Sessions the hub already manages when the channel starts. */\n sessions?: readonly HubSessionScope[];\n}\n\nconst DEFAULT_SESSION: HubSessionScope = { sessionId: 's1', cwd: '/repo' };\n\n/**\n * A plugin's hub channel, started against a real host.\n *\n * The hub calls `payloadFor` once per subscriber and pushes through `publish`\n * afterwards, and a channel that only answers one of those looks correct in a\n * test that exercises the other.\n */\nexport function hubChannelHarness(channel: WebHubChannel, options: HubChannelHarnessOptions = {}): HubChannelHarness {\n const sessions = [...(options.sessions ?? [DEFAULT_SESSION])];\n const published: ChannelFrame[] = [];\n const notices: string[] = [];\n const host: HubChannelHost = {\n sessions: () => sessions,\n publish: (sessionId, payload) => {\n published.push({ type: channel.frameType, sessionId, payload });\n },\n onNotice: (message) => notices.push(message),\n };\n const source = channel.start(host);\n\n return {\n source,\n published,\n notices,\n snapshot(sessionId) {\n const scope = sessions.find((candidate) => candidate.sessionId === sessionId) ?? sessions[0] ?? DEFAULT_SESSION;\n return source.payloadFor(scope);\n },\n addSession(scope) {\n sessions.push(scope);\n source.sessionAdded?.(scope);\n },\n removeSession(sessionId) {\n const index = sessions.findIndex((candidate) => candidate.sessionId === sessionId);\n if (index >= 0) sessions.splice(index, 1);\n source.sessionRemoved?.(sessionId);\n },\n close: () => source.close(),\n };\n}\n"],"mappings":"AA8BA,SAAgB,EACd,EACA,EACA,EACiB,CACjB,IAAM,EAAkB,EAAQ,MAAM,CAAO,EAG7C,OAFI,IAAW,KAAa,CAAE,SAAU,EAAM,GAC9C,EAAQ,MAAM,EAAW,CAAM,EACxB,CAAE,SAAU,EAAK,EAC1B,CAsBA,MAAM,EAAmC,CAAE,UAAW,KAAM,IAAK,OAAQ,EASzE,SAAgB,EAAkB,EAAwB,EAAoC,CAAC,EAAsB,CACnH,IAAM,EAAW,CAAC,GAAI,EAAQ,UAAY,CAAC,CAAe,CAAE,EACtD,EAA4B,CAAC,EAC7B,EAAoB,CAAC,EAQrB,EAAS,EAAQ,MAAM,CAN3B,aAAgB,EAChB,SAAU,EAAW,IAAY,CAC/B,EAAU,KAAK,CAAE,KAAM,EAAQ,UAAW,YAAW,SAAQ,CAAC,CAChE,EACA,SAAW,GAAY,EAAQ,KAAK,CAAO,CAEb,CAAC,EAEjC,MAAO,CACL,SACA,YACA,UACA,SAAS,EAAW,CAClB,IAAM,EAAQ,EAAS,KAAM,GAAc,EAAU,YAAc,CAAS,GAAK,EAAS,IAAM,EAChG,OAAO,EAAO,WAAW,CAAK,CAChC,EACA,WAAW,EAAO,CAChB,EAAS,KAAK,CAAK,EACnB,EAAO,eAAe,CAAK,CAC7B,EACA,cAAc,EAAW,CACvB,IAAM,EAAQ,EAAS,UAAW,GAAc,EAAU,YAAc,CAAS,EAC7E,GAAS,GAAG,EAAS,OAAO,EAAO,CAAC,EACxC,EAAO,iBAAiB,CAAS,CACnC,EACA,UAAa,EAAO,MAAM,CAC5B,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("react"),t=require("react-dom/server");function n(e){return e.replaceAll(/<[^>]*>/gu,` `).replaceAll(/\s+/gu,` `).trim()}function r(r,i){try{let a=(0,t.renderToStaticMarkup)((0,e.createElement)(r,i));return{html:a,error:void 0,includes:e=>n(a).includes(e)}}catch(e){return{html:``,error:e instanceof Error?e:Error(String(e)),includes:()=>!1}}}exports.renderPlugin=r;
|
|
2
|
+
//# sourceMappingURL=render.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.cjs","names":["renderToStaticMarkup","createElement"],"sources":["../../../src/services/testing/render.ts"],"sourcesContent":["import { createElement } from 'react';\nimport { renderToStaticMarkup } from 'react-dom/server';\nimport type { ComponentType } from 'react';\n\n/**\n * One plugin component, rendered.\n *\n * No plugin component in the repository is imported by any test: stores and\n * pure helpers are covered per package, and the components are exercised only\n * by the browser suite. So the ordinary failures go unseen until a page is\n * opened, and the most ordinary of all is a component that throws on a state\n * the plugin can reach, which the host then catches and replaces with a\n * fallback nobody notices.\n *\n * Static markup rather than a DOM, matching the shared components package. It\n * proves a component mounts, reads the props the host actually sends, and puts\n * the expected text on the page. Clicks, effects, and Radix portals need a real\n * browser and stay with the Playwright suite.\n *\n * Callers pass components as values, never as JSX, so a plugin package needs no\n * JSX configuration in the project that holds its tests.\n */\n\nexport interface RenderedPlugin {\n /** The markup, or an empty string when the component threw. */\n html: string;\n /** What the component threw, which the host would have swallowed. */\n error: Error | undefined;\n /** Whether the rendered markup contains this text, ignoring the tags around it. */\n includes(text: string): boolean;\n}\n\n/** Tag-stripped text, so an assertion reads the page rather than the markup. */\nfunction textOf(html: string): string {\n return html\n .replaceAll(/<[^>]*>/gu, ' ')\n .replaceAll(/\\s+/gu, ' ')\n .trim();\n}\n\nexport function renderPlugin<Props extends object>(component: ComponentType<Props>, props: Props): RenderedPlugin {\n try {\n const html = renderToStaticMarkup(createElement(component, props));\n return { html, error: undefined, includes: (text) => textOf(html).includes(text) };\n } catch (error) {\n // Returned rather than rethrown: the host catches a throwing item and keeps\n // the page up, so a test wanting to prove that needs the error as a value.\n return {\n html: '',\n error: error instanceof Error ? error : new Error(String(error)),\n includes: () => false,\n };\n }\n}\n"],"mappings":"qDAiCA,SAAS,EAAO,EAAsB,CACpC,OAAO,EACJ,WAAW,YAAa,GAAG,CAAC,CAC5B,WAAW,QAAS,GAAG,CAAC,CACxB,KAAK,CACV,CAEA,SAAgB,EAAmC,EAAiC,EAA8B,CAChH,GAAI,CACF,IAAM,GAAA,EAAOA,EAAAA,qBAAAA,EAAAA,EAAqBC,EAAAA,cAAAA,CAAc,EAAW,CAAK,CAAC,EACjE,MAAO,CAAE,OAAM,MAAO,IAAA,GAAW,SAAW,GAAS,EAAO,CAAI,CAAC,CAAC,SAAS,CAAI,CAAE,CACnF,OAAS,EAAO,CAGd,MAAO,CACL,KAAM,GACN,MAAO,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,EAC/D,aAAgB,EAClB,CACF,CACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
//#region src/services/testing/render.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* One plugin component, rendered.
|
|
5
|
+
*
|
|
6
|
+
* No plugin component in the repository is imported by any test: stores and
|
|
7
|
+
* pure helpers are covered per package, and the components are exercised only
|
|
8
|
+
* by the browser suite. So the ordinary failures go unseen until a page is
|
|
9
|
+
* opened, and the most ordinary of all is a component that throws on a state
|
|
10
|
+
* the plugin can reach, which the host then catches and replaces with a
|
|
11
|
+
* fallback nobody notices.
|
|
12
|
+
*
|
|
13
|
+
* Static markup rather than a DOM, matching the shared components package. It
|
|
14
|
+
* proves a component mounts, reads the props the host actually sends, and puts
|
|
15
|
+
* the expected text on the page. Clicks, effects, and Radix portals need a real
|
|
16
|
+
* browser and stay with the Playwright suite.
|
|
17
|
+
*
|
|
18
|
+
* Callers pass components as values, never as JSX, so a plugin package needs no
|
|
19
|
+
* JSX configuration in the project that holds its tests.
|
|
20
|
+
*/
|
|
21
|
+
interface RenderedPlugin {
|
|
22
|
+
/** The markup, or an empty string when the component threw. */
|
|
23
|
+
html: string;
|
|
24
|
+
/** What the component threw, which the host would have swallowed. */
|
|
25
|
+
error: Error | undefined;
|
|
26
|
+
/** Whether the rendered markup contains this text, ignoring the tags around it. */
|
|
27
|
+
includes(text: string): boolean;
|
|
28
|
+
}
|
|
29
|
+
declare function renderPlugin<Props extends object>(component: ComponentType<Props>, props: Props): RenderedPlugin;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { RenderedPlugin, renderPlugin };
|
|
32
|
+
//# sourceMappingURL=render.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.cts","names":[],"sources":["../../../src/services/testing/render.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;UAuBiB;;EAEf;;EAEA,OAAO;;EAEP,SAAS;;iBAWK,aAAa,sBAAsB,WAAW,cAAc,QAAQ,OAAO,QAAQ"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
//#region src/services/testing/render.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* One plugin component, rendered.
|
|
5
|
+
*
|
|
6
|
+
* No plugin component in the repository is imported by any test: stores and
|
|
7
|
+
* pure helpers are covered per package, and the components are exercised only
|
|
8
|
+
* by the browser suite. So the ordinary failures go unseen until a page is
|
|
9
|
+
* opened, and the most ordinary of all is a component that throws on a state
|
|
10
|
+
* the plugin can reach, which the host then catches and replaces with a
|
|
11
|
+
* fallback nobody notices.
|
|
12
|
+
*
|
|
13
|
+
* Static markup rather than a DOM, matching the shared components package. It
|
|
14
|
+
* proves a component mounts, reads the props the host actually sends, and puts
|
|
15
|
+
* the expected text on the page. Clicks, effects, and Radix portals need a real
|
|
16
|
+
* browser and stay with the Playwright suite.
|
|
17
|
+
*
|
|
18
|
+
* Callers pass components as values, never as JSX, so a plugin package needs no
|
|
19
|
+
* JSX configuration in the project that holds its tests.
|
|
20
|
+
*/
|
|
21
|
+
interface RenderedPlugin {
|
|
22
|
+
/** The markup, or an empty string when the component threw. */
|
|
23
|
+
html: string;
|
|
24
|
+
/** What the component threw, which the host would have swallowed. */
|
|
25
|
+
error: Error | undefined;
|
|
26
|
+
/** Whether the rendered markup contains this text, ignoring the tags around it. */
|
|
27
|
+
includes(text: string): boolean;
|
|
28
|
+
}
|
|
29
|
+
declare function renderPlugin<Props extends object>(component: ComponentType<Props>, props: Props): RenderedPlugin;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { RenderedPlugin, renderPlugin };
|
|
32
|
+
//# sourceMappingURL=render.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.mts","names":[],"sources":["../../../src/services/testing/render.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;UAuBiB;;EAEf;;EAEA,OAAO;;EAEP,SAAS;;iBAWK,aAAa,sBAAsB,WAAW,cAAc,QAAQ,OAAO,QAAQ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createElement as e}from"react";import{renderToStaticMarkup as t}from"react-dom/server";function n(e){return e.replaceAll(/<[^>]*>/gu,` `).replaceAll(/\s+/gu,` `).trim()}function r(r,i){try{let a=t(e(r,i));return{html:a,error:void 0,includes:e=>n(a).includes(e)}}catch(e){return{html:``,error:e instanceof Error?e:Error(String(e)),includes:()=>!1}}}export{r as renderPlugin};
|
|
2
|
+
//# sourceMappingURL=render.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.mjs","names":[],"sources":["../../../src/services/testing/render.ts"],"sourcesContent":["import { createElement } from 'react';\nimport { renderToStaticMarkup } from 'react-dom/server';\nimport type { ComponentType } from 'react';\n\n/**\n * One plugin component, rendered.\n *\n * No plugin component in the repository is imported by any test: stores and\n * pure helpers are covered per package, and the components are exercised only\n * by the browser suite. So the ordinary failures go unseen until a page is\n * opened, and the most ordinary of all is a component that throws on a state\n * the plugin can reach, which the host then catches and replaces with a\n * fallback nobody notices.\n *\n * Static markup rather than a DOM, matching the shared components package. It\n * proves a component mounts, reads the props the host actually sends, and puts\n * the expected text on the page. Clicks, effects, and Radix portals need a real\n * browser and stay with the Playwright suite.\n *\n * Callers pass components as values, never as JSX, so a plugin package needs no\n * JSX configuration in the project that holds its tests.\n */\n\nexport interface RenderedPlugin {\n /** The markup, or an empty string when the component threw. */\n html: string;\n /** What the component threw, which the host would have swallowed. */\n error: Error | undefined;\n /** Whether the rendered markup contains this text, ignoring the tags around it. */\n includes(text: string): boolean;\n}\n\n/** Tag-stripped text, so an assertion reads the page rather than the markup. */\nfunction textOf(html: string): string {\n return html\n .replaceAll(/<[^>]*>/gu, ' ')\n .replaceAll(/\\s+/gu, ' ')\n .trim();\n}\n\nexport function renderPlugin<Props extends object>(component: ComponentType<Props>, props: Props): RenderedPlugin {\n try {\n const html = renderToStaticMarkup(createElement(component, props));\n return { html, error: undefined, includes: (text) => textOf(html).includes(text) };\n } catch (error) {\n // Returned rather than rethrown: the host catches a throwing item and keeps\n // the page up, so a test wanting to prove that needs the error as a value.\n return {\n html: '',\n error: error instanceof Error ? error : new Error(String(error)),\n includes: () => false,\n };\n }\n}\n"],"mappings":"8FAiCA,SAAS,EAAO,EAAsB,CACpC,OAAO,EACJ,WAAW,YAAa,GAAG,CAAC,CAC5B,WAAW,QAAS,GAAG,CAAC,CACxB,KAAK,CACV,CAEA,SAAgB,EAAmC,EAAiC,EAA8B,CAChH,GAAI,CACF,IAAM,EAAO,EAAqB,EAAc,EAAW,CAAK,CAAC,EACjE,MAAO,CAAE,OAAM,MAAO,IAAA,GAAW,SAAW,GAAS,EAAO,CAAI,CAAC,CAAC,SAAS,CAAI,CAAE,CACnF,OAAS,EAAO,CAGd,MAAO,CACL,KAAM,GACN,MAAO,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,EAC/D,aAAgB,EAClB,CACF,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e={}){let t=[];return{props:{sessionId:e.sessionId===void 0?`s1`:e.sessionId,statuses:e.statuses??{},openTab:e=>{t.push({action:`openTab`,target:e})},openTransientTab:e=>{t.push({action:`openTransientTab`,target:e.id})},closeTransientTab:e=>{t.push({action:`closeTransientTab`,target:e})},sendSessionFrame:(e,n)=>{t.push({action:`sendSessionFrame`,target:e,frame:n})},renderThread:t=>e.thread?.(t)??null,renderSlot:t=>e.slotContent?.[t]??null,slotData:t=>e.slotData?.[t.slot]??[]},actions:t,frames:()=>t.flatMap(e=>e.frame?[e.frame]:[])}}function t(t){let n=e(t),r=t.result===void 0?null:t.result;return{...n,props:{...n.props,toolCallId:t.toolCallId??`call-1`,toolName:t.toolName,args:t.args??{},result:r,output:t.output??``,running:t.running??!1,isError:t.isError??!1}}}exports.slotPropsFixture=e,exports.toolMessagePropsFixture=t;
|
|
2
|
+
//# sourceMappingURL=slotProps.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slotProps.cjs","names":[],"sources":["../../../src/services/testing/slotProps.ts"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type {\n SlotDataFill,\n SlotDeclaration,\n ToolMessageRenderProps,\n ToolResultView,\n TransientTab,\n WebPluginSlotProps,\n} from '../../types/webPlugin.ts';\n\n/**\n * The props the cockpit hands a plugin's components, built for a test.\n *\n * A plugin component takes fifteen props and reads three of them. Written out\n * by hand in each test, the other twelve are noise that goes stale the moment\n * the contract grows a member; written as a cast, the component compiles\n * against props the host never sends. This builds all of them, records what the\n * component did with the actions, and lets a test override the two or three it\n * cares about.\n */\n\nexport interface RecordedSlotAction {\n action: 'openTab' | 'openTransientTab' | 'closeTransientTab' | 'sendSessionFrame';\n /** The tab id, the transient tab's id, or the target session of a frame. */\n target: string | null;\n /** The frame a component sent, for `sendSessionFrame`. */\n frame?: Record<string, unknown>;\n}\n\nexport interface SlotPropsFixture {\n props: WebPluginSlotProps;\n /** Everything the component did through its props, in order. */\n readonly actions: readonly RecordedSlotAction[];\n /** Frames the component sent, which is what most assertions want. */\n frames(): readonly Record<string, unknown>[];\n}\n\nexport interface SlotPropsOptions {\n sessionId?: string | null;\n statuses?: Readonly<Record<string, string>>;\n /** Component fills this slot owner should see; keyed by slot name. */\n slotContent?: Readonly<Record<string, ReactNode>>;\n /** Data fills this slot owner should read back, keyed by slot name. */\n slotData?: Readonly<Record<string, readonly SlotDataFill[]>>;\n /** What `renderThread` returns for a plugin that renders one. */\n thread?: (threadId: string) => ReactNode;\n}\n\nconst DEFAULT_SESSION_ID = 's1';\n\nexport function slotPropsFixture(options: SlotPropsOptions = {}): SlotPropsFixture {\n const actions: RecordedSlotAction[] = [];\n const props: WebPluginSlotProps = {\n sessionId: options.sessionId === undefined ? DEFAULT_SESSION_ID : options.sessionId,\n statuses: options.statuses ?? {},\n openTab: (tabId) => {\n actions.push({ action: 'openTab', target: tabId });\n },\n openTransientTab: (tab: TransientTab) => {\n actions.push({ action: 'openTransientTab', target: tab.id });\n },\n closeTransientTab: (tabId: string) => {\n actions.push({ action: 'closeTransientTab', target: tabId });\n },\n sendSessionFrame: (sessionId, frame) => {\n actions.push({ action: 'sendSessionFrame', target: sessionId, frame });\n },\n renderThread: (threadId) => options.thread?.(threadId) ?? null,\n renderSlot: (slot) => options.slotContent?.[slot] ?? null,\n // The host resolves fills by slot name and hands the owner its own typed\n // view, so a test declares them by name too.\n slotData: <Data>(slot: SlotDeclaration<Data>) =>\n (options.slotData?.[slot.slot] ?? []) as readonly SlotDataFill<Data>[],\n };\n\n return {\n props,\n actions,\n frames: () => actions.flatMap((entry) => (entry.frame ? [entry.frame] : [])),\n };\n}\n\nexport interface ToolMessagePropsOptions extends SlotPropsOptions {\n toolCallId?: string;\n toolName: string;\n args?: Record<string, unknown>;\n /** The newest result; null is what the host sends before any output. */\n result?: ToolResultView | null;\n /** The result's text blocks joined, as the host's own fallback item shows them. */\n output?: string;\n running?: boolean;\n isError?: boolean;\n}\n\nexport interface ToolMessagePropsFixture extends Omit<SlotPropsFixture, 'props'> {\n props: ToolMessageRenderProps;\n}\n\nconst DEFAULT_TOOL_CALL_ID = 'call-1';\n\n/** The props a tool's timeline item receives, in any of its four states. */\nexport function toolMessagePropsFixture(options: ToolMessagePropsOptions): ToolMessagePropsFixture {\n const base = slotPropsFixture(options);\n const result = options.result === undefined ? null : options.result;\n return {\n ...base,\n props: {\n ...base.props,\n toolCallId: options.toolCallId ?? DEFAULT_TOOL_CALL_ID,\n toolName: options.toolName,\n args: options.args ?? {},\n result,\n output: options.output ?? '',\n running: options.running ?? false,\n isError: options.isError ?? false,\n },\n };\n}\n"],"mappings":"AAkDA,SAAgB,EAAiB,EAA4B,CAAC,EAAqB,CACjF,IAAM,EAAgC,CAAC,EAwBvC,MAAO,CACL,MAAA,CAvBA,UAAW,EAAQ,YAAc,IAAA,GAAY,KAAqB,EAAQ,UAC1E,SAAU,EAAQ,UAAY,CAAC,EAC/B,QAAU,GAAU,CAClB,EAAQ,KAAK,CAAE,OAAQ,UAAW,OAAQ,CAAM,CAAC,CACnD,EACA,iBAAmB,GAAsB,CACvC,EAAQ,KAAK,CAAE,OAAQ,mBAAoB,OAAQ,EAAI,EAAG,CAAC,CAC7D,EACA,kBAAoB,GAAkB,CACpC,EAAQ,KAAK,CAAE,OAAQ,oBAAqB,OAAQ,CAAM,CAAC,CAC7D,EACA,kBAAmB,EAAW,IAAU,CACtC,EAAQ,KAAK,CAAE,OAAQ,mBAAoB,OAAQ,EAAW,OAAM,CAAC,CACvE,EACA,aAAe,GAAa,EAAQ,SAAS,CAAQ,GAAK,KAC1D,WAAa,GAAS,EAAQ,cAAc,IAAS,KAGrD,SAAiB,GACd,EAAQ,WAAW,EAAK,OAAS,CAAC,CAIjC,EACJ,UACA,WAAc,EAAQ,QAAS,GAAW,EAAM,MAAQ,CAAC,EAAM,KAAK,EAAI,CAAC,CAAE,CAC7E,CACF,CAqBA,SAAgB,EAAwB,EAA2D,CACjG,IAAM,EAAO,EAAiB,CAAO,EAC/B,EAAS,EAAQ,SAAW,IAAA,GAAY,KAAO,EAAQ,OAC7D,MAAO,CACL,GAAG,EACH,MAAO,CACL,GAAG,EAAK,MACR,WAAY,EAAQ,YAAc,SAClC,SAAU,EAAQ,SAClB,KAAM,EAAQ,MAAQ,CAAC,EACvB,SACA,OAAQ,EAAQ,QAAU,GAC1B,QAAS,EAAQ,SAAW,GAC5B,QAAS,EAAQ,SAAW,EAC9B,CACF,CACF"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { SlotDataFill, ToolMessageRenderProps, ToolResultView, WebPluginSlotProps } from "../../types/webPlugin.cjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/services/testing/slotProps.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The props the cockpit hands a plugin's components, built for a test.
|
|
6
|
+
*
|
|
7
|
+
* A plugin component takes fifteen props and reads three of them. Written out
|
|
8
|
+
* by hand in each test, the other twelve are noise that goes stale the moment
|
|
9
|
+
* the contract grows a member; written as a cast, the component compiles
|
|
10
|
+
* against props the host never sends. This builds all of them, records what the
|
|
11
|
+
* component did with the actions, and lets a test override the two or three it
|
|
12
|
+
* cares about.
|
|
13
|
+
*/
|
|
14
|
+
interface RecordedSlotAction {
|
|
15
|
+
action: 'openTab' | 'openTransientTab' | 'closeTransientTab' | 'sendSessionFrame';
|
|
16
|
+
/** The tab id, the transient tab's id, or the target session of a frame. */
|
|
17
|
+
target: string | null;
|
|
18
|
+
/** The frame a component sent, for `sendSessionFrame`. */
|
|
19
|
+
frame?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
interface SlotPropsFixture {
|
|
22
|
+
props: WebPluginSlotProps;
|
|
23
|
+
/** Everything the component did through its props, in order. */
|
|
24
|
+
readonly actions: readonly RecordedSlotAction[];
|
|
25
|
+
/** Frames the component sent, which is what most assertions want. */
|
|
26
|
+
frames(): readonly Record<string, unknown>[];
|
|
27
|
+
}
|
|
28
|
+
interface SlotPropsOptions {
|
|
29
|
+
sessionId?: string | null;
|
|
30
|
+
statuses?: Readonly<Record<string, string>>;
|
|
31
|
+
/** Component fills this slot owner should see; keyed by slot name. */
|
|
32
|
+
slotContent?: Readonly<Record<string, ReactNode>>;
|
|
33
|
+
/** Data fills this slot owner should read back, keyed by slot name. */
|
|
34
|
+
slotData?: Readonly<Record<string, readonly SlotDataFill[]>>;
|
|
35
|
+
/** What `renderThread` returns for a plugin that renders one. */
|
|
36
|
+
thread?: (threadId: string) => ReactNode;
|
|
37
|
+
}
|
|
38
|
+
declare function slotPropsFixture(options?: SlotPropsOptions): SlotPropsFixture;
|
|
39
|
+
interface ToolMessagePropsOptions extends SlotPropsOptions {
|
|
40
|
+
toolCallId?: string;
|
|
41
|
+
toolName: string;
|
|
42
|
+
args?: Record<string, unknown>;
|
|
43
|
+
/** The newest result; null is what the host sends before any output. */
|
|
44
|
+
result?: ToolResultView | null;
|
|
45
|
+
/** The result's text blocks joined, as the host's own fallback item shows them. */
|
|
46
|
+
output?: string;
|
|
47
|
+
running?: boolean;
|
|
48
|
+
isError?: boolean;
|
|
49
|
+
}
|
|
50
|
+
interface ToolMessagePropsFixture extends Omit<SlotPropsFixture, 'props'> {
|
|
51
|
+
props: ToolMessageRenderProps;
|
|
52
|
+
}
|
|
53
|
+
/** The props a tool's timeline item receives, in any of its four states. */
|
|
54
|
+
declare function toolMessagePropsFixture(options: ToolMessagePropsOptions): ToolMessagePropsFixture;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { RecordedSlotAction, SlotPropsFixture, SlotPropsOptions, ToolMessagePropsFixture, ToolMessagePropsOptions, slotPropsFixture, toolMessagePropsFixture };
|
|
57
|
+
//# sourceMappingURL=slotProps.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slotProps.d.cts","names":[],"sources":["../../../src/services/testing/slotProps.ts"],"mappings":";;;;;;;;;;;;;UAqBiB;EACf;;EAEA;;EAEA,QAAQ;;UAGO;EACf,OAAO;;WAEE,kBAAkB;;EAE3B,mBAAmB;;UAGJ;EACf;EACA,WAAW,SAAS;;EAEpB,cAAc,SAAS,eAAe;;EAEtC,WAAW,SAAS,wBAAwB;;EAE5C,UAAU,qBAAqB;;iBAKjB,iBAAiB,UAAS,mBAAwB;UAgCjD,gCAAgC;EAC/C;EACA;EACA,OAAO;;EAEP,SAAS;;EAET;EACA;EACA;;UAGe,gCAAgC,KAAK;EACpD,OAAO;;;iBAMO,wBAAwB,SAAS,0BAA0B"}
|