@loomhelm/multi-task-flow 0.2.8 → 0.2.10

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.
Files changed (62) hide show
  1. package/README.md +14 -14
  2. package/package.json +2 -2
  3. package/src/cli/flow.ts +13 -4
  4. package/src/cli/service.ts +26 -0
  5. package/src/cli.ts +3 -1
  6. package/toolkit/artifact-manifest.json +69 -73
  7. package/toolkit/package.json +1 -1
  8. package/toolkit/skills/bin/check_source_policy.ts +13 -7
  9. package/toolkit/skills/bin/multi_task_platform_gate.ts +2 -2
  10. package/toolkit/skills/bin/tests/core/package_scripts.test.ts +1 -1
  11. package/toolkit/skills/bin/tests/plan/multi-task-flow.test.ts +3 -3
  12. package/toolkit/skills/catalog.json +0 -1
  13. package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/data-and-scripts.md +8 -6
  14. package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/execution.md +1 -1
  15. package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/plan-revision-and-change-audit.md +1 -1
  16. package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/web-template.md +1 -1
  17. package/toolkit/skills/docs/vendor/dot-codex/specs/a4ge9-stable-preview-runtime/IMPLEMENTATION.md +0 -2
  18. package/toolkit/skills/docs/vendor/dot-codex/specs/a4ge9-stable-preview-runtime/SPEC.md +0 -1
  19. package/toolkit/skills/docs/vendor/dot-codex/specs/dev-plan-standalone-typescript-finalizer/IMPLEMENTATION.md +1 -1
  20. package/toolkit/skills/docs/vendor/dot-codex/specs/multi-task-flow/IMPLEMENTATION.md +3 -3
  21. package/toolkit/skills/docs/vendor/dot-codex/specs/multi-task-flow/SCHEMA.md +4 -4
  22. package/toolkit/skills/docs/vendor/dot-codex/specs/stable-preview-runtime/IMPLEMENTATION.md +0 -2
  23. package/toolkit/skills/docs/vendor/dot-codex/specs/stable-preview-runtime/SPEC.md +0 -1
  24. package/toolkit/skills/multi-task-flow/SKILL.md +65 -47
  25. package/toolkit/skills/multi-task-flow/dist/app.css +1 -1
  26. package/toolkit/skills/multi-task-flow/dist/app.js +74 -74
  27. package/toolkit/skills/multi-task-flow/dist/index.html +3 -3
  28. package/toolkit/skills/multi-task-flow/lib/atomic-orchestration.ts +1 -0
  29. package/toolkit/skills/multi-task-flow/lib/engine.ts +65 -7
  30. package/toolkit/skills/multi-task-flow/lib/package-source.ts +5 -5
  31. package/toolkit/skills/multi-task-flow/lib/schema.ts +82 -2
  32. package/toolkit/skills/multi-task-flow/lib/service-pages.ts +1 -26
  33. package/toolkit/skills/multi-task-flow/lib/view.ts +16 -0
  34. package/toolkit/skills/multi-task-flow/references/shared-viewer.md +1 -1
  35. package/toolkit/skills/multi-task-flow/scripts/check-client-skills.ts +7 -4
  36. package/toolkit/skills/multi-task-flow/scripts/check-skills-installation.ts +14 -8
  37. package/toolkit/skills/multi-task-flow/scripts/import-client-skills.ts +1 -1
  38. package/toolkit/skills/multi-task-flow/scripts/package-client-skills.ts +4 -4
  39. package/toolkit/skills/multi-task-flow/scripts/test-viewer-source.ts +1 -1
  40. package/toolkit/skills/multi-task-flow/tests/atomic-orchestration-schema.test.ts +5 -0
  41. package/toolkit/skills/multi-task-flow/tests/browser/README.md +1 -1
  42. package/toolkit/skills/multi-task-flow/tests/check-skills-installation.test.ts +12 -38
  43. package/toolkit/skills/multi-task-flow/tests/delivery-topology.test.ts +33 -0
  44. package/toolkit/skills/multi-task-flow/tests/orchestrate.test.ts +3 -5
  45. package/toolkit/skills/multi-task-flow/tests/service-fixture.ts +10 -2
  46. package/toolkit/skills/multi-task-flow/tests/shared-service.test.ts +13 -0
  47. package/toolkit/skills/multi-task-flow/tests/ui/details-content.test.ts +9 -0
  48. package/toolkit/skills/multi-task-flow/tests/ui/responsive-width-contract.test.ts +25 -1
  49. package/toolkit/skills/multi-task-flow/tests/ui/task-context-menu.test.ts +2 -1
  50. package/toolkit/skills/multi-task-flow/web/app.css +64 -9
  51. package/toolkit/skills/multi-task-flow/web/app.ts +33 -14
  52. package/toolkit/skills/multi-task-flow/web/atomic-graph-view.tsx +1 -1
  53. package/toolkit/skills/multi-task-flow/web/details-content.ts +49 -38
  54. package/toolkit/skills/multi-task-flow/web/graph-view.tsx +1 -1
  55. package/toolkit/skills/multi-task-flow/web/index.html +3 -3
  56. package/toolkit/skills/multi-task-flow/web/model.ts +1 -1
  57. package/toolkit/skills/multi-task-flow/web/timeline.ts +11 -8
  58. package/toolkit/skills/package.json +2 -2
  59. package/toolkit/skills/source-manifest.json +693 -400
  60. package/toolkit/skills/support-manifest.json +1 -0
  61. package/toolkit/skills/multi-task-flow/scripts/serve.ts +0 -218
  62. package/toolkit/skills/multi-task-flow/tests/serve.test.ts +0 -194
