@loomhelm/multi-task-flow 0.2.8 → 0.2.9
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 +14 -14
- package/package.json +2 -2
- package/src/cli/service.ts +26 -0
- package/src/cli.ts +3 -1
- package/toolkit/artifact-manifest.json +67 -71
- package/toolkit/package.json +1 -1
- package/toolkit/skills/bin/check_source_policy.ts +13 -7
- package/toolkit/skills/bin/multi_task_platform_gate.ts +2 -2
- package/toolkit/skills/bin/tests/core/package_scripts.test.ts +1 -1
- package/toolkit/skills/bin/tests/plan/multi-task-flow.test.ts +3 -3
- package/toolkit/skills/catalog.json +0 -1
- package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/data-and-scripts.md +8 -6
- package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/execution.md +1 -1
- package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/plan-revision-and-change-audit.md +1 -1
- package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/web-template.md +1 -1
- package/toolkit/skills/docs/vendor/dot-codex/specs/a4ge9-stable-preview-runtime/IMPLEMENTATION.md +0 -2
- package/toolkit/skills/docs/vendor/dot-codex/specs/a4ge9-stable-preview-runtime/SPEC.md +0 -1
- package/toolkit/skills/docs/vendor/dot-codex/specs/dev-plan-standalone-typescript-finalizer/IMPLEMENTATION.md +1 -1
- package/toolkit/skills/docs/vendor/dot-codex/specs/multi-task-flow/IMPLEMENTATION.md +3 -3
- package/toolkit/skills/docs/vendor/dot-codex/specs/multi-task-flow/SCHEMA.md +4 -4
- package/toolkit/skills/docs/vendor/dot-codex/specs/stable-preview-runtime/IMPLEMENTATION.md +0 -2
- package/toolkit/skills/docs/vendor/dot-codex/specs/stable-preview-runtime/SPEC.md +0 -1
- package/toolkit/skills/multi-task-flow/SKILL.md +65 -47
- package/toolkit/skills/multi-task-flow/dist/app.css +1 -1
- package/toolkit/skills/multi-task-flow/dist/app.js +74 -74
- package/toolkit/skills/multi-task-flow/dist/index.html +3 -3
- package/toolkit/skills/multi-task-flow/lib/atomic-orchestration.ts +1 -0
- package/toolkit/skills/multi-task-flow/lib/engine.ts +65 -7
- package/toolkit/skills/multi-task-flow/lib/package-source.ts +5 -5
- package/toolkit/skills/multi-task-flow/lib/schema.ts +82 -2
- package/toolkit/skills/multi-task-flow/lib/service-pages.ts +1 -26
- package/toolkit/skills/multi-task-flow/lib/view.ts +16 -0
- package/toolkit/skills/multi-task-flow/references/shared-viewer.md +1 -1
- package/toolkit/skills/multi-task-flow/scripts/check-client-skills.ts +7 -4
- package/toolkit/skills/multi-task-flow/scripts/check-skills-installation.ts +14 -8
- package/toolkit/skills/multi-task-flow/scripts/import-client-skills.ts +1 -1
- package/toolkit/skills/multi-task-flow/scripts/package-client-skills.ts +4 -4
- package/toolkit/skills/multi-task-flow/scripts/test-viewer-source.ts +1 -1
- package/toolkit/skills/multi-task-flow/tests/atomic-orchestration-schema.test.ts +5 -0
- package/toolkit/skills/multi-task-flow/tests/browser/README.md +1 -1
- package/toolkit/skills/multi-task-flow/tests/check-skills-installation.test.ts +12 -38
- package/toolkit/skills/multi-task-flow/tests/delivery-topology.test.ts +33 -0
- package/toolkit/skills/multi-task-flow/tests/orchestrate.test.ts +3 -5
- package/toolkit/skills/multi-task-flow/tests/ui/details-content.test.ts +9 -0
- package/toolkit/skills/multi-task-flow/tests/ui/responsive-width-contract.test.ts +25 -1
- package/toolkit/skills/multi-task-flow/tests/ui/task-context-menu.test.ts +2 -1
- package/toolkit/skills/multi-task-flow/web/app.css +64 -9
- package/toolkit/skills/multi-task-flow/web/app.ts +33 -14
- package/toolkit/skills/multi-task-flow/web/atomic-graph-view.tsx +1 -1
- package/toolkit/skills/multi-task-flow/web/details-content.ts +49 -38
- package/toolkit/skills/multi-task-flow/web/graph-view.tsx +1 -1
- package/toolkit/skills/multi-task-flow/web/index.html +3 -3
- package/toolkit/skills/multi-task-flow/web/model.ts +1 -1
- package/toolkit/skills/multi-task-flow/web/timeline.ts +11 -8
- package/toolkit/skills/package.json +2 -2
- package/toolkit/skills/source-manifest.json +691 -398
- package/toolkit/skills/support-manifest.json +1 -0
- package/toolkit/skills/multi-task-flow/scripts/serve.ts +0 -218
- package/toolkit/skills/multi-task-flow/tests/serve.test.ts +0 -194
|
@@ -1,45 +1,19 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import {
|
|
3
|
-
import { cpSync, existsSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { cpSync, existsSync, mkdtempSync, realpathSync, rmSync } from 'node:fs';
|
|
4
3
|
import { tmpdir } from 'node:os';
|
|
5
4
|
import { join, resolve } from 'node:path';
|
|
6
5
|
import { test } from 'bun:test';
|
|
7
|
-
import { checkSkillsInstallation } from '../scripts/check-skills-installation.ts';
|
|
8
6
|
|
|
9
|
-
test('
|
|
7
|
+
test('the installable Skill tree excludes npm runtime resources', () => {
|
|
10
8
|
const isolated = realpathSync(mkdtempSync(join(tmpdir(), 'loomhelm-skills-only-')));
|
|
11
|
-
const source = resolve(import.meta.dirname, '
|
|
12
|
-
const manifest = JSON.parse(readFileSync(join(source,'source-manifest.json'),'utf8'));
|
|
13
|
-
const sources = new Set([...manifest.files,...manifest.additions].map(file => resolve(source,'..',file.path)));
|
|
9
|
+
const source = resolve(import.meta.dirname, '../../multi-task-flow');
|
|
14
10
|
try {
|
|
15
|
-
cpSync(source, join(isolated,'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const report = checkSkillsInstallation(isolated,{probe:true});
|
|
25
|
-
assert.equal(report.ok,true,JSON.stringify(report)); assert.equal(report.ready,true);
|
|
26
|
-
assert.equal(report.skills,29); assert.deepEqual(report.requiredSparsePaths,['skills']);
|
|
27
|
-
assert.equal(existsSync(join(isolated,'bin')),false);
|
|
28
|
-
assert.equal(existsSync(join(isolated,'skills/bin/plan_review_view.js')),false);
|
|
29
|
-
assert.equal(existsSync(join(isolated,'skills/bin/native/posix.node')),false);
|
|
30
|
-
const plan=readFileSync(join(isolated,'skills/dev-plan/references/plan-cases/positive-fast-track.md'),'utf8');
|
|
31
|
-
const finalized=spawnSync('/bin/bash',['-p',join(isolated,'skills/bin/plan-cli.sh'),'--input','-'],{cwd:source,env:process.env,input:plan,encoding:'utf8',timeout:30000});
|
|
32
|
-
assert.equal(finalized.status,0,finalized.stderr); assert.match(finalized.stdout,/<proposed_plan>/);
|
|
33
|
-
const context=join(isolated,'skills/dot-codex-development/scripts/context.ts');
|
|
34
|
-
const original=readFileSync(context); writeFileSync(context,'// tampered');
|
|
35
|
-
assert.equal(checkSkillsInstallation(isolated).sourceComplete,false);
|
|
36
|
-
writeFileSync(context,original);
|
|
37
|
-
renameSync(context,join(isolated,'context.ts')); symlinkSync(join(isolated,'context.ts'),context);
|
|
38
|
-
assert.equal(checkSkillsInstallation(isolated).ok,false);
|
|
39
|
-
rmSync(context); renameSync(join(isolated,'context.ts'),context);
|
|
40
|
-
renameSync(join(isolated,'skills/bin'),join(isolated,'bin'));
|
|
41
|
-
const broken=checkSkillsInstallation(isolated,{probe:true});
|
|
42
|
-
assert.equal(broken.ok,false); assert.equal(broken.ready,false);
|
|
43
|
-
assert.ok(broken.missing.includes('skills/bin/plan_review_view.ts'));
|
|
44
|
-
} finally { rmSync(isolated,{recursive:true,force:true}); }
|
|
45
|
-
},60000);
|
|
11
|
+
cpSync(source, join(isolated, 'multi-task-flow'), { recursive: true });
|
|
12
|
+
assert.equal(existsSync(join(isolated, 'multi-task-flow', 'SKILL.md')), true);
|
|
13
|
+
for (const name of ['data', 'dist', 'examples', 'lib', 'scripts', 'tests', 'web', 'package.json', 'bun.lock']) {
|
|
14
|
+
assert.equal(existsSync(join(isolated, 'multi-task-flow', name)), false, name);
|
|
15
|
+
}
|
|
16
|
+
} finally {
|
|
17
|
+
rmSync(isolated, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
}, 60000);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { deliveryTopologyForGroup, validateDeliveryTopologies } from "../lib/schema";
|
|
3
|
+
|
|
4
|
+
const task = (id: string, group_id: string, policy: "shared_delivery_unit" | "child_pr") => ({
|
|
5
|
+
id,
|
|
6
|
+
group_id,
|
|
7
|
+
kind: "contribution" as const,
|
|
8
|
+
disposition: "active" as const,
|
|
9
|
+
execution_owner: policy === "shared_delivery_unit" ? "delivery_agent" as const : "task_agent" as const,
|
|
10
|
+
conversation_policy: policy === "shared_delivery_unit" ? "delivery_owned" as const : "task_agent" as const,
|
|
11
|
+
delivery_policy: policy,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("derives shared topology from one contribution task", () => {
|
|
15
|
+
const tasks = [task("task-a", "du-a", "shared_delivery_unit")];
|
|
16
|
+
expect(deliveryTopologyForGroup(tasks as any, "du-a")).toBe("shared_delivery");
|
|
17
|
+
expect(() => validateDeliveryTopologies(tasks as any, true)).not.toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("derives multi-task topology without allowing shared policies", () => {
|
|
21
|
+
const tasks = [task("task-a", "du-a", "child_pr"), task("task-b", "du-a", "child_pr")];
|
|
22
|
+
expect(deliveryTopologyForGroup(tasks as any, "du-a")).toBe("multi_task_integration");
|
|
23
|
+
expect(() => validateDeliveryTopologies(tasks as any, true)).not.toThrow();
|
|
24
|
+
expect(() => validateDeliveryTopologies([task("task-a", "du-a", "shared_delivery_unit"), task("task-b", "du-a", "child_pr")] as any, true)).toThrow("multi-task delivery unit");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("does not count verification tasks when deriving topology", () => {
|
|
28
|
+
const tasks = [
|
|
29
|
+
task("task-a", "du-a", "shared_delivery_unit"),
|
|
30
|
+
{ id: "verify-a", group_id: "du-a", kind: "verification", disposition: "active" },
|
|
31
|
+
];
|
|
32
|
+
expect(deliveryTopologyForGroup(tasks as any, "du-a")).toBe("shared_delivery");
|
|
33
|
+
});
|
|
@@ -627,7 +627,7 @@ describe("orchestrate init", () => {
|
|
|
627
627
|
});
|
|
628
628
|
|
|
629
629
|
test("documents init as an explicit mutation boundary", () => {
|
|
630
|
-
const skill = readFileSync(join(import.meta.dir, "
|
|
630
|
+
const skill = readFileSync(join(import.meta.dir, "../../multi-task-flow/SKILL.md"), "utf8");
|
|
631
631
|
const design = readFileSync(
|
|
632
632
|
join(
|
|
633
633
|
import.meta.dir,
|
|
@@ -636,10 +636,8 @@ describe("orchestrate init", () => {
|
|
|
636
636
|
"utf8",
|
|
637
637
|
);
|
|
638
638
|
|
|
639
|
-
expect(skill).toContain("
|
|
640
|
-
expect(skill).toContain(
|
|
641
|
-
"must not call `init` or a native conversation-creation tool",
|
|
642
|
-
);
|
|
639
|
+
expect(skill).toContain("初始化是有副作用的运行时操作");
|
|
640
|
+
expect(skill).toContain("只读审查、诊断、代码探索或仅制定计划时不得调用本 Skill");
|
|
643
641
|
expect(design).toContain("失败返回 `INVALID_INITIAL_STATE`");
|
|
644
642
|
expect(design).toContain("不创建任务会话");
|
|
645
643
|
});
|
|
@@ -22,6 +22,15 @@ describe("dashboard details content", () => {
|
|
|
22
22
|
expect(document.querySelector(".detail-duration")?.textContent).toBe("未知");
|
|
23
23
|
expect(duration(details.groupPreview(group))).toBe("未知");
|
|
24
24
|
});
|
|
25
|
+
test("group details include the full Agent output at the end", () => {
|
|
26
|
+
const group = { id: "core", title: "核心", state: "closed", agent_thread_id: "thread-group", parent_pr: { url: null }, latest_agent_output: { thread_id: "thread-group", agent_state: "idle", stale: false, state: "available", text: "交付单元已完成", turn_id: null, message_id: null, phase: null, message_status: null, observed_at: null } } as Group;
|
|
27
|
+
const view = { groups: [group], tasks: [], actual_intervals: [] } as unknown as View;
|
|
28
|
+
const details = createDetailContent({ getData: () => view, labels: {}, labelTask: () => "", time: () => "", safeExternal: () => null });
|
|
29
|
+
details.showGroup(group);
|
|
30
|
+
const labels = [...document.querySelectorAll("#details-content .detail-label")].map((node) => node.textContent);
|
|
31
|
+
expect(labels.at(-1)).toBe("Agent 输出");
|
|
32
|
+
expect(document.querySelector(".detail-agent-output")?.textContent).toContain("交付单元已完成");
|
|
33
|
+
});
|
|
25
34
|
test("keeps the summary outside the scroll body and preserves only the same object's reading position", () => {
|
|
26
35
|
const group = { id: "same", title: "交付单元", state: "closed", parent_pr: { url: null } } as Group;
|
|
27
36
|
const view = { groups: [group], tasks: [], actual_intervals: [] } as unknown as View;
|
|
@@ -17,11 +17,35 @@ test("medium details stay in the static bottom layout instead of a side-by-side
|
|
|
17
17
|
expect(medium).toContain("border: 0; border-top: 1px solid var(--border); border-radius: 0; background: transparent;");
|
|
18
18
|
expect(medium).toContain(".details-heading button { display: none; }");
|
|
19
19
|
expect(medium).toContain(".details-slot { position: static; padding-top: 16px; }");
|
|
20
|
-
expect(medium).toContain(".details-heading { padding: 0 0 12px; display: flex; align-items:
|
|
20
|
+
expect(medium).toContain(".details-heading { padding: 0 0 12px; display: flex; align-items: center;");
|
|
21
|
+
expect(medium).toContain("#details-summary { min-width: 0; display: flex; align-items: center; gap: 12px; }");
|
|
22
|
+
expect(medium).toContain(".detail-title { min-width: 0; margin: 0; padding-right: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }");
|
|
23
|
+
expect(medium).toContain(".detail-state { flex: 0 0 auto; margin: 0; white-space: nowrap; }");
|
|
21
24
|
expect(medium).toContain("#details-content { padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));");
|
|
25
|
+
expect(medium).toContain("grid-auto-rows: max-content; align-content: start; align-items: start;");
|
|
26
|
+
expect(medium).toContain("#details-content[data-detail-kind=\"task\"] { grid-template-columns: minmax(220px, .9fr) repeat(3, minmax(0, 1fr)); }");
|
|
27
|
+
expect(medium).toContain("#details-content[data-detail-kind=\"group\"] { grid-template-columns: minmax(260px, 1.25fr) repeat(3, minmax(0, 1fr)); }");
|
|
28
|
+
expect(medium).toContain("#details-content > .detail-field:has(.atom-list) { grid-column: span 1; }");
|
|
29
|
+
expect(medium).toContain("#details-content > .detail-field:has(.atom-list) { display: block; padding-block: 7px; }");
|
|
30
|
+
expect(medium).toContain("text-overflow: ellipsis; white-space: nowrap;");
|
|
31
|
+
expect(medium).toContain("#details-content > .detail-group-facts { grid-column: 2 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));");
|
|
32
|
+
expect(medium).toContain("#details-content > .detail-group-facts > .detail-field { min-width: 0; padding-block: 7px; display: block; }");
|
|
22
33
|
expect(medium).toContain('.workspace[data-view-tab="tasks"] .details-slot { grid-column: 1; grid-row: 3; }');
|
|
23
34
|
});
|
|
24
35
|
|
|
36
|
+
test("tall medium details provide an independently scrollable content body", () => {
|
|
37
|
+
const tall = css.split("@media (min-width: 768px) and (max-width: 1444px) and (min-height: 961px) {")[1]!.split("@media (min-width: 768px) and (max-width: 1444px) and (max-height: 960px)")[0]!;
|
|
38
|
+
expect(tall).toContain("display: flex; flex-direction: column;");
|
|
39
|
+
expect(tall).toContain(".workspace:not(.has-details) { grid-template-rows: minmax(240px, 1fr); }");
|
|
40
|
+
expect(tall).toContain(".workspace[data-view-tab=\"tasks\"]:not(.has-details) { grid-template-rows: auto minmax(240px, 1fr); }");
|
|
41
|
+
expect(tall).toContain("#details-content { flex: 1 1 auto; min-height: 0; overflow: auto; }");
|
|
42
|
+
expect(tall).toContain(".workspace-view > .timeline-view { min-height: 0; display: flex; flex-direction: column; }");
|
|
43
|
+
expect(tall).toContain("height: auto !important; min-height: 0;");
|
|
44
|
+
expect(tall).toContain(".workspace-view > .canvas-view #graph { height: 100% !important; min-height: 0; }");
|
|
45
|
+
expect(tall).toContain(".workspace-view > .canvas-view #atomic-graph { height: 100% !important; min-height: 0; }");
|
|
46
|
+
expect(tall).toContain("overflow-anchor: none;");
|
|
47
|
+
});
|
|
48
|
+
|
|
25
49
|
test("identity actions follow the project name instead of filling the title row", () => {
|
|
26
50
|
expect(css).toContain(".identity-copy { flex: 0 1 auto; min-width: 0; }");
|
|
27
51
|
expect(css).toContain("text-overflow: ellipsis; white-space: nowrap;");
|
|
@@ -70,5 +70,6 @@ test("keyboard opening, navigation and Escape; blank space retains native menu",
|
|
|
70
70
|
});
|
|
71
71
|
test("authentication redirects only accept same-service run links and legal view state", () => {
|
|
72
72
|
expect(parseViewerTarget('/runs/run-a/?selected=task%3Atask.v2&detail=1&plan=plan.v2&q=hidden', 'http://localhost', { allowHome: false })?.target).toBe('/runs/run-a/?selected=task%3Atask.v2&detail=1&plan=plan.v2&q=hidden');
|
|
73
|
-
|
|
73
|
+
expect(parseViewerTarget('/runs/run-a/?tab=tasks&selected=group%3AG2&detail=1&future=ignored', 'http://localhost', { allowHome: false })?.target).toContain('tab=tasks');
|
|
74
|
+
for (const value of ['https://evil.test/runs/run-a/?selected=task:C2', '//evil.test/runs/run-a/?selected=task:C2', '/login?selected=task:C2', '/runs/run-a/../', '/runs/%2e%2e/', '/runs\\run-a/', 'http://user:pass@localhost/runs/run-a/?selected=task:C2', '/runs/run-a/?ticket=secret', '/runs/run-a/?q=%0d%0aLocation:%20https://evil.test']) expect(parseViewerTarget(value, 'http://localhost', { allowHome: false })).toBeNull();
|
|
74
75
|
});
|
|
@@ -191,11 +191,11 @@ h1 { margin: 3px 0 0; color: var(--text); font-size: 26px; line-height: 1.14; fo
|
|
|
191
191
|
.summary-label { align-self: end; color: var(--muted); font-size: 11px; }
|
|
192
192
|
.summary-item strong { align-self: start; color: var(--text); font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 21px; line-height: 1.05; font-weight: 400; font-variant-numeric: tabular-nums; /* taste-skill-ignore: changing counts align in a technical face. */ }
|
|
193
193
|
|
|
194
|
-
.workspace { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 336px; gap: var(--gap); min-height: 610px; margin-top: 24px; }
|
|
194
|
+
.workspace { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 336px; gap: var(--gap); min-height: 610px; margin-top: 24px; overflow-anchor: none; }
|
|
195
195
|
.workspace:not(.has-details) { grid-template-columns: minmax(0, 1fr); }
|
|
196
196
|
.workspace-main { min-width: 0; }
|
|
197
197
|
.workspace-view, .workspace-tasks { min-width: 0; }
|
|
198
|
-
.view-header { min-height: 44px; display: grid; grid-template-columns: 148px
|
|
198
|
+
.view-header { min-height: 44px; display: grid; grid-template-columns: 148px minmax(0, 1fr) minmax(190px, 260px) auto; align-items: stretch; gap: 18px; border-bottom: 1px solid var(--border); background: transparent; }
|
|
199
199
|
.view-heading { min-width: 0; padding: 7px 16px 0 0; border-right: 1px solid var(--rule); }
|
|
200
200
|
.view-context { display: block; margin-top: 1px; color: var(--text); font-size: 13px; font-weight: 500; }
|
|
201
201
|
.view-count { display: block; margin-top: 1px; color: var(--faint); font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; /* taste-skill-ignore: compact counts belong to the control plane. */ }
|
|
@@ -208,6 +208,8 @@ h1 { margin: 3px 0 0; color: var(--text); font-size: 26px; line-height: 1.14; fo
|
|
|
208
208
|
.search svg { position: absolute; left: 11px; width: 14px; height: 14px; flex: none; pointer-events: none; }
|
|
209
209
|
.search input { width: 100%; min-width: 0; min-height: 30px; padding: 0 12px 0 33px; border: 0; border-radius: inherit; outline: 0; background: transparent; color: var(--text); }
|
|
210
210
|
.search input::placeholder { color: var(--faint); }
|
|
211
|
+
.task-view-tools { display: none; align-items: center; justify-self: end; gap: 12px; white-space: nowrap; }
|
|
212
|
+
.workspace[data-view-tab="tasks"] .task-view-tools { display: flex; }
|
|
211
213
|
.filters { min-height: 56px; padding: 12px 0; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--rule); background: transparent; }
|
|
212
214
|
.filter-caption { margin-right: 3px; color: var(--faint); font-size: 11px; }
|
|
213
215
|
.filters > .menu-control { position: relative; display: block; max-width: 180px; }
|
|
@@ -264,11 +266,14 @@ h1 { margin: 3px 0 0; color: var(--text); font-size: 26px; line-height: 1.14; fo
|
|
|
264
266
|
.graph-a11y-node:hover, .graph-a11y-node[aria-pressed="true"] { background: var(--active); }
|
|
265
267
|
.empty { display: grid; place-items: center; min-height: 260px; color: var(--faint); }
|
|
266
268
|
.empty[hidden], [hidden] { display: none !important; }
|
|
267
|
-
.timeline-view { min-height: 360px; padding: 12px; }
|
|
269
|
+
.timeline-view { min-height: 360px; padding: 12px; overflow: hidden; overscroll-behavior: contain; }
|
|
268
270
|
#planned-timeline, #actual-timeline { height: 284px; }
|
|
269
271
|
.timeline-view .vis-timeline { height: 100% !important; border: 0; font-family: inherit; user-select: none; }
|
|
270
272
|
.timeline-view .vis-loading-screen { display: none !important; }
|
|
271
|
-
.timeline-view .vis-panel.vis-center { cursor: grab; touch-action: pan-y; }
|
|
273
|
+
.timeline-view .vis-panel.vis-center { cursor: grab; touch-action: pan-x pan-y !important; overflow: auto !important; scrollbar-width: none; }
|
|
274
|
+
.timeline-view .vis-panel.vis-center::-webkit-scrollbar { display: none; }
|
|
275
|
+
.timeline-view .vis-panel.vis-left { cursor: grab; touch-action: pan-x pan-y !important; overflow: hidden !important; }
|
|
276
|
+
.timeline-view .vis-panel.vis-center { overscroll-behavior: contain; }
|
|
272
277
|
.timeline-view .vis-panel.vis-center:active { cursor: grabbing; }
|
|
273
278
|
.timeline-view .vis-panel.vis-center.is-panning { cursor: grabbing; }
|
|
274
279
|
.timeline-view .vis-panel, .timeline-view .vis-labelset .vis-label, .timeline-view .vis-foreground .vis-group { border-color: var(--rule); background: transparent; }
|
|
@@ -351,6 +356,7 @@ h1 { margin: 3px 0 0; color: var(--text); font-size: 26px; line-height: 1.14; fo
|
|
|
351
356
|
@keyframes task-status-hourglass { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
|
|
352
357
|
|
|
353
358
|
.details-slot { min-width: 0; align-self: stretch; }
|
|
359
|
+
.details-resize { display: none; }
|
|
354
360
|
.details { position: fixed; top: var(--details-top, 16px); left: var(--details-left); width: var(--details-width); max-height: calc(100dvh - var(--details-top, 16px) - 16px); z-index: 10; display: flex; flex-direction: column; overflow: hidden; min-width: 0; container: detail-panel / inline-size; --details-padding-inline: 20px; --details-padding-bottom: 24px; padding: 0; background: var(--raised); border: 1px solid var(--border); border-radius: var(--radius-card); }
|
|
355
361
|
.details-heading { position: relative; flex: none; padding-inline: var(--details-padding-inline); }
|
|
356
362
|
#details-content { padding: 0 var(--details-padding-inline) var(--details-padding-bottom); min-height: 0; overflow: auto; overscroll-behavior: contain; overflow-anchor: none; scrollbar-gutter: stable; }
|
|
@@ -421,6 +427,9 @@ time[data-local-timestamp] { cursor: help; font-variant-numeric: tabular-nums; }
|
|
|
421
427
|
@media (min-width: 768px) and (max-width: 1444px) {
|
|
422
428
|
html { scrollbar-gutter: stable; }
|
|
423
429
|
.main { container-type: inline-size; }
|
|
430
|
+
.view-header { min-height: 86px; grid-template-rows: minmax(86px, auto); }
|
|
431
|
+
.view-heading { padding-top: 7px; padding-bottom: 7px; }
|
|
432
|
+
.tabs { min-height: 52px; }
|
|
424
433
|
.workspace {
|
|
425
434
|
--detail-min-width: 243px;
|
|
426
435
|
--detail-max-width: 740px;
|
|
@@ -438,10 +447,23 @@ time[data-local-timestamp] { cursor: help; font-variant-numeric: tabular-nums; }
|
|
|
438
447
|
.details-slot { position: static; padding-top: 16px; }
|
|
439
448
|
.details { position: static; width: auto; max-height: none; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: transparent; box-shadow: none; padding: 16px 0 4px; display: block; }
|
|
440
449
|
.details-heading button { display: none; }
|
|
441
|
-
.details-heading { padding: 0 0 12px; display: flex; align-items:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
450
|
+
.details-heading { padding: 0 0 12px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--rule); }
|
|
451
|
+
#details-summary { min-width: 0; display: flex; align-items: center; gap: 12px; }
|
|
452
|
+
.detail-title { min-width: 0; margin: 0; padding-right: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
453
|
+
.detail-state { flex: 0 0 auto; margin: 0; white-space: nowrap; }
|
|
454
|
+
#details-content { padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: max-content; align-content: start; align-items: start; column-gap: 20px; }
|
|
455
|
+
#details-content[data-detail-kind="task"] { grid-template-columns: minmax(220px, .9fr) repeat(3, minmax(0, 1fr)); }
|
|
456
|
+
#details-content[data-detail-kind="group"] { grid-template-columns: minmax(260px, 1.25fr) repeat(3, minmax(0, 1fr)); }
|
|
457
|
+
#details-content > .detail-field:has(.detail-agent-output), #details-content > .detail-field:has(.detail-empty-output) { grid-column: 1 / -1; }
|
|
458
|
+
#details-content > .detail-field:has(.atom-list) { grid-column: span 1; }
|
|
459
|
+
#details-content > .detail-field:has(.atom-list) { display: block; padding-block: 7px; }
|
|
460
|
+
#details-content > .detail-field:has(.atom-list) > .detail-value { margin-top: 6px; }
|
|
461
|
+
#details-content > .detail-field:has(.atom-list) .status-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
462
|
+
#details-content > .detail-group-facts { grid-column: 2 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-content: start; align-items: start; column-gap: 16px; }
|
|
463
|
+
#details-content > .detail-group-facts > .detail-field { min-width: 0; padding-block: 7px; display: block; }
|
|
464
|
+
#details-content > .detail-group-facts > .detail-field > .detail-label { display: block; margin-bottom: 4px; }
|
|
465
|
+
#details-content > .detail-group-facts > .detail-field > .detail-value { display: block; }
|
|
466
|
+
#details-content > .detail-group-facts > .detail-field:has(.detail-agent-output), #details-content > .detail-group-facts > .detail-field:has(.detail-empty-output) { grid-column: 1 / -1; }
|
|
445
467
|
.details-backdrop { display: none; }
|
|
446
468
|
.workspace .task-layout-grid, .workspace .group-cards {
|
|
447
469
|
grid-template-columns: repeat(auto-fill, var(--stable-task-card-width)); gap: 20px;
|
|
@@ -452,6 +474,38 @@ time[data-local-timestamp] { cursor: help; font-variant-numeric: tabular-nums; }
|
|
|
452
474
|
.task-layout-board > .task-column { flex-basis: auto; min-width: 0; }
|
|
453
475
|
}
|
|
454
476
|
|
|
477
|
+
@media (min-width: 768px) and (max-width: 1444px) and (min-height: 961px) {
|
|
478
|
+
.workspace { height: max(610px, calc(100dvh - 190px)); max-height: none; min-height: 610px; overflow: visible; grid-template-rows: minmax(240px, 1fr) var(--details-height, 40vh); }
|
|
479
|
+
.workspace[data-view-tab="tasks"] { grid-template-rows: auto minmax(240px, 1fr) var(--details-height, 40vh); }
|
|
480
|
+
.workspace:not(.has-details) { grid-template-rows: minmax(240px, 1fr); }
|
|
481
|
+
.workspace[data-view-tab="tasks"]:not(.has-details) { grid-template-rows: auto minmax(240px, 1fr); }
|
|
482
|
+
.workspace-view, .workspace-tasks { min-height: 0; align-self: stretch; overflow: visible; }
|
|
483
|
+
.workspace-view { display: flex; flex-direction: column; }
|
|
484
|
+
.workspace-view > .canvas-view, .workspace-view > .timeline-view { flex: 1 1 auto; min-height: 0; margin-bottom: 0; }
|
|
485
|
+
.workspace-view > .canvas-view #graph { height: 100% !important; min-height: 0; }
|
|
486
|
+
.workspace-view > .canvas-view #atomic-graph { height: 100% !important; min-height: 0; }
|
|
487
|
+
.workspace-view > .timeline-view { min-height: 0; display: flex; flex-direction: column; }
|
|
488
|
+
.workspace-view > .timeline-view > #planned-timeline, .workspace-view > .timeline-view > #actual-timeline { flex: 1 1 auto; height: auto !important; min-height: 0; }
|
|
489
|
+
.workspace-view > .timeline-view > .axis-timezone, .workspace-view > .timeline-view > .unknown { flex: none; }
|
|
490
|
+
.workspace-tasks { overflow-y: auto; overflow-x: hidden; }
|
|
491
|
+
.details-slot { grid-row: 2 !important; padding-top: 0; min-height: 0; }
|
|
492
|
+
.workspace[data-view-tab="tasks"] .details-slot { grid-row: 3 !important; }
|
|
493
|
+
.details-resize { display: block; height: 12px; cursor: ns-resize; border-top: 1px solid var(--border); background: linear-gradient(var(--control), var(--control)) center / 42px 3px no-repeat; touch-action: none; overflow-anchor: none; }
|
|
494
|
+
.details-resize:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
495
|
+
.details { height: calc(var(--details-height, 40vh) - 12px); max-height: none; padding-top: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
496
|
+
.details-heading { flex: none; }
|
|
497
|
+
#details-content { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
@media (min-width: 768px) and (max-width: 1444px) and (max-height: 960px) {
|
|
501
|
+
.details { position: fixed; top: auto; right: 0; bottom: 0; left: 0; width: auto; max-height: min(82dvh, calc(100dvh - 16px)); padding: 18px 0 0; border-width: 1px 0 0; border-radius: var(--radius-sheet) var(--radius-sheet) 0 0; box-shadow: 0 -12px 32px rgba(0, 0, 0, .24); transform: translateY(102%); }
|
|
502
|
+
.details.open { transform: translateY(0); }
|
|
503
|
+
.details::before { flex: none; width: 42px; height: 3px; margin: -7px auto 14px; display: block; border-radius: 2px; content: ""; background: var(--control); }
|
|
504
|
+
.details-resize { display: none; }
|
|
505
|
+
.details-backdrop { position: fixed; inset: 0; z-index: 15; display: block; background: #0d101488; pointer-events: auto; }
|
|
506
|
+
.details-backdrop[hidden] { display: none; }
|
|
507
|
+
}
|
|
508
|
+
|
|
455
509
|
@media (max-width: 767px) {
|
|
456
510
|
.context-menu-mobile-trigger { display: inline-grid; }
|
|
457
511
|
.topbar { min-height: 50px; padding: 12px 16px; flex-wrap: wrap; gap: 12px; }
|
|
@@ -562,7 +616,8 @@ time[data-local-timestamp] { cursor: help; font-variant-numeric: tabular-nums; }
|
|
|
562
616
|
.profile-caption { margin: 0; font-size: 11px; line-height: 1.6; overflow-wrap: anywhere; color: var(--muted); }
|
|
563
617
|
.profile-error { margin: 0; font-size: 12px; color: var(--negative); overflow-wrap: anywhere; }
|
|
564
618
|
.profile-error:empty { display: none; }
|
|
565
|
-
.detail-field:has(.execution-profile-editor) {
|
|
619
|
+
.detail-field:has(.execution-profile-editor) { align-items: center; }
|
|
620
|
+
.detail-field:has(.execution-profile-editor) .execution-profile-editor { width: fit-content; max-width: 100%; }
|
|
566
621
|
.run-profile-control { flex: 0 0 auto; min-width: 0; margin-left: -1px; }
|
|
567
622
|
.run-profile-control .profile-trigger { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; min-height: 30px; padding: 0; border: 0; border-color: transparent; border-radius: 50%; color: var(--muted); background: transparent; }
|
|
568
623
|
.run-profile-control .profile-trigger svg { width: 16px; height: 16px; flex: 0 0 16px; transform: translateX(-0.5px); color: currentColor; }
|
|
@@ -16,14 +16,14 @@ import { createHoverController } from "./hover-controller";
|
|
|
16
16
|
import { createDetailContent } from "./details-content";
|
|
17
17
|
import { deriveConnectionStatus, type Task, type View, type ViewResponse, type Theme } from "./model";
|
|
18
18
|
import { defaultTabForPhase, derivePhase, type DisplayPhase } from "./phase";
|
|
19
|
-
import { DETAILS_PANEL_BREAKPOINT, detailsPanelFits,
|
|
19
|
+
import { DETAILS_PANEL_BREAKPOINT, detailsPanelFits, resolveDetailSurface, selectedObjectFocusSelector } from "./interaction-policy";
|
|
20
20
|
import { applyDetailSurface, installDetailViewport } from "./detail-surface";
|
|
21
21
|
import { firstVisibleElement, focusAfterRerender } from "./focus";
|
|
22
22
|
import { installViewStateNavigation } from "./navigation";
|
|
23
23
|
import { createHoverPreviewBridge, type HoverPreviewBridge } from "./hover-preview";
|
|
24
24
|
import { formatDuration, formatLocalTime } from "./time";
|
|
25
25
|
import { renderTimestamp, refreshTimestamps } from "./timestamp";
|
|
26
|
-
import { planStateSeverity, renderPlanInfoContent
|
|
26
|
+
import { planStateSeverity, renderPlanInfoContent } from "./status-row";
|
|
27
27
|
import { taskKindLabel, STATE_LABELS } from "./labels";
|
|
28
28
|
import { DEFAULT_VIEW_STATE, defaultTaskLayout, hasValidLayoutParam, canonicalViewUrl, hasValidTabParam, parseViewState, replaceViewState, sanitizeViewState, type DashboardTab, type TaskLayout, type ViewState } from "./view-state";
|
|
29
29
|
const ICONS = { Contrast, Network, Search, X, Plus, Minus, Maximize2, ChevronDown, CircleDashed, CircleDot, CirclePlay, LoaderCircle, Hourglass, CircleAlert, CircleHelp, CircleCheck, GitMerge, Columns3, LayoutGrid, MessageCircle, GitPullRequest, TreePalm, ArchiveX, ArchiveRestore, Archive, RefreshCw, Info, Settings2, SunMoon, Sun, Moon };
|
|
@@ -222,13 +222,6 @@ function refreshCardDurations(): void {
|
|
|
222
222
|
function updateTaskLayoutControls(): void {
|
|
223
223
|
for (const [id, layout] of [["task-layout-board", "board"], ["task-layout-grid", "grid"]] as const) $<HTMLButtonElement>(id).setAttribute("aria-pressed", String(viewState.layout === layout));
|
|
224
224
|
$("group-section").hidden = viewState.layout === "board";
|
|
225
|
-
const heading = $(viewState.layout === "board" ? "task-heading" : "group-heading").parentElement!;
|
|
226
|
-
const actions = document.querySelector<HTMLElement>(".section-heading-actions")!;
|
|
227
|
-
if (actions.parentElement !== heading) {
|
|
228
|
-
const focused = actions.contains(document.activeElement) ? document.activeElement as HTMLElement : null;
|
|
229
|
-
heading.append(actions);
|
|
230
|
-
focused?.focus({ preventScroll: true });
|
|
231
|
-
}
|
|
232
225
|
}
|
|
233
226
|
function syncStableGridCardWidth(): void {
|
|
234
227
|
const workspace = document.querySelector<HTMLElement>(".workspace");
|
|
@@ -306,6 +299,30 @@ function canUseDetailsPanel(): boolean {
|
|
|
306
299
|
const workspace = document.querySelector<HTMLElement>(".workspace");
|
|
307
300
|
return detailsPanelFits(workspace?.getBoundingClientRect().width ?? 0);
|
|
308
301
|
}
|
|
302
|
+
const DETAILS_MIN_HEIGHT = 120;
|
|
303
|
+
const detailsHeightKey = `loomhelm:details-height:${location.pathname}`;
|
|
304
|
+
function detailsHeightBounds(): { min: number; max: number } {
|
|
305
|
+
const workspace = document.querySelector<HTMLElement>(".workspace");
|
|
306
|
+
const available = Math.max(DETAILS_MIN_HEIGHT, workspace?.getBoundingClientRect().height ?? window.innerHeight);
|
|
307
|
+
return { min: DETAILS_MIN_HEIGHT, max: Math.max(DETAILS_MIN_HEIGHT, Math.min(window.innerHeight * 0.6, available - 240)) };
|
|
308
|
+
}
|
|
309
|
+
function setDetailsHeight(value: number, persist = true): void {
|
|
310
|
+
const bounds = detailsHeightBounds(); const height = Math.round(Math.max(bounds.min, Math.min(bounds.max, value)));
|
|
311
|
+
document.documentElement.style.setProperty("--details-height", `${height}px`);
|
|
312
|
+
const separator = document.getElementById("details-resize");
|
|
313
|
+
separator?.setAttribute("aria-valuemin", String(bounds.min)); separator?.setAttribute("aria-valuemax", String(bounds.max)); separator?.setAttribute("aria-valuenow", String(height));
|
|
314
|
+
if (persist) localStorage.setItem(detailsHeightKey, String(height));
|
|
315
|
+
}
|
|
316
|
+
const savedDetailsHeight = Number(localStorage.getItem(detailsHeightKey));
|
|
317
|
+
setDetailsHeight(Number.isFinite(savedDetailsHeight) && savedDetailsHeight > 0 ? savedDetailsHeight : window.innerHeight * 0.4, false);
|
|
318
|
+
const detailsResize = document.getElementById("details-resize");
|
|
319
|
+
if (detailsResize) {
|
|
320
|
+
let startY = 0; let startHeight = 0;
|
|
321
|
+
const move = (event: PointerEvent) => { event.preventDefault(); setDetailsHeight(startHeight + startY - event.clientY); };
|
|
322
|
+
const end = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", end); };
|
|
323
|
+
detailsResize.addEventListener("pointerdown", event => { event.preventDefault(); startY = event.clientY; startHeight = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--details-height")) || window.innerHeight * 0.4; detailsResize.setPointerCapture?.(event.pointerId); window.addEventListener("pointermove", move, { passive: false }); window.addEventListener("pointerup", end); });
|
|
324
|
+
detailsResize.addEventListener("keydown", event => { const current = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--details-height")) || window.innerHeight * 0.4; if (event.key === "ArrowUp") { event.preventDefault(); setDetailsHeight(current + 24); } else if (event.key === "ArrowDown") { event.preventDefault(); setDetailsHeight(current - 24); } else if (event.key === "Home") { event.preventDefault(); setDetailsHeight(detailsHeightBounds().min); } else if (event.key === "End") { event.preventDefault(); setDetailsHeight(detailsHeightBounds().max); } });
|
|
325
|
+
}
|
|
309
326
|
const hoverController = createHoverController({
|
|
310
327
|
element: $,
|
|
311
328
|
getData: () => data,
|
|
@@ -324,7 +341,8 @@ const hoverController = createHoverController({
|
|
|
324
341
|
const { supportsHoverDetails, showHoverPreview, hideHoverPreview, recordPointerType, showCurrentSelectionPreview, dismiss: dismissHoverPreview, pointerTypeFromEvent } = hoverController;
|
|
325
342
|
function updateDetailsSurface(open: boolean, focus = false): ReturnType<typeof resolveDetailSurface> {
|
|
326
343
|
const detailPanelViewport = canUseDetailsPanel();
|
|
327
|
-
const
|
|
344
|
+
const mediumShort = window.innerWidth >= DETAILS_PANEL_BREAKPOINT && window.innerWidth <= 1444 && window.innerHeight <= 960;
|
|
345
|
+
const drawerViewport = window.innerWidth < DETAILS_PANEL_BREAKPOINT || mediumShort;
|
|
328
346
|
const surface = resolveDetailSurface({ selected: hasSelectedObject(), detailOpen: open, hoverCapable: supportsHoverDetails(), keyboardSelection, detailPanelViewport, drawerViewport });
|
|
329
347
|
applyDetailSurface({ workspace: document.querySelector<HTMLElement>(".workspace")!, background: [document.querySelector<HTMLElement>(".topbar")!, document.querySelector<HTMLElement>(".overview")!, document.querySelector<HTMLElement>(".workspace-main")!], details: $("details"), detailsBackdrop: $("details-backdrop"), hoverPreview: $("hover-preview"), closeDetails: $("close-details") }, surface, detailPanelViewport, focus);
|
|
330
348
|
return surface;
|
|
@@ -561,7 +579,6 @@ function render(): void {
|
|
|
561
579
|
const planWarning = $("plan-warning");
|
|
562
580
|
const planState = data.plan_state;
|
|
563
581
|
renderPlanInfo();
|
|
564
|
-
renderPlanState($("plan-state"), planState);
|
|
565
582
|
const authorizationLabel = planState?.authorization_label ??
|
|
566
583
|
(planState?.approval_pending ? "Not approved" : "Schedulable");
|
|
567
584
|
const runtimeBlockers = planState?.runtime_readiness?.status === "blocked"
|
|
@@ -635,7 +652,9 @@ async function refresh(): Promise<void> {
|
|
|
635
652
|
}
|
|
636
653
|
if (serial !== requestSerial || result.revision < revision) return;
|
|
637
654
|
const previousPhase = phase; revision = result.revision; activePlanId = result.active_plan_id; data = result.view; applyOutputs({ output_epoch: result.output_epoch ?? "static-demo", output_revision: result.output_revision ?? 0, outputs: Object.fromEntries([...data.tasks.map(t => [t.id, t.latest_agent_output ?? initialOutput(t.thread_id)]), ...data.groups.map(g => [`group:${g.id}`, g.latest_agent_output ?? initialOutput(g.agent_thread_id)])]) }, outputEpoch); const currentPhase = derivePhase(data.tasks, data.execution_phase);
|
|
638
|
-
|
|
655
|
+
const explicitUrlState = parseViewState(window.location.search);
|
|
656
|
+
if (hasValidTabParam(window.location.search)) viewState = { ...viewState, tab: explicitUrlState.tab };
|
|
657
|
+
else if (!hydrated && !userChangedTab) viewState = { ...viewState, tab: defaultTabForPhase(currentPhase) };
|
|
639
658
|
if (!hasValidLayoutParam(window.location.search)) viewState = { ...viewState, layout: defaultTaskLayout(data.groups.length) };
|
|
640
659
|
const requestedTask = viewState.selected?.kind === "task" ? viewState.selected.id : missingLinkedTask;
|
|
641
660
|
const missingTask = requestedTask && !data.tasks.some(task => task.id === requestedTask);
|
|
@@ -666,7 +685,7 @@ $("version-menu").addEventListener("keydown", (event) => { if (event.key !== "En
|
|
|
666
685
|
$("task-layout-board").addEventListener("click", () => setTaskLayout("board")); $("task-layout-grid").addEventListener("click", () => setTaskLayout("grid"));
|
|
667
686
|
$("plan-info-trigger").addEventListener("click", (event) => { event.stopPropagation(); if (planInfoIsOpen()) closePlanInfo(false); else openPlanInfo(); });
|
|
668
687
|
$("close-plan-info").addEventListener("click", () => closePlanInfo()); $("plan-info-backdrop").addEventListener("click", () => closePlanInfo()); $("plan-info-panel").addEventListener("click", (event) => event.stopPropagation());
|
|
669
|
-
document.addEventListener("click", (event) => { const target = event.target as HTMLElement; if (!target.closest("[data-menu]")) closeMenus(); if (planInfoIsOpen() && !target.closest("#plan-info-panel, #plan-info-trigger")) closePlanInfo();
|
|
688
|
+
document.addEventListener("click", (event) => { const target = event.target as HTMLElement; if (!target.closest("[data-menu]")) closeMenus(); if (planInfoIsOpen() && !target.closest("#plan-info-panel, #plan-info-trigger")) closePlanInfo(); });
|
|
670
689
|
document.addEventListener("pointerdown", (event) => { keyboardSelection = false; recordPointerType(event.pointerType); }, true); document.addEventListener("pointermove", (event) => recordPointerType(event.pointerType), true);
|
|
671
690
|
document.addEventListener("keydown", (event) => { if ((event.key === "Enter" || event.key === " ") && (event.target as HTMLElement).closest(".task-card-select, .task-column-select, .task-row")) keyboardSelection = true; }, true);
|
|
672
691
|
document.addEventListener("keydown", (event) => { const target = event.target as HTMLElement; const trigger = target.closest<HTMLButtonElement>(".menu-trigger, .theme-trigger"); const option = target.closest<HTMLButtonElement>("[role=option]"); if (event.key === "Escape" && trigger) { event.preventDefault(); event.stopPropagation(); closeMenus(); trigger.focus(); } else if (trigger && (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ")) { event.preventDefault(); const id = trigger.id.replace(/-trigger$/, "") as MenuId; openMenu(id); } else if (option && (event.key === "ArrowDown" || event.key === "ArrowUp")) { event.preventDefault(); const options = [...option.closest<HTMLElement>("[role=listbox]")!.querySelectorAll<HTMLButtonElement>("[role=option]")]; const index = options.indexOf(option); options[(index + (event.key === "ArrowDown" ? 1 : -1) + options.length) % options.length]?.focus(); } else if (option && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); const menu = option.closest<HTMLElement>("[role=listbox]"); const id = menu?.id.replace(/-menu$/, "") as MenuId; chooseMenu(id, option.dataset.menuValue ?? ""); } else if (option && event.key === "Escape") { event.preventDefault(); event.stopPropagation(); closeMenus(); menuTrigger(option.closest<HTMLElement>("[role=listbox]")?.id.replace(/-menu$/, "") as MenuId).focus(); } });
|
|
@@ -675,7 +694,7 @@ $("zoom-in").addEventListener("click", () => (viewState.tab === "atomic" ? atomi
|
|
|
675
694
|
$("timeline-zoom-in").addEventListener("click", () => timelineController.adjustWindow("in")); $("timeline-zoom-out").addEventListener("click", () => timelineController.adjustWindow("out")); $("timeline-fit").addEventListener("click", () => timelineController.fit());
|
|
676
695
|
installViewStateNavigation(window, { getSearch: () => window.location.search, getValidation: (search) => { const targetPlan = search ? new URLSearchParams(search).get("plan") : null; if ((targetPlan ?? activePlanId) !== (viewState.plan ?? activePlanId)) return { plans: new Set(data?.plans.map((plan) => plan.id) ?? []) }; return data ? stateValidation() : {}; }, getPhase: () => data ? phase ?? derivePhase(data.tasks) : null, restore: (next, hasExplicitTab) => { const planChanged = next.plan !== viewState.plan; const closingDetails = viewState.detail && !next.detail; userChangedTab = hasExplicitTab; if (closingDetails) { keyboardSelection = false; hideHoverPreview(); restoreClosedDetailsOnNextRender = true; } viewState = next; selectedTask = next.selected?.kind === "task" ? next.selected.id : null; selectedGroup = next.selected?.kind === "group" ? next.selected.id : null; selectedMilestone = next.selected?.kind === "milestone" ? next.selected.id : null; activePlan = next.plan ?? ""; if (next.detail) focusDetailsOnNextRender = true; if (planChanged) void refresh(); else if (data) render(); } });
|
|
677
696
|
window.addEventListener("keydown", (event) => { trapDetailsFocus(event); trapPlanInfoFocus(event); if (event.key === "Escape" && planInfoIsOpen()) { event.preventDefault(); closePlanInfo(); } else if (event.key === "Escape" && viewState.detail) closeDetails(); });
|
|
678
|
-
window.addEventListener("resize", () => { refreshResponsiveDetails(); syncPlanInfoSurface(); if (data && viewState.tab === "graph" && graphController.rankDir() !== (window.innerWidth < 768 ? "TB" : "LR")) graphController.render(filteredTasks(), viewState.group ? new Set([viewState.group]) : undefined); window.requestAnimationFrame(() => timelineController.redraw()); });
|
|
697
|
+
window.addEventListener("resize", () => { setDetailsHeight(Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--details-height")) || window.innerHeight * 0.4, false); refreshResponsiveDetails(); syncPlanInfoSurface(); if (data && viewState.tab === "graph" && graphController.rankDir() !== (window.innerWidth < 768 ? "TB" : "LR")) graphController.render(filteredTasks(), viewState.group ? new Set([viewState.group]) : undefined); window.requestAnimationFrame(() => timelineController.redraw()); });
|
|
679
698
|
anyHoverCapability.addEventListener("change", refreshResponsiveDetails); anyFinePointerCapability.addEventListener("change", refreshResponsiveDetails);
|
|
680
699
|
const stream = new EventSource("api/events"); stream.addEventListener("open", () => { connected = true; connection("live"); void refresh(); }); stream.addEventListener("revision", () => void refresh()); stream.addEventListener("stale", () => { connected = false; connection("stale"); }); stream.addEventListener("heartbeat", (event) => { try { if (JSON.parse((event as MessageEvent).data).watching && !connected) { connected = true; void refresh(); } } catch { connection("stale"); } }); stream.addEventListener("error", () => { connected = false; connection("stale"); });
|
|
681
700
|
hoverBridge = createHoverPreviewBridge($("hover-preview"), dismissHoverPreview);
|
|
@@ -149,7 +149,7 @@ export function createAtomicGraphController(context: GraphContext) {
|
|
|
149
149
|
pending = false;
|
|
150
150
|
container.style.height = `${Math.max(420, result.height + 80)}px`;
|
|
151
151
|
update?.();
|
|
152
|
-
requestAnimationFrame(() => { if (serial !== generation) return; instance?.setViewport({ x: Math.max(24, (container.clientWidth - result.width) / 2), y: 24, zoom: 1 }); context.setZoom(1); highlightAccessibleSelection(projection, context); });
|
|
152
|
+
requestAnimationFrame(() => { if (serial !== generation) return; 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); highlightAccessibleSelection(projection, context); });
|
|
153
153
|
}).catch(() => { if (serial !== generation) return; pending = false; update?.(); });
|
|
154
154
|
}
|
|
155
155
|
|