@agimon-ai/doompi-team 0.0.1-alpha.43 → 0.0.1-alpha.45

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.
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./src/adapters/teamCatalogApi.cjs");exports.TEAM_API_BASE_PATH=e.TEAM_API_BASE_PATH,exports.TEAM_CATALOG_ROUTE=e.TEAM_CATALOG_ROUTE,exports.api=e.api,exports.createTeamCatalogApi=e.createTeamCatalogApi;
@@ -0,0 +1,2 @@
1
+ import { TEAM_API_BASE_PATH, TEAM_CATALOG_ROUTE, TeamCatalogApiOptions, TeamCatalogSnapshot, api, createTeamCatalogApi } from "./src/adapters/teamCatalogApi.cjs";
2
+ export { TEAM_API_BASE_PATH, TEAM_CATALOG_ROUTE, type TeamCatalogApiOptions, type TeamCatalogSnapshot, api, createTeamCatalogApi };
@@ -0,0 +1,2 @@
1
+ import { TEAM_API_BASE_PATH, TEAM_CATALOG_ROUTE, TeamCatalogApiOptions, TeamCatalogSnapshot, api, createTeamCatalogApi } from "./src/adapters/teamCatalogApi.mjs";
2
+ export { TEAM_API_BASE_PATH, TEAM_CATALOG_ROUTE, type TeamCatalogApiOptions, type TeamCatalogSnapshot, api, createTeamCatalogApi };
@@ -0,0 +1 @@
1
+ import{TEAM_API_BASE_PATH as e,TEAM_CATALOG_ROUTE as t,api as n,createTeamCatalogApi as r}from"./src/adapters/teamCatalogApi.mjs";export{e as TEAM_API_BASE_PATH,t as TEAM_CATALOG_ROUTE,n as api,r as createTeamCatalogApi};
@@ -0,0 +1,2 @@
1
+ const e=require("./agents/discovery.cjs"),t=`team`;function n(t){let n=new e.AgentDiscoveryService,r=t.read??(t=>({agents:n.discover(t,`both`).agents,models:e.resolveActiveTeamModelSpecs()??[]}));return{fetch(e){let n=new URL(e.url);if(e.method!==`GET`||n.pathname!==`/catalog`)return Response.json({error:`Not found.`},{status:404});try{return Response.json(r(t.cwd))}catch(e){let t=e instanceof Error?e.message:String(e);return Response.json({error:t},{status:500})}},close:()=>void 0}}const r={basePath:t,start(e){return n({cwd:e.cwd??process.cwd()})}};exports.TEAM_API_BASE_PATH=t,exports.TEAM_CATALOG_ROUTE=`/catalog`,exports.api=r,exports.createTeamCatalogApi=n;
2
+ //# sourceMappingURL=teamCatalogApi.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teamCatalogApi.cjs","names":["AgentDiscoveryService","resolveActiveTeamModelSpecs"],"sources":["../../../src/adapters/teamCatalogApi.ts"],"sourcesContent":["import type { DoomApi, DoomApiHandler } from '@agimon-ai/doompi-extension-contracts/package-api';\nimport type { CatalogAgentInput } from '../services/webSubagentCatalog.ts';\nimport { AgentDiscoveryService, resolveActiveTeamModelSpecs } from './agents/discovery.ts';\n\nexport const TEAM_API_BASE_PATH = 'team';\nexport const TEAM_CATALOG_ROUTE = '/catalog';\n\nexport interface TeamCatalogSnapshot {\n agents: CatalogAgentInput[];\n models: string[];\n}\n\nexport interface TeamCatalogApiOptions {\n cwd: string;\n read?: (cwd: string) => TeamCatalogSnapshot;\n}\n\n/** Serves catalog discovery from the session process, which owns the active domain environment. */\nexport function createTeamCatalogApi(options: TeamCatalogApiOptions): DoomApiHandler {\n const discovery = new AgentDiscoveryService();\n const read =\n options.read ??\n ((cwd: string): TeamCatalogSnapshot => ({\n agents: discovery.discover(cwd, 'both').agents,\n models: resolveActiveTeamModelSpecs() ?? [],\n }));\n\n return {\n fetch(request) {\n const url = new URL(request.url);\n if (request.method !== 'GET' || url.pathname !== TEAM_CATALOG_ROUTE) {\n return Response.json({ error: 'Not found.' }, { status: 404 });\n }\n try {\n return Response.json(read(options.cwd));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return Response.json({ error: message }, { status: 500 });\n }\n },\n close: () => undefined,\n };\n}\n\n/** Session package API entry loaded by doompi-server. */\nexport const api: DoomApi = {\n basePath: TEAM_API_BASE_PATH,\n start(context) {\n return createTeamCatalogApi({ cwd: context.cwd ?? process.cwd() });\n },\n};\n"],"mappings":"0CAIa,EAAqB,OAclC,SAAgB,EAAqB,EAAgD,CACnF,IAAM,EAAY,IAAIA,EAAAA,sBAChB,EACJ,EAAQ,OACN,IAAsC,CACtC,OAAQ,EAAU,SAAS,EAAK,MAAM,CAAC,CAAC,OACxC,OAAQC,EAAAA,4BAA4B,GAAK,CAAC,CAC5C,IAEF,MAAO,CACL,MAAM,EAAS,CACb,IAAM,EAAM,IAAI,IAAI,EAAQ,GAAG,EAC/B,GAAI,EAAQ,SAAW,OAAS,EAAI,WAAA,WAClC,OAAO,SAAS,KAAK,CAAE,MAAO,YAAa,EAAG,CAAE,OAAQ,GAAI,CAAC,EAE/D,GAAI,CACF,OAAO,SAAS,KAAK,EAAK,EAAQ,GAAG,CAAC,CACxC,OAAS,EAAO,CACd,IAAM,EAAU,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EACrE,OAAO,SAAS,KAAK,CAAE,MAAO,CAAQ,EAAG,CAAE,OAAQ,GAAI,CAAC,CAC1D,CACF,EACA,UAAa,IAAA,EACf,CACF,CAGA,MAAa,EAAe,CAC1B,SAAU,EACV,MAAM,EAAS,CACb,OAAO,EAAqB,CAAE,IAAK,EAAQ,KAAO,QAAQ,IAAI,CAAE,CAAC,CACnE,CACF"}
@@ -0,0 +1,20 @@
1
+ import { CatalogAgentInput } from "../services/webSubagentCatalog.cjs";
2
+ import { DoomApi, DoomApiHandler } from "@agimon-ai/doompi-extension-contracts/package-api";
3
+ //#region src/adapters/teamCatalogApi.d.ts
4
+ declare const TEAM_API_BASE_PATH = "team";
5
+ declare const TEAM_CATALOG_ROUTE = "/catalog";
6
+ interface TeamCatalogSnapshot {
7
+ agents: CatalogAgentInput[];
8
+ models: string[];
9
+ }
10
+ interface TeamCatalogApiOptions {
11
+ cwd: string;
12
+ read?: (cwd: string) => TeamCatalogSnapshot;
13
+ }
14
+ /** Serves catalog discovery from the session process, which owns the active domain environment. */
15
+ declare function createTeamCatalogApi(options: TeamCatalogApiOptions): DoomApiHandler;
16
+ /** Session package API entry loaded by doompi-server. */
17
+ declare const api: DoomApi;
18
+ //#endregion
19
+ export { TEAM_API_BASE_PATH, TEAM_CATALOG_ROUTE, TeamCatalogApiOptions, TeamCatalogSnapshot, api, createTeamCatalogApi };
20
+ //# sourceMappingURL=teamCatalogApi.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teamCatalogApi.d.cts","names":[],"sources":["../../../src/adapters/teamCatalogApi.ts"],"mappings":";;;cAIa;cACA;UAEI;EACf,QAAQ;EACR;;UAGe;EACf;EACA,QAAQ,gBAAgB;;;iBAIV,qBAAqB,SAAS,wBAAwB;;cA2BzD,KAAK"}
@@ -0,0 +1,20 @@
1
+ import { CatalogAgentInput } from "../services/webSubagentCatalog.mjs";
2
+ import { DoomApi, DoomApiHandler } from "@agimon-ai/doompi-extension-contracts/package-api";
3
+ //#region src/adapters/teamCatalogApi.d.ts
4
+ declare const TEAM_API_BASE_PATH = "team";
5
+ declare const TEAM_CATALOG_ROUTE = "/catalog";
6
+ interface TeamCatalogSnapshot {
7
+ agents: CatalogAgentInput[];
8
+ models: string[];
9
+ }
10
+ interface TeamCatalogApiOptions {
11
+ cwd: string;
12
+ read?: (cwd: string) => TeamCatalogSnapshot;
13
+ }
14
+ /** Serves catalog discovery from the session process, which owns the active domain environment. */
15
+ declare function createTeamCatalogApi(options: TeamCatalogApiOptions): DoomApiHandler;
16
+ /** Session package API entry loaded by doompi-server. */
17
+ declare const api: DoomApi;
18
+ //#endregion
19
+ export { TEAM_API_BASE_PATH, TEAM_CATALOG_ROUTE, TeamCatalogApiOptions, TeamCatalogSnapshot, api, createTeamCatalogApi };
20
+ //# sourceMappingURL=teamCatalogApi.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teamCatalogApi.d.mts","names":[],"sources":["../../../src/adapters/teamCatalogApi.ts"],"mappings":";;;cAIa;cACA;UAEI;EACf,QAAQ;EACR;;UAGe;EACf;EACA,QAAQ,gBAAgB;;;iBAIV,qBAAqB,SAAS,wBAAwB;;cA2BzD,KAAK"}
@@ -0,0 +1,2 @@
1
+ import{AgentDiscoveryService as e,resolveActiveTeamModelSpecs as t}from"./agents/discovery.mjs";const n=`team`,r=`/catalog`;function i(n){let r=new e,i=n.read??(e=>({agents:r.discover(e,`both`).agents,models:t()??[]}));return{fetch(e){let t=new URL(e.url);if(e.method!==`GET`||t.pathname!==`/catalog`)return Response.json({error:`Not found.`},{status:404});try{return Response.json(i(n.cwd))}catch(e){let t=e instanceof Error?e.message:String(e);return Response.json({error:t},{status:500})}},close:()=>void 0}}const a={basePath:n,start(e){return i({cwd:e.cwd??process.cwd()})}};export{n as TEAM_API_BASE_PATH,r as TEAM_CATALOG_ROUTE,a as api,i as createTeamCatalogApi};
2
+ //# sourceMappingURL=teamCatalogApi.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teamCatalogApi.mjs","names":[],"sources":["../../../src/adapters/teamCatalogApi.ts"],"sourcesContent":["import type { DoomApi, DoomApiHandler } from '@agimon-ai/doompi-extension-contracts/package-api';\nimport type { CatalogAgentInput } from '../services/webSubagentCatalog.ts';\nimport { AgentDiscoveryService, resolveActiveTeamModelSpecs } from './agents/discovery.ts';\n\nexport const TEAM_API_BASE_PATH = 'team';\nexport const TEAM_CATALOG_ROUTE = '/catalog';\n\nexport interface TeamCatalogSnapshot {\n agents: CatalogAgentInput[];\n models: string[];\n}\n\nexport interface TeamCatalogApiOptions {\n cwd: string;\n read?: (cwd: string) => TeamCatalogSnapshot;\n}\n\n/** Serves catalog discovery from the session process, which owns the active domain environment. */\nexport function createTeamCatalogApi(options: TeamCatalogApiOptions): DoomApiHandler {\n const discovery = new AgentDiscoveryService();\n const read =\n options.read ??\n ((cwd: string): TeamCatalogSnapshot => ({\n agents: discovery.discover(cwd, 'both').agents,\n models: resolveActiveTeamModelSpecs() ?? [],\n }));\n\n return {\n fetch(request) {\n const url = new URL(request.url);\n if (request.method !== 'GET' || url.pathname !== TEAM_CATALOG_ROUTE) {\n return Response.json({ error: 'Not found.' }, { status: 404 });\n }\n try {\n return Response.json(read(options.cwd));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return Response.json({ error: message }, { status: 500 });\n }\n },\n close: () => undefined,\n };\n}\n\n/** Session package API entry loaded by doompi-server. */\nexport const api: DoomApi = {\n basePath: TEAM_API_BASE_PATH,\n start(context) {\n return createTeamCatalogApi({ cwd: context.cwd ?? process.cwd() });\n },\n};\n"],"mappings":"gGAIA,MAAa,EAAqB,OACrB,EAAqB,WAalC,SAAgB,EAAqB,EAAgD,CACnF,IAAM,EAAY,IAAI,EAChB,EACJ,EAAQ,OACN,IAAsC,CACtC,OAAQ,EAAU,SAAS,EAAK,MAAM,CAAC,CAAC,OACxC,OAAQ,EAA4B,GAAK,CAAC,CAC5C,IAEF,MAAO,CACL,MAAM,EAAS,CACb,IAAM,EAAM,IAAI,IAAI,EAAQ,GAAG,EAC/B,GAAI,EAAQ,SAAW,OAAS,EAAI,WAAA,WAClC,OAAO,SAAS,KAAK,CAAE,MAAO,YAAa,EAAG,CAAE,OAAQ,GAAI,CAAC,EAE/D,GAAI,CACF,OAAO,SAAS,KAAK,EAAK,EAAQ,GAAG,CAAC,CACxC,OAAS,EAAO,CACd,IAAM,EAAU,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EACrE,OAAO,SAAS,KAAK,CAAE,MAAO,CAAQ,EAAG,CAAE,OAAQ,GAAI,CAAC,CAC1D,CACF,EACA,UAAa,IAAA,EACf,CACF,CAGA,MAAa,EAAe,CAC1B,SAAU,EACV,MAAM,EAAS,CACb,OAAO,EAAqB,CAAE,IAAK,EAAQ,KAAO,QAAQ,IAAI,CAAE,CAAC,CACnE,CACF"}
@@ -1,2 +1,2 @@
1
- const e=require("./agents/discovery.cjs"),t=require("../services/webSubagentCatalog.cjs"),n=require("../types/webSubagents.cjs");function r(){let t=new e.AgentDiscoveryService;return n=>({agents:t.discover(n,`both`).agents,models:e.resolveActiveTeamModelSpecs()??[]})}function i(e=r(),i=15e3){return{frameType:n.SUBAGENT_CATALOG_TYPE,start(n){let r=new Map,a=new Map,o=n=>{try{let{agents:r,models:i}=e(n.cwd);return{cwd:n.cwd,agents:t.presentCatalog(r),models:t.catalogModels(r,i)}}catch(e){let t=e instanceof Error?e.message:String(e);return{cwd:n.cwd,agents:[],models:[],warning:t}}},s=(e,t)=>{let r=o(e),i=JSON.stringify(r);return a.get(e.sessionId)===i?r:(a.set(e.sessionId,i),r.warning!==void 0&&n.onNotice(`subagent catalog for ${e.cwd} is unavailable (${r.warning})`),t&&n.publish(e.sessionId,r),r)},c=setInterval(()=>{for(let e of r.values())s(e,!0)},i);return{payloadFor(e){return r.has(e.sessionId)?s(e,!1):void 0},sessionAdded(e){r.set(e.sessionId,e),s(e,!0)},sessionRemoved(e){r.delete(e),a.delete(e)},close(){clearInterval(c),r.clear(),a.clear()}}}}}exports.createSubagentCatalogChannel=i,exports.defaultCatalogReader=r;
1
+ const e=require("./agents/discovery.cjs"),t=require("./teamCatalogApi.cjs"),n=require("../services/webSubagentCatalog.cjs"),r=require("../types/webSubagents.cjs");function i(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function a(e){if(!i(e)||!Array.isArray(e.agents)||!e.agents.every(i)||!Array.isArray(e.models))throw Error(`Session catalog API returned an invalid payload.`);return{agents:e.agents,models:e.models.filter(e=>typeof e==`string`)}}async function o(e,n,r,o){let s=await e.requestSessionApi(n,{basePath:t.TEAM_API_BASE_PATH,path:t.TEAM_CATALOG_ROUTE,method:`GET`,signal:r});if(s.status===404)return await o(n.cwd);let c=await s.json();if(!s.ok){let e=i(c)&&typeof c.error==`string`?c.error:`HTTP ${String(s.status)}`;throw Error(`Session catalog API failed: ${e}`)}return a(c)}function s(t,i=15e3){return{frameType:r.SUBAGENT_CATALOG_TYPE,start(r){let a=new Map,s=new Map,c=new Map,l=new Map,u=new AbortController,d=new e.AgentDiscoveryService,f=t=>({agents:d.discover(t,`both`).agents,models:e.resolveActiveTeamModelSpecs()??[]}),p=!1,m=(e,t,n)=>{let i=a.get(e.sessionId);if(p||i?.cwd!==e.cwd||l.get(e.sessionId)!==t)return;let o=JSON.stringify(n);s.get(e.sessionId)!==o&&(s.set(e.sessionId,o),c.set(e.sessionId,n),n.warning!==void 0&&r.onNotice(`subagent catalog for ${e.cwd} is unavailable (${n.warning})`),r.publish(e.sessionId,n))},h=async e=>{let i=(l.get(e.sessionId)??0)+1;l.set(e.sessionId,i);try{let a=await(t===void 0?o(r,e,u.signal,f):t(e.cwd));m(e,i,{cwd:e.cwd,agents:n.presentCatalog(a.agents),models:n.catalogModels(a.agents,a.models)})}catch(t){if(p||u.signal.aborted)return;let n=t instanceof Error?t.message:String(t);m(e,i,{cwd:e.cwd,agents:[],models:[],warning:n})}},g=setInterval(()=>{for(let e of a.values())h(e)},i);return{payloadFor(e){if(a.has(e.sessionId))return h(e),c.get(e.sessionId)},sessionAdded(e){a.set(e.sessionId,e),h(e)},sessionRemoved(e){a.delete(e),s.delete(e),c.delete(e),l.delete(e)},close(){p=!0,u.abort(),clearInterval(g),a.clear(),s.clear(),c.clear(),l.clear()}}}}}exports.createSubagentCatalogChannel=s;
2
2
  //# sourceMappingURL=webSubagentCatalogChannel.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"webSubagentCatalogChannel.cjs","names":["AgentDiscoveryService","resolveActiveTeamModelSpecs","SUBAGENT_CATALOG_TYPE","presentCatalog","catalogModels"],"sources":["../../../src/adapters/webSubagentCatalogChannel.ts"],"sourcesContent":["import type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { type CatalogAgentInput, catalogModels, presentCatalog } from '../services/webSubagentCatalog.ts';\nimport { SUBAGENT_CATALOG_TYPE, type SubagentCatalogPayload } from '../types/webSubagents.ts';\nimport { AgentDiscoveryService, resolveActiveTeamModelSpecs } from './agents/discovery.ts';\n\n/** Agent files change rarely; discovery's own cache makes each look cheap. */\nconst CATALOG_REFRESH_MS = 15_000;\n\n/** Reads the launchable agents for one directory; injectable so a test needs no agent files on disk. */\nexport type CatalogReader = (cwd: string) => { agents: CatalogAgentInput[]; models: string[] };\n\nexport function defaultCatalogReader(): CatalogReader {\n const discovery = new AgentDiscoveryService();\n return (cwd) => ({\n agents: discovery.discover(cwd, 'both').agents,\n models: resolveActiveTeamModelSpecs() ?? [],\n });\n}\n\n/**\n * The subagent catalog data channel: what each managed session's directory\n * can launch, published as 'subagent_catalog' payloads. Discovery runs once\n * per session on arrival, again at every subscribe, and on a slow tick so an\n * agent file written while the page is open shows up without a reload. A\n * failed read publishes an empty list with the reason rather than nothing.\n */\nexport function createSubagentCatalogChannel(\n read: CatalogReader = defaultCatalogReader(),\n refreshMs = CATALOG_REFRESH_MS,\n): WebHubChannel {\n return {\n frameType: SUBAGENT_CATALOG_TYPE,\n start(host) {\n const scopes = new Map<string, HubSessionScope>();\n const latest = new Map<string, string>();\n\n const compute = (scope: HubSessionScope): SubagentCatalogPayload => {\n try {\n const { agents, models } = read(scope.cwd);\n return { cwd: scope.cwd, agents: presentCatalog(agents), models: catalogModels(agents, models) };\n } catch (error) {\n const warning = error instanceof Error ? error.message : String(error);\n return { cwd: scope.cwd, agents: [], models: [], warning };\n }\n };\n\n /** Recomputes and remembers; publishes only what changed, so the tick stays quiet. */\n const refresh = (scope: HubSessionScope, publish: boolean): SubagentCatalogPayload => {\n const payload = compute(scope);\n const json = JSON.stringify(payload);\n if (latest.get(scope.sessionId) === json) return payload;\n latest.set(scope.sessionId, json);\n if (payload.warning !== undefined) {\n host.onNotice(`subagent catalog for ${scope.cwd} is unavailable (${payload.warning})`);\n }\n if (publish) host.publish(scope.sessionId, payload);\n return payload;\n };\n\n const timer = setInterval(() => {\n for (const scope of scopes.values()) refresh(scope, true);\n }, refreshMs);\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n // The snapshot travels on its own, so a fresh read is remembered but not also published.\n return scopes.has(scope.sessionId) ? refresh(scope, false) : undefined;\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n refresh(scope, true);\n },\n sessionRemoved(sessionId) {\n scopes.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n clearInterval(timer);\n scopes.clear();\n latest.clear();\n },\n };\n return source;\n },\n };\n}\n"],"mappings":"iIAWA,SAAgB,GAAsC,CACpD,IAAM,EAAY,IAAIA,EAAAA,sBACtB,MAAQ,KAAS,CACf,OAAQ,EAAU,SAAS,EAAK,MAAM,CAAC,CAAC,OACxC,OAAQC,EAAAA,4BAA4B,GAAK,CAAC,CAC5C,EACF,CASA,SAAgB,EACd,EAAsB,EAAqB,EAC3C,EAAY,KACG,CACf,MAAO,CACL,UAAWC,EAAAA,sBACX,MAAM,EAAM,CACV,IAAM,EAAS,IAAI,IACb,EAAS,IAAI,IAEb,EAAW,GAAmD,CAClE,GAAI,CACF,GAAM,CAAE,SAAQ,UAAW,EAAK,EAAM,GAAG,EACzC,MAAO,CAAE,IAAK,EAAM,IAAK,OAAQC,EAAAA,eAAe,CAAM,EAAG,OAAQC,EAAAA,cAAc,EAAQ,CAAM,CAAE,CACjG,OAAS,EAAO,CACd,IAAM,EAAU,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EACrE,MAAO,CAAE,IAAK,EAAM,IAAK,OAAQ,CAAC,EAAG,OAAQ,CAAC,EAAG,SAAQ,CAC3D,CACF,EAGM,GAAW,EAAwB,IAA6C,CACpF,IAAM,EAAU,EAAQ,CAAK,EACvB,EAAO,KAAK,UAAU,CAAO,EAOnC,OANI,EAAO,IAAI,EAAM,SAAS,IAAM,EAAa,GACjD,EAAO,IAAI,EAAM,UAAW,CAAI,EAC5B,EAAQ,UAAY,IAAA,IACtB,EAAK,SAAS,wBAAwB,EAAM,IAAI,mBAAmB,EAAQ,QAAQ,EAAE,EAEnF,GAAS,EAAK,QAAQ,EAAM,UAAW,CAAO,EAC3C,EACT,EAEM,EAAQ,gBAAkB,CAC9B,IAAK,IAAM,KAAS,EAAO,OAAO,EAAG,EAAQ,EAAO,EAAI,CAC1D,EAAG,CAAS,EAqBZ,MAAO,CAlBL,WAAW,EAAO,CAEhB,OAAO,EAAO,IAAI,EAAM,SAAS,EAAI,EAAQ,EAAO,EAAK,EAAI,IAAA,EAC/D,EACA,aAAa,EAAO,CAClB,EAAO,IAAI,EAAM,UAAW,CAAK,EACjC,EAAQ,EAAO,EAAI,CACrB,EACA,eAAe,EAAW,CACxB,EAAO,OAAO,CAAS,EACvB,EAAO,OAAO,CAAS,CACzB,EACA,OAAQ,CACN,cAAc,CAAK,EACnB,EAAO,MAAM,EACb,EAAO,MAAM,CACf,CAEU,CACd,CACF,CACF"}
1
+ {"version":3,"file":"webSubagentCatalogChannel.cjs","names":["TEAM_API_BASE_PATH","TEAM_CATALOG_ROUTE","SUBAGENT_CATALOG_TYPE","AgentDiscoveryService","resolveActiveTeamModelSpecs","presentCatalog","catalogModels"],"sources":["../../../src/adapters/webSubagentCatalogChannel.ts"],"sourcesContent":["import type { HubChannelHost, HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { type CatalogAgentInput, catalogModels, presentCatalog } from '../services/webSubagentCatalog.ts';\nimport { SUBAGENT_CATALOG_TYPE, type SubagentCatalogPayload } from '../types/webSubagents.ts';\nimport { AgentDiscoveryService, resolveActiveTeamModelSpecs } from './agents/discovery.ts';\nimport { TEAM_API_BASE_PATH, TEAM_CATALOG_ROUTE, type TeamCatalogSnapshot } from './teamCatalogApi.ts';\n\n/** Agent files change rarely; discovery's own cache makes each look cheap. */\nconst CATALOG_REFRESH_MS = 15_000;\n\n/** Injectable catalog read for tests and alternate hosts. Production reads through the session API. */\nexport type CatalogReader = (cwd: string) => TeamCatalogSnapshot | Promise<TeamCatalogSnapshot>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction parseCatalogSnapshot(value: unknown): TeamCatalogSnapshot {\n if (\n !isRecord(value) ||\n !Array.isArray(value.agents) ||\n !value.agents.every(isRecord) ||\n !Array.isArray(value.models)\n ) {\n throw new Error('Session catalog API returned an invalid payload.');\n }\n return {\n agents: value.agents as unknown as CatalogAgentInput[],\n models: value.models.filter((model): model is string => typeof model === 'string'),\n };\n}\n\nasync function readSessionCatalog(\n host: HubChannelHost,\n scope: HubSessionScope,\n signal: AbortSignal,\n fallback: CatalogReader,\n): Promise<TeamCatalogSnapshot> {\n const response = await host.requestSessionApi(scope, {\n basePath: TEAM_API_BASE_PATH,\n path: TEAM_CATALOG_ROUTE,\n method: 'GET',\n signal,\n });\n if (response.status === 404) return await fallback(scope.cwd);\n const payload: unknown = await response.json();\n if (!response.ok) {\n const reason =\n isRecord(payload) && typeof payload.error === 'string' ? payload.error : `HTTP ${String(response.status)}`;\n throw new Error(`Session catalog API failed: ${reason}`);\n }\n return parseCatalogSnapshot(payload);\n}\n\n/**\n * The subagent catalog data channel: what each managed session can launch.\n *\n * Discovery runs inside the session API because that process owns the active\n * domain projection and its plugin agent directories. The hub refreshes and\n * publishes that result, with local discovery only for older sessions lacking\n * the API route.\n */\nexport function createSubagentCatalogChannel(read?: CatalogReader, refreshMs = CATALOG_REFRESH_MS): WebHubChannel {\n return {\n frameType: SUBAGENT_CATALOG_TYPE,\n start(host) {\n const scopes = new Map<string, HubSessionScope>();\n const latestJson = new Map<string, string>();\n const latestPayload = new Map<string, SubagentCatalogPayload>();\n const generations = new Map<string, number>();\n const abort = new AbortController();\n const fallbackDiscovery = new AgentDiscoveryService();\n const fallback: CatalogReader = (cwd) => ({\n agents: fallbackDiscovery.discover(cwd, 'both').agents,\n models: resolveActiveTeamModelSpecs() ?? [],\n });\n let closed = false;\n\n const commit = (scope: HubSessionScope, generation: number, payload: SubagentCatalogPayload): void => {\n const active = scopes.get(scope.sessionId);\n if (closed || active?.cwd !== scope.cwd || generations.get(scope.sessionId) !== generation) return;\n const json = JSON.stringify(payload);\n if (latestJson.get(scope.sessionId) === json) return;\n latestJson.set(scope.sessionId, json);\n latestPayload.set(scope.sessionId, payload);\n if (payload.warning !== undefined) {\n host.onNotice(`subagent catalog for ${scope.cwd} is unavailable (${payload.warning})`);\n }\n host.publish(scope.sessionId, payload);\n };\n\n const refresh = async (scope: HubSessionScope): Promise<void> => {\n const generation = (generations.get(scope.sessionId) ?? 0) + 1;\n generations.set(scope.sessionId, generation);\n try {\n const snapshot = await (read === undefined\n ? readSessionCatalog(host, scope, abort.signal, fallback)\n : read(scope.cwd));\n commit(scope, generation, {\n cwd: scope.cwd,\n agents: presentCatalog(snapshot.agents),\n models: catalogModels(snapshot.agents, snapshot.models),\n });\n } catch (error) {\n if (closed || abort.signal.aborted) return;\n const warning = error instanceof Error ? error.message : String(error);\n commit(scope, generation, { cwd: scope.cwd, agents: [], models: [], warning });\n }\n };\n\n const timer = setInterval(() => {\n for (const scope of scopes.values()) void refresh(scope);\n }, refreshMs);\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n if (!scopes.has(scope.sessionId)) return undefined;\n void refresh(scope);\n return latestPayload.get(scope.sessionId);\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n void refresh(scope);\n },\n sessionRemoved(sessionId) {\n scopes.delete(sessionId);\n latestJson.delete(sessionId);\n latestPayload.delete(sessionId);\n generations.delete(sessionId);\n },\n close() {\n closed = true;\n abort.abort();\n clearInterval(timer);\n scopes.clear();\n latestJson.clear();\n latestPayload.clear();\n generations.clear();\n },\n };\n return source;\n },\n };\n}\n"],"mappings":"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"}
@@ -1,17 +1,15 @@
1
- import { CatalogAgentInput } from "../services/webSubagentCatalog.cjs";
1
+ import { TeamCatalogSnapshot } from "./teamCatalogApi.cjs";
2
2
  import { WebHubChannel } from "@agimon-ai/doompi-web-contracts";
3
3
  //#region src/adapters/webSubagentCatalogChannel.d.ts
4
- /** Reads the launchable agents for one directory; injectable so a test needs no agent files on disk. */
5
- type CatalogReader = (cwd: string) => {
6
- agents: CatalogAgentInput[];
7
- models: string[];
8
- };
4
+ /** Injectable catalog read for tests and alternate hosts. Production reads through the session API. */
5
+ type CatalogReader = (cwd: string) => TeamCatalogSnapshot | Promise<TeamCatalogSnapshot>;
9
6
  /**
10
- * The subagent catalog data channel: what each managed session's directory
11
- * can launch, published as 'subagent_catalog' payloads. Discovery runs once
12
- * per session on arrival, again at every subscribe, and on a slow tick so an
13
- * agent file written while the page is open shows up without a reload. A
14
- * failed read publishes an empty list with the reason rather than nothing.
7
+ * The subagent catalog data channel: what each managed session can launch.
8
+ *
9
+ * Discovery runs inside the session API because that process owns the active
10
+ * domain projection and its plugin agent directories. The hub refreshes and
11
+ * publishes that result, with local discovery only for older sessions lacking
12
+ * the API route.
15
13
  */
16
14
  declare function createSubagentCatalogChannel(read?: CatalogReader, refreshMs?: number): WebHubChannel;
17
15
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"webSubagentCatalogChannel.d.cts","names":[],"sources":["../../../src/adapters/webSubagentCatalogChannel.ts"],"mappings":";;;;KASY,iBAAiB;EAAkB,QAAQ;EAAqB;;;;;;;;;iBAiB5D,6BACd,OAAM,eACN,qBACC"}
1
+ {"version":3,"file":"webSubagentCatalogChannel.d.cts","names":[],"sources":["../../../src/adapters/webSubagentCatalogChannel.ts"],"mappings":";;;;KAUY,iBAAiB,gBAAgB,sBAAsB,QAAQ;;;;;;;;;iBAmD3D,6BAA6B,OAAO,eAAe,qBAAiC"}
@@ -1,17 +1,15 @@
1
- import { CatalogAgentInput } from "../services/webSubagentCatalog.mjs";
1
+ import { TeamCatalogSnapshot } from "./teamCatalogApi.mjs";
2
2
  import { WebHubChannel } from "@agimon-ai/doompi-web-contracts";