@@ -32,6 +32,7 @@ function clearDetails(): HTMLElement {
32
32
  const container = document.getElementById("details-content")!;
33
33
  document.getElementById("details-summary")?.replaceChildren();
34
34
  delete container.dataset.selection;
35
+ delete container.dataset.detailKind;
35
36
  container.replaceChildren();
36
37
  container.scrollTop = 0;
37
38
  container.className = "details-empty";
@@ -159,6 +160,7 @@ export function createDetailContent(context: DetailContentContext) {
159
160
  const selection = `${kind}:${id}`;
160
161
  const top = container.dataset.selection === selection ? container.scrollTop : 0;
161
162
  container.dataset.selection = selection;
163
+ container.dataset.detailKind = kind;
162
164
  container.replaceChildren();
163
165
  container.className = "";
164
166
  const summary = document.getElementById("details-summary")!;
@@ -169,17 +171,18 @@ export function createDetailContent(context: DetailContentContext) {
169
171
  const data = context.getData();
170
172
  if (!data) return;
171
173
  const { container, restore } = begin("task", task.id, task.title, context.labelTask(task));
174
+ const facts = text("div", "", "detail-group-facts");
172
175
  const currentGroup = data.groups.find((group) => group.id === task.group_id)?.title ?? task.group_id;
173
176
  const executionGroups = [...new Set(data.actual_intervals.filter((row) => row.task_id === task.id).map((row) => row.group_id))].map((id) => data.groups.find((group) => group.id === id)?.title ?? id);
174
- field(container, "所属交付单元", currentGroup);
175
- if (executionGroups.length > 1) field(container, "执行交付单元历史", executionGroups.join("、"));
176
- field(container, "类型", taskKindLabel(task.kind));
177
+ field(facts, "所属交付单元", currentGroup);
178
+ if (executionGroups.length > 1) field(facts, "执行交付单元历史", executionGroups.join("、"));
179
+ field(facts, "类型", taskKindLabel(task.kind));
177
180
  if (context.profileContext) {
178
181
  const profileContext = context.profileContext();
179
- field(container, "执行配置", createProfileEditor(profileContext, "task", task.id, task.execution_profile ?? null, task.effective_execution_profile ?? data.orchestration.execution_profile ?? null, data.profile_read_only ? "当前视图只读或尚未迁移" : task.profile_freeze_reason ?? (task.thread_id ? "对话已创建" : null), task.execution_profile_source === "recommendation" ? `按任务类型 ${task.kind} 推荐 · ${task.execution_profile_rule ?? "kind-v1"}` : null));
182
+ field(facts, "执行配置", createProfileEditor(profileContext, "task", task.id, task.execution_profile ?? null, task.effective_execution_profile ?? data.orchestration.execution_profile ?? null, data.profile_read_only ? "当前视图只读或尚未迁移" : task.profile_freeze_reason ?? (task.thread_id ? "对话已创建" : null), task.execution_profile_source === "recommendation" ? `按任务类型 ${task.kind} 推荐 · ${task.execution_profile_rule ?? "kind-v1"}` : null));
180
183
  }
181
- if (task.disposition && task.disposition !== "active") field(container, "任务处置", task.disposition);
182
- if (task.is_held && task.hold_reason) field(container, "计划修订暂停", task.hold_reason);
184
+ if (task.disposition && task.disposition !== "active") field(facts, "任务处置", task.disposition);
185
+ if (task.is_held && task.hold_reason) field(facts, "计划修订暂停", task.hold_reason);
183
186
  const node = data.nodes.find((item) => item.kind === "task" && item.ref_id === task.id);
184
187
  const projection = buildGraphProjection(data);
185
188
  const predecessors = dependenciesFor(projection, node?.id, "incoming", data);
@@ -192,24 +195,24 @@ export function createDetailContent(context: DetailContentContext) {
192
195
  return { name: source?.title ?? edge.from_atom_id, state: "", status: source?.status ?? "unknown", completedAt: source?.completed_at };
193
196
  });
194
197
  }, null, { kind: "task", id: task.id }));
