@agimon-ai/doompi-task 0.0.1-alpha.39 → 0.0.1-alpha.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,10 @@
1
1
  # @agimon-ai/doompi-task
2
2
 
3
- A persistent, dependency-aware task graph for Pi, with an optional delegation bridge to DoomPi
4
- Team.
3
+ Track persistent, dependency-aware tasks in Pi and optionally delegate them through DoomPi Team.
5
4
 
6
5
  Part of the [DoomPi distribution](https://www.npmjs.com/package/@agimon-ai/doompi).
7
6
 
8
- Task owns task records and `tasks.json`. Team owns agents and runs. Loading both lets Task delegate
9
- pending work through Team's session-bound `doom/delegation` Cordis service; it does not merge their
10
- persistence.
7
+ Task owns task records and `tasks.json`. Team owns agents and runs. When both are loaded, Task delegates pending work through Team without merging their stored state.
11
8
 
12
9
  > **Alpha:** task and delegation contracts may change between releases.
13
10
 
@@ -0,0 +1,2 @@
1
+ const e=require("./store/paths.cjs"),t=require("./store/taskStore.cjs"),n=require("../types/webTasks.cjs");function r(e){return{tasks:e.tasks.flatMap(e=>{if(e.status===`deleted`)return[];let t={id:e.id,subject:e.subject,status:e.status,blockedBy:e.blockedBy??[]};return e.description&&(t.description=e.description),e.activeForm&&(t.activeForm=e.activeForm),e.owner&&(t.owner=e.owner),e.updatedAt&&(t.updatedAt=e.updatedAt),e.delegation&&(t.delegation={agent:e.delegation.agent,state:e.delegation.state}),[t]}),rev:e.rev}}function i(i={}){return{frameType:n.TASKS_CHANNEL_TYPE,start(n){let a=new Map,o=o=>{if(a.has(o.sessionId))return;let s=i.storeFor?.(o)??new t.TaskStore({cwd:o.cwd,storePath:e.resolveStorePath(o.cwd,process.env,o.sessionId)}),c=s.read(),l=s.onExternalChange(e=>n.publish(o.sessionId,r(e)));a.set(o.sessionId,{store:s,unwatch:l}),c.tasks.length>0&&n.publish(o.sessionId,r(c))},s=e=>{let t=a.get(e);t&&(t.unwatch(),t.store.dispose(),a.delete(e))};for(let e of n.sessions())o(e);return{payloadFor(e){o(e);let t=a.get(e.sessionId)?.store.snapshot;return t===void 0?void 0:r(t)},sessionAdded:o,sessionRemoved:s,close(){for(let e of a.keys())s(e)}}}}}const a=[i()];exports.createTasksChannel=i,exports.webHubChannels=a;
2
+ //# sourceMappingURL=webTasksChannel.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webTasksChannel.cjs","names":["TASKS_CHANNEL_TYPE","TaskStore","resolveStorePath"],"sources":["../../src/adapters/webTasksChannel.ts"],"sourcesContent":["import type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { resolveStorePath } from './store/paths.ts';\nimport { TaskStore } from './store/taskStore.ts';\nimport type { TaskDocument } from '../services/store/types.ts';\nimport { TASKS_CHANNEL_TYPE, type WebTask, type WebTasksPayload } from '../types/webTasks.ts';\n\ninterface TaskStoreSource {\n readonly snapshot: TaskDocument;\n read(): TaskDocument;\n onExternalChange(listener: (document: TaskDocument) => void): () => void;\n dispose(): void;\n}\n\nexport interface TasksChannelOptions {\n /** Injectable for tests. The default opens the session tree's durable task store. */\n storeFor?: (scope: HubSessionScope) => TaskStoreSource;\n}\n\nfunction present(document: TaskDocument): WebTasksPayload {\n const tasks: WebTask[] = document.tasks.flatMap((task) => {\n if (task.status === 'deleted') return [];\n const view: WebTask = {\n id: task.id,\n subject: task.subject,\n status: task.status,\n blockedBy: task.blockedBy ?? [],\n };\n if (task.description) view.description = task.description;\n if (task.activeForm) view.activeForm = task.activeForm;\n if (task.owner) view.owner = task.owner;\n if (task.updatedAt) view.updatedAt = task.updatedAt;\n if (task.delegation) {\n view.delegation = { agent: task.delegation.agent, state: task.delegation.state };\n }\n return [view];\n });\n return { tasks, rev: document.rev };\n}\n\n/** Publishes each managed session's durable task graph and follows external subagent writes. */\nexport function createTasksChannel(options: TasksChannelOptions = {}): WebHubChannel {\n return {\n frameType: TASKS_CHANNEL_TYPE,\n start(host) {\n const stores = new Map<string, { store: TaskStoreSource; unwatch: () => void }>();\n const open = (scope: HubSessionScope): void => {\n if (stores.has(scope.sessionId)) return;\n const store =\n options.storeFor?.(scope) ??\n new TaskStore({ cwd: scope.cwd, storePath: resolveStorePath(scope.cwd, process.env, scope.sessionId) });\n const document = store.read();\n const unwatch = store.onExternalChange((changed) => host.publish(scope.sessionId, present(changed)));\n stores.set(scope.sessionId, { store, unwatch });\n if (document.tasks.length > 0) host.publish(scope.sessionId, present(document));\n };\n const close = (sessionId: string): void => {\n const source = stores.get(sessionId);\n if (!source) return;\n source.unwatch();\n source.store.dispose();\n stores.delete(sessionId);\n };\n for (const scope of host.sessions()) open(scope);\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n open(scope);\n const document = stores.get(scope.sessionId)?.store.snapshot;\n return document === undefined ? undefined : present(document);\n },\n sessionAdded: open,\n sessionRemoved: close,\n close() {\n for (const sessionId of stores.keys()) close(sessionId);\n },\n };\n return source;\n },\n };\n}\n\n/** The named export the generated hub registry imports. */\nexport const webHubChannels: readonly WebHubChannel[] = [createTasksChannel()];\n"],"mappings":"2GAkBA,SAAS,EAAQ,EAAyC,CAkBxD,MAAO,CAAE,MAjBgB,EAAS,MAAM,QAAS,GAAS,CACxD,GAAI,EAAK,SAAW,UAAW,MAAO,CAAC,EACvC,IAAM,EAAgB,CACpB,GAAI,EAAK,GACT,QAAS,EAAK,QACd,OAAQ,EAAK,OACb,UAAW,EAAK,WAAa,CAAC,CAChC,EAQA,OAPI,EAAK,cAAa,EAAK,YAAc,EAAK,aAC1C,EAAK,aAAY,EAAK,WAAa,EAAK,YACxC,EAAK,QAAO,EAAK,MAAQ,EAAK,OAC9B,EAAK,YAAW,EAAK,UAAY,EAAK,WACtC,EAAK,aACP,EAAK,WAAa,CAAE,MAAO,EAAK,WAAW,MAAO,MAAO,EAAK,WAAW,KAAM,GAE1E,CAAC,CAAI,CACd,CACa,EAAG,IAAK,EAAS,GAAI,CACpC,CAGA,SAAgB,EAAmB,EAA+B,CAAC,EAAkB,CACnF,MAAO,CACL,UAAWA,EAAAA,mBACX,MAAM,EAAM,CACV,IAAM,EAAS,IAAI,IACb,EAAQ,GAAiC,CAC7C,GAAI,EAAO,IAAI,EAAM,SAAS,EAAG,OACjC,IAAM,EACJ,EAAQ,WAAW,CAAK,GACxB,IAAIC,EAAAA,UAAU,CAAE,IAAK,EAAM,IAAK,UAAWC,EAAAA,iBAAiB,EAAM,IAAK,QAAQ,IAAK,EAAM,SAAS,CAAE,CAAC,EAClG,EAAW,EAAM,KAAK,EACtB,EAAU,EAAM,iBAAkB,GAAY,EAAK,QAAQ,EAAM,UAAW,EAAQ,CAAO,CAAC,CAAC,EACnG,EAAO,IAAI,EAAM,UAAW,CAAE,QAAO,SAAQ,CAAC,EAC1C,EAAS,MAAM,OAAS,GAAG,EAAK,QAAQ,EAAM,UAAW,EAAQ,CAAQ,CAAC,CAChF,EACM,EAAS,GAA4B,CACzC,IAAM,EAAS,EAAO,IAAI,CAAS,EAC9B,IACL,EAAO,QAAQ,EACf,EAAO,MAAM,QAAQ,EACrB,EAAO,OAAO,CAAS,EACzB,EACA,IAAK,IAAM,KAAS,EAAK,SAAS,EAAG,EAAK,CAAK,EAc/C,MAAO,CAXL,WAAW,EAAO,CAChB,EAAK,CAAK,EACV,IAAM,EAAW,EAAO,IAAI,EAAM,SAAS,CAAC,EAAE,MAAM,SACpD,OAAO,IAAa,IAAA,GAAY,IAAA,GAAY,EAAQ,CAAQ,CAC9D,EACA,aAAc,EACd,eAAgB,EAChB,OAAQ,CACN,IAAK,IAAM,KAAa,EAAO,KAAK,EAAG,EAAM,CAAS,CACxD,CAEU,CACd,CACF,CACF,CAGA,MAAa,EAA2C,CAAC,EAAmB,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { TaskDocument } from "../services/store/types.cjs";
2
+ import { HubSessionScope, WebHubChannel } from "@agimon-ai/doompi-web-contracts";
3
+ //#region src/adapters/webTasksChannel.d.ts
4
+ interface TaskStoreSource {
5
+ readonly snapshot: TaskDocument;
6
+ read(): TaskDocument;
7
+ onExternalChange(listener: (document: TaskDocument) => void): () => void;
8
+ dispose(): void;
9
+ }
10
+ interface TasksChannelOptions {
11
+ /** Injectable for tests. The default opens the session tree's durable task store. */
12
+ storeFor?: (scope: HubSessionScope) => TaskStoreSource;
13
+ }
14
+ /** Publishes each managed session's durable task graph and follows external subagent writes. */
15
+ declare function createTasksChannel(options?: TasksChannelOptions): WebHubChannel;
16
+ /** The named export the generated hub registry imports. */
17
+ declare const webHubChannels: readonly WebHubChannel[];
18
+ //#endregion
19
+ export { TasksChannelOptions, createTasksChannel, webHubChannels };
20
+ //# sourceMappingURL=webTasksChannel.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webTasksChannel.d.cts","names":[],"sources":["../../src/adapters/webTasksChannel.ts"],"mappings":";;;UAMU;WACC,UAAU;EACnB,QAAQ;EACR,iBAAiB,WAAW,UAAU;EACtC;;UAGe;;EAEf,YAAY,OAAO,oBAAoB;;;iBAyBzB,mBAAmB,UAAS,sBAA2B;;cA0C1D,yBAAyB"}
@@ -0,0 +1,20 @@
1
+ import { TaskDocument } from "../services/store/types.mjs";
2
+ import { HubSessionScope, WebHubChannel } from "@agimon-ai/doompi-web-contracts";
3
+ //#region src/adapters/webTasksChannel.d.ts
4
+ interface TaskStoreSource {
5
+ readonly snapshot: TaskDocument;
6
+ read(): TaskDocument;
7
+ onExternalChange(listener: (document: TaskDocument) => void): () => void;
8
+ dispose(): void;
9
+ }
10
+ interface TasksChannelOptions {
11
+ /** Injectable for tests. The default opens the session tree's durable task store. */
12
+ storeFor?: (scope: HubSessionScope) => TaskStoreSource;
13
+ }
14
+ /** Publishes each managed session's durable task graph and follows external subagent writes. */
15
+ declare function createTasksChannel(options?: TasksChannelOptions): WebHubChannel;
16
+ /** The named export the generated hub registry imports. */
17
+ declare const webHubChannels: readonly WebHubChannel[];
18
+ //#endregion
19
+ export { TasksChannelOptions, createTasksChannel, webHubChannels };
20
+ //# sourceMappingURL=webTasksChannel.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webTasksChannel.d.mts","names":[],"sources":["../../src/adapters/webTasksChannel.ts"],"mappings":";;;UAMU;WACC,UAAU;EACnB,QAAQ;EACR,iBAAiB,WAAW,UAAU;EACtC;;UAGe;;EAEf,YAAY,OAAO,oBAAoB;;;iBAyBzB,mBAAmB,UAAS,sBAA2B;;cA0C1D,yBAAyB"}
@@ -0,0 +1,2 @@
1
+ import{resolveStorePath as e}from"./store/paths.mjs";import{TaskStore as t}from"./store/taskStore.mjs";import{TASKS_CHANNEL_TYPE as n}from"../types/webTasks.mjs";function r(e){return{tasks:e.tasks.flatMap(e=>{if(e.status===`deleted`)return[];let t={id:e.id,subject:e.subject,status:e.status,blockedBy:e.blockedBy??[]};return e.description&&(t.description=e.description),e.activeForm&&(t.activeForm=e.activeForm),e.owner&&(t.owner=e.owner),e.updatedAt&&(t.updatedAt=e.updatedAt),e.delegation&&(t.delegation={agent:e.delegation.agent,state:e.delegation.state}),[t]}),rev:e.rev}}function i(i={}){return{frameType:n,start(n){let a=new Map,o=o=>{if(a.has(o.sessionId))return;let s=i.storeFor?.(o)??new t({cwd:o.cwd,storePath:e(o.cwd,process.env,o.sessionId)}),c=s.read(),l=s.onExternalChange(e=>n.publish(o.sessionId,r(e)));a.set(o.sessionId,{store:s,unwatch:l}),c.tasks.length>0&&n.publish(o.sessionId,r(c))},s=e=>{let t=a.get(e);t&&(t.unwatch(),t.store.dispose(),a.delete(e))};for(let e of n.sessions())o(e);return{payloadFor(e){o(e);let t=a.get(e.sessionId)?.store.snapshot;return t===void 0?void 0:r(t)},sessionAdded:o,sessionRemoved:s,close(){for(let e of a.keys())s(e)}}}}}const a=[i()];export{i as createTasksChannel,a as webHubChannels};
2
+ //# sourceMappingURL=webTasksChannel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webTasksChannel.mjs","names":[],"sources":["../../src/adapters/webTasksChannel.ts"],"sourcesContent":["import type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { resolveStorePath } from './store/paths.ts';\nimport { TaskStore } from './store/taskStore.ts';\nimport type { TaskDocument } from '../services/store/types.ts';\nimport { TASKS_CHANNEL_TYPE, type WebTask, type WebTasksPayload } from '../types/webTasks.ts';\n\ninterface TaskStoreSource {\n readonly snapshot: TaskDocument;\n read(): TaskDocument;\n onExternalChange(listener: (document: TaskDocument) => void): () => void;\n dispose(): void;\n}\n\nexport interface TasksChannelOptions {\n /** Injectable for tests. The default opens the session tree's durable task store. */\n storeFor?: (scope: HubSessionScope) => TaskStoreSource;\n}\n\nfunction present(document: TaskDocument): WebTasksPayload {\n const tasks: WebTask[] = document.tasks.flatMap((task) => {\n if (task.status === 'deleted') return [];\n const view: WebTask = {\n id: task.id,\n subject: task.subject,\n status: task.status,\n blockedBy: task.blockedBy ?? [],\n };\n if (task.description) view.description = task.description;\n if (task.activeForm) view.activeForm = task.activeForm;\n if (task.owner) view.owner = task.owner;\n if (task.updatedAt) view.updatedAt = task.updatedAt;\n if (task.delegation) {\n view.delegation = { agent: task.delegation.agent, state: task.delegation.state };\n }\n return [view];\n });\n return { tasks, rev: document.rev };\n}\n\n/** Publishes each managed session's durable task graph and follows external subagent writes. */\nexport function createTasksChannel(options: TasksChannelOptions = {}): WebHubChannel {\n return {\n frameType: TASKS_CHANNEL_TYPE,\n start(host) {\n const stores = new Map<string, { store: TaskStoreSource; unwatch: () => void }>();\n const open = (scope: HubSessionScope): void => {\n if (stores.has(scope.sessionId)) return;\n const store =\n options.storeFor?.(scope) ??\n new TaskStore({ cwd: scope.cwd, storePath: resolveStorePath(scope.cwd, process.env, scope.sessionId) });\n const document = store.read();\n const unwatch = store.onExternalChange((changed) => host.publish(scope.sessionId, present(changed)));\n stores.set(scope.sessionId, { store, unwatch });\n if (document.tasks.length > 0) host.publish(scope.sessionId, present(document));\n };\n const close = (sessionId: string): void => {\n const source = stores.get(sessionId);\n if (!source) return;\n source.unwatch();\n source.store.dispose();\n stores.delete(sessionId);\n };\n for (const scope of host.sessions()) open(scope);\n\n const source: HubChannelSource = {\n payloadFor(scope) {\n open(scope);\n const document = stores.get(scope.sessionId)?.store.snapshot;\n return document === undefined ? undefined : present(document);\n },\n sessionAdded: open,\n sessionRemoved: close,\n close() {\n for (const sessionId of stores.keys()) close(sessionId);\n },\n };\n return source;\n },\n };\n}\n\n/** The named export the generated hub registry imports. */\nexport const webHubChannels: readonly WebHubChannel[] = [createTasksChannel()];\n"],"mappings":"kKAkBA,SAAS,EAAQ,EAAyC,CAkBxD,MAAO,CAAE,MAjBgB,EAAS,MAAM,QAAS,GAAS,CACxD,GAAI,EAAK,SAAW,UAAW,MAAO,CAAC,EACvC,IAAM,EAAgB,CACpB,GAAI,EAAK,GACT,QAAS,EAAK,QACd,OAAQ,EAAK,OACb,UAAW,EAAK,WAAa,CAAC,CAChC,EAQA,OAPI,EAAK,cAAa,EAAK,YAAc,EAAK,aAC1C,EAAK,aAAY,EAAK,WAAa,EAAK,YACxC,EAAK,QAAO,EAAK,MAAQ,EAAK,OAC9B,EAAK,YAAW,EAAK,UAAY,EAAK,WACtC,EAAK,aACP,EAAK,WAAa,CAAE,MAAO,EAAK,WAAW,MAAO,MAAO,EAAK,WAAW,KAAM,GAE1E,CAAC,CAAI,CACd,CACa,EAAG,IAAK,EAAS,GAAI,CACpC,CAGA,SAAgB,EAAmB,EAA+B,CAAC,EAAkB,CACnF,MAAO,CACL,UAAW,EACX,MAAM,EAAM,CACV,IAAM,EAAS,IAAI,IACb,EAAQ,GAAiC,CAC7C,GAAI,EAAO,IAAI,EAAM,SAAS,EAAG,OACjC,IAAM,EACJ,EAAQ,WAAW,CAAK,GACxB,IAAI,EAAU,CAAE,IAAK,EAAM,IAAK,UAAW,EAAiB,EAAM,IAAK,QAAQ,IAAK,EAAM,SAAS,CAAE,CAAC,EAClG,EAAW,EAAM,KAAK,EACtB,EAAU,EAAM,iBAAkB,GAAY,EAAK,QAAQ,EAAM,UAAW,EAAQ,CAAO,CAAC,CAAC,EACnG,EAAO,IAAI,EAAM,UAAW,CAAE,QAAO,SAAQ,CAAC,EAC1C,EAAS,MAAM,OAAS,GAAG,EAAK,QAAQ,EAAM,UAAW,EAAQ,CAAQ,CAAC,CAChF,EACM,EAAS,GAA4B,CACzC,IAAM,EAAS,EAAO,IAAI,CAAS,EAC9B,IACL,EAAO,QAAQ,EACf,EAAO,MAAM,QAAQ,EACrB,EAAO,OAAO,CAAS,EACzB,EACA,IAAK,IAAM,KAAS,EAAK,SAAS,EAAG,EAAK,CAAK,EAc/C,MAAO,CAXL,WAAW,EAAO,CAChB,EAAK,CAAK,EACV,IAAM,EAAW,EAAO,IAAI,EAAM,SAAS,CAAC,EAAE,MAAM,SACpD,OAAO,IAAa,IAAA,GAAY,IAAA,GAAY,EAAQ,CAAQ,CAC9D,EACA,aAAc,EACd,eAAgB,EAChB,OAAQ,CACN,IAAK,IAAM,KAAa,EAAO,KAAK,EAAG,EAAM,CAAS,CACxD,CAEU,CACd,CACF,CACF,CAGA,MAAa,EAA2C,CAAC,EAAmB,CAAC"}
@@ -0,0 +1,2 @@
1
+ exports.TASKS_CHANNEL_TYPE=`task_graph`;
2
+ //# sourceMappingURL=webTasks.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webTasks.cjs","names":[],"sources":["../../src/types/webTasks.ts"],"sourcesContent":["/** Browser-safe task view published by the cockpit hub channel. */\nexport const TASKS_CHANNEL_TYPE = 'task_graph';\n\nexport type WebTaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'deleted';\n\nexport interface WebTask {\n id: number;\n subject: string;\n description?: string;\n activeForm?: string;\n status: WebTaskStatus;\n blockedBy: number[];\n owner?: string;\n updatedAt?: string;\n delegation?: {\n agent?: string;\n state?: string;\n };\n}\n\nexport interface WebTasksPayload {\n tasks: WebTask[];\n rev: number;\n}\n"],"mappings":""}
@@ -0,0 +1,2 @@
1
+ const e=`task_graph`;export{e as TASKS_CHANNEL_TYPE};
2
+ //# sourceMappingURL=webTasks.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webTasks.mjs","names":[],"sources":["../../src/types/webTasks.ts"],"sourcesContent":["/** Browser-safe task view published by the cockpit hub channel. */\nexport const TASKS_CHANNEL_TYPE = 'task_graph';\n\nexport type WebTaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'deleted';\n\nexport interface WebTask {\n id: number;\n subject: string;\n description?: string;\n activeForm?: string;\n status: WebTaskStatus;\n blockedBy: number[];\n owner?: string;\n updatedAt?: string;\n delegation?: {\n agent?: string;\n state?: string;\n };\n}\n\nexport interface WebTasksPayload {\n tasks: WebTask[];\n rev: number;\n}\n"],"mappings":"AACA,MAAa,EAAqB"}
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./adapters/webTasksChannel.cjs");exports.createTasksChannel=e.createTasksChannel,exports.webHubChannels=e.webHubChannels;
@@ -0,0 +1,2 @@
1
+ import { TasksChannelOptions, createTasksChannel, webHubChannels } from "./adapters/webTasksChannel.cjs";
2
+ export { type TasksChannelOptions, createTasksChannel, webHubChannels };
@@ -0,0 +1,2 @@
1
+ import { TasksChannelOptions, createTasksChannel, webHubChannels } from "./adapters/webTasksChannel.mjs";
2
+ export { type TasksChannelOptions, createTasksChannel, webHubChannels };
@@ -0,0 +1 @@
1
+ import{createTasksChannel as e,webHubChannels as t}from"./adapters/webTasksChannel.mjs";export{e as createTasksChannel,t as webHubChannels};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-task",
3
- "version": "0.0.1-alpha.39",
3
+ "version": "0.0.1-alpha.40",
4
4
  "description": "Persistent, dependency-aware task graphs and subagent delegation for Pi coding sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -28,6 +28,7 @@
28
28
  "files": [
29
29
  "dist",
30
30
  "web",
31
+ "src/types/webTasks.ts",
31
32
  "package.json"
32
33
  ],
33
34
  "type": "module",
@@ -130,6 +131,11 @@
130
131
  "import": "./dist/tool/taskTool.mjs",
131
132
  "require": "./dist/tool/taskTool.cjs"
132
133
  },