3
3
  //#region src/adapters/webSubagentCatalogChannel.d.ts
4
- /** Reads the launchable agents for one directory; injectable so a test needs no agent files on disk. */
5
- type CatalogReader = (cwd: string) => {
6
- agents: CatalogAgentInput[];
7
- models: string[];
8
- };
4
+ /** Injectable catalog read for tests and alternate hosts. Production reads through the session API. */
5
+ type CatalogReader = (cwd: string) => TeamCatalogSnapshot | Promise<TeamCatalogSnapshot>;
9
6
  /**
10
- * The subagent catalog data channel: what each managed session's directory
11
- * can launch, published as 'subagent_catalog' payloads. Discovery runs once
12
- * per session on arrival, again at every subscribe, and on a slow tick so an
13
- * agent file written while the page is open shows up without a reload. A
14
- * failed read publishes an empty list with the reason rather than nothing.
7
+ * The subagent catalog data channel: what each managed session can launch.
8
+ *
9
+ * Discovery runs inside the session API because that process owns the active
10
+ * domain projection and its plugin agent directories. The hub refreshes and
11
+ * publishes that result, with local discovery only for older sessions lacking
12
+ * the API route.
15
13
  */
16
14
  declare function createSubagentCatalogChannel(read?: CatalogReader, refreshMs?: number): WebHubChannel;