195
- field(container, "前驱", renderDependencyTooltip(predecessors, predecessors.length ? `查看 ${predecessors.length} 项依赖` : "无依赖"));
196
- field(container, "后继", renderDependencyTooltip(successors, successors.length ? `查看 ${successors.length} 项依赖` : "无依赖"));
198
+ container.append(facts);
199
+ field(facts, "前驱", renderDependencyTooltip(predecessors, predecessors.length ? `查看 ${predecessors.length} 项依赖` : "无依赖"));
200
+ field(facts, "后继", renderDependencyTooltip(successors, successors.length ? `查看 ${successors.length} 项依赖` : "无依赖"));
197
201
  const projectedNode = projection.nodes.find((item) => item.sourceNodeId === node?.id);
198
202
  const plannedGroup = projectedNode?.plannedGroupId && projectedNode.plannedGroupId !== task.group_id ? data.groups.find((group) => group.id === projectedNode.plannedGroupId)?.title ?? projectedNode.plannedGroupId : null;
199
- if (plannedGroup) field(container, "计划归属交付单元", plannedGroup);
200
- if (task.blocker) field(container, "等待原因", task.blocker);
201
- field(container, "开始时间", renderTimestamp(task.started_at));
202
- field(container, "完成时间", renderTimestamp(task.finished_at));
203
- field(container, "持续时间", durationValue(task.started_at, task.finished_at));
203
+ if (plannedGroup) field(facts, "计划归属交付单元", plannedGroup);
204
+ if (task.blocker) field(facts, "等待原因", task.blocker);
205
+ field(facts, "开始时间", renderTimestamp(task.started_at));
206
+ field(facts, "完成时间", renderTimestamp(task.finished_at));
207
+ field(facts, "持续时间", durationValue(task.started_at, task.finished_at));
204
208
  const output = text("div", "", "detail-agent-output");
205
209
  output.dataset.outputTaskId = task.id; output.dataset.outputFull = "true";
206
210
  paintOutput(output, task.latest_agent_output ?? initialOutput(task.thread_id), true);
207
- field(container, "最新 Agent 输出", output);
208
- if (task.state === "agent_idle") field(container, "运行时提示", "Agent 当前没有活跃 turn,可能需要人工恢复");
211
+ if (task.state === "agent_idle") field(facts, "运行时提示", "Agent 当前没有活跃 turn,可能需要人工恢复");
209
212
  if (task.state === "agent_idle") {
210
213
  const observed = task.latest_agent_output;
211
- if (observed?.observed_at) field(container, "运行时观察时间", renderTimestamp(observed.observed_at));
212
- if (observed?.runtime_turn_id) field(container, "观察 Turn", observed.runtime_turn_id);
214
+ if (observed?.observed_at) field(facts, "运行时观察时间", renderTimestamp(observed.observed_at));
215
+ if (observed?.runtime_turn_id) field(facts, "观察 Turn", observed.runtime_turn_id);
213
216
  }
214
217
  const conversation = text("div", "", "detail-conversation");
215
218
  const conversationActions = text("div", "", "detail-conversation-actions");
