@minonl/dia 0.0.13 → 0.0.14
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/dist/cli.js +48 -17
- package/docs/design/graph-web-mockup.html +220 -0
- package/docs/design/graph-web.md +61 -0
- package/docs/index.md +6 -0
- package/docs/worktree.md +15 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4776,12 +4776,29 @@ function build(opts = {}) {
|
|
|
4776
4776
|
// src/graph-tui.ts
|
|
4777
4777
|
var ORDER = ["doing", "blocked", "review", "todo", "awaiting_acceptance", "accepted", "done", "dropped", "rejected", "escalated"];
|
|
4778
4778
|
var color = (s, code, on) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
4779
|
+
var clip2 = (s, n) => {
|
|
4780
|
+
const one = String(s || "").replace(/\s+/g, " ").trim();
|
|
4781
|
+
return one.length > n ? `${one.slice(0, n - 1)}…` : one;
|
|
4782
|
+
};
|
|
4783
|
+
var shortId = (id) => id ? id.replace(/^ses_/, "").slice(-8) : "";
|
|
4779
4784
|
function render2(g, opts = {}) {
|
|
4780
4785
|
const on = opts.color ?? false;
|
|
4781
|
-
const
|
|
4782
|
-
const
|
|
4786
|
+
const width = Math.max(32, opts.width ?? 96);
|
|
4787
|
+
const tasksAll = g.nodes.filter((n) => n.kind === "task");
|
|
4788
|
+
const tasks = tasksAll.filter((t) => (!opts.status || t.status === opts.status) && (!opts.owner || t.owner === opts.owner));
|
|
4789
|
+
const taskById = new Map(tasksAll.map((t) => [t.id, t]));
|
|
4783
4790
|
const edgesFrom = (id, kind) => g.edges.filter((e) => e.from === id && e.kind === kind);
|
|
4784
4791
|
const lines = [];
|
|
4792
|
+
const counts = new Map;
|
|
4793
|
+
for (const t of tasksAll)
|
|
4794
|
+
counts.set(t.status || "todo", (counts.get(t.status || "todo") || 0) + 1);
|
|
4795
|
+
const countText = [...ORDER.filter((s) => counts.has(s)), ...[...counts.keys()].filter((s) => !ORDER.includes(s))].map((s) => `${s}=${counts.get(s)}`).join(" ");
|
|
4796
|
+
const claims = g.edges.filter((e) => e.kind === "claims");
|
|
4797
|
+
const filter = [opts.status ? `status=${opts.status}` : "", opts.owner ? `owner=${opts.owner}` : ""].filter(Boolean).join(" ");
|
|
4798
|
+
lines.push(color(`dia graph tasks=${tasksAll.length} ${countText} claims=${claims.length}${filter ? ` filter: ${filter}` : ""}`, "1", on));
|
|
4799
|
+
if (opts.now)
|
|
4800
|
+
lines.push(color(`updated ${opts.now.toLocaleTimeString()}`, "2", on));
|
|
4801
|
+
lines.push("");
|
|
4785
4802
|
const groups = new Map;
|
|
4786
4803
|
for (const t of tasks) {
|
|
4787
4804
|
const st = t.status || "todo";
|
|
@@ -4790,34 +4807,41 @@ function render2(g, opts = {}) {
|
|
|
4790
4807
|
for (const st of [...ORDER.filter((s) => groups.has(s)), ...[...groups.keys()].filter((s) => !ORDER.includes(s))]) {
|
|
4791
4808
|
lines.push(color(`[${st}]`, "1;36", on));
|
|
4792
4809
|
for (const t of groups.get(st).sort((a, b) => a.label.localeCompare(b.label))) {
|
|
4793
|
-
|
|
4810
|
+
const owner = t.owner ? ` @${shortId(t.owner)}` : "";
|
|
4811
|
+
const deps = edgesFrom(t.id, "depends_on").length;
|
|
4812
|
+
lines.push(` ${color("●", "32", on)} ${color(clip2(t.label, width - 24), "1", on)} ${color(shortId(t.id.replace(/^task:/, "")), "2", on)}${owner}${deps ? ` deps=${deps}` : ""}`);
|
|
4794
4813
|
for (const e of edgesFrom(t.id, "depends_on")) {
|
|
4795
4814
|
const dep = taskById.get(e.to);
|
|
4796
|
-
lines.push(` ${color("↳ depends", "34", on)} ${dep?.label || e.to}${dep ? ` [${dep.status}]` : ""}`);
|
|
4815
|
+
lines.push(` ${color("↳ depends", "34", on)} ${clip2(dep?.label || e.to, width - 20)}${dep ? ` [${dep.status}]` : ""}`);
|
|
4797
4816
|
}
|
|
4798
4817
|
for (const e of edgesFrom(t.id, "touches"))
|
|
4799
|
-
lines.push(` ${color("↳ touches", "35", on)} ${e.to.replace(/^path:/, "")}`);
|
|
4800
|
-
if (t.blocked_by?.length)
|
|
4801
|
-
|
|
4818
|
+
lines.push(` ${color("↳ touches", "35", on)} ${clip2(e.to.replace(/^path:/, ""), width - 20)}`);
|
|
4819
|
+
if (t.blocked_by?.length) {
|
|
4820
|
+
const names = t.blocked_by.map((id) => taskById.get(`task:${id}`)?.label || id);
|
|
4821
|
+
lines.push(` ${color(`⚠ blocked_by ${names.map((x) => clip2(x, 40)).join(", ")}`, "31", on)}`);
|
|
4822
|
+
}
|
|
4802
4823
|
}
|
|
4803
4824
|
lines.push("");
|
|
4804
4825
|
}
|
|
4805
4826
|
if (!tasks.length)
|
|
4806
|
-
lines.push("(no tasks)");
|
|
4807
|
-
|
|
4808
|
-
if (claimEdges.length) {
|
|
4827
|
+
lines.push(opts.status || opts.owner ? "(no matching tasks)" : "(no tasks)");
|
|
4828
|
+
if (claims.length) {
|
|
4809
4829
|
lines.push(color("claims", "1;36", on));
|
|
4810
|
-
for (const e of
|
|
4811
|
-
|
|
4830
|
+
for (const e of claims) {
|
|
4831
|
+
const holder = e.from.replace(/^session:/, "");
|
|
4832
|
+
if (opts.owner && holder !== opts.owner)
|
|
4833
|
+
continue;
|
|
4834
|
+
lines.push(` ${shortId(holder)} → ${clip2(e.to.replace(/^path:/, ""), width - 16)}`);
|
|
4835
|
+
}
|
|
4812
4836
|
lines.push("");
|
|
4813
4837
|
}
|
|
4814
4838
|
for (const c of g.cycles)
|
|
4815
|
-
lines.push(color(`⚠ cycle: ${c.join(" -> ")}`, "31", on));
|
|
4839
|
+
lines.push(color(`⚠ cycle: ${c.map((x) => x.replace(/^task:/, "")).join(" -> ")}`, "31", on));
|
|
4816
4840
|
return lines.join(`
|
|
4817
4841
|
`);
|
|
4818
4842
|
}
|
|
4819
4843
|
function watch(build, opts = {}) {
|
|
4820
|
-
const paint = () => process.stdout.write("\x1B[2J\x1B[H" + render2(build(), {
|
|
4844
|
+
const paint = () => process.stdout.write("\x1B[2J\x1B[H" + render2(build(), { ...opts, now: new Date }) + `
|
|
4821
4845
|
`);
|
|
4822
4846
|
paint();
|
|
4823
4847
|
const timer = setInterval(() => {
|
|
@@ -8565,7 +8589,7 @@ usage:
|
|
|
8565
8589
|
dia agents list|register|unregister|discover
|
|
8566
8590
|
dia msg deliver <agent> <id> [--transport tmux|acp]
|
|
8567
8591
|
dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
|
|
8568
|
-
dia graph [--json] [--task t-...] [--tui [--watch] [--no-color]]
|
|
8592
|
+
dia graph [--json] [--task t-...] [--tui [--watch] [--status S] [--owner S] [--width N] [--no-color]]
|
|
8569
8593
|
dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] [--gc [--days 7]] | dia unclaim <path|id> [--force]
|
|
8570
8594
|
dia team run <id> --mock | --acp|--dsh --workspace DIR
|
|
8571
8595
|
dia team gate|accept|reject|next
|
|
@@ -8959,10 +8983,17 @@ async function taskCli(argv) {
|
|
|
8959
8983
|
async function graphCli(argv) {
|
|
8960
8984
|
const asJson = has(argv, "--json");
|
|
8961
8985
|
if (has(argv, "--tui") || has(argv, "--watch")) {
|
|
8962
|
-
const
|
|
8986
|
+
const width = Number(flag(argv, "--width") || 0);
|
|
8987
|
+
const tuiOpts = {
|
|
8988
|
+
color: !!process.stdout.isTTY && !has(argv, "--no-color"),
|
|
8989
|
+
status: flag(argv, "--status"),
|
|
8990
|
+
owner: flag(argv, "--owner"),
|
|
8991
|
+
width: width > 0 ? width : undefined
|
|
8992
|
+
};
|
|
8993
|
+
const paint = (g) => out(render2(g, tuiOpts));
|
|
8963
8994
|
try {
|
|
8964
8995
|
if (has(argv, "--watch"))
|
|
8965
|
-
return await watch(() => build({ task: flag(argv, "--task") }),
|
|
8996
|
+
return await watch(() => build({ task: flag(argv, "--task") }), tuiOpts);
|
|
8966
8997
|
const g = build({ task: flag(argv, "--task") });
|
|
8967
8998
|
paint(g);
|
|
8968
8999
|
return g.cycles.length ? 3 : 0;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>dia graph · 视觉设计稿</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{
|
|
9
|
+
--bg:#f6f7f9; --panel:#fff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb;
|
|
10
|
+
--blue:#2563eb; --violet:#7c3aed; --cyan:#0891b2; --green:#16a34a; --red:#dc2626; --amber:#d97706;
|
|
11
|
+
--shadow:0 1px 2px rgba(17,24,39,.06),0 8px 24px rgba(17,24,39,.06);
|
|
12
|
+
}
|
|
13
|
+
@media (prefers-color-scheme: dark){
|
|
14
|
+
:root{ --bg:#0f1115; --panel:#171a21; --ink:#e5e7eb; --muted:#9ca3af; --line:#2a2f3a;
|
|
15
|
+
--shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.25); }
|
|
16
|
+
}
|
|
17
|
+
*{box-sizing:border-box}
|
|
18
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
19
|
+
header{display:flex;align-items:center;gap:16px;padding:16px 22px;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
20
|
+
header h1{font-size:17px;margin:0;letter-spacing:.2px}
|
|
21
|
+
header .sub{color:var(--muted);font-size:12px}
|
|
22
|
+
header .spacer{flex:1}
|
|
23
|
+
.pill{border:1px solid var(--line);border-radius:999px;padding:4px 10px;font-size:12px;color:var(--muted);background:var(--bg)}
|
|
24
|
+
main{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:18px;padding:18px;max-width:1480px;margin:0 auto}
|
|
25
|
+
.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
|
|
26
|
+
.toolbar{display:flex;gap:8px;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap}
|
|
27
|
+
.seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
|
|
28
|
+
.seg button{border:0;background:transparent;color:var(--muted);padding:6px 10px;font:inherit;font-size:12px;cursor:pointer}
|
|
29
|
+
.seg button.on{background:var(--blue);color:#fff}
|
|
30
|
+
.toolbar .hint{margin-left:auto;color:var(--muted);font-size:12px}
|
|
31
|
+
.canvas{position:relative;height:640px;overflow:auto;padding:10px}
|
|
32
|
+
svg.edges{position:absolute;inset:0;pointer-events:none}
|
|
33
|
+
svg.edges line{stroke:#94a3b8;stroke-width:1.2;opacity:.8}
|
|
34
|
+
svg.edges line.depends_on{stroke:var(--blue)}
|
|
35
|
+
svg.edges line.touches{stroke:var(--violet);stroke-dasharray:3 4}
|
|
36
|
+
svg.edges line.claims{stroke:var(--cyan);stroke-dasharray:5 4}
|
|
37
|
+
.node{position:absolute;width:206px;border:1px solid var(--line);border-left:4px solid var(--muted);border-radius:10px;background:var(--panel);padding:9px 11px;box-shadow:var(--shadow);cursor:pointer;transition:transform .12s ease,box-shadow .12s ease}
|
|
38
|
+
.node:hover{transform:translateY(-1px);box-shadow:0 10px 28px rgba(17,24,39,.12)}
|
|
39
|
+
.node.task.doing{border-left-color:var(--blue)}
|
|
40
|
+
.node.task.blocked{border-left-color:var(--red)}
|
|
41
|
+
.node.task.review{border-left-color:var(--amber)}
|
|
42
|
+
.node.task.done,.node.task.accepted{border-left-color:var(--green)}
|
|
43
|
+
.node.path{border-left-color:var(--violet)}
|
|
44
|
+
.node.session{border-left-color:var(--cyan)}
|
|
45
|
+
.node b{display:block;font-size:13px;line-height:1.35;margin-bottom:3px}
|
|
46
|
+
.node .meta{color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
47
|
+
.badge{display:inline-block;font-size:10px;padding:1px 6px;border-radius:999px;border:1px solid var(--line);color:var(--muted);margin-top:4px}
|
|
48
|
+
.col-label{position:absolute;top:10px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
|
49
|
+
aside{display:flex;flex-direction:column;gap:14px}
|
|
50
|
+
.side-title{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--line)}
|
|
51
|
+
.side-title h2{font-size:13px;margin:0}
|
|
52
|
+
.side-title span{font-size:11px;color:var(--muted)}
|
|
53
|
+
.list{padding:6px 14px 12px}
|
|
54
|
+
.item{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
55
|
+
.item:last-child{border-bottom:0}
|
|
56
|
+
.item .k{color:var(--muted)}
|
|
57
|
+
.dot{display:inline-block;width:7px;height:7px;border-radius:99px;background:var(--muted);margin-right:6px;vertical-align:1px}
|
|
58
|
+
.dot.blue{background:var(--blue)} .dot.red{background:var(--red)} .dot.green{background:var(--green)} .dot.amber{background:var(--amber)}
|
|
59
|
+
.legend{padding:10px 14px 14px;color:var(--muted);font-size:12px;display:grid;gap:6px}
|
|
60
|
+
.legend i{display:inline-block;width:18px;height:2px;margin-right:8px;vertical-align:3px}
|
|
61
|
+
.attention{padding:8px 14px 14px}
|
|
62
|
+
.attention .row{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
63
|
+
.attention .row:last-child{border-bottom:0}
|
|
64
|
+
.tag{font-size:10px;border-radius:6px;padding:1px 6px;background:rgba(220,38,38,.12);color:var(--red);white-space:nowrap}
|
|
65
|
+
.tag.amber{background:rgba(217,119,6,.12);color:var(--amber)}
|
|
66
|
+
.detail{padding:14px;display:none}
|
|
67
|
+
.detail.show{display:block}
|
|
68
|
+
.detail h3{font-size:14px;margin:0 0 6px}
|
|
69
|
+
.detail p{margin:0 0 8px;color:var(--muted);font-size:12px}
|
|
70
|
+
.kv{display:grid;grid-template-columns:80px 1fr;gap:4px 10px;font-size:12px}
|
|
71
|
+
.kv .k{color:var(--muted)}
|
|
72
|
+
footer{padding:0 22px 22px;color:var(--muted);font-size:11px}
|
|
73
|
+
@media (max-width:980px){ main{grid-template-columns:1fr} .canvas{height:560px} }
|
|
74
|
+
</style>
|
|
75
|
+
</head>
|
|
76
|
+
<body>
|
|
77
|
+
<header>
|
|
78
|
+
<h1>dia graph</h1>
|
|
79
|
+
<span class="sub">只读依赖图 · 视觉设计稿 v0.1</span>
|
|
80
|
+
<span class="pill">静态 mockup · 无网络</span>
|
|
81
|
+
<span class="spacer"></span>
|
|
82
|
+
<span class="pill">updated 18:42</span>
|
|
83
|
+
</header>
|
|
84
|
+
|
|
85
|
+
<main>
|
|
86
|
+
<section class="panel">
|
|
87
|
+
<div class="toolbar">
|
|
88
|
+
<div class="seg" id="filter">
|
|
89
|
+
<button class="on" data-f="all">全部</button>
|
|
90
|
+
<button data-f="doing">进行中</button>
|
|
91
|
+
<button data-f="blocked">阻塞</button>
|
|
92
|
+
<button data-f="review">待评审</button>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="seg" id="density">
|
|
95
|
+
<button class="on" data-d="comfortable">舒适</button>
|
|
96
|
+
<button data-d="compact">紧凑</button>
|
|
97
|
+
</div>
|
|
98
|
+
<span class="hint">点击节点查看详情 · 边:依赖 / 触碰 / claim</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="canvas" id="canvas">
|
|
101
|
+
<svg class="edges" id="edges" width="1200" height="640"></svg>
|
|
102
|
+
<div class="col-label" style="left:18px">sessions</div>
|
|
103
|
+
<div class="col-label" style="left:250px">todo</div>
|
|
104
|
+
<div class="col-label" style="left:470px">doing</div>
|
|
105
|
+
<div class="col-label" style="left:690px">review</div>
|
|
106
|
+
<div class="col-label" style="left:910px">paths</div>
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
|
|
110
|
+
<aside>
|
|
111
|
+
<div class="panel">
|
|
112
|
+
<div class="side-title"><h2>概览</h2><span>live data</span></div>
|
|
113
|
+
<div class="list">
|
|
114
|
+
<div class="item"><span class="k"><i class="dot blue"></i>任务</span><b>7</b></div>
|
|
115
|
+
<div class="item"><span class="k"><i class="dot red"></i>阻塞</span><b>1</b></div>
|
|
116
|
+
<div class="item"><span class="k"><i class="dot amber"></i>待评审</span><b>1</b></div>
|
|
117
|
+
<div class="item"><span class="k"><i class="dot green"></i>完成</span><b>2</b></div>
|
|
118
|
+
<div class="item"><span class="k">claims</span><b>3</b></div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="panel">
|
|
123
|
+
<div class="side-title"><h2>需要人</h2><span>attention</span></div>
|
|
124
|
+
<div class="attention">
|
|
125
|
+
<div class="row"><span class="tag">阻塞</span><div><b>install.sh 迁移</b><br><span class="k">等待数据根切换完成</span></div></div>
|
|
126
|
+
<div class="row"><span class="tag amber">claim</span><div><b>src/collab.ts</b><br><span class="k">session-a 持有中</span></div></div>
|
|
127
|
+
<div class="row"><span class="tag amber">评审</span><div><b>graph TUI</b><br><span class="k">等待独立 verifier</span></div></div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="panel">
|
|
132
|
+
<div class="side-title"><h2>图例</h2><span>legend</span></div>
|
|
133
|
+
<div class="legend">
|
|
134
|
+
<div><i style="background:var(--blue)"></i>depends_on(任务依赖)</div>
|
|
135
|
+
<div><i style="background:var(--violet)"></i>touches(触碰路径)</div>
|
|
136
|
+
<div><i style="background:var(--cyan)"></i>claims(会话声明)</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="panel detail" id="detail">
|
|
141
|
+
<div class="side-title"><h2>节点详情</h2><button onclick="document.getElementById('detail').classList.remove('show')" style="border:0;background:transparent;color:var(--muted);cursor:pointer">关闭</button></div>
|
|
142
|
+
<div class="detail" style="display:block" id="detail-body"></div>
|
|
143
|
+
</div>
|
|
144
|
+
</aside>
|
|
145
|
+
</main>
|
|
146
|
+
<footer>视觉稿:布局 / 信息层级 / 边语义。真实实现需先解除「框架无网络模块」限制并单独评审。</footer>
|
|
147
|
+
|
|
148
|
+
<script>
|
|
149
|
+
const nodes = [
|
|
150
|
+
{id:'session:ses_a', kind:'session', label:'session-a', x:20, y:60},
|
|
151
|
+
{id:'session:ses_b', kind:'session', label:'session-b', x:20, y:200},
|
|
152
|
+
{id:'task:t-1', kind:'task', status:'todo', label:'数据根迁移', x:240, y:40, owner:'ses_a'},
|
|
153
|
+
{id:'task:t-2', kind:'task', status:'doing', label:'install.sh 拆分', x:460, y:40, owner:'ses_a'},
|
|
154
|
+
{id:'task:t-3', kind:'task', status:'blocked', label:'launchd 安装脚本', x:680, y:40, owner:'ses_b'},
|
|
155
|
+
{id:'task:t-4', kind:'task', status:'review', label:'graph TUI', x:680, y:190, owner:'ses_b'},
|
|
156
|
+
{id:'task:t-5', kind:'task', status:'done', label:'dia msg M0', x:240, y:190},
|
|
157
|
+
{id:'task:t-6', kind:'task', status:'accepted', label:'claim 机制', x:240, y:320},
|
|
158
|
+
{id:'task:t-7', kind:'task', status:'doing', label:'handoff + claim', x:460, y:320, owner:'ses_a'},
|
|
159
|
+
{id:'path:install.sh', kind:'path', label:'install.sh', x:900, y:40},
|
|
160
|
+
{id:'path:src/collab.ts', kind:'path', label:'src/collab.ts', x:900, y:180},
|
|
161
|
+
{id:'path:docs/msg.md', kind:'path', label:'docs/msg.md', x:900, y:320}
|
|
162
|
+
];
|
|
163
|
+
const edges = [
|
|
164
|
+
{from:'task:t-2', to:'task:t-1', kind:'depends_on'},
|
|
165
|
+
{from:'task:t-3', to:'task:t-2', kind:'depends_on'},
|
|
166
|
+
{from:'task:t-7', to:'task:t-6', kind:'depends_on'},
|
|
167
|
+
{from:'task:t-2', to:'path:install.sh', kind:'touches'},
|
|
168
|
+
{from:'task:t-4', to:'path:src/collab.ts', kind:'touches'},
|
|
169
|
+
{from:'task:t-7', to:'path:src/collab.ts', kind:'touches'},
|
|
170
|
+
{from:'session:ses_a', to:'path:src/collab.ts', kind:'claims'},
|
|
171
|
+
{from:'session:ses_b', to:'path:install.sh', kind:'claims'}
|
|
172
|
+
];
|
|
173
|
+
const canvas = document.getElementById('canvas');
|
|
174
|
+
const svg = document.getElementById('edges');
|
|
175
|
+
const byId = new Map(nodes.map(n=>[n.id,n]));
|
|
176
|
+
function card(n){
|
|
177
|
+
const el=document.createElement('div');
|
|
178
|
+
el.className='node '+n.kind+(n.status?' '+n.status:'');
|
|
179
|
+
el.style.left=n.x+'px'; el.style.top=n.y+'px'; el.dataset.id=n.id;
|
|
180
|
+
const meta = n.kind==='task' ? (n.owner?'@'+n.owner:'unassigned') : n.id.split(':')[0];
|
|
181
|
+
el.innerHTML='<b>'+esc(n.label)+'</b><div class="meta">'+esc(n.id)+'</div>'+
|
|
182
|
+
(n.status?'<span class="badge">'+esc(n.status)+'</span>':'')+
|
|
183
|
+
'<div class="meta" style="margin-top:4px">'+esc(meta)+'</div>';
|
|
184
|
+
el.onclick=()=>show(n);
|
|
185
|
+
return el;
|
|
186
|
+
}
|
|
187
|
+
function esc(s){return String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]))}
|
|
188
|
+
function draw(){
|
|
189
|
+
[...canvas.querySelectorAll('.node')].forEach(e=>e.remove());
|
|
190
|
+
nodes.forEach(n=>canvas.appendChild(card(n)));
|
|
191
|
+
const maxX=Math.max(...nodes.map(n=>n.x))+240, maxY=Math.max(...nodes.map(n=>n.y))+120;
|
|
192
|
+
svg.setAttribute('width',maxX); svg.setAttribute('height',maxY);
|
|
193
|
+
svg.innerHTML=edges.map(e=>{
|
|
194
|
+
const a=byId.get(e.from), b=byId.get(e.to); if(!a||!b) return '';
|
|
195
|
+
return '<line class="'+e.kind+'" x1="'+(a.x+103)+'" y1="'+(a.y+30)+'" x2="'+(b.x+103)+'" y2="'+(b.y+30)+'" />';
|
|
196
|
+
}).join('');
|
|
197
|
+
}
|
|
198
|
+
function show(n){
|
|
199
|
+
const d=document.getElementById('detail'), body=document.getElementById('detail-body');
|
|
200
|
+
const out=edges.filter(e=>e.from===n.id||e.to===n.id).map(e=>{
|
|
201
|
+
const other=e.from===n.id?e.to:e.from;
|
|
202
|
+
return '<div class="item"><span class="k">'+e.kind+'</span><span>'+esc(other)+'</span></div>';
|
|
203
|
+
}).join('') || '<p class="muted">无边</p>';
|
|
204
|
+
body.innerHTML='<h3>'+esc(n.label)+'</h3><p>'+esc(n.id)+'</p><div class="kv"><div class="k">kind</div><div>'+esc(n.kind)+'</div><div class="k">status</div><div>'+esc(n.status||'-')+'</div><div class="k">owner</div><div>'+esc(n.owner||'-')+'</div></div><h3 style="margin-top:14px">关联</h3>'+out;
|
|
205
|
+
d.classList.add('show');
|
|
206
|
+
}
|
|
207
|
+
document.querySelectorAll('#filter button').forEach(b=>b.onclick=()=>{
|
|
208
|
+
document.querySelectorAll('#filter button').forEach(x=>x.classList.remove('on')); b.classList.add('on');
|
|
209
|
+
const f=b.dataset.f;
|
|
210
|
+
nodes.forEach(n=>{ if(n.kind==='task'){ const el=document.querySelector('[data-id="'+n.id+'"]'); if(el) el.style.display=(f==='all'||n.status===f)?'':'none'; }});
|
|
211
|
+
});
|
|
212
|
+
document.querySelectorAll('#density button').forEach(b=>b.onclick=()=>{
|
|
213
|
+
document.querySelectorAll('#density button').forEach(x=>x.classList.remove('on')); b.classList.add('on');
|
|
214
|
+
const compact=b.dataset.d==='compact';
|
|
215
|
+
document.querySelectorAll('.node').forEach(el=>{el.style.padding=compact?'5px 8px':'9px 11px';el.style.width=compact?'180px':'206px';});
|
|
216
|
+
});
|
|
217
|
+
draw();
|
|
218
|
+
</script>
|
|
219
|
+
</body>
|
|
220
|
+
</html>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# dia graph Web 视觉设计稿(v0.1)
|
|
2
|
+
|
|
3
|
+
> 静态 mockup:[`graph-web-mockup.html`](graph-web-mockup.html)(直接用浏览器打开,无网络/无依赖)
|
|
4
|
+
> 数据契约:`dia graph --json`(schema 1);本稿只做视觉与信息层级,不是实现。
|
|
5
|
+
|
|
6
|
+
## 1. 布局
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
┌ header: dia graph · 只读 · 更新时间 ──────────────────────────┐
|
|
10
|
+
├ 工具栏:状态过滤 | 密度 | 提示 │
|
|
11
|
+
├ 主画布(卡片 + SVG 边) ┌ 右侧 320px ────┐
|
|
12
|
+
│ sessions | todo | doing | review | paths │ 概览 / 需要人 │
|
|
13
|
+
│ │ 图例 / 节点详情 │
|
|
14
|
+
└ footer: 数据来源 / 约束 ───────────────────┴────────────────┘
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- 主画布按状态分列:session → todo → doing → review → path;边的方向从左到右。
|
|
18
|
+
- 右侧固定 320px:概览计数、attention(阻塞/claim/待评审)、图例、节点详情。
|
|
19
|
+
- 节点卡片颜色只表达 **kind + status**,不用于装饰。
|
|
20
|
+
|
|
21
|
+
## 2. 信息层级
|
|
22
|
+
|
|
23
|
+
| 层 | 内容 | 设计 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| 第一层 | task 标题 + status | 加粗 13px;状态色只出现在左边框和 badge |
|
|
26
|
+
| 第二层 | task id / owner | 11px muted;owner 显示短 id(`@a1b2c3d4`) |
|
|
27
|
+
| 第三层 | 依赖/触碰/claim | SVG 边 + 右侧详情;不在卡片里堆文字 |
|
|
28
|
+
| attention | 阻塞 / claim 冲突 / 待评审 | 右上角固定区,不藏在 hover 里 |
|
|
29
|
+
|
|
30
|
+
## 3. 交互
|
|
31
|
+
|
|
32
|
+
- 点击节点 → 右侧详情:kind/status/owner + 关联边列表;
|
|
33
|
+
- 顶部过滤:all / doing / blocked / review(客户端过滤,不改数据);
|
|
34
|
+
- 密度切换:comfortable / compact;
|
|
35
|
+
- 刷新:MVP 静态导出;live 版用 `dia graph --json` 轮询(2s),不做 WebSocket;
|
|
36
|
+
- 不做:编辑、拖拽改依赖、写 claim(只读)。
|
|
37
|
+
|
|
38
|
+
## 4. 与现有数据契约的映射
|
|
39
|
+
|
|
40
|
+
| UI | `dia graph --json` |
|
|
41
|
+
|---|---|
|
|
42
|
+
| 卡片 | `nodes[]`:task/session/path |
|
|
43
|
+
| 依赖边 | `edges[kind=depends_on]` |
|
|
44
|
+
| 触碰边 | `edges[kind=touches]` |
|
|
45
|
+
| claim 边 | `edges[kind=claims]` |
|
|
46
|
+
| 阻塞提示 | task 节点 `blocked_by[]`(派生,不是边) |
|
|
47
|
+
| 环 | `cycles[]`,顶部红条提示 |
|
|
48
|
+
|
|
49
|
+
## 5. 实现约束(重要)
|
|
50
|
+
|
|
51
|
+
仓库的 `tests/static.test.ts` 禁止框架 import `node:http` / `fetch` / 网络模块。因此:
|
|
52
|
+
|
|
53
|
+
1. **本稿只是静态 HTML**,不进入 `src/`,不启服务;
|
|
54
|
+
2. 若将来要做真实 Web:
|
|
55
|
+
- 方案 A:`dia graph --export-html` 生成静态文件(推荐,不需要网络);
|
|
56
|
+
- 方案 B:独立可选包 `@dia/graph-web`(明确例外 + 只绑 loopback + 单独评审),不改核心框架规则。
|
|
57
|
+
|
|
58
|
+
## 6. 后续
|
|
59
|
+
|
|
60
|
+
- 先确认视觉稿的列布局/信息密度/右侧 attention 位置;
|
|
61
|
+
- 确认后先做 `--export-html` 静态导出(零网络、零 daemon),再评估 live 轮询。
|
package/docs/index.md
CHANGED
|
@@ -88,3 +88,9 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
|
|
|
88
88
|
|---|---|
|
|
89
89
|
| [框架启动方案(bootstrap)](history/framework-bootstrap.md) | 已归档(2026-11-03):本文的启动/分发方案(release tarball + agentctl + framework.lock)未采用; |
|
|
90
90
|
|
|
91
|
+
## 其他
|
|
92
|
+
|
|
93
|
+
| 文档 | 一句话 |
|
|
94
|
+
|---|---|
|
|
95
|
+
| [dia graph Web 视觉设计稿(v0.1)](design/graph-web.md) | 静态 mockup:graph-web-mockup.html(直接用浏览器打开,无网络/无依赖) |
|
|
96
|
+
|
package/docs/worktree.md
CHANGED
|
@@ -11,27 +11,29 @@ dia wt adopt <repo> --apply # 写入配置 + 三个守卫钩子
|
|
|
11
11
|
dia wt adopt --all --apply # 批量:仓根下所有主 checkout(跳过 *-worktree--*)
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
写入内容(幂等,**不改仓内文件、不写全局配置**):
|
|
15
15
|
|
|
16
|
-
|
|
|
17
|
-
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
16
|
+
| 仓类型 | 做法 | 记录 |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| 普通仓 | 钩子写 `<git-common-dir>/dia/hooks/`,并把**仓库本地** `core.hooksPath` 指过去(绝对路径) | `hooksManager=local` |
|
|
19
|
+
| **simple-git-hooks 仓** | 不碰 `.git/hooks`、不改 `package.json`;守卫写进 `SIMPLE_GIT_HOOKS_RC` 指向的脚本(钩子会 source 它) | `hooksManager=simple-git-hooks` |
|
|
20
|
+
| 已有其它 hooksPath(husky / `.githooks`) | **不抢占**,只报告(建议改用 simple-git-hooks) | `hooksManager=external` |
|
|
21
|
+
|
|
22
|
+
> 判定:`package.json` 里有 `simple-git-hooks`(配置或 devDependency)→ RC 模式;否则看 `core.hooksPath` 是否已被别人占用。
|
|
21
23
|
|
|
22
24
|
守卫行为:
|
|
23
25
|
|
|
24
26
|
| 钩子 | 规则 |
|
|
25
27
|
|---|---|
|
|
26
28
|
| `pre-commit` | 主 checkout 上、分支属于 protected(默认分支 + main/master/develop/dev)→ 拒绝 |
|
|
27
|
-
| `pre-push` | 主 checkout 上**任何 push**(含 tag / feature 分支)→ 拒绝;推 `main/master/develop/dev` →
|
|
29
|
+
| `pre-push` | 主 checkout 上**任何 push**(含 tag / feature 分支)→ 拒绝;推 `main/master/develop/dev` → 拒绝;并对推送内容做敏感信息扫描 |
|
|
28
30
|
| `post-checkout` | 主 checkout 切到非默认分支 → 黄色提示(不阻断) |
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
**钩子链**:本地钩子先跑守卫,再执行 `<common>/hooks/<name>`(simple-git-hooks 等管理器生成处);`pre-push` 的 stdin 原样转发。
|
|
33
|
+
`SIMPLE_GIT_HOOKS_RC` 由 adopt 写入 `~/.zshenv`(带 `# dia-wt:` 标记),脚本内容固定为 `dia wt guard <hook>`。
|
|
32
34
|
|
|
33
|
-
> ⚠️
|
|
34
|
-
> `
|
|
35
|
+
> ⚠️ husky / `.githooks` 仓:本地 `core.hooksPath` 会覆盖一切,守卫不生效(doctor WARN)。
|
|
36
|
+
> 迁移办法:把钩子迁到 simple-git-hooks(`postinstall: simple-git-hooks` + `simple-git-hooks` 配置),再 `dia wt adopt --apply`。
|
|
35
37
|
|
|
36
38
|
## 2. 巡检
|
|
37
39
|
|
|
@@ -53,6 +55,8 @@ dia wt doctor --all --fix # 无损修复:重装三个钩子 + 校正 hooksPa
|
|
|
53
55
|
| D06 | 无 prunable / locked |
|
|
54
56
|
| D07 | 同一分支不被多个 worktree 占用 |
|
|
55
57
|
| D11 | 默认分支可解析(config > origin/HEAD(校验仍存在)> main > master) |
|
|
58
|
+
| D20 | 全局 `core.hooksPath` 不得指向 dia 旧钩子目录(防外溢;有则 FAIL + 清理命令) |
|
|
59
|
+
| D21 | 仓库本地 `core.hooksPath` 与模式一致:local=dia 目录;external=只 WARN |
|
|
56
60
|
|
|
57
61
|
挂成日巡检(mac launchd / linux cron 由 `dia job` 统一):
|
|
58
62
|
|