17
15
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"webSubagentCatalogChannel.d.mts","names":[],"sources":["../../../src/adapters/webSubagentCatalogChannel.ts"],"mappings":";;;;KASY,iBAAiB;EAAkB,QAAQ;EAAqB;;;;;;;;;iBAiB5D,6BACd,OAAM,eACN,qBACC"}
1
+ {"version":3,"file":"webSubagentCatalogChannel.d.mts","names":[],"sources":["../../../src/adapters/webSubagentCatalogChannel.ts"],"mappings":";;;;KAUY,iBAAiB,gBAAgB,sBAAsB,QAAQ;;;;;;;;;iBAmD3D,6BAA6B,OAAO,eAAe,qBAAiC"}
@@ -1,2 +1,2 @@
1
- import{AgentDiscoveryService as e,resolveActiveTeamModelSpecs as t}from"./agents/discovery.mjs";import{catalogModels as n,presentCatalog as r}from"../services/webSubagentCatalog.mjs";import{SUBAGENT_CATALOG_TYPE as i}from"../types/webSubagents.mjs";function a(){let n=new e;return e=>({agents:n.discover(e,`both`).agents,models:t()??[]})}function o(e=a(),t=15e3){return{frameType:i,start(i){let a=new Map,o=new Map,s=t=>{try{let{agents:i,models:a}=e(t.cwd);return{cwd:t.cwd,agents:r(i),models:n(i,a)}}catch(e){let n=e instanceof Error?e.message:String(e);return{cwd:t.cwd,agents:[],models:[],warning:n}}},c=(e,t)=>{let n=s(e),r=JSON.stringify(n);return o.get(e.sessionId)===r?n:(o.set(e.sessionId,r),n.warning!==void 0&&i.onNotice(`subagent catalog for ${e.cwd} is unavailable (${n.warning})`),t&&i.publish(e.sessionId,n),n)},l=setInterval(()=>{for(let e of a.values())c(e,!0)},t);return{payloadFor(e){return a.has(e.sessionId)?c(e,!1):void 0},sessionAdded(e){a.set(e.sessionId,e),c(e,!0)},sessionRemoved(e){a.delete(e),o.delete(e)},close(){clearInterval(l),a.clear(),o.clear()}}}}}export{o as createSubagentCatalogChannel,a as defaultCatalogReader};
1
+ import{AgentDiscoveryService as e,resolveActiveTeamModelSpecs as t}from"./agents/discovery.mjs";import{TEAM_API_BASE_PATH as n,TEAM_CATALOG_ROUTE as r}from"./teamCatalogApi.mjs";import{catalogModels as i,presentCatalog as a}from"../services/webSubagentCatalog.mjs";import{SUBAGENT_CATALOG_TYPE as o}from"../types/webSubagents.mjs";function s(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function c(e){if(!s(e)||!Array.isArray(e.agents)||!e.agents.every(s)||!Array.isArray(e.models))throw Error(`Session catalog API returned an invalid payload.`);return{agents:e.agents,models:e.models.filter(e=>typeof e==`string`)}}async function l(e,t,i,a){let o=await e.requestSessionApi(t,{basePath:n,path:r,method:`GET`,signal:i});if(o.status===404)return await a(t.cwd);let l=await o.json();if(!o.ok){let e=s(l)&&typeof l.error==`string`?l.error:`HTTP ${String(o.status)}`;throw Error(`Session catalog API failed: ${e}`)}return c(l)}function u(n,r=15e3){return{frameType:o,start(o){let s=new Map,c=new Map,u=new Map,d=new Map,f=new AbortController,p=new e,m=e=>({agents:p.discover(e,`both`).agents,models:t()??[]}),h=!1,g=(e,t,n)=>{let r=s.get(e.sessionId);if(h||r?.cwd!==e.cwd||d.get(e.sessionId)!==t)return;let i=JSON.stringify(n);c.get(e.sessionId)!==i&&(c.set(e.sessionId,i),u.set(e.sessionId,n),n.warning!==void 0&&o.onNotice(`subagent catalog for ${e.cwd} is unavailable (${n.warning})`),o.publish(e.sessionId,n))},_=async e=>{let t=(d.get(e.sessionId)??0)+1;d.set(e.sessionId,t);try{let r=await(n===void 0?l(o,e,f.signal,m):n(e.cwd));g(e,t,{cwd:e.cwd,agents:a(r.agents),models:i(r.agents,r.models)})}catch(n){if(h||f.signal.aborted)return;let r=n instanceof Error?n.message:String(n);g(e,t,{cwd:e.cwd,agents:[],models:[],warning:r})}},v=setInterval(()=>{for(let e of s.values())_(e)},r);return{payloadFor(e){if(s.has(e.sessionId))return _(e),u.get(e.sessionId)},sessionAdded(e){s.set(e.sessionId,e),_(e)},sessionRemoved(e){s.delete(e),c.delete(e),u.delete(e),d.delete(e)},close(){h=!0,f.abort(),clearInterval(v),s.clear(),c.clear(),u.clear(),d.clear()}}}}}export{u as createSubagentCatalogChannel};
2
2
  //# sourceMappingURL=webSubagentCatalogChannel.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"webSubagentCatalogChannel.mjs","names":[],"sources":["../../../src/adapters/webSubagentCatalogChannel.ts"],"sourcesContent":["import type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { type CatalogAgentInput, catalogModels, presentCatalog } from '../services/webSubagentCatalog.ts';\nimport { SUBAGENT_CATALOG_TYPE, type SubagentCatalogPayload } from '../types/webSubagents.ts';\nimport { AgentDiscoveryService, resolveActiveTeamModelSpecs } from './agents/discovery.ts';\n\n/** Agent files change rarely; discovery's own cache makes each look cheap. */\nconst CATALOG_REFRESH_MS = 15_000;\n\n/** Reads the launchable agents for one directory; injectable so a test needs no agent files on disk. */\nexport type CatalogReader = (cwd: string) => { agents: CatalogAgentInput[]; models: string[] };\n\nexport function defaultCatalogReader(): CatalogReader {\n const discovery = new AgentDiscoveryService();\n return (cwd) => ({\n agents: discovery.discover(cwd, 'both').agents,\n models: resolveActiveTeamModelSpecs() ?? [],\n });\n}\n\n/**\n * The subagent catalog data channel: what each managed session's directory\n * can launch, published as 'subagent_catalog' payloads. Discovery runs once\n * per session on arrival, again at every subscribe, and on a slow tick so an\n * agent file written while the page is open shows up without a reload. A\n * failed read publishes an empty list with the reason rather than nothing.\n */\nexport function createSubagentCatalogChannel(\n read: CatalogReader = defaultCatalogReader(),\n refreshMs = CATALOG_REFRESH_MS,\n): WebHubChannel {\n return {\n frameType: SUBAGENT_CATALOG_TYPE,\n start(host) {\n const scopes = new Map<string, HubSessionScope>();\n const latest = new Map<string, string>();\n\n const compute = (scope: HubSessionScope): SubagentCatalogPayload => {\n try {\n const { agents, models } = read(scope.cwd);\n return { cwd: scope.cwd, agents: presentCatalog(agents), models: catalogModels(agents, models) };\n } catch (error) {\n const warning = error instanceof Error ? error.message : String(error);\n return { cwd: scope.cwd, agents: [], models: [], warning };\n }\n };\n\n /** Recomputes and remembers; publishes only what changed, so the tick stays quiet. */\n const refresh = (scope: HubSessionScope, publish: boolean): SubagentCatalogPayload => {\n const payload = compute(scope);\n const json = JSON.stringify(payload);\n if (latest.get(scope.sessionId) === json) return payload;\n latest.set(scope.sessionId, json);\n if (payload.warning !== undefined) {\n host.onNotice(`subagent catalog for ${scope.cwd} is unavailable (${payload.warning})`);\n }\n if (publish) host.publish(scope.sessionId, payload);\n return payload;\n };\n\n const timer = setInterval(() => {\n for (const scope of scopes.values()) refresh(scope, true);\n }, refreshMs);\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n // The snapshot travels on its own, so a fresh read is remembered but not also published.\n return scopes.has(scope.sessionId) ? refresh(scope, false) : undefined;\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n refresh(scope, true);\n },\n sessionRemoved(sessionId) {\n scopes.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n clearInterval(timer);\n scopes.clear();\n latest.clear();\n },\n };\n return source;\n },\n };\n}\n"],"mappings":"yPAWA,SAAgB,GAAsC,CACpD,IAAM,EAAY,IAAI,EACtB,MAAQ,KAAS,CACf,OAAQ,EAAU,SAAS,EAAK,MAAM,CAAC,CAAC,OACxC,OAAQ,EAA4B,GAAK,CAAC,CAC5C,EACF,CASA,SAAgB,EACd,EAAsB,EAAqB,EAC3C,EAAY,KACG,CACf,MAAO,CACL,UAAW,EACX,MAAM,EAAM,CACV,IAAM,EAAS,IAAI,IACb,EAAS,IAAI,IAEb,EAAW,GAAmD,CAClE,GAAI,CACF,GAAM,CAAE,SAAQ,UAAW,EAAK,EAAM,GAAG,EACzC,MAAO,CAAE,IAAK,EAAM,IAAK,OAAQ,EAAe,CAAM,EAAG,OAAQ,EAAc,EAAQ,CAAM,CAAE,CACjG,OAAS,EAAO,CACd,IAAM,EAAU,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EACrE,MAAO,CAAE,IAAK,EAAM,IAAK,OAAQ,CAAC,EAAG,OAAQ,CAAC,EAAG,SAAQ,CAC3D,CACF,EAGM,GAAW,EAAwB,IAA6C,CACpF,IAAM,EAAU,EAAQ,CAAK,EACvB,EAAO,KAAK,UAAU,CAAO,EAOnC,OANI,EAAO,IAAI,EAAM,SAAS,IAAM,EAAa,GACjD,EAAO,IAAI,EAAM,UAAW,CAAI,EAC5B,EAAQ,UAAY,IAAA,IACtB,EAAK,SAAS,wBAAwB,EAAM,IAAI,mBAAmB,EAAQ,QAAQ,EAAE,EAEnF,GAAS,EAAK,QAAQ,EAAM,UAAW,CAAO,EAC3C,EACT,EAEM,EAAQ,gBAAkB,CAC9B,IAAK,IAAM,KAAS,EAAO,OAAO,EAAG,EAAQ,EAAO,EAAI,CAC1D,EAAG,CAAS,EAqBZ,MAAO,CAlBL,WAAW,EAAO,CAEhB,OAAO,EAAO,IAAI,EAAM,SAAS,EAAI,EAAQ,EAAO,EAAK,EAAI,IAAA,EAC/D,EACA,aAAa,EAAO,CAClB,EAAO,IAAI,EAAM,UAAW,CAAK,EACjC,EAAQ,EAAO,EAAI,CACrB,EACA,eAAe,EAAW,CACxB,EAAO,OAAO,CAAS,EACvB,EAAO,OAAO,CAAS,CACzB,EACA,OAAQ,CACN,cAAc,CAAK,EACnB,EAAO,MAAM,EACb,EAAO,MAAM,CACf,CAEU,CACd,CACF,CACF"}
1
+ {"version":3,"file":"webSubagentCatalogChannel.mjs","names":[],"sources":["../../../src/adapters/webSubagentCatalogChannel.ts"],"sourcesContent":["import type { HubChannelHost, HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { type CatalogAgentInput, catalogModels, presentCatalog } from '../services/webSubagentCatalog.ts';\nimport { SUBAGENT_CATALOG_TYPE, type SubagentCatalogPayload } from '../types/webSubagents.ts';\nimport { AgentDiscoveryService, resolveActiveTeamModelSpecs } from './agents/discovery.ts';\nimport { TEAM_API_BASE_PATH, TEAM_CATALOG_ROUTE, type TeamCatalogSnapshot } from './teamCatalogApi.ts';\n\n/** Agent files change rarely; discovery's own cache makes each look cheap. */\nconst CATALOG_REFRESH_MS = 15_000;\n\n/** Injectable catalog read for tests and alternate hosts. Production reads through the session API. */\nexport type CatalogReader = (cwd: string) => TeamCatalogSnapshot | Promise<TeamCatalogSnapshot>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction parseCatalogSnapshot(value: unknown): TeamCatalogSnapshot {\n if (\n !isRecord(value) ||\n !Array.isArray(value.agents) ||\n !value.agents.every(isRecord) ||\n !Array.isArray(value.models)\n ) {\n throw new Error('Session catalog API returned an invalid payload.');\n }\n return {\n agents: value.agents as unknown as CatalogAgentInput[],\n models: value.models.filter((model): model is string => typeof model === 'string'),\n };\n}\n\nasync function readSessionCatalog(\n host: HubChannelHost,\n scope: HubSessionScope,\n signal: AbortSignal,\n fallback: CatalogReader,\n): Promise<TeamCatalogSnapshot> {\n const response = await host.requestSessionApi(scope, {\n basePath: TEAM_API_BASE_PATH,\n path: TEAM_CATALOG_ROUTE,\n method: 'GET',\n signal,\n });\n if (response.status === 404) return await fallback(scope.cwd);\n const payload: unknown = await response.json();\n if (!response.ok) {\n const reason =\n isRecord(payload) && typeof payload.error === 'string' ? payload.error : `HTTP ${String(response.status)}`;\n throw new Error(`Session catalog API failed: ${reason}`);\n }\n return parseCatalogSnapshot(payload);\n}\n\n/**\n * The subagent catalog data channel: what each managed session can launch.\n *\n * Discovery runs inside the session API because that process owns the active\n * domain projection and its plugin agent directories. The hub refreshes and\n * publishes that result, with local discovery only for older sessions lacking\n * the API route.\n */\nexport function createSubagentCatalogChannel(read?: CatalogReader, refreshMs = CATALOG_REFRESH_MS): WebHubChannel {\n return {\n frameType: SUBAGENT_CATALOG_TYPE,\n start(host) {\n const scopes = new Map<string, HubSessionScope>();\n const latestJson = new Map<string, string>();\n const latestPayload = new Map<string, SubagentCatalogPayload>();\n const generations = new Map<string, number>();\n const abort = new AbortController();\n const fallbackDiscovery = new AgentDiscoveryService();\n const fallback: CatalogReader = (cwd) => ({\n agents: fallbackDiscovery.discover(cwd, 'both').agents,\n models: resolveActiveTeamModelSpecs() ?? [],\n });\n let closed = false;\n\n const commit = (scope: HubSessionScope, generation: number, payload: SubagentCatalogPayload): void => {\n const active = scopes.get(scope.sessionId);\n if (closed || active?.cwd !== scope.cwd || generations.get(scope.sessionId) !== generation) return;\n const json = JSON.stringify(payload);\n if (latestJson.get(scope.sessionId) === json) return;\n latestJson.set(scope.sessionId, json);\n latestPayload.set(scope.sessionId, payload);\n if (payload.warning !== undefined) {\n host.onNotice(`subagent catalog for ${scope.cwd} is unavailable (${payload.warning})`);\n }\n host.publish(scope.sessionId, payload);\n };\n\n const refresh = async (scope: HubSessionScope): Promise<void> => {\n const generation = (generations.get(scope.sessionId) ?? 0) + 1;\n generations.set(scope.sessionId, generation);\n try {\n const snapshot = await (read === undefined\n ? readSessionCatalog(host, scope, abort.signal, fallback)\n : read(scope.cwd));\n commit(scope, generation, {\n cwd: scope.cwd,\n agents: presentCatalog(snapshot.agents),\n models: catalogModels(snapshot.agents, snapshot.models),\n });\n } catch (error) {\n if (closed || abort.signal.aborted) return;\n const warning = error instanceof Error ? error.message : String(error);\n commit(scope, generation, { cwd: scope.cwd, agents: [], models: [], warning });\n }\n };\n\n const timer = setInterval(() => {\n for (const scope of scopes.values()) void refresh(scope);\n }, refreshMs);\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n if (!scopes.has(scope.sessionId)) return undefined;\n void refresh(scope);\n return latestPayload.get(scope.sessionId);\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n void refresh(scope);\n },\n sessionRemoved(sessionId) {\n scopes.delete(sessionId);\n latestJson.delete(sessionId);\n latestPayload.delete(sessionId);\n generations.delete(sessionId);\n },\n close() {\n closed = true;\n abort.abort();\n clearInterval(timer);\n scopes.clear();\n latestJson.clear();\n latestPayload.clear();\n generations.clear();\n },\n };\n return source;\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-team",
3
- "version": "0.0.1-alpha.43",
3
+ "version": "0.0.1-alpha.45",
4
4
  "description": "Asynchronous named subagents, team runs, intercom, and model policy for Pi coding agents.",
5
5
  "keywords": [
6
6
  "agent-team",
@@ -53,15 +53,15 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@deepseek-ai/cordis": "4.0.1",
56
- "typebox": "1.3.16",
56
+ "typebox": "1.3.19",
57
57
  "yaml": "2.9.0",
58
- "@agimon-ai/doompi-cache": "0.0.1-alpha.10",
59
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.43",
60
- "@agimon-ai/doompi-config": "0.0.1-alpha.43",
61
- "@agimon-ai/doompi-ui": "0.0.1-alpha.43",
62
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.43",
63
- "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.6",
64
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.4"
58
+ "@agimon-ai/doompi-cache": "0.0.1-alpha.11",
59
+ "@agimon-ai/doompi-config": "0.0.1-alpha.44",
60
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.45",
61
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.44",
62
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.5",
63
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.44",
64
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.8"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@earendil-works/pi-agent-core": "0.84.4",
@@ -70,8 +70,8 @@
70
70
  "@earendil-works/pi-tui": "0.84.4",
71
71
  "@tanstack/react-store": "0.11.1",
72
72
  "@tanstack/store": "0.11.1",
73
- "@types/node": "26.2.0",
74
- "@types/react": "19.2.7",
73
+ "@types/node": "26.4.0",
74
+ "@types/react": "19.2.18",
75
75
  "@vitest/coverage-v8": "4.1.11",
76
76
  "react": "19.2.8",
77
77
  "typescript": "7.0.2",
@@ -100,6 +100,13 @@
100
100
  "engines": {
101
101
  "node": ">=22.19.0"
102
102
  },
103
+ "doompiApi": {
104
+ "basePath": "team",
105
+ "session": {
106
+ "entry": "./src/exports/sessionApi.ts",
107
+ "dist": "./dist/sessionApi.mjs"
108
+ }
109
+ },
103
110
  "doompiWeb": {
104
111
  "pluginId": "subagents",
105
112
  "channels": [
@@ -2,13 +2,22 @@ import {
2
2
  Badge,
3
3
  Button,
4
4
  cn,
5
- CloseIcon,
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuTrigger,
6
10
  EmptyState,
11
+ KebabIcon,
7
12
  Kbd,
8
13
  Panel,
14
+ Sheet,
15
+ SheetBody,
16
+ SheetContent,
17
+ SheetHeader,
18
+ SheetTitle,
9
19
  STATUS_EDGE,
10
20
  StatusBadge,
11
- StreamCursor,
12
21
  } from '@agimon-ai/doompi-web-components';
13
22
  import type { SessionFrameSender, WebPluginSlotProps } from '@agimon-ai/doompi-web-contracts';
14
23
  import { useStore } from '@tanstack/react-store';
@@ -21,10 +30,25 @@ import { abbreviateCwd } from './format.ts';
21
30
  import { LaunchAgentDialog } from './LaunchAgentDialog.tsx';
22
31
  import { RUN_ACTIONS_SLOT } from './runActionsSlot.ts';
23
32
  import { elapsedRun, RUN_BADGE, RunControl } from './RunControl.tsx';
24
- import { clearAutoOpen, openRun, subagents, visibleRuns } from './subagentsStore.ts';
33
+ import {
34
+ clearAutoOpen,
35
+ dismissRun,
36
+ isTerminalRun,
37
+ openRun,
38
+ requestRunStop,
39
+ subagents,
40
+ visibleRuns,
41
+ } from './subagentsStore.ts';
25
42
 
26
43
  const TICK_MS = 10_000;
27
44
 
45
+ /**
46
+ * How much of a run's own conversation a card carries: enough to read what it
47
+ * is doing right now, not enough to read the run. The whole thread is one
48
+ * click away in the run's tab.
49
+ */
50
+ const CARD_THREAD_ENTRIES = 8;
51
+
28
52
  /** The card's work line: a task binding wins; otherwise the delegation prompt. */
29
53
  function WorkLine({ run }: { run: SubagentRun }) {
30
54
  const firstLine = run.task.split('\n').find((line) => line.trim() !== '') ?? '';
@@ -50,111 +74,202 @@ function WorkLine({ run }: { run: SubagentRun }) {
50
74
  );
51
75
  }
52
76
 
53
- /** What fills the tail: live output while running, the report when done, the error when failed. */
54
- function tailLines(run: SubagentRun): { text: string; className: string }[] {
55
- if (run.state === 'failed' && run.error) {
56
- return [
57
- ...run.tail.map((line) => ({ text: line, className: 'text-doom-dim' })),
58
- { text: run.error, className: 'text-doom-red' },
59
- ];
60
- }
61
- if (run.summary) {
62
- return run.summary
63
- .split('\n')
64
- .filter((line) => line.trim() !== '')
65
- .slice(0, 6)
66
- .map((line) => ({ text: line, className: 'text-doom-text' }));
67
- }
68
- const lines = run.tail.map((line) => ({ text: line, className: 'text-doom-dim' }));
69
- if (run.currentTool) lines.push({ text: run.currentTool, className: 'text-doom-dim' });
70
- return lines;
77
+ /**
78
+ * Everything a card can do, behind one control.
79
+ *
80
+ * A card already spends its width on the run's own stream, and stop, clear,
81
+ * detail and the run's tab are four verbs: as buttons they crowd the footer
82
+ * and each one has to defend itself from the card's own click.
83
+ */
84
+ function RunMenu({
85
+ sessionId,
86
+ run,
87
+ stopping,
88
+ send,
89
+ onOpenThread,
90
+ onOpenDetail,
91
+ }: {
92
+ sessionId: string;
93
+ run: SubagentRun;
94
+ stopping: boolean;
95
+ send: SessionFrameSender;
96
+ onOpenThread: () => void;
97
+ onOpenDetail: () => void;
98
+ }) {
99
+ return (
100
+ <DropdownMenu>
101
+ <DropdownMenuTrigger asChild>
102
+ <Button
103
+ variant="ghost"
104
+ size="icon"
105
+ data-testid={`run-menu-${run.runId}`}
106
+ title="what this run can do"
107
+ aria-label="run actions"
108
+ onClick={(event) => event.stopPropagation()}
109
+ >
110
+ <KebabIcon className="h-3 w-3" />
111
+ </Button>
112
+ </DropdownMenuTrigger>
113
+ <DropdownMenuContent data-testid={`run-menu-content-${run.runId}`}>
114
+ <DropdownMenuItem data-testid={`run-open-${run.runId}`} onSelect={onOpenThread}>
115
+ open thread
116
+ </DropdownMenuItem>
117
+ <DropdownMenuItem data-testid={`run-detail-${run.runId}`} onSelect={onOpenDetail}>
118
+ details
119
+ </DropdownMenuItem>
120
+ <DropdownMenuSeparator />
121
+ {isTerminalRun(run) ? (
122
+ <DropdownMenuItem data-testid={`run-clear-${run.runId}`} onSelect={() => dismissRun(sessionId, run.runId)}>
123
+ clear from the grid
124
+ </DropdownMenuItem>
125
+ ) : (
126
+ <DropdownMenuItem
127
+ variant="destructive"
128
+ data-testid={`run-stop-${run.runId}`}
129
+ data-stopping={stopping}
130
+ disabled={stopping}
131
+ onSelect={() => requestRunStop(send, sessionId, run.runId)}
132
+ >
133
+ {stopping ? 'stopping…' : 'stop'}
134
+ </DropdownMenuItem>
135
+ )}
136
+ </DropdownMenuContent>
137
+ </DropdownMenu>
138
+ );
71
139
  }
72
140
 
141
+ /**
142
+ * One run in the grid: its live conversation under a header, on the host's own
143
+ * transcript so a tool call reads the same here as in the session.
144
+ *
145
+ * The body is a glance, so it takes the newest entries only and does not take
146
+ * the pointer: the whole card is the way into the run's tab.
147
+ */
73
148
  function RunCard({
74
149
  sessionId,
75
150
  run,
76
151
  now,
77
152
  stopping,
78
153
  send,
79
- onOpen,
154
+ renderThread,
155
+ onOpenThread,
156
+ onOpenDetail,
80
157
  }: {
81
158
  sessionId: string;
82
159
  run: SubagentRun;
83
160
  now: number;
84
161
  stopping: boolean;
85
162
  send: SessionFrameSender;
86
- onOpen: () => void;
163
+ renderThread: WebPluginSlotProps['renderThread'];
164
+ onOpenThread: () => void;
165
+ onOpenDetail: () => void;
87
166
  }) {
88
167
  const badge = RUN_BADGE[run.state];
89
168
  const onKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>): void => {
90
169
  if (event.target !== event.currentTarget) return;
91
170
  if (event.key === 'Enter' || event.key === ' ') {
92
171
  event.preventDefault();
93
- onOpen();
172
+ onOpenThread();
94
173
  }
95
174
  };
96
- // A div rather than a button: the card carries buttons of its own, and a
175
+ // A div rather than a button: the card carries a menu of its own, and a
97
176
  // button cannot contain another.
98
177
  return (
99
178
  <Panel
100
179
  asChild
101
180
  className={cn(
102
- 'flex min-h-[210px] min-w-0 cursor-pointer flex-col text-left transition-colors outline-none hover:border-doom-blue/50 focus-visible:border-doom-blue',
181
+ 'flex h-[280px] min-w-0 flex-col text-left transition-colors outline-none hover:border-doom-blue/50 focus-within:border-doom-blue',
103
182
  STATUS_EDGE[badge.tone],
104
183
  )}
105
184
  >
106
- <div
107
- role="button"
108
- tabIndex={0}
109
- data-testid={`run-card-${run.runId}`}
110
- data-run-state={run.state}
111
- onClick={onOpen}
112
- onKeyDown={onKeyDown}
113
- >
114
- <div className="flex items-center gap-2 px-3 pt-2.5 pb-1.5">
115
- <span data-testid="run-agent" className="truncate text-[12px] font-bold text-doom-hi">
116
- {run.agent}
117
- </span>
118
- <span className="min-w-0 flex-1" />
119
- <span className="shrink-0 text-[9px] text-doom-faint">{elapsedRun(run, now)}</span>
120
- <StatusBadge tone={badge.tone} data-testid="run-state">
121
- {badge.label}
122
- </StatusBadge>
123
- </div>
124
- <WorkLine run={run} />
125
- <div className="flex min-w-0 flex-1 flex-col gap-1 overflow-hidden border-t border-doom-border-soft bg-doom-deep px-3 py-2.5">
126
- {tailLines(run).map((line, index) => (
127
- <span key={index} className={`truncate text-[10px] ${line.className}`}>
128
- {line.text}
185
+ <div data-testid={`run-card-${run.runId}`} data-run-state={run.state}>
186
+ <div
187
+ role="button"
188
+ tabIndex={0}
189
+ data-testid={`run-open-card-${run.runId}`}
190
+ className="flex min-h-0 flex-1 cursor-pointer flex-col outline-none"
191
+ title="open this run's own thread in a tab"
192
+ onClick={onOpenThread}
193
+ onKeyDown={onKeyDown}
194
+ >
195
+ <div className="flex items-center gap-2 px-3 pt-2.5 pb-1.5">
196
+ <span data-testid="run-agent" className="truncate text-[12px] font-bold text-doom-hi">
197
+ {run.agent}
129
198
  </span>
130
- ))}
131
- {run.state === 'running' ? <StreamCursor className="mt-0.5 ml-0 h-[11px] w-1.5 translate-y-0" /> : null}
199
+ <span className="min-w-0 flex-1" />
200
+ <span className="shrink-0 text-[9px] text-doom-faint">{elapsedRun(run, now)}</span>
201
+ <StatusBadge tone={badge.tone} data-testid="run-state">
202
+ {badge.label}
203
+ </StatusBadge>
204
+ </div>
205
+ <WorkLine run={run} />
206
+ {/* The run's own journal, drawn by the host's timeline: the newest
207
+ entries only, and inert, so the card stays one click. */}
208
+ <div
209
+ data-testid="run-stream"
210
+ className="pointer-events-none flex min-h-0 flex-1 flex-col overflow-hidden border-t border-doom-border-soft bg-doom-deep"
211
+ >
212
+ {renderThread(run.runId, { limit: CARD_THREAD_ENTRIES, compact: true })}
213
+ </div>
214
+ {run.state === 'failed' && run.error ? (
215
+ <span
216
+ data-testid="run-error"
217
+ className="truncate border-t border-doom-border-soft px-3 py-1.5 text-[10px] text-doom-red"
218
+ >
219
+ {run.error}
220
+ </span>
221
+ ) : null}
132
222
  </div>
133
- <div className="flex items-center gap-3 px-3 py-2">
223
+ <div className="flex shrink-0 items-center gap-3 border-t border-doom-border-soft px-3 py-1.5">
134
224
  <span className="truncate text-[9px] text-doom-faint">
135
225
  {run.toolCount !== undefined ? `${run.toolCount} tools` : '—'}
136
226
  {run.tokens !== undefined ? ` · ${run.tokens.toLocaleString()} tk` : ''}
137
227
  {run.model ? ` · ${run.model.split('/').pop() ?? run.model}` : ''}
138
228
  </span>
139
229
  <span className="min-w-0 flex-1" />
140
- <RunControl sessionId={sessionId} run={run} stopping={stopping} send={send} />
141
- <span className="text-[9px] font-bold text-doom-blue">detail</span>
230
+ <RunMenu
231
+ sessionId={sessionId}
232
+ run={run}
233
+ stopping={stopping}
234
+ send={send}
235
+ onOpenThread={onOpenThread}
236
+ onOpenDetail={onOpenDetail}
237
+ />
142
238
  </div>
143
239
  </div>
144
240
  </Panel>
145
241
  );
146
242
  }
147
243
 
148
- function DrawerRow({ label, value, tone = 'text-doom-dim' }: { label: string; value: string; tone?: string }) {
244
+ function SheetRow({ label, value, tone = 'text-doom-dim' }: { label: string; value: string; tone?: string }) {
149
245
  return (
150
246
  <div className="flex gap-2.5">
151
247
  <span className="w-11 shrink-0 text-[9px] text-doom-faint">{label}</span>
152
- <span className={`min-w-0 truncate text-[10px] ${tone}`}>{value}</span>
248
+ <span className={`min-w-0 break-words text-[10px] ${tone}`}>{value}</span>
153
249
  </div>
154
250
  );
155
251
  }
156
252
 
157
- function RunDrawer({
253
+ function SheetBlock({ label, text, testId, tone }: { label: string; text: string; testId: string; tone?: string }) {
254
+ return (
255
+ <div className="flex flex-col gap-1 rounded-md border border-doom-border bg-doom-deep px-3 py-2.5">
256
+ <span className="text-[8px] font-bold tracking-[0.14em] text-doom-faint">{label}</span>
257
+ <pre
258
+ data-testid={testId}
259
+ className={`overflow-x-auto whitespace-pre-wrap break-words text-[10px] leading-relaxed ${tone ?? 'text-doom-text'}`}
260
+ >
261
+ {text}
262
+ </pre>
263
+ </div>
264
+ );
265
+ }
266
+
267
+ /**
268
+ * What the run was asked to do and what it was given to do it with, over the
269
+ * grid rather than beside it: the conversation is on the card and in the run's
270
+ * tab, so this reads as a definition and takes no width from the fleet.
271
+ */
272
+ function RunDetailSheet({
158
273
  sessionId,
159
274
  run,
160
275
  now,
@@ -172,60 +287,45 @@ function RunDrawer({
172
287
  onClose: () => void;
173
288
  }) {
174
289
  const badge = RUN_BADGE[run.state];
175
- const output = run.summary ?? run.tail.join('\n');
176
290
  return (
177
- <aside
178
- data-testid="run-drawer"
179
- className="flex w-[min(440px,calc(100vw-24px))] shrink-0 flex-col overflow-hidden border-l border-doom-border bg-doom-rail"
291
+ <Sheet
292
+ open
293
+ onOpenChange={(open) => {
294
+ if (!open) onClose();
295
+ }}
180
296
  >
181
- <div className="flex h-11 shrink-0 items-center gap-2.5 border-b border-doom-border px-4">
182
- <span data-testid="drawer-agent" className="truncate text-[13px] font-bold text-doom-hi">
183
- {run.agent}
184
- </span>
185
- <StatusBadge tone={badge.tone}>{badge.label}</StatusBadge>
186
- <span className="min-w-0 flex-1" />
187
- <span className="text-[10px] text-doom-faint">{elapsedRun(run, now)}</span>
188
- {renderSlot(RUN_ACTIONS_SLOT.slot)}
189
- <RunControl sessionId={sessionId} run={run} stopping={stopping} send={send} />
190
- <Button variant="ghost" size="icon" data-testid="drawer-close" title="close the run detail" onClick={onClose}>
191
- <CloseIcon className="h-3 w-3" />
192
- </Button>
193
- </div>
194
- <div className="flex flex-col gap-1.5 px-4 py-2.5">
195
- <DrawerRow label="run" value={run.runId.slice(0, 8)} />
196
- {run.taskRef ? <DrawerRow label="task" value={run.taskRef} tone="text-doom-violet" /> : null}
197
- {run.model ? <DrawerRow label="model" value={run.model} /> : null}
198
- <DrawerRow label="cwd" value={abbreviateCwd(run.cwd)} />
199
- </div>
200
- <div className="px-4 pb-2.5">
201
- <div className="flex flex-col gap-1 rounded-md border border-doom-border bg-doom-deep px-3 py-2.5">
202
- <span className="text-[8px] font-bold tracking-[0.14em] text-doom-faint">PROMPT FROM THE MAIN AGENT</span>
203
- <pre
204
- data-testid="drawer-task"
205
- className="max-h-40 overflow-y-auto whitespace-pre-wrap break-words text-[10px] leading-relaxed text-doom-text"
206
- >
207
- {run.task || '(no prompt recorded)'}
208
- </pre>
209
- </div>
210
- </div>
211
- <div className="flex items-center justify-between px-4 pb-1.5">
212
- <span className="text-[9px] font-bold tracking-[0.14em] text-doom-faint">
213
- {run.summary ? 'REPORT' : run.state === 'failed' ? 'FAILURE' : 'OUTPUT · live'}
214
- </span>
215
- <span className="text-[9px] text-doom-faint">
216
- {run.toolCount !== undefined ? `${run.toolCount} tools` : ''}
217
- </span>
218
- </div>
219
- <div className="flex-1 overflow-y-auto border-t border-doom-border-soft bg-doom-deep px-4 py-3">
220
- <pre
221
- data-testid="drawer-output"
222
- className="whitespace-pre-wrap break-words text-[10px] leading-relaxed text-doom-text"
223
- >
224
- {run.state === 'failed' && run.error ? run.error : output || run.currentTool || '(nothing reported yet)'}
225
- </pre>
226
- {run.state === 'running' ? <StreamCursor className="mt-1 ml-0 h-[11px] w-1.5 translate-y-0" /> : null}
227
- </div>
228
- </aside>
297
+ <SheetContent data-testid="run-sheet" aria-describedby={undefined}>
298
+ <SheetHeader closeLabel="close the run detail">
299
+ <SheetTitle data-testid="sheet-agent">{run.agent}</SheetTitle>
300
+ <StatusBadge tone={badge.tone}>{badge.label}</StatusBadge>
301
+ <span className="min-w-0 flex-1" />
302
+ <span className="text-[10px] text-doom-faint">{elapsedRun(run, now)}</span>
303
+ {renderSlot(RUN_ACTIONS_SLOT.slot)}
304
+ <RunControl sessionId={sessionId} run={run} stopping={stopping} send={send} />
305
+ </SheetHeader>
306
+ <SheetBody>
307
+ <div className="flex flex-col gap-1.5">
308
+ <SheetRow label="run" value={run.runId} />
309
+ {run.taskRef ? <SheetRow label="task" value={run.taskRef} tone="text-doom-violet" /> : null}
310
+ <SheetRow label="model" value={run.model ?? 'the runtime default'} />
311
+ <SheetRow label="cwd" value={abbreviateCwd(run.cwd)} />
312
+ <SheetRow
313
+ label="work"
314
+ value={`${run.toolCount === undefined ? 'no' : String(run.toolCount)} tools${
315
+ run.tokens === undefined ? '' : ` · ${run.tokens.toLocaleString()} tk`
316
+ }`}
317
+ />
318
+ </div>
319
+ <SheetBlock
320
+ label="PROMPT FROM THE MAIN AGENT"
321
+ testId="sheet-task"
322
+ text={run.task || '(no prompt recorded)'}
323
+ />
324
+ {run.summary ? <SheetBlock label="REPORT" testId="sheet-summary" text={run.summary} /> : null}
325
+ {run.error ? <SheetBlock label="FAILURE" testId="sheet-error" text={run.error} tone="text-doom-red" /> : null}
326
+ </SheetBody>
327
+ </SheetContent>
328
+ </Sheet>
229
329
  );
230
330
  }
231
331
 
@@ -233,12 +333,19 @@ function RunDrawer({
233
333
  * The subagents tab: the focused session's fleet as a computed grid.
234
334
  *
235
335
  * Columns come from the available width (auto-fill over a 420px minimum), and
236
- * every card keeps a 210px minimum height with the tail absorbing the slack,
237
- * so short runs read as calmly as busy ones. The drawer on the right shows one
238
- * thing at a time: a card's run detail, or the agent catalog to launch from.
239
- * A launch that produced a run opens that run's own tab once it appears.
336
+ * every card is the same height with the run's own live conversation
337
+ * absorbing the slack, so the grid reads as a fleet rather than a ragged list.
338
+ * A card opens the run's tab; its menu opens the detail sheet over the grid.
339
+ * The catalog to launch from still takes the right rail, because picking an
340
+ * agent is a list beside the fleet rather than a thing about one run.
240
341
  */
241
- export function SubagentsPanel({ sessionId, sendSessionFrame, renderSlot, openTransientTab }: WebPluginSlotProps) {
342
+ export function SubagentsPanel({
343
+ sessionId,
344
+ sendSessionFrame,
345
+ renderSlot,
346
+ renderThread,
347
+ openTransientTab,
348
+ }: WebPluginSlotProps) {
242
349
  const runs = useStore(subagents.store, (state) => visibleRuns(subagents.select(state, sessionId)));
243
350
  const { openRunId, stopRequested, autoOpenRunId } = useStore(subagents.store, (state) =>
244
351
  subagents.select(state, sessionId),
@@ -317,7 +424,12 @@ export function SubagentsPanel({ sessionId, sendSessionFrame, renderSlot, openTr
317
424
  now={now}
318
425
  stopping={stopping(run)}
319
426
  send={sendSessionFrame}
320
- onOpen={() => {
427
+ renderThread={renderThread}
428
+ onOpenThread={() => {
429
+ closeCatalog(sessionId);
430
+ openTransientTab(agentThreadTab(run));
431
+ }}
432
+ onOpenDetail={() => {
321
433
  closeCatalog(sessionId);
322
434
  openRun(sessionId, run.runId);
323
435
  }}
@@ -325,7 +437,7 @@ export function SubagentsPanel({ sessionId, sendSessionFrame, renderSlot, openTr
325
437
  ))}
326
438
  </div>
327
439
  <span className="pt-3 text-[9px] text-doom-faint">
328
- click a card for the run detail · stop asks the runtime · clear hides a finished run · finished runs leave
440
+ click a card to open the run's thread · the card menu holds details, stop and clear · finished runs leave
329
441
  the grid after 10m
330
442
  </span>
331
443
  </>
@@ -337,8 +449,9 @@ export function SubagentsPanel({ sessionId, sendSessionFrame, renderSlot, openTr
337
449
  onClose={() => closeCatalog(sessionId)}
338
450
  onLaunch={(agent, fork) => openLaunch(sessionId, agent.name, fork)}
339
451
  />
340
- ) : shownRun && sessionId !== null ? (
341
- <RunDrawer
452
+ ) : null}
453
+ {shownRun && sessionId !== null ? (
454
+ <RunDetailSheet
342
455
  sessionId={sessionId}
343
456
  run={shownRun}
344
457
  now={now}
package/web/index.ts CHANGED
@@ -40,6 +40,6 @@ export const webPlugin = defineWebPlugin({
40
40
  // footer's one-line summary.
41
41
  activitySections: [{ id: 'agents', component: AgentsActivitySection }],
42
42
  toolRenderers: teamToolRenderers,
43
- // The run drawer's action row is open to independent plugins.
43
+ // The run detail sheet's action row is open to independent plugins.
44
44
  slots: [RUN_ACTIONS_SLOT],
45
45
  });
@@ -1,7 +1,7 @@
1
1
  import { defineSlot } from '@agimon-ai/doompi-web-contracts';
2
2
 
3
3
  /**
4
- * The slot the run drawer opens beside its stop control: an independent
4
+ * The slot the run detail sheet opens beside its stop control: an independent
5
5
  * plugin may put its own action for the shown run there, without this
6
6
  * package knowing it exists. Fills receive the panel's own props.
7
7
  */
@@ -19,7 +19,7 @@ export interface SubagentsSession {
19
19
  dismissed: string[];
20
20
  /** Runs a stop was asked for and that have not yet reported a final state. */
21
21
  stopRequested: string[];
22
- /** The run whose drawer the subagents tab shows. */
22
+ /** The run whose detail sheet the subagents tab shows. */
23
23
  openRunId: string | undefined;
24
24
  /** A launch this page asked for, until a run of that agent it had not seen shows up. */
25
25
  pendingLaunch: { agent: string; knownRunIds: string[] } | undefined;