@@ -221,18 +224,19 @@ export function createDetailContent(context: DetailContentContext) {
221
224
  const presentation = archivePresentation(task);
222
225
  conversation.append(text("p", `${presentation.label}:${presentation.description}`, "detail-conversation-notice"));
223
226
  }
224
- field(container, "任务对话", conversation);
225
- field(container, "执行 Agent", task.execution_owner === "main" ? "Main Agent" : task.execution_owner === "delivery_agent" ? "交付 Agent" : task.execution_owner === "task_agent" ? "Task Agent" : "未声明");
226
- field(container, "对话策略", task.conversation_policy ?? "未声明");
227
- field(container, "交付策略", task.delivery_policy ?? "未声明");
228
- field(container, "调度状态", task.dispatch_readiness === "blocked" ? `不可调度:${task.dispatch_diagnostics?.map((item) => item.message).join(";") || "未知原因"}` : task.dispatch_readiness === "ready" ? "可调度" : "未知");
227
+ field(facts, "任务对话", conversation);
228
+ field(facts, "执行 Agent", task.execution_owner === "main" ? "Main Agent" : task.execution_owner === "delivery_agent" ? "交付 Agent" : task.execution_owner === "task_agent" ? "Task Agent" : "未声明");
229
+ field(facts, "对话策略", task.conversation_policy ?? "未声明");
230
+ field(facts, "交付策略", task.delivery_policy ?? "未声明");
231
+ field(facts, "调度状态", task.dispatch_readiness === "blocked" ? `不可调度:${task.dispatch_diagnostics?.map((item) => item.message).join(";") || "未知原因"}` : task.dispatch_readiness === "ready" ? "可调度" : "未知");
229
232
  const pr = context.safeExternal(task.pr?.url ?? null);
230
- if (pr) field(container, "子 PR", link("查看 PR", pr));
231
- field(container, "任务要求", requirementsValue(task.requirements_path, "task", task.id));
233
+ if (pr) field(facts, "子 PR", link("查看 PR", pr));
234
+ field(facts, "任务要求", requirementsValue(task.requirements_path, "task", task.id));
232
235
  for (const follow of data.follow_prs.filter((item) => item.origin_task_id === task.id)) {
233
236
  const url = context.safeExternal(follow.pr.url);
234
- field(container, "Follow PR", url ? link(context.labels[follow.state] ?? follow.state, url) : context.labels[follow.state] ?? follow.state);
237
+ field(facts, "Follow PR", url ? link(context.labels[follow.state] ?? follow.state, url) : context.labels[follow.state] ?? follow.state);
235
238
  }
239
+ field(facts, "Agent 输出", output);
236
240
  restore();
237
241
  };
238
242
 
@@ -255,23 +259,30 @@ export function createDetailContent(context: DetailContentContext) {
255
259
  }
256
260
  return items;
257
261
  }, focusedAtomId, { kind: "group", id: group.id }));
258
- if (group.coordination_execution?.state === "waiting") field(container, "协调等待", group.coordination_execution.reason ?? "等待贡献任务");
259
- if (group.coordination_execution?.state === "blocked") field(container, "协调阻塞", group.coordination_execution.reason ?? "未知原因");
260
- field(container, "交付协调者", coordinatorValue(group));
261
- field(container, "运行就绪", readinessValue(group));
262
- field(container, "任务数", `${tasks.length} 个任务`);
263
- if (historicalTaskIds.length) field(container, "历史执行任务", historicalTaskIds.map((id) => data.tasks.find((task) => task.id === id)?.title ?? id).join("、"));
264
- field(container, "持续时间", durationValue(group.started_at ?? null, group.finished_at ?? null));
262
+ const facts = text("div", "", "detail-group-facts");
263
+ container.append(facts);
264
+ if (group.coordination_execution?.state === "waiting") field(facts, "协调等待", group.coordination_execution.reason ?? "等待贡献任务");
265
+ if (group.coordination_execution?.state === "blocked") field(facts, "协调阻塞", group.coordination_execution.reason ?? "未知原因");
266
+ field(facts, "交付协调者", coordinatorValue(group));
267
+ field(facts, "运行就绪", readinessValue(group));
268
+ field(facts, "任务数", `${tasks.length} 个任务`);
269
+ if (historicalTaskIds.length) field(facts, "历史执行任务", historicalTaskIds.map((id) => data.tasks.find((task) => task.id === id)?.title ?? id).join("、"));
270
+ field(facts, "持续时间", durationValue(group.started_at ?? null, group.finished_at ?? null));
265
271
  if (context.profileContext) {
266
272
  const profileContext = context.profileContext();
267
- field(container, "执行配置", createProfileEditor(profileContext, "group", group.id, group.execution_profile ?? null, group.effective_execution_profile ?? data.orchestration.execution_profile ?? null, data.profile_read_only ? "当前视图只读或尚未迁移" : group.profile_freeze_reason ?? (group.agent_thread_id ? "对话已创建" : null)));
273
+ field(facts, "执行配置", createProfileEditor(profileContext, "group", group.id, group.execution_profile ?? null, group.effective_execution_profile ?? data.orchestration.execution_profile ?? null, data.profile_read_only ? "当前视图只读或尚未迁移" : group.profile_freeze_reason ?? (group.agent_thread_id ? "对话已创建" : null)));
268
274
  }
