@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 +2 -5
- package/dist/adapters/webTasksChannel.cjs +2 -0
- package/dist/adapters/webTasksChannel.cjs.map +1 -0
- package/dist/adapters/webTasksChannel.d.cts +20 -0
- package/dist/adapters/webTasksChannel.d.cts.map +1 -0
- package/dist/adapters/webTasksChannel.d.mts +20 -0
- package/dist/adapters/webTasksChannel.d.mts.map +1 -0
- package/dist/adapters/webTasksChannel.mjs +2 -0
- package/dist/adapters/webTasksChannel.mjs.map +1 -0
- package/dist/types/webTasks.cjs +2 -0
- package/dist/types/webTasks.cjs.map +1 -0
- package/dist/types/webTasks.mjs +2 -0
- package/dist/types/webTasks.mjs.map +1 -0
- package/dist/webHub.cjs +1 -0
- package/dist/webHub.d.cts +2 -0
- package/dist/webHub.d.mts +2 -0
- package/dist/webHub.mjs +1 -0
- package/package.json +20 -8
- package/src/types/webTasks.ts +24 -0
- package/web/TasksActivitySection.tsx +207 -0
- package/web/index.ts +6 -2
- package/web/tasksStore.ts +72 -0
package/README.md
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
# @agimon-ai/doompi-task
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/dist/webHub.cjs
ADDED
|
@@ -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;
|
package/dist/webHub.mjs
ADDED
|
@@ -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.
|
|
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.
|
|
163
|
-
"@agimon-ai/doompi-
|
|
164
|
-
"@agimon-ai/doompi-
|
|
165
|
-
"@agimon-ai/doompi-web-components": "0.0.1-alpha.
|
|
166
|
-
"@agimon-ai/doompi-web-contracts": "0.0.1-alpha.
|
|
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
|
-
|
|
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
|
|
6
|
-
*
|
|
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
|
+
}
|