134
+ "./web-hub": {
135
+ "types": "./dist/webHub.d.mts",
136
+ "import": "./dist/webHub.mjs",
137
+ "require": "./dist/webHub.cjs"
138
+ },
133
139
  "./tui/format": {
134
140
  "types": "./dist/tui/format.d.mts",
135
141
  "import": "./dist/tui/format.mjs",
@@ -159,11 +165,11 @@
159
165
  "dependencies": {
160
166
  "@deepseek-ai/cordis": "4.0.1",
161
167
  "typebox": "1.3.16",
162
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.39",
163
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.39",
164
- "@agimon-ai/doompi-ui": "0.0.1-alpha.39",
165
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.0",
166
- "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.2"
168
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.40",
169
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.40",
170
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.40",
171
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.1",
172
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.3"
167
173
  },
168
174
  "devDependencies": {
169
175
  "@earendil-works/pi-coding-agent": "0.84.3",
@@ -195,8 +201,14 @@
195
201
  },
196
202
  "doompiWeb": {
197
203
  "pluginId": "task",
198
- "channels": [],
199
- "client": "./web/index.ts"
204
+ "channels": [
205
+ "task_graph"
206
+ ],
207
+ "client": "./web/index.ts",
208
+ "hub": {
209
+ "entry": "./src/exports/webHub.ts",
210
+ "dist": "./dist/webHub.mjs"
211
+ }
200
212
  },
201
213
  "pi": {
202
214
  "extensions": [
@@ -0,0 +1,24 @@
1
+ /** Browser-safe task view published by the cockpit hub channel. */
2
+ export const TASKS_CHANNEL_TYPE = 'task_graph';
3
+
4
+ export type WebTaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'deleted';
5
+
6
+ export interface WebTask {
7
+ id: number;
8
+ subject: string;
9
+ description?: string;
10
+ activeForm?: string;
11
+ status: WebTaskStatus;
12
+ blockedBy: number[];
13
+ owner?: string;
14
+ updatedAt?: string;
15
+ delegation?: {
16
+ agent?: string;
17
+ state?: string;
18
+ };
19
+ }
20
+
21
+ export interface WebTasksPayload {
22
+ tasks: WebTask[];
23
+ rev: number;
24
+ }
@@ -0,0 +1,207 @@
1
+ import {
2
+ Button,
3
+ ChevronDownIcon,
4
+ ChevronRightIcon,
5
+ Dot,
6
+ Textarea,
7
+ type DotTone,
8
+ } from '@agimon-ai/doompi-web-components';
9
+ import type { WebPluginSlotProps } from '@agimon-ai/doompi-web-contracts';
10
+ import { useStore } from '@tanstack/react-store';
11
+ import { useState } from 'react';
12
+ import type { WebTask } from '../src/types/webTasks.ts';
13
+ import { requestTaskInstruction, requestTaskRemoval, tasks, type TaskInstructionKind } from './tasksStore.ts';
14
+
15
+ const STATUS_TONE: Readonly<Record<WebTask['status'], DotTone>> = {
16
+ pending: 'muted',
17
+ in_progress: 'yellow',
18
+ completed: 'green',
19
+ failed: 'red',
20
+ deleted: 'muted',
21
+ };
22
+
23
+ interface EditorState {
24
+ taskId: number;
25
+ kind: TaskInstructionKind;
26
+ value: string;
27
+ }
28
+
29
+ function TaskRow({
30
+ task,
31
+ sessionId,
32
+ sendSessionFrame,
33
+ editor,
34
+ setEditor,
35
+ }: {
36
+ task: WebTask;
37
+ sessionId: string;
38
+ sendSessionFrame: WebPluginSlotProps['sendSessionFrame'];
39
+ editor: EditorState | undefined;
40
+ setEditor: (editor: EditorState | undefined) => void;
41
+ }) {
42
+ const editing = editor?.taskId === task.id;
43
+ const detail = task.status === 'in_progress' ? task.activeForm : task.description;
44
+ return (
45
+ <div
46
+ data-testid={`activity-task-${task.id}`}
47
+ data-task-status={task.status}
48
+ className="flex flex-col gap-1 rounded-[5px] px-1 py-1"
49
+ >
50
+ <div className="flex min-w-0 items-center gap-1.5">
51
+ <Dot tone={STATUS_TONE[task.status]} pulse={task.status === 'in_progress'} />
52
+ <span
53
+ className={`min-w-0 flex-1 truncate text-[10px] font-bold ${task.status === 'failed' ? 'text-doom-red' : task.status === 'completed' ? 'text-doom-dim' : 'text-doom-hi'}`}
54
+ >
55
+ <span className="text-doom-faint">#{task.id}</span> {task.subject}
56
+ </span>
57
+ <span className="shrink-0 text-[8px] text-doom-faint">{task.status.replace('_', ' ')}</span>
58
+ </div>
59
+ {detail || task.delegation?.agent || task.blockedBy.length > 0 ? (
60
+ <span className="truncate pl-3 text-[9px] text-doom-faint">
61
+ {task.delegation?.agent ? `[${task.delegation.agent}] ` : ''}
62
+ {detail ?? ''}
63
+ {task.blockedBy.length > 0 ? ` · blocked by ${task.blockedBy.map((id) => `#${id}`).join(', ')}` : ''}
64
+ </span>
65
+ ) : null}
66
+ {task.status !== 'completed' && task.status !== 'deleted' ? (
67
+ <div className="flex items-center gap-2 pl-3">
68
+ <Button
69
+ variant="link"
70
+ size="xs"
71
+ className="h-auto px-0 text-[8px]"
72
+ onClick={() => setEditor({ taskId: task.id, kind: 'edit', value: '' })}
73
+ >
74
+ edit
75
+ </Button>
76
+ <Button
77
+ variant="link"
78
+ size="xs"
79
+ className="h-auto px-0 text-[8px]"
80
+ onClick={() => setEditor({ taskId: task.id, kind: 'note', value: '' })}
81
+ >
82
+ note
83
+ </Button>
84
+ <Button
85
+ variant="link"
86
+ size="xs"
87
+ className="h-auto px-0 text-[8px] text-doom-red"
88
+ onClick={() => requestTaskRemoval(sendSessionFrame, sessionId, task.id)}
89
+ >
90
+ remove
91
+ </Button>
92
+ </div>
93
+ ) : null}
94
+ {editing ? (
95
+ <form
96
+ className="flex flex-col gap-1 pl-3"
97
+ onSubmit={(event) => {
98
+ event.preventDefault();
99
+ if (!editor.value.trim()) return;
100
+ requestTaskInstruction(sendSessionFrame, sessionId, task.id, editor.kind, editor.value);
101
+ setEditor(undefined);
102
+ }}
103
+ >
104
+ <Textarea
105
+ autoFocus
106
+ rows={2}
107
+ size="sm"
108
+ value={editor.value}
109
+ placeholder={editor.kind === 'note' ? 'note for the agent' : 'what should change'}
110
+ onChange={(event) => setEditor({ ...editor, value: event.target.value })}
111
+ className="text-[9px]"
112
+ />
113
+ <div className="flex gap-2">
114
+ <Button type="submit" variant="link" size="xs" className="h-auto px-0 text-[8px]">
115
+ send
116
+ </Button>
117
+ <Button
118
+ type="button"
119
+ variant="link"
120
+ size="xs"
121
+ className="h-auto px-0 text-[8px] text-doom-faint"
122
+ onClick={() => setEditor(undefined)}
123
+ >
124
+ cancel
125
+ </Button>
126
+ </div>
127
+ </form>
128
+ ) : null}
129
+ </div>
130
+ );
131
+ }
132
+
133
+ /** Session task graph in the activity dock. Empty graphs have no cockpit presence. */
134
+ export function TasksActivitySection({ sessionId, sendSessionFrame }: WebPluginSlotProps) {
135
+ const sessionTasks = useStore(tasks.store, (state) => tasks.select(state, sessionId).tasks);
136
+ const [historyOpen, setHistoryOpen] = useState(false);
137
+ const [editor, setEditor] = useState<EditorState>();
138
+ if (sessionId === null || sessionTasks.length === 0) return null;
139
+
140
+ const active = sessionTasks.filter((task) => task.status === 'pending' || task.status === 'in_progress');
141
+ const history = sessionTasks.filter((task) => task.status === 'completed' || task.status === 'failed');
142
+ return (
143
+ <section data-testid="activity-tasks" className="flex flex-col gap-2 border-b border-doom-border-soft px-3 py-3">
144
+ <div className="flex items-center gap-2 px-1">
145
+ <span
146
+ aria-hidden
147
+ className={
148
+ active.length > 0
149
+ ? 'animate-pulse text-[11px] font-bold text-doom-yellow'
150
+ : 'text-[11px] font-bold text-doom-faint'
151
+ }
152
+ >
153
+ #
154
+ </span>
155
+ <span className="flex-1 text-[11px] font-bold text-doom-text">tasks</span>
156
+ <span className="text-[9px] text-doom-faint">{active.length} active</span>
157
+ </div>
158
+ {active.length > 0 ? (
159
+ <div className="flex flex-col gap-0.5">
160
+ {active.map((task) => (
161
+ <TaskRow
162
+ key={task.id}
163
+ task={task}
164
+ sessionId={sessionId}
165
+ sendSessionFrame={sendSessionFrame}
166
+ editor={editor}
167
+ setEditor={setEditor}
168
+ />
169
+ ))}
170
+ </div>
171
+ ) : null}
172
+ {history.length > 0 ? (
173
+ <div className="flex flex-col gap-0.5">
174
+ <Button
175
+ variant="ghost"
176
+ size="xs"
177
+ aria-expanded={historyOpen}
178
+ onClick={() => setHistoryOpen((open) => !open)}
179
+ className="justify-start gap-1.5 rounded-[5px] px-1 py-0.5 hover:bg-doom-panel"
180
+ >
181
+ {historyOpen ? (
182
+ <ChevronDownIcon className="h-2.5 w-2.5 text-doom-faint" />
183
+ ) : (
184
+ <ChevronRightIcon className="h-2.5 w-2.5 text-doom-faint" />
185
+ )}
186
+ <span className="text-[10px] font-bold text-doom-dim">session</span>
187
+ <span className="text-[9px] text-doom-faint">{history.length}</span>
188
+ </Button>
189
+ {historyOpen ? (
190
+ <div className="flex flex-col gap-0.5 pl-2">
191
+ {history.map((task) => (
192
+ <TaskRow
193
+ key={task.id}
194
+ task={task}
195
+ sessionId={sessionId}
196
+ sendSessionFrame={sendSessionFrame}
197
+ editor={editor}
198
+ setEditor={setEditor}
199
+ />
200
+ ))}
201
+ </div>
202
+ ) : null}
203
+ </div>
204
+ ) : null}
205
+ </section>
206
+ );
207
+ }
package/web/index.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import { defineWebPlugin } from '@agimon-ai/doompi-web-contracts';
2
+ import { TasksActivitySection } from './TasksActivitySection.tsx';
2
3
  import { TaskToolMessage } from './TaskToolMessage.tsx';
4
+ import { tasksChannel } from './tasksStore.ts';
3
5
 
4
6
  /**
5
- * This package's cockpit presence: the task tool's timeline card, the web
6
- * half of src/tui/format.ts (renderTaskCall and renderTaskResult).
7
+ * This package's cockpit presence: the live session task graph in the activity
8
+ * dock and the task tool's timeline card.
7
9
  */
8
10
  export const webPlugin = defineWebPlugin({
9
11
  id: 'task',
12
+ channels: [tasksChannel],
13
+ activitySections: [{ id: 'tasks', component: TasksActivitySection }],
10
14
  toolRenderers: [{ tools: ['task'], message: TaskToolMessage }],
11
15
  });
@@ -0,0 +1,72 @@
1
+ import { defineSessionStore, type SessionFrameSender } from '@agimon-ai/doompi-web-contracts';
2
+ import { TASKS_CHANNEL_TYPE, type WebTask, type WebTasksPayload } from '../src/types/webTasks.ts';
3
+
4
+ export interface TasksSession {
5
+ tasks: WebTask[];
6
+ rev: number;
7
+ }
8
+
9
+ export const tasks = defineSessionStore<TasksSession>({ tasks: [], rev: 0 });
10
+
11
+ function isRecord(value: unknown): value is Record<string, unknown> {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+
15
+ function parseTask(value: unknown): WebTask | null {
16
+ if (!isRecord(value) || typeof value.id !== 'number' || typeof value.subject !== 'string') return null;
17
+ if (!['pending', 'in_progress', 'completed', 'failed'].includes(String(value.status))) return null;
18
+ const task: WebTask = {
19
+ id: value.id,
20
+ subject: value.subject,
21
+ status: value.status as WebTask['status'],
22
+ blockedBy: Array.isArray(value.blockedBy)
23
+ ? value.blockedBy.filter((id): id is number => typeof id === 'number')
24
+ : [],
25
+ };
26
+ if (typeof value.description === 'string') task.description = value.description;
27
+ if (typeof value.activeForm === 'string') task.activeForm = value.activeForm;
28
+ if (typeof value.owner === 'string') task.owner = value.owner;
29
+ if (typeof value.updatedAt === 'string') task.updatedAt = value.updatedAt;
30
+ if (isRecord(value.delegation)) {
31
+ task.delegation = {};
32
+ if (typeof value.delegation.agent === 'string') task.delegation.agent = value.delegation.agent;
33
+ if (typeof value.delegation.state === 'string') task.delegation.state = value.delegation.state;
34
+ }
35
+ return task;
36
+ }
37
+
38
+ export const tasksChannel = tasks.channel<WebTasksPayload>({
39
+ channel: TASKS_CHANNEL_TYPE,
40
+ parse(input) {
41
+ if (!isRecord(input) || !Array.isArray(input.tasks) || typeof input.rev !== 'number') return null;
42
+ return { tasks: input.tasks.map(parseTask).filter((task): task is WebTask => task !== null), rev: input.rev };
43
+ },
44
+ reduce: (_current, payload) => payload,
45
+ });
46
+
47
+ export type TaskInstructionKind = 'edit' | 'note';
48
+
49
+ export function taskInstruction(taskId: number, kind: TaskInstructionKind, instruction: string): string {
50
+ const detail = instruction.trim();
51
+ if (kind === 'note') {
52
+ return `Send this note to the agent working on task #${taskId}, then update the task if needed: ${detail}`;
53
+ }
54
+ return `Update task #${taskId} according to this instruction. Preserve a valid dependency graph: ${detail}`;
55
+ }
56
+
57
+ export function requestTaskInstruction(
58
+ send: SessionFrameSender,
59
+ sessionId: string,
60
+ taskId: number,
61
+ kind: TaskInstructionKind,
62
+ instruction: string,
63
+ ): void {
64
+ send(sessionId, { type: 'prompt', message: taskInstruction(taskId, kind, instruction) });
65
+ }
66
+
67
+ export function requestTaskRemoval(send: SessionFrameSender, sessionId: string, taskId: number): void {
68
+ send(sessionId, {
69
+ type: 'prompt',
70
+ message: `Remove task #${taskId}. Also remove or repair every dependency that references it so the remaining task graph is valid.`,
71
+ });
72
+ }