269
- field(container, "交付单元对话", group.agent_thread_id ? link("打开对话", conversationUrl(group.agent_thread_id)) : "尚未创建");
270
- field(container, "交付 Agent", group.delivery_unit_kind === "bootstrap" ? "Main Agent(Bootstrap 特例)" : group.delivery_agent_readiness === "bound" ? "已绑定" : "未绑定");
271
- field(container, "调度状态", group.dispatch_readiness === "blocked" ? `不可调度:${group.dispatch_diagnostics?.map((item) => item.message).join(";") || "未知原因"}` : group.dispatch_readiness === "ready" ? "可调度" : "未知");
275
+ field(facts, "交付单元对话", group.agent_thread_id ? link("打开对话", conversationUrl(group.agent_thread_id)) : "尚未创建");
276
+ field(facts, "交付 Agent", group.delivery_unit_kind === "bootstrap" ? "Main Agent(Bootstrap 特例)" : group.delivery_agent_readiness === "bound" ? "已绑定" : "未绑定");
277
+ field(facts, "调度状态", group.dispatch_readiness === "blocked" ? `不可调度:${group.dispatch_diagnostics?.map((item) => item.message).join(";") || "未知原因"}` : group.dispatch_readiness === "ready" ? "可调度" : "未知");
272
278
  const pr = context.safeExternal(group.parent_pr?.url ?? null);
273
- field(container, "父 PR", pr ? link("查看 PR", pr) : "尚未创建");
274
- field(container, "交付单元要求", requirementsValue(group.requirements_path, "group", group.id));
279
+ field(facts, "父 PR", pr ? link("查看 PR", pr) : "尚未创建");
280
+ field(facts, "交付单元要求", requirementsValue(group.requirements_path, "group", group.id));
281
+ const output = text("div", "", "detail-agent-output");
282
+ output.dataset.outputGroupId = group.id;
283
+ output.dataset.outputFull = "true";
284
+ paintOutput(output, group.latest_agent_output ?? initialOutput(group.agent_thread_id), true);
285
+ field(facts, "Agent 输出", output);
275
286
  restore();
276
287
  };
277
288
 
@@ -65,7 +65,7 @@ export function createGraphController(context: GraphContext, calculateLayout = l
65
65
  signature=key;layout=null;const serial=++generation;pending=true;error='';update?.();
66
66
  calculateLayout(next,direction).then(result=>{
67
67
  if(serial!==generation)return;const byId=new Map(projection!.nodes.map(n=>[n.id,n]));layout={...result,nodes:result.nodes.map(p=>({...p,node:byId.get(p.node.id)!}))};pending=false;container.style.height = `${Math.max(420,result.height+80)}px`;update?.();
68
- requestAnimationFrame(()=>{if(serial!==generation)return;if(!viewportInitialized){instance?.setViewport({x:Math.max(24,(container.clientWidth-result.width)/2),y:24,zoom:1});context.setZoom(1);viewportInitialized=true;} highlightSelection();});
68
+ requestAnimationFrame(()=>{if(serial!==generation)return;if(!viewportInitialized){instance?.setViewport({x:Math.max(24,(container.clientWidth-result.width)/2),y:Math.max(24,(container.clientHeight-result.height)/2),zoom:1});context.setZoom(1);viewportInitialized=true;} highlightSelection();});
69
69
  }).catch(e=>{if(serial!==generation)return;pending=false;error=`依赖图布局失败:${e instanceof Error?e.message:String(e)}`;signature='';update?.();});
70
70
  }
71
71
  return {render,reset:()=>{container.replaceChildren();root.render(<Surface/>);},highlightSelection,zoomIn:()=>instance?.zoomIn(),zoomOut:()=>instance?.zoomOut(),fit:()=>instance?.fitView({padding:0.08}),rankDir:()=>direction==='DOWN'?'TB':'LR',projection:()=>projection};
@@ -50,6 +50,7 @@
50
50
  <button id="tab-tasks" type="button" role="tab" aria-selected="false" aria-controls="tasks-view" data-tab="tasks">任务</button>
51
51
  </div>
52
52
  <label class="search input"><i data-lucide="search" aria-hidden="true"></i><span class="sr-only">搜索任务</span><input id="search" type="search" placeholder="搜索任务"></label>
53
+ <div id="task-view-tools" class="task-view-tools" aria-label="任务视图工具"><span id="task-count" class="section-count"></span><div id="task-layout-switch" class="layout-switch" role="group" aria-label="对象布局"><button id="task-layout-board" type="button" class="icon-button layout-option" aria-label="看板布局" aria-describedby="task-layout-board-tooltip" aria-pressed="true"><i data-lucide="columns-3"></i><span id="task-layout-board-tooltip" class="layout-tooltip" role="tooltip">看板布局:按交付单元分列展示任务</span></button><button id="task-layout-grid" type="button" class="icon-button layout-option" aria-label="网格布局" aria-describedby="task-layout-grid-tooltip" aria-pressed="false"><i data-lucide="layout-grid"></i><span id="task-layout-grid-tooltip" class="layout-tooltip" role="tooltip">网格布局:平铺展示任务</span></button></div></div>
53
54
  </div>
54
55
  <div class="filters">
55
56
  <span class="filter-caption">筛选视图</span>
@@ -70,7 +71,6 @@
70
71
  <div id="connection-notice" class="notice" role="status" hidden></div>
71
72
  <div id="phase-notice" class="notice notice-neutral" role="status" hidden><span id="phase-notice-text"></span><button type="button" id="phase-jump" class="text-button">查看实际进度</button></div>
72
73
  <div id="operation-warning" class="notice" role="status" hidden></div>
73
- <div id="plan-state" class="notice plan-state" aria-label="计划与运行状态" hidden></div>
74
74
  <div id="plan-warning" class="notice notice-warning" role="status" hidden></div>
75
75
  <div id="load-error" class="notice notice-error" role="alert" hidden></div>
76
76
  <div id="graph-view" class="canvas-view" role="tabpanel" aria-labelledby="tab-graph"><div id="graph-projection-warning" class="notice notice-warning" role="status" hidden></div><div id="graph" aria-label="任务依赖图"></div><div id="atomic-graph" aria-label="原子任务依赖图" hidden></div><div id="graph-a11y" class="graph-a11y" role="list" aria-label="依赖图节点列表"></div><div id="graph-empty" class="empty" hidden>暂无任务</div></div>
@@ -79,11 +79,11 @@
79
79
  </div>
80
80
  <div id="tasks-view" class="workspace-tasks" role="tabpanel" aria-labelledby="tab-tasks" hidden>
81
81
  <section id="group-section" class="task-list-section" aria-labelledby="group-heading"><div class="section-heading"><h2 id="group-heading">交付单元</h2></div><div id="group-list" class="task-list" role="list"></div></section>
82
- <section id="task-section" class="task-list-section" aria-labelledby="task-heading"><div class="section-heading"><h2 id="task-heading">任务</h2><div class="section-heading-actions"><span id="task-count" class="section-count"></span><div id="task-layout-switch" class="layout-switch" role="group" aria-label="对象布局"><button id="task-layout-board" type="button" class="icon-button layout-option" aria-label="看板布局" aria-describedby="task-layout-board-tooltip" aria-pressed="true"><i data-lucide="columns-3"></i><span id="task-layout-board-tooltip" class="layout-tooltip" role="tooltip">看板布局:按交付单元分列展示任务</span></button><button id="task-layout-grid" type="button" class="icon-button layout-option" aria-label="网格布局" aria-describedby="task-layout-grid-tooltip" aria-pressed="false"><i data-lucide="layout-grid"></i><span id="task-layout-grid-tooltip" class="layout-tooltip" role="tooltip">网格布局:平铺展示任务</span></button></div></div></div><div id="task-list" class="task-list" role="list"></div></section>
82
+ <section id="task-section" class="task-list-section"><div id="task-list" class="task-list" role="list"></div></section>
83
83
  </div>
84
84
  </div>
85
85
  <div id="details-backdrop" class="details-backdrop" hidden></div>
86
- <div class="details-slot" hidden><aside id="details" class="details" aria-label="详情" aria-hidden="true" tabindex="-1"><div class="details-heading"><button type="button" id="close-details" class="icon-button mobile-close" data-tooltip="关闭详情" aria-label="关闭详情"><i data-lucide="x"></i></button><div id="details-summary"></div></div><div id="details-content" class="details-empty"><span class="empty-rule" aria-hidden="true"></span><strong>选择交付单元或任务</strong><span>查看依赖、运行时间和原生会话链接</span></div></aside></div>
86
+ <div class="details-slot" hidden><div id="details-resize" class="details-resize" role="separator" aria-orientation="horizontal" aria-label="调整详情高度" tabindex="0"></div><aside id="details" class="details" aria-label="详情" aria-hidden="true" tabindex="-1"><div class="details-heading"><button type="button" id="close-details" class="icon-button mobile-close" data-tooltip="关闭详情" aria-label="关闭详情"><i data-lucide="x"></i></button><div id="details-summary"></div></div><div id="details-content" class="details-empty"><span class="empty-rule" aria-hidden="true"></span><strong>选择交付单元或任务</strong><span>查看依赖、运行时间和原生会话链接</span></div></aside></div>
87
87
  </section>
88
88
  </main>
89
89
  </div>
@@ -115,7 +115,7 @@ export type Task = {
115
115
  execution_profile_rule?: string | null;
116
116
  execution_owner?: "main" | "delivery_agent" | "task_agent" | null;
117
117
  conversation_policy?: "main_owned" | "delivery_owned" | "task_agent" | "none" | null;
118
- delivery_policy?: "parent_pr" | "child_pr" | "evidence" | "none" | null;
118
+ delivery_policy?: "parent_pr" | "child_pr" | "shared_delivery_unit" | "evidence" | "none" | null;
119
119
  dispatch_readiness?: "ready" | "blocked" | "unknown";
120
120
  dispatch_diagnostics?: { code: string; message: string }[];
121
121
  };
@@ -132,34 +132,37 @@ export function createTimelineController(context: TimelineContext) {
132
132
  const center = target.querySelector<HTMLElement>(".vis-panel.vis-center");
133
133
  if (!center || panBound.has(center)) return;
134
134
  panBound.add(center);
135
- let pan: { pointerId: number; originX: number; originY: number; start: number; end: number; width: number; active: boolean } | null = null;
135
+ let pan: { pointerId: number; originX: number; originY: number; originScrollTop: number; start: number; end: number; width: number; axis: "x" | "y" | null; active: boolean } | null = null;
136
136
  const isCurrent = (): boolean => (mode === "planned" ? plannedTimeline : actualTimeline) === timeline && (mode === "planned" ? plannedRenderSerial : actualRenderSerial) === renderSerial;
137
137
  const finish = (event: PointerEvent): void => {
138
138
  if (!pan || event.pointerId !== pan.pointerId) return;
139
139
  if (pan.active && center.hasPointerCapture(event.pointerId)) center.releasePointerCapture(event.pointerId);
140
140
  center.classList.remove("is-panning");
141
- if (pan.active && isCurrent()) syncTimelineRange(mode, timeline);
141
+ if (pan.active && pan.axis === "x" && isCurrent()) syncTimelineRange(mode, timeline);
142
142
  pan = null;
143
143
  };
144
144
  center.addEventListener("pointerdown", (event) => {
145
145
  if (!event.isPrimary || event.button !== 0 || (event.target instanceof HTMLElement && event.target.closest(".vis-item"))) return;
146
146
  const range = timeline.getWindow();
147
147
  if (!range.start || !range.end || center.clientWidth <= 0) return;
148
- pan = { pointerId: event.pointerId, originX: event.clientX, originY: event.clientY, start: range.start.getTime(), end: range.end.getTime(), width: center.clientWidth, active: false };
148
+ pan = { pointerId: event.pointerId, originX: event.clientX, originY: event.clientY, originScrollTop: center.scrollTop, start: range.start.getTime(), end: range.end.getTime(), width: center.clientWidth, axis: null, active: false };
149
149
  }, true);
150
150
  center.addEventListener("pointermove", (event) => {
151
151
  if (!pan || event.pointerId !== pan.pointerId || !isCurrent()) return;
152
152
  const deltaX = event.clientX - pan.originX;
153
153
  const deltaY = event.clientY - pan.originY;
154
154
  if (!pan.active) {
155
- if (Math.abs(deltaY) > Math.abs(deltaX)) { pan = null; return; }
156
- if (Math.abs(deltaX) < 4) return;
155
+ if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
156
+ pan.axis = Math.abs(deltaY) > Math.abs(deltaX) ? "y" : "x";
157
157
  pan.active = true;
158
158
  center.setPointerCapture(event.pointerId);
159
159
  center.classList.add("is-panning");
160
160
  }
161
- const shift = -deltaX * ((pan.end - pan.start) / pan.width);
162
- applyWindow(mode, timeline, new Date(pan.start + shift), new Date(pan.end + shift), false);
161
+ if (pan.axis === "y") center.scrollTop = Math.max(0, pan.originScrollTop - deltaY);
162
+ else {
163
+ const shift = -deltaX * ((pan.end - pan.start) / pan.width);
164
+ applyWindow(mode, timeline, new Date(pan.start + shift), new Date(pan.end + shift), false);
165
+ }
163
166
  event.preventDefault();
164
167
  event.stopPropagation();
165
168
  }, true);
@@ -205,7 +208,7 @@ export function createTimelineController(context: TimelineContext) {
205
208
  const initialWindow = requestedWindow.start && requestedWindow.end ? { start: new Date(requestedWindow.start), end: new Date(requestedWindow.end) } : fittedWindow.start && fittedWindow.end ? { start: fittedWindow.start, end: fittedWindow.end } : null;
206
209
  if (mode === "planned") plannedWindow = initialWindow; else actualWindow = initialWindow;
207
210
  target.style.height = `${Math.max(284, nodes.length * 28 + 76)}px`;
208
- const timeline = new Timeline(target, new DataSet(entries), groups, { editable: false, stack: false, groupOrder: "order", selectable: true, moveable: true, zoomable: true, horizontalScroll: false, zoomKey: "ctrlKey", ...(mode === "planned" ? { format: { minorLabels: { minute: "T+mm分", hour: "T+HH时" }, majorLabels: { day: "理论时间" } } } : {}), ...(requestedWindow.start && requestedWindow.end ? requestedWindow : fittedWindow) });
211
+ const timeline = new Timeline(target, new DataSet(entries), groups, { editable: false, stack: false, groupOrder: "order", orientation: { item: "top" }, selectable: true, moveable: true, zoomable: true, horizontalScroll: true, verticalScroll: true, zoomKey: "ctrlKey", ...(mode === "planned" ? { format: { minorLabels: { minute: "T+mm分", hour: "T+HH时" }, majorLabels: { day: "理论时间" } } } : {}), ...(requestedWindow.start && requestedWindow.end ? requestedWindow : fittedWindow) });
209
212
  if (mode === "planned") plannedTimeline = timeline; else actualTimeline = timeline;
210
213
  bindWheel(target, timeline, mode);
211
214
  bindPan(target, timeline, mode, renderSerial);
@@ -3,10 +3,10 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {
6
- "build": "bun run --cwd multi-task-flow build",
6
+ "build": "bun run --cwd multi-task-flow-runtime build",
7
7
  "test": "bun test ./bin/tests ./bin/native-tests",
8
8
  "skills:test": "bun test ./bin/tests ./bin/native-tests",
9
- "skills:build": "bun run --cwd multi-task-flow build",
9
+ "skills:build": "bun run --cwd multi-task-flow-runtime build",
10
10
  "test:core": "bun test ./bin/tests/core",
11
11
  "test:hosting": "bun test ./bin/tests/hosting",
12
12
  "test:flow": "bun test ./bin/tests/flow",