wezard 1.4.1 → 2.0.0

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 (71) hide show
  1. package/CLAUDE.md +12 -7
  2. package/README.md +227 -112
  3. package/cli/wezard.sh +6 -3
  4. package/dist/daemon/approval.js +9 -3
  5. package/dist/daemon/approval.js.map +1 -1
  6. package/dist/daemon/cc-bridge.js.map +1 -1
  7. package/dist/daemon/chat-name.js +8 -14
  8. package/dist/daemon/chat-name.js.map +1 -1
  9. package/dist/daemon/detail.js +94 -9
  10. package/dist/daemon/detail.js.map +1 -1
  11. package/dist/daemon/graph.js +5 -7
  12. package/dist/daemon/graph.js.map +1 -1
  13. package/dist/daemon/inbound.js +135 -158
  14. package/dist/daemon/inbound.js.map +1 -1
  15. package/dist/daemon/index.js +452 -156
  16. package/dist/daemon/index.js.map +1 -1
  17. package/dist/daemon/jobs.js +2 -2
  18. package/dist/daemon/mirror-bridge.js +284 -140
  19. package/dist/daemon/mirror-bridge.js.map +1 -1
  20. package/dist/daemon/mirror-store.js.map +1 -1
  21. package/dist/daemon/model-select.js +74 -0
  22. package/dist/daemon/model-select.js.map +1 -0
  23. package/dist/daemon/peers.js +10 -16
  24. package/dist/daemon/peers.js.map +1 -1
  25. package/dist/daemon/spawn-tmux.js +27 -4
  26. package/dist/daemon/spawn-tmux.js.map +1 -1
  27. package/dist/daemon/task-gate.js +97 -0
  28. package/dist/daemon/task-gate.js.map +1 -0
  29. package/dist/daemon/task-registry.js +132 -0
  30. package/dist/daemon/task-registry.js.map +1 -0
  31. package/dist/daemon/tasks.js +126 -54
  32. package/dist/daemon/tasks.js.map +1 -1
  33. package/dist/daemon/wizard-memory.js +57 -0
  34. package/dist/daemon/wizard-memory.js.map +1 -0
  35. package/dist/daemon/wizard.js +84 -36
  36. package/dist/daemon/wizard.js.map +1 -1
  37. package/dist/mcp/server.js +138 -76
  38. package/dist/mcp/server.js.map +1 -1
  39. package/dist/shared/chat-http.js +193 -96
  40. package/dist/shared/chat-http.js.map +1 -1
  41. package/dist/shared/chat-render.js +31 -24
  42. package/dist/shared/chat-render.js.map +1 -1
  43. package/dist/shared/chat-view.js +14 -2
  44. package/dist/shared/chat-view.js.map +1 -1
  45. package/dist/shared/config.js +23 -4
  46. package/dist/shared/config.js.map +1 -1
  47. package/dist/shared/detail-render.js +14 -7
  48. package/dist/shared/detail-render.js.map +1 -1
  49. package/dist/shared/detail-store.js +12 -5
  50. package/dist/shared/detail-store.js.map +1 -1
  51. package/dist/shared/role-render.js +52 -0
  52. package/dist/shared/role-render.js.map +1 -0
  53. package/dist/shared/role-view.js +210 -0
  54. package/dist/shared/role-view.js.map +1 -0
  55. package/dist/shared/schedule-spec.js +92 -116
  56. package/dist/shared/schedule-spec.js.map +1 -1
  57. package/dist/shared/session-label.js +103 -72
  58. package/dist/shared/session-label.js.map +1 -1
  59. package/dist/shared/std.js +20 -0
  60. package/dist/shared/std.js.map +1 -1
  61. package/dist/shared/task-file.js +120 -0
  62. package/dist/shared/task-file.js.map +1 -0
  63. package/dist/shared/trigger.js +232 -0
  64. package/dist/shared/trigger.js.map +1 -0
  65. package/dist/shared/world.js +193 -0
  66. package/dist/shared/world.js.map +1 -0
  67. package/dist/svr/index.js +68 -3
  68. package/dist/svr/index.js.map +1 -1
  69. package/package.json +1 -1
  70. package/web/chat.css +609 -147
  71. package/web/chat.js +1274 -215
package/web/chat.css CHANGED
@@ -1,140 +1,293 @@
1
- /* Chat 详情视图外壳。turn 正文的样式来自 detail-render 的 SHARED_CSS/TURN_CSS,
2
- 由 /chat/app.css 拼在本文件之前 —— 这里只管三栏骨架、会话列表与页脚总账。 */
1
+ /* Rolepage 外壳。消息正文 (turn 卡片) 的样式来自 detail-render 的 SHARED_CSS/TURN_CSS,
2
+ 由 /chat/app.css 拼在本文件之前 —— 这里管骨架、会话列表、左右气泡与页脚总账。
3
+ 视觉只押一处: 「视角」。我说的靠右、染成视角色; 别人说的靠左、带头像; 气泡对面
4
+ 那片空白是一道门 —— 点下去就换成对方的视角。其余一律安静。 */
5
+
6
+ :root {
7
+ --paper: #eef1f5; /* 对话底 —— 冷灰, 让白色与视角色的两种气泡都站得住 */
8
+ --card: #fff;
9
+ --ink: #1c2430;
10
+ --mute: #6a7483;
11
+ --faint: #9aa3af;
12
+ --line: #d9dee6;
13
+ --mine: #eef3fd; /* 当前视角说的话 */
14
+ --mine-soft: #e2eafa; /* 它内部还要再沉一档的块 (代码/结果/折叠头) */
15
+ --mine-line: #c9d7f3;
16
+ --accent: #2f55c9; /* 视角色: 名字的点、选中态、自己的气泡描边 */
17
+ --priv: #9a5b10; /* 私聊 */
18
+ --pub: #237a55; /* 公开 */
19
+ }
3
20
 
4
21
  html, body { height: 100% }
5
22
  /* overscroll-behavior:none 阻止整页橡皮筋回弹 —— webview 里它会和内层
6
23
  .thread 抢滚动手势, 表现为"滚一下卡住/整页在弹"。 */
7
- body { overflow: hidden; overscroll-behavior: none }
24
+ body { overflow: hidden; overscroll-behavior: none; color: var(--ink) }
25
+ button { font: inherit; color: inherit }
26
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px }
8
27
 
9
28
  .app { display: flex; height: 100vh; height: 100dvh }
29
+ @keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .3 } }
30
+
31
+ /* 名字 = 地址。点是地址的一部分, 也是这一页唯一的颜色记号。 */
32
+ .nm { font-weight: 600 }
33
+ .nm::before { content: "."; color: var(--accent); font-weight: 700 }
34
+ .nm.human::before, .nm.task::before, .nm.chat::before { content: none }
10
35
 
11
- /* ── 侧栏: 会话列表 ─────────────────────────────────────────────── */
36
+ /* ── 侧栏: 我 + 会话 ─────────────────────────────────────────────── */
12
37
  .side {
13
- width: 288px; flex: none; background: #fff; border-right: 1px solid #d0d7de;
38
+ width: 312px; flex: none; background: var(--card); border-right: 1px solid var(--line);
14
39
  display: flex; flex-direction: column; min-height: 0;
15
40
  }
16
- .side-head { padding: 13px 16px 11px; border-bottom: 1px solid #eaeef2 }
17
- .side-head .l1 { display: flex; align-items: baseline; gap: 8px }
18
- .side-head .t { font-size: 14px; font-weight: 600 }
19
- .side-head .n {
20
- margin-left: auto; font-size: 11px; color: #8c959f;
21
- font-variant-numeric: tabular-nums;
41
+
42
+ /* ── 左栏顶: 当前 role 的名片 (身份 + 关系/日程入口) ──── */
43
+ .rolebar {
44
+ position: relative; flex: none; padding: 16px 16px 14px; border-bottom: 1px solid var(--line);
45
+ display: flex; flex-direction: column; gap: 8px; background: var(--card);
22
46
  }
23
- .side-head .s {
24
- font-size: 11px; color: #8c959f; font-family: ui-monospace, Menlo, monospace;
25
- word-break: break-all; margin-top: 3px;
47
+ .rolebar .conn { position: absolute; top: 12px; right: 14px }
48
+ .rolebar .id { display: flex; gap: 10px; align-items: center; min-width: 0; padding-right: 14px }
49
+ .rolebar .id .av { font-size: 26px; line-height: 1; flex: none }
50
+ .rolebar .id .l { min-width: 0; display: flex; flex-direction: column; gap: 2px }
51
+ .rolebar .id .nm { font-size: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
52
+ .rolebar .id .k { font-size: 11.5px; color: var(--mute) }
53
+ .rolebar .job { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--ink); opacity: .8 }
54
+ .rolebar .facts { display: flex; gap: 6px 12px; flex-wrap: wrap; font-size: 11.5px; color: var(--mute) }
55
+ .rolebar .facts span { white-space: nowrap }
56
+ .rolebar .facts .go { cursor: pointer; color: var(--accent) }
57
+ .rolebar .facts .go:hover { text-decoration: underline }
58
+ .rolebar .acts { display: flex; gap: 6px; flex-wrap: wrap; align-items: center }
59
+ .rolebar .acts:empty { display: none }
60
+ /* session 切换: 按钮 + 浮层列表, 取代原生 <select>。 */
61
+ .sp { position: relative; flex: 1 1 100%; min-width: 0 }
62
+ .sp-btn {
63
+ width: 100%; display: flex; align-items: center; gap: 8px; cursor: pointer; text-align: left;
64
+ font-size: 12px; background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 5px 10px;
65
+ transition: border-color .12s, box-shadow .12s;
26
66
  }
67
+ .sp-btn:hover, .sp.open .sp-btn { border-color: var(--accent) }
68
+ .sp.open .sp-btn { box-shadow: 0 0 0 3px #2f55c91f }
69
+ .sp-btn .lb { font-size: 10px; color: var(--faint); text-transform: uppercase; letter-spacing: .4px; flex: none }
70
+ .sp-btn .v { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums }
71
+ .sp-btn .car {
72
+ flex: none; width: 6px; height: 6px; margin: -3px 2px 0 0; border: solid var(--mute); border-width: 0 1.5px 1.5px 0;
73
+ transform: rotate(45deg); transition: transform .15s;
74
+ }
75
+ .sp.open .sp-btn .car { transform: rotate(-135deg); margin-top: 3px }
76
+ .sp-list {
77
+ position: absolute; z-index: 20; left: 0; right: 0; top: calc(100% + 4px);
78
+ max-height: 280px; overflow-y: auto; overscroll-behavior: contain;
79
+ background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 4px;
80
+ box-shadow: 0 8px 24px #1c24301f, 0 2px 6px #1c24300f;
81
+ opacity: 0; transform: translateY(-4px); pointer-events: none; transition: opacity .12s, transform .12s;
82
+ }
83
+ .sp.open .sp-list { opacity: 1; transform: none; pointer-events: auto }
84
+ .sp-it {
85
+ width: 100%; display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: baseline;
86
+ background: none; border: 0; border-radius: 6px; padding: 6px 8px; cursor: pointer; text-align: left; font-size: 12px;
87
+ }
88
+ .sp-it:hover { background: var(--paper) }
89
+ .sp-it.on { background: var(--mine); color: var(--accent) }
90
+ .sp-it .id { font-family: ui-monospace, Menlo, monospace; font-weight: 600 }
91
+ .sp-it .ago { color: var(--mute) }
92
+ .sp-it .n { color: var(--faint); font-variant-numeric: tabular-nums; grid-column: 3 }
93
+ .sp-it.all { border-bottom: 1px solid var(--line); border-radius: 6px 6px 0 0; margin-bottom: 3px }
94
+ .sp-it.all .id { font-family: inherit }
27
95
 
28
- .tags { flex: 1; min-height: 0; overflow-y: auto; padding: 6px 6px 12px }
29
-
30
- .tag-row {
31
- position: relative; display: flex; gap: 10px; align-items: flex-start;
32
- padding: 8px 10px 8px 11px; border-radius: 8px; cursor: pointer;
33
- border: 1px solid transparent; transition: background .12s;
96
+ /* 左栏底: session 切换 + 用量, 都是轮次的事 —— 与顶上的身份分开, 钉在会话列表下面。 */
97
+ .sidefoot {
98
+ flex: none; border-top: 1px solid var(--line); padding: 10px 12px 12px;
99
+ display: flex; flex-direction: column; gap: 8px; background: var(--paper);
34
100
  }
35
- .tag-row:hover { background: #f6f8fa }
36
- /* 选中态用左侧色条而不是整块蓝底 —— 底色会和内部的 badge/预览抢注意力。 */
37
- .tag-row.on { background: #f0f4f9 }
38
- .tag-row.on::before {
39
- content: ""; position: absolute; left: 2px; top: 9px; bottom: 9px;
40
- width: 3px; border-radius: 2px; background: #0969da;
101
+ .sidefoot[hidden] { display: none }
102
+ #rb-sess:empty, #rb-prof:empty { display: none }
103
+ /* 在底部, 浮层往上开。 */
104
+ .sidefoot .sp-list { top: auto; bottom: calc(100% + 4px); transform: translateY(4px) }
105
+ .sidefoot .sp.open .sp-list { transform: none }
106
+
107
+ /* profile: 这个 role 自己跑过的轮次的总账 —— 平铺在左栏底, 不套卡片。 */
108
+ .prof { font-family: ui-monospace, Menlo, monospace }
109
+ .prof .hd { display: flex; align-items: center; gap: 8px; font-size: 11.5px; margin-bottom: 6px }
110
+ .prof .hd .lb { font-size: 10px; color: var(--faint); text-transform: uppercase; letter-spacing: .4px; font-family: system-ui, sans-serif }
111
+ .prof .model {
112
+ margin-left: auto; color: var(--mute);
113
+ max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
41
114
  }
115
+ .prof .grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px 4px }
116
+ .prof .kv { display: flex; flex-direction: column; align-items: flex-start; min-width: 0 }
117
+ .prof .kv .v { font-size: 13px; font-weight: 700; color: #1f2328; font-variant-numeric: tabular-nums; white-space: nowrap }
118
+ .prof .kv .k { font-size: 9.5px; color: #8c959f; text-transform: uppercase; letter-spacing: .4px }
119
+ .prof .io { margin-top: 8px; display: flex; flex-direction: column; gap: 6px }
120
+ .prof .bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: #eaeef2 }
121
+ .prof .bar .seg { height: 100% }
122
+ .prof .leg { display: flex; gap: 4px 9px; flex-wrap: wrap }
123
+ .prof .lg { display: flex; align-items: center; gap: 4px; font-size: 10.5px; color: #8c959f }
124
+ .prof .lg i { width: 8px; height: 8px; border-radius: 2px; flex: none }
125
+ .prof .lg b { color: #1f2328; font-weight: 700; font-variant-numeric: tabular-nums }
126
+ .conn { display: flex; align-items: center; gap: 5px }
127
+ .conn .d { width: 7px; height: 7px; border-radius: 50%; background: #1a7f37 }
128
+ .conn.off .d { background: #cf222e }
42
129
 
43
- .tag-av {
44
- width: 32px; height: 32px; flex: none; border-radius: 10px; background: #f1f3f6;
45
- display: flex; align-items: center; justify-content: center; font-size: 17px;
46
- position: relative; border: 1px solid #eaeef2;
130
+ /* 群聊头像 = 参与者头像拼图: 1 格居中, 2 格左右, 3/4 格田字。 */
131
+ .mosaic {
132
+ display: grid !important; width: 34px; height: 34px; border-radius: 10px; overflow: hidden; flex: none;
133
+ background: #eef1f6; border: 1px solid var(--line); gap: 1px; padding: 1px;
134
+ font-size: 13px !important; line-height: 1 !important;
47
135
  }
48
- .tag-av .live {
49
- position: absolute; right: -3px; bottom: -3px; width: 10px; height: 10px;
50
- border-radius: 50%; background: #1a7f37; border: 2px solid #fff;
51
- animation: pulse 1.4s infinite;
136
+ .mosaic i { font-style: normal; display: grid; place-items: center; background: #fff; min-width: 0; min-height: 0 }
137
+ .mosaic.n1 { grid-template: 1fr / 1fr; font-size: 20px !important }
138
+ .mosaic.n2 { grid-template: 1fr / 1fr 1fr; font-size: 14px !important }
139
+ .mosaic.n3, .mosaic.n4 { grid-template: 1fr 1fr / 1fr 1fr }
140
+ .mosaic.n3 i:first-child { grid-row: span 2 }
141
+
142
+ .convs { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 8px 16px }
143
+ .convs h2 {
144
+ margin: 14px 8px 4px; font-size: 12px; font-weight: 600; color: var(--mute);
145
+ display: flex; align-items: baseline; gap: 6px;
52
146
  }
53
- @keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .3 } }
147
+ .convs h2 span { font-weight: 400; color: var(--faint); font-variant-numeric: tabular-nums }
148
+ .convs .none { padding: 6px 10px; font-size: 12px; color: var(--faint) }
54
149
 
55
- .tag-main { min-width: 0; flex: 1 }
56
- .tag-l1 { display: flex; align-items: baseline; gap: 6px }
57
- .tag-name {
58
- font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden;
59
- text-overflow: ellipsis; font-family: ui-monospace, Menlo, monospace;
150
+ .ci {
151
+ display: flex; gap: 10px; align-items: flex-start; width: 100%; text-align: left; overflow: hidden;
152
+ padding: 8px 10px; border: 0; border-radius: 9px; background: transparent; cursor: pointer;
60
153
  }
61
- .tag-row.running .tag-name { color: #1a7f37 }
62
- .tag-ts {
63
- margin-left: auto; flex: none; font-size: 11px; color: #8c959f;
64
- font-family: ui-monospace, Menlo, monospace;
154
+ .ci:hover { background: #f4f6f9 }
155
+ .ci.on { background: #eaf0fc }
156
+ .ci { align-items: center }
157
+ .ci > .av { font-size: 22px; line-height: 1; flex: none; width: 34px; text-align: center }
158
+ .ci .b { min-width: 0; flex: 1; overflow: hidden }
159
+ .ci .pv, .ci .l1 { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
160
+ .ci .l1 { display: flex }
161
+ .ci .l1 { display: flex; align-items: baseline; gap: 6px }
162
+ .ci .t { font-size: 13.5px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
163
+ .ci .ts { margin-left: auto; flex: none; font-size: 11px; color: var(--faint); font-variant-numeric: tabular-nums }
164
+ .ci .pv { font-size: 12px; color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
165
+ .ci .pv:empty { display: none }
166
+ .ci .tag { font-size: 10.5px; border-radius: 4px; padding: 0 4px; flex: none }
167
+ .ci .tag.priv { color: var(--priv); background: #9a5b1014 }
168
+
169
+ /* 群里的其他 role —— 缩进挂在群下, 点开是「我与它在这个群里」的往来。 */
170
+ .subs { margin: 0 0 6px 34px; border-left: 1px solid var(--line); padding-left: 6px }
171
+ .si {
172
+ display: flex; align-items: center; gap: 7px; width: 100%; text-align: left;
173
+ padding: 4px 8px; border: 0; border-radius: 7px; background: transparent; cursor: pointer;
174
+ font-size: 12.5px; color: var(--ink);
65
175
  }
66
- .tag-prev {
67
- font-size: 12px; color: #656d76; margin-top: 2px; overflow: hidden;
68
- display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
69
- line-height: 1.45;
176
+ .si:hover { background: #f4f6f9 }
177
+ .si.on { background: #eaf0fc; color: var(--accent) }
178
+ .si .n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
179
+ .si .c { margin-left: auto; font-size: 11px; color: var(--faint); font-variant-numeric: tabular-nums }
180
+ .si.quiet { color: var(--faint) }
181
+
182
+ /* ── 右栏头: 这个群聊 / 私聊是什么 ───────────────────────────────── */
183
+ .main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--paper) }
184
+ .chathead {
185
+ flex: none; display: flex; align-items: center; gap: 12px; padding: 10px 20px; min-height: 58px;
186
+ background: var(--card); border-bottom: 1px solid var(--line);
70
187
  }
71
- /* 每行第三层: 轮数 / 工具数 / token —— 不点进去也能判断哪个会话在真干活。 */
72
- .tag-meta {
73
- display: flex; gap: 4px; align-items: center; margin-top: 4px;
74
- font-size: 10.5px; color: #8c959f; font-family: ui-monospace, Menlo, monospace;
75
- font-variant-numeric: tabular-nums;
76
- }
77
- .tag-meta .sep { opacity: .45 }
78
- .tag-meta .hot { color: #9a6700 }
79
- .tags .empty-side { padding: 28px 12px; text-align: center; color: #8c959f; font-size: 12px }
80
-
81
- /* ── 主区 ───────────────────────────────────────────────────────── */
82
- /* min-height:0 给纵向 flex (mobile) 用: 缺了它 .main 的 min-height:auto 会被
83
- 内容撑破 100dvh, .thread 不产生内部滚动, 底部被 body overflow:hidden 裁掉。
84
- 横向 (桌面) 下无副作用 —— 高度本来就由 stretch 定死。 */
85
- .main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column }
86
- .topbar {
87
- flex: none; display: flex; align-items: center; gap: 10px; padding: 11px 20px;
88
- background: #fff; border-bottom: 1px solid #d0d7de;
89
- }
90
- .topbar .em { font-size: 18px }
91
- .topbar .h { font-size: 15px; font-weight: 600; font-family: ui-monospace, Menlo, monospace }
92
- .topbar .cwd {
93
- flex: none; font-size: 11px; color: #57606a; font-family: ui-monospace, Menlo, monospace;
94
- background: #f6f8fa; border: 1px solid #eaeef2; border-radius: 5px; padding: 1px 6px;
95
- max-width: 42%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
96
- }
97
- .topbar .sub {
98
- font-size: 11px; color: #8c959f; font-family: ui-monospace, Menlo, monospace;
99
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
188
+ .chathead .back { display: none; border: 0; background: none; font-size: 22px; color: var(--mute); cursor: pointer; padding: 0 4px }
189
+ .ch-who { display: flex; gap: 11px; align-items: center; min-width: 0; flex: 1 }
190
+ .ch-who > .av:not(.mosaic) { font-size: 26px; line-height: 1; flex: none }
191
+ .ch-who .l { min-width: 0; display: flex; flex-direction: column; gap: 1px }
192
+ .ch-who .t { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
193
+ .ch-who > .t { flex: none }
194
+ .ch-who .sub { font-size: 11.5px; color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
195
+ .ch-acts { display: flex; gap: 8px; align-items: center; flex: none; font-size: 12px; color: var(--mute) }
196
+ .vb {
197
+ font-size: 12.5px; background: var(--card); border: 1px solid var(--line); border-radius: 7px;
198
+ padding: 4px 11px; cursor: pointer; white-space: nowrap;
100
199
  }
101
- .topbar .back { display: none; font-size: 18px; color: #656d76; cursor: pointer; padding: 0 4px }
102
-
103
- /* ── graph 运行条 ───────────────────────────────────────────────────
104
- 一条线的流水线不值得画成节点图 —— steps 结构上不可能分叉, 画出来的永远是
105
- 一条直线。这里只把它压成一行: 步骤序列 + 当前停在哪一步 + 轮次进度。 */
106
- .gbar {
107
- flex: none; display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
108
- padding: 7px 20px; background: #fbf9ff; border-bottom: 1px solid #8250df33;
109
- font-size: 11.5px; color: #6639ba; font-family: ui-monospace, Menlo, monospace;
110
- }
111
- .gbar .gid { background: #8250df14; border-radius: 5px; padding: 1px 6px }
112
- .gbar .pipe { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; min-width: 0 }
113
- .gbar .nd {
114
- display: flex; align-items: center; gap: 3px; padding: 1px 7px; border-radius: 10px;
115
- background: #fff; border: 1px solid #d8cdf0; color: #57606a; cursor: pointer;
116
- }
117
- .gbar .nd:hover { border-color: #8250df }
118
- .gbar .nd.on { background: #8250df; border-color: #8250df; color: #fff; font-weight: 700 }
119
- .gbar .nd.on.live { animation: pulse 1.4s infinite }
120
- .gbar .arw { color: #b3a3d6 }
121
- .gbar .prog { margin-left: auto; font-variant-numeric: tabular-nums }
122
- .gbar .done { color: #8c959f }
123
- /* 侧栏里标出"这个会话是 graph 在驱动的" —— 否则它看着就是某个 tag 在自说自话。 */
124
- .tag-meta .gtag { color: #6639ba }
200
+ .vb:hover { border-color: var(--accent); color: var(--accent) }
201
+ .vb.on { background: var(--accent); border-color: var(--accent); color: #fff }
202
+ .vb b { font-weight: 600; margin-left: 3px; font-variant-numeric: tabular-nums }
125
203
 
204
+ /* ── 消息 ─────────────────────────────────────────────────────────── */
126
205
  .thread {
127
206
  flex: 1; min-height: 0; overflow-y: auto; overflow-anchor: none;
128
- padding: 16px 20px 24px; -webkit-overflow-scrolling: touch;
129
- overscroll-behavior: contain;
207
+ padding: 16px 20px 28px; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
130
208
  }
131
- .thread-in { max-width: 920px; margin: 0 auto; display: flex; flex-direction: column; gap: 18px }
132
- .empty { color: #8c959f; text-align: center; padding: 60px 0; font-size: 13px }
209
+ .thread-in { max-width: 960px; margin: 0 auto; display: flex; flex-direction: column; gap: 14px }
210
+ .empty { color: var(--faint); text-align: center; padding: 60px 0; font-size: 13px }
133
211
  .more-btn {
134
- align-self: center; background: #fff; border: 1px solid #d0d7de; border-radius: 16px;
135
- padding: 5px 16px; font-size: 12px; color: #57606a; cursor: pointer; font-family: inherit;
212
+ align-self: center; background: var(--card); border: 1px solid var(--line); border-radius: 16px;
213
+ padding: 5px 16px; font-size: 12px; color: var(--mute); cursor: pointer;
214
+ }
215
+ .more-btn:hover { color: var(--accent); border-color: var(--accent) }
216
+
217
+ .mrow { display: flex; align-items: flex-start; gap: 10px }
218
+ .mrow .av {
219
+ flex: none; width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--line);
220
+ background: var(--card); font-size: 19px; line-height: 32px; text-align: center; cursor: default; padding: 0;
221
+ }
222
+ .mcol { min-width: 0; max-width: min(78%, 760px); display: flex; flex-direction: column; gap: 4px }
223
+ .mrow.mine { flex-direction: row-reverse }
224
+ .mrow.mine .mcol { align-items: flex-end }
225
+ .mwho { font-size: 11.5px; color: var(--mute); display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap }
226
+ .mwho .to { color: var(--faint) }
227
+ .mwho .ch { font-size: 10.5px; border-radius: 4px; padding: 0 4px }
228
+ .mwho .ch.priv { color: var(--priv); background: #9a5b1014 }
229
+ /* 时刻只此一处 —— 气泡外, 跟着名字走。等宽数字让一列时间对得齐。 */
230
+ .mwho .mt { color: var(--faint); font-family: ui-monospace, Menlo, monospace; font-variant-numeric: tabular-nums }
231
+ .mb { min-width: 0; max-width: 100% }
232
+ /* 卡片里那份时刻是详情页要的, 这里已经写在 .mwho 上了 —— 同一条消息不报两次时间。 */
233
+ .mb .tg-time { display: none }
234
+
235
+ /* 入消息 (问句) 是一颗气泡; 出消息 (回复) 是一张带工具细节的卡片 —— 两者都按视角着色。 */
236
+ .mb > .bubble.mq { border-radius: 14px; border-color: var(--line) }
237
+ .mb .mq .mmeta { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; padding: 8px 14px 0; font-size: 11px; color: var(--faint) }
238
+ .mb .mq .md-body { padding: 10px 14px }
239
+ .mb .mq .mmeta + .md-body { padding-top: 4px }
240
+ .mchip { border-radius: 4px; padding: 0 5px; background: #f1f3f6; color: var(--mute) }
241
+ .mchip.priv { color: var(--priv); background: #9a5b1014 }
242
+ .mchip.pub { color: var(--pub); background: #237a5514 }
243
+ .mchip.graph { color: #6639ba; background: #8250df12 }
244
+
245
+ /* 自己发出的那条: 整块就是视角色。TURN_CSS 里写死的白卡 / 灰底 (嵌套气泡、
246
+ 代码块、折叠头) 会在蓝底上显成一张张贴纸 —— 这里把它们收回到气泡自己的
247
+ 底色上, 一条消息读起来是一块颜色, 需要再沉一档的才用 --mine-soft。 */
248
+ .mrow.mine .mb > .bubble.mq, .mrow.mine .mb > .turn-group { background: var(--mine); border-color: var(--mine-line) }
249
+ .mrow.mine .mb .bubble, .mrow.mine .mb .typing, .mrow.mine .mb .stats { background: transparent; border-color: var(--mine-line) }
250
+ .mrow.mine .mb .bubble.user, .mrow.mine .mb .bubble.approval,
251
+ .mrow.mine .mb .md-body code, .mrow.mine .mb .md-body pre,
252
+ .mrow.mine .mb .tile, .mrow.mine .mb details > summary,
253
+ .mrow.mine .mb .tool-call > summary:hover { background: var(--mine-soft); border-color: var(--mine-line) }
254
+ .mrow.mine .mb .md-body pre code { background: transparent }
255
+ .mrow.mine .mb .bubble details, .mrow.mine .mb .tg-head { border-color: var(--mine-line) }
256
+ .mrow.mine .mb .tool-body { border-left-color: var(--mine-line) }
257
+ .mrow.mine .mb .io-bar { background: var(--mine-soft) }
258
+
259
+ /* 气泡对面那片空白 = 换视角的门。平时不可见, 悬停才露出要去谁那里。 */
260
+ .flip {
261
+ flex: 1; align-self: stretch; min-width: 40px; border: 0; background: transparent; cursor: pointer;
262
+ display: flex; align-items: center; padding: 0 12px; border-radius: 10px; color: transparent;
263
+ font-size: 12px; transition: background .15s, color .15s;
264
+ }
265
+ .mrow.them .flip { justify-content: flex-start }
266
+ .mrow.mine .flip { justify-content: flex-end }
267
+ /* 气泡可以比视窗还高, 居中的箭头就跑出屏幕了 —— 让它在按钮内 sticky, 上下各留半屏,
268
+ 于是只要这一行还在视窗里, 箭头就钉在视窗中线附近。 */
269
+ .flip .fi {
270
+ position: sticky; top: calc(50% - 10px); bottom: calc(50% - 10px);
271
+ display: inline-flex; align-items: center; gap: 4px;
272
+ }
273
+ .mrow.mine .flip .fi { flex-direction: row-reverse }
274
+ .mrow.mine .flip .fc { transform: scaleX(-1) }
275
+ .flip .fc { flex: none }
276
+ .flip:hover, .flip:focus-visible { background: #2f55c90d; color: var(--accent) }
277
+ .flip[disabled] { cursor: default; background: transparent !important; color: transparent !important }
278
+ .mrow .av.go { cursor: pointer }
279
+ .mrow .av.go:hover { border-color: var(--accent) }
280
+
281
+ .mrow.mark { justify-content: center }
282
+ .mrow.mark > * { flex: 1; max-width: 520px }
283
+
284
+ /* 视角切换的那一下: 整列消息淡出再按新视角排回来 —— 这一页唯一的编排动效。 */
285
+ .thread-in.flipping { animation: flip .26s ease }
286
+ @keyframes flip { 0% { opacity: 1 } 45% { opacity: 0; transform: scaleX(.985) } 100% { opacity: 1 } }
287
+ @media (prefers-reduced-motion: reduce) {
288
+ .thread-in.flipping { animation: none }
289
+ .flip { transition: none }
136
290
  }
137
- .more-btn:hover { background: #f6f8fa; color: #0969da; border-color: #0969da55 }
138
291
 
139
292
  /* ── turn 分组 ──────────────────────────────────────────────────── */
140
293
  /* section 的默认样式给了白底+边框, 但没有内边距 —— 气泡/工具行会直接贴着卡片
@@ -167,49 +320,358 @@ body { overflow: hidden; overscroll-behavior: none }
167
320
  /* graph 派的一轮: 卡片本身也着色, 滚动时不必读归因条就知道这不是人发的。 */
168
321
  .turn-group.graph { border-color: #8250df44 }
169
322
 
170
- /* ── 页脚总账 ───────────────────────────────────────────────────── */
171
- .statusbar {
172
- flex: none; display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
173
- /* viewport-fit=cover 已开, 底部给 home indicator 让位 (桌面 env=0, 无感)。 */
174
- padding: 7px 20px calc(7px + env(safe-area-inset-bottom));
175
- background: #fff; border-top: 1px solid #d0d7de;
176
- font-size: 11.5px; color: #57606a; font-family: ui-monospace, Menlo, monospace;
177
- }
178
- .statusbar .st { display: flex; align-items: baseline; gap: 5px }
179
- .statusbar .st .k {
180
- color: #8c959f; text-transform: uppercase; letter-spacing: .4px; font-size: 10px;
181
- }
182
- .statusbar .st .v { color: #1f2328; font-weight: 700; font-variant-numeric: tabular-nums }
183
- /* 缺数据的指标压暗, 不再显示成有意义的 "0" —— 0 与「没采到」是两回事。 */
184
- .statusbar .st.void .v { color: #b1bac4; font-weight: 400 }
185
- .statusbar .pill { display: flex; align-items: center; gap: 5px; font-weight: 700 }
186
- .statusbar .pill .d { width: 7px; height: 7px; border-radius: 50%; background: #8c959f }
187
- .statusbar .pill.run { color: #1a7f37 }
188
- .statusbar .pill.run .d { background: #1a7f37; animation: pulse 1.4s infinite }
189
- .statusbar .model {
190
- color: #57606a; background: #f1f3f6; border-radius: 9px; padding: 1px 7px;
191
- max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
192
- }
193
- /* token I/O 堆叠条 + 图例: 色块必须有名字, 否则底部那条彩带谁也读不懂。 */
194
- .sb-io { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0 }
195
- .sb-io > .k { color: #8c959f; text-transform: uppercase; letter-spacing: .4px; font-size: 10px }
196
- .sb-bar { display: flex; height: 6px; width: 120px; border-radius: 3px; overflow: hidden; background: #eaeef2 }
197
- .sb-bar .seg { height: 100% }
198
- .sb-leg { display: flex; align-items: center; gap: 9px; flex-wrap: wrap }
199
- .sb-leg .lg { display: flex; align-items: center; gap: 4px; font-size: 10.5px; color: #8c959f }
200
- .sb-leg .lg i { width: 8px; height: 8px; border-radius: 2px; flex: none }
201
- .sb-leg .lg b { color: #1f2328; font-weight: 700; font-variant-numeric: tabular-nums }
202
- .conn { margin-left: auto; display: flex; align-items: center; gap: 5px }
203
- .conn .d { width: 7px; height: 7px; border-radius: 50%; background: #1a7f37 }
204
- .conn.off .d { background: #cf222e }
323
+ /* ── 🏓 keepalive 折叠 ──────────────────────────────────────────────
324
+ 保温 ping 不是对话, 但它是真花销 —— 所以留在时间轴上, 只是收成一行虚线。 */
325
+ .ping-fold { border: 1px dashed #d8dee4; border-radius: 10px; background: #fbfcfd }
326
+ .ping-fold > .ping-sum {
327
+ display: flex; align-items: center; gap: 8px; padding: 6px 12px;
328
+ cursor: pointer; list-style: none; font-size: 11.5px; color: #8c959f;
329
+ font-family: ui-monospace, Menlo, monospace; font-variant-numeric: tabular-nums;
330
+ }
331
+ .ping-fold > .ping-sum::-webkit-details-marker { display: none }
332
+ .ping-fold > .ping-sum::after {
333
+ content: "›"; margin-left: auto; font-size: 14px; color: #b1bac4;
334
+ transition: transform .12s;
335
+ }
336
+ .ping-fold[open] > .ping-sum::after { transform: rotate(90deg) }
337
+ .ping-fold > .ping-sum:hover { color: #57606a }
338
+ .ping-fold .pb { color: #57606a; font-weight: 700; flex: none }
339
+ .ping-fold .ps { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
340
+ .ping-fold .pt { flex: none; opacity: .8 }
341
+ .ping-fold .ping-body {
342
+ display: flex; flex-direction: column; gap: 10px; padding: 2px 10px 10px;
343
+ }
344
+ /* 展开后的 ping 轮次压一档: 它们是背景噪声, 不该和真对话卡片一样醒目。 */
345
+ .ping-fold .turn-group { box-shadow: none }
346
+ .ping-fold .mcol { max-width: 100% }
347
+ .ping-fold .flip { display: none }
205
348
 
206
349
  @media (max-width: 760px) {
207
350
  .app { flex-direction: column }
208
- .side { width: auto; border-right: 0; border-bottom: 1px solid #d0d7de; max-height: 44vh; max-height: 44dvh }
351
+ .side { width: auto; flex: 1; border-right: 0 }
352
+ .main { display: none }
209
353
  .app.reading .side { display: none }
210
- .app.reading .topbar .back { display: block }
354
+ .app.reading .main { display: flex }
355
+ .chathead { padding: 8px 12px; gap: 8px }
356
+ .chathead .back { display: block }
211
357
  .thread { padding: 12px }
358
+ .mcol { max-width: 86% }
359
+ .flip { min-width: 16px; padding: 0 4px; font-size: 0 }
212
360
  .turn-group { padding: 12px }
213
- .statusbar { gap: 10px; padding: 7px 12px calc(7px + env(safe-area-inset-bottom)) }
214
- .sb-io, .statusbar .model { display: none }
361
+ .thread { padding-bottom: calc(12px + env(safe-area-inset-bottom)) }
215
362
  }
363
+
364
+ /* UA 的 [hidden] 规则和 `.gbar{display:flex}` 这类类选择器同权重, 而作者样式
365
+ 胜出 —— 于是 el.hidden=true 的元素照旧显示 (空的 gbar 会剩一条带边框的窄带)。
366
+ 一条 !important 把 hidden 的语义钉死, 三块视图容器的显隐都靠它。 */
367
+ [hidden] { display: none !important }
368
+
369
+
370
+ /* ══ 关系图 ═════════════════════════════════════════════════════════ */
371
+ .wtools {
372
+ flex: none; display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
373
+ padding: 8px 20px; background: #fff; border-bottom: 1px solid #d0d7de;
374
+ font-size: 11.5px; color: #57606a;
375
+ }
376
+ .wlegend { display: flex; gap: 6px; flex-wrap: wrap }
377
+ /* 图例同时是开关 —— 边一多就得能关掉几种; 把"图例"和"过滤器"做成两样东西
378
+ 只会让人在两处之间对照颜色。 */
379
+ .wtools .chip {
380
+ display: flex; align-items: center; gap: 5px; font: inherit; cursor: pointer;
381
+ background: #fff; border: 1px solid #d0d7de; border-radius: 13px; padding: 2px 10px;
382
+ color: #8c959f;
383
+ }
384
+ .wtools .chip i { width: 14px; height: 0; border-top: 2px solid var(--c); opacity: .35 }
385
+ .wtools .chip b { font-variant-numeric: tabular-nums; font-weight: 700 }
386
+ .wtools .chip.on { color: #1f2328; border-color: var(--c) }
387
+ .wtools .chip.on i { opacity: 1 }
388
+ .wtools .chip.clear, .wtools .chip.only { border-color: #d0d7de; color: #57606a }
389
+ .wtools .chip.only.on { border-color: #0969da; color: #0550ae; background: #ddf4ff }
390
+ .wtools .wstat { margin-left: auto; color: #8c959f; font-variant-numeric: tabular-nums }
391
+ .wtools .wstat b { color: #0969da }
392
+ .wtools .warn { color: #9a6700 }
393
+ .wtools .hint { margin-left: 10px; color: #b1bac4 }
394
+
395
+ /* position 不是装饰: 力导向的画布是 `position:absolute; inset:0`, 没有这一行它
396
+ 就贴到更外层的定位祖先上去 —— 坐标原点跑到顶栏, 节点飘到工具条上面, 而
397
+ overflow:hidden 也拦不住一个容器块在自己之外的绝对定位子元素。 */
398
+ .wscroll { position: relative; flex: 1; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch }
399
+ .wmap { position: relative; padding: 18px 20px 28px; min-width: min-content }
400
+ /* 有关系的节点带一道左侧色条 —— 76 个会话里哪几个真的在协作, 不点开也看得见。 */
401
+ .wnode.rel::before {
402
+ content: ""; position: absolute; left: 1px; top: 8px; bottom: 8px;
403
+ width: 2px; border-radius: 1px; background: #0969da66;
404
+ }
405
+ /* 连线层铺满整张图并且不吃事件 —— 节点的点击/双击必须照常穿过去。
406
+ z-index 把它抬到卡片之上: 卡片是不透明的, 压在下面时一条跨卡片的边会被中途
407
+ 吃掉半截, 读出来像是断的。.wgrid 不设 z-index (保持 auto) 才不会自成一个层叠
408
+ 上下文 —— 那样悬停/聚焦的节点就再也抬不过这条线了。 */
409
+ .wedges { position: absolute; inset: 0; pointer-events: none; overflow: visible; z-index: 2 }
410
+ /* 反过来: 正在看的那个节点要压过连线, 否则文字被线划着读。只抬这两种 —— 一直
411
+ 抬着某个节点等于把"线被卡片吃掉"这个毛病在它身上留一份。 */
412
+ .wnode:hover, .wnode.sel { z-index: 3 }
413
+ /* 多列流式而不是 grid: 聊天卡片的高度天差地别 (一个群 2 个 wizard, 另一个 20 个),
414
+ grid 的行高按本行最高的那张对齐, 于是矮卡片下面全是空白 —— 图看着比实际稀疏
415
+ 一倍。CSS columns 是砌砖式的: 卡片顺着列往下码, 码满了换列, 没有行的概念。
416
+ 列宽钉在 320 附近也顺带收住了"两张卡片各占半个屏"的过度拉伸。 */
417
+ .wgrid { position: relative; columns: 320px; column-gap: 16px }
418
+
419
+ .wchat {
420
+ background: #fff; border: 1px solid #d0d7de; border-radius: 12px; overflow: hidden;
421
+ /* 一张卡片不能被切成两列 —— 断在半截的家谱读不出父子关系。 */
422
+ break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid;
423
+ margin-bottom: 16px;
424
+ }
425
+ /* 卡片头可点 = 进那个群 (见 chat.js 的 openNode)。 */
426
+ .wch.go { cursor: pointer }
427
+ .wch.go:hover .nm { color: #0969da; text-decoration: underline }
428
+ /* 当前聊天是读图的锚点, 给它一道明确的边 —— 其余的群一律等价, 不再分轻重。 */
429
+ .wchat.self { border-color: #0969da66; box-shadow: 0 0 0 3px #0969da10 }
430
+ .wch {
431
+ display: flex; align-items: baseline; gap: 8px; padding: 9px 13px;
432
+ border-bottom: 1px solid #eaeef2; background: #f6f8fa;
433
+ }
434
+ .wchat.self .wch { background: #f0f6ff }
435
+ .wch .nm {
436
+ font-size: 13px; font-weight: 700; min-width: 0; overflow: hidden;
437
+ text-overflow: ellipsis; white-space: nowrap;
438
+ }
439
+ .wch .here {
440
+ font-size: 10px; color: #0969da; background: #0969da14; border-radius: 4px; padding: 1px 5px;
441
+ }
442
+ .wch .ct {
443
+ margin-left: auto; font-size: 11px; color: #8c959f; flex: none;
444
+ font-family: ui-monospace, Menlo, monospace;
445
+ }
446
+ .wch .ct em { color: #1a7f37; font-style: normal; font-weight: 700 }
447
+ /* 左内边距是同卡片内连线的车道 —— 一条 `#a → #b` 的派活边从这里绕过去, 不必
448
+ 拐出卡片 (拐出去就会被滚动容器裁掉)。 */
449
+ .wrows { padding: 7px 10px 10px 22px; display: flex; flex-direction: column; gap: 3px }
450
+
451
+ .wnode {
452
+ position: relative; display: flex; gap: 8px; align-items: flex-start;
453
+ padding: 6px 8px; border-radius: 8px; cursor: pointer;
454
+ border: 1px solid transparent; transition: opacity .15s, background .12s;
455
+ }
456
+ .wnode:hover { background: #f6f8fa }
457
+ .wnode.self { background: #fffbe8 }
458
+ .wnode.sel { background: #eef4fc; border-color: #0969da55 }
459
+ /* 聚焦时无关节点压暗而不是移除 —— 位置不动, 连点几个节点版面也不跳。 */
460
+ .wnode.dim { opacity: .3 }
461
+ /* 双击能走进去的那些 —— 外聊天的节点现在也算 (整页跳过去)。 */
462
+ .wnode.go { cursor: pointer }
463
+ .wnode.cold .wav { filter: grayscale(1); opacity: .55 }
464
+ /* 缩进本身就是家谱; 这道折线只是把"缩给谁看"指实。 */
465
+ .wnode .lin {
466
+ position: absolute; left: -9px; top: 0; bottom: 50%; width: 9px;
467
+ border-left: 1.5px solid #d8cdf0; border-bottom: 1.5px solid #d8cdf0;
468
+ border-bottom-left-radius: 6px;
469
+ }
470
+ /* 子 wizard 不是 fork: 折线换成虚线、换色, 与分身一眼分开 (同关系图的 spawn 边)。 */
471
+ .wnode .lin.sp { border-style: dashed; border-color: #f0d2bd }
472
+ .wav {
473
+ position: relative; width: 26px; height: 26px; flex: none; border-radius: 8px;
474
+ background: #f1f3f6; border: 1px solid #eaeef2;
475
+ display: flex; align-items: center; justify-content: center; font-size: 14px;
476
+ }
477
+ .wav .live {
478
+ position: absolute; right: -3px; bottom: -3px; width: 9px; height: 9px;
479
+ border-radius: 50%; background: #1a7f37; border: 2px solid #fff;
480
+ animation: pulse 1.4s infinite;
481
+ }
482
+ .wbody { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 1px }
483
+ .wl1 { display: flex; align-items: baseline; gap: 6px; min-width: 0 }
484
+ .wname {
485
+ font-size: 12.5px; font-weight: 600; min-width: 0; overflow: hidden;
486
+ text-overflow: ellipsis; white-space: nowrap;
487
+ }
488
+ .wnode.run .wname { color: #1a7f37 }
489
+ .wtag {
490
+ flex: none; font-size: 10.5px; color: #57606a; background: #f1f3f6;
491
+ border-radius: 4px; padding: 0 5px; font-family: ui-monospace, Menlo, monospace;
492
+ }
493
+ .wtag.def { color: #8c959f }
494
+ .wih { flex: none; font-size: 11px; color: #6639ba }
495
+ .wts {
496
+ margin-left: auto; flex: none; font-size: 10.5px; color: #8c959f;
497
+ font-family: ui-monospace, Menlo, monospace;
498
+ }
499
+ .wjob { font-size: 11.5px; color: #0550ae; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
500
+ .wprev {
501
+ font-size: 11.5px; color: #656d76; overflow: hidden; line-height: 1.4;
502
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
503
+ }
504
+ .wmeta {
505
+ font-size: 10.5px; color: #8c959f; font-family: ui-monospace, Menlo, monospace;
506
+ display: flex; gap: 4px; flex-wrap: wrap; margin-top: 1px;
507
+ }
508
+ .wmeta .sep { opacity: .45 }
509
+
510
+ /* ══ 日程 ═══════════════════════════════════════════════════════════ */
511
+ .plan-in { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 20px 28px }
512
+ .psec {
513
+ max-width: 900px; margin: 0 auto 30px;
514
+ /* 通用 section 规则给了白底+边框却没有内边距 —— 不补这一层, 刻度条/日程行/工单
515
+ 行就直接贴着卡片的边。底边留小一点: 末行自己还带 10px 的 margin。 */
516
+ padding: 14px 18px 8px;
517
+ }
518
+ .psec h3 {
519
+ display: flex; align-items: baseline; gap: 8px; font-size: 13px; margin: 0 0 14px;
520
+ padding-bottom: 8px; border-bottom: 1px solid #d0d7de;
521
+ }
522
+ .psec h3 span {
523
+ font-size: 11px; color: #8c959f; font-weight: 400;
524
+ font-family: ui-monospace, Menlo, monospace;
525
+ }
526
+ .pempty { color: #8c959f; font-size: 12px; padding: 18px 0; text-align: center }
527
+
528
+ /* 未来 12 小时的刻度条 —— 「今晚还有几枪要响」比一张日期列表更该先看见。 */
529
+ .strip {
530
+ background: #fff; border: 1px solid #d0d7de; border-radius: 10px;
531
+ padding: 12px 16px 11px; margin-bottom: 16px;
532
+ }
533
+ .strip .axis {
534
+ position: relative; height: 13px; font-size: 10px; color: #8c959f;
535
+ font-family: ui-monospace, Menlo, monospace;
536
+ }
537
+ .strip .tk { position: absolute; transform: translateX(-50%); white-space: nowrap }
538
+ .strip .tk:first-child { transform: none }
539
+ .strip .rail {
540
+ position: relative; height: 8px; border-radius: 4px; background: #eaeef2; margin: 3px 0 7px;
541
+ }
542
+ .strip .pin {
543
+ position: absolute; top: -3px; width: 8px; height: 14px; margin-left: -4px;
544
+ border-radius: 3px; background: #9a6700; box-shadow: 0 0 0 2px #fff;
545
+ }
546
+ .strip .cap { font-size: 11px; color: #8c959f }
547
+
548
+ .prow {
549
+ display: flex; gap: 16px; background: #fff; border: 1px solid #d0d7de;
550
+ border-radius: 10px; padding: 12px 16px; margin-bottom: 10px;
551
+ }
552
+ .prow .pl { flex: none; width: 152px; border-right: 1px dashed #eaeef2; padding-right: 14px }
553
+ .pwhen { font-size: 11.5px; color: #9a6700; font-weight: 600 }
554
+ .pnext {
555
+ display: flex; align-items: baseline; gap: 6px; margin-top: 5px;
556
+ font-family: ui-monospace, Menlo, monospace;
557
+ }
558
+ .pnext b { font-size: 12.5px; font-variant-numeric: tabular-nums }
559
+ .pnext span { font-size: 10.5px; color: #8c959f }
560
+ .prow .pr { min-width: 0; flex: 1 }
561
+ .ph { display: flex; align-items: center; gap: 7px; flex-wrap: wrap }
562
+ .ph .note { font-size: 11px; color: #57606a }
563
+ .ph .pid {
564
+ margin-left: auto; font-size: 10px; color: #b1bac4;
565
+ font-family: ui-monospace, Menlo, monospace;
566
+ }
567
+ .ptext { font-size: 12px; color: #1f2328; margin-top: 6px; line-height: 1.5; word-break: break-word }
568
+ .pfoot { font-size: 10.5px; color: #8c959f; margin-top: 6px }
569
+
570
+ /* wizard 徽标 —— 日程/工单里凡是提到一个 wizard 都用它, 同聊天的可点进线程。 */
571
+ .wchip {
572
+ display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px;
573
+ background: #f1f3f6; border: 1px solid #eaeef2; border-radius: 12px; padding: 1px 8px;
574
+ max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
575
+ }
576
+ .wchip.go { cursor: pointer; border-color: #0969da33; color: #0550ae }
577
+ .wchip.go:hover { background: #ddf4ff; border-color: #0969da }
578
+
579
+ .jrow { background: #fff; border: 1px solid #d0d7de; border-radius: 10px; padding: 12px 16px; margin-bottom: 10px }
580
+ .jrow.open { border-color: #1a7f3755; box-shadow: 0 0 0 3px #1a7f370d }
581
+ .jh { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap }
582
+ .jid {
583
+ font-size: 10.5px; color: #57606a; background: #f1f3f6; border-radius: 4px;
584
+ padding: 0 6px; font-family: ui-monospace, Menlo, monospace;
585
+ }
586
+ .jt { font-size: 13px; font-weight: 600; min-width: 0 }
587
+ .jst { font-size: 10.5px; color: #8c959f }
588
+ .jrow.open .jst { color: #1a7f37; font-weight: 700 }
589
+ .jts {
590
+ margin-left: auto; font-size: 10.5px; color: #8c959f;
591
+ font-family: ui-monospace, Menlo, monospace;
592
+ }
593
+ .jm { display: flex; flex-direction: column; gap: 5px; margin-top: 9px }
594
+ .jmm { display: flex; align-items: center; gap: 6px; min-width: 0 }
595
+ .jmm em {
596
+ font-style: normal; font-size: 11.5px; color: #656d76; min-width: 0;
597
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
598
+ }
599
+ .jmm .tmp {
600
+ font-style: normal; font-size: 9.5px; color: #9a6700; background: #d4a72c22;
601
+ border-radius: 3px; padding: 0 4px;
602
+ }
603
+ .jmm.none { font-size: 11.5px; color: #b1bac4 }
604
+ .jsum {
605
+ font-size: 12px; color: #1f2328; margin-top: 10px; padding-top: 10px;
606
+ border-top: 1px dashed #eaeef2; line-height: 1.5; white-space: pre-wrap;
607
+ }
608
+
609
+ @media (max-width: 760px) {
610
+ .chathead .vb { padding: 3px 8px }
611
+ .wmap { padding: 12px }
612
+ .wgrid { columns: 1 }
613
+ .plan-in { padding: 12px }
614
+ .prow { flex-direction: column; gap: 6px }
615
+ .prow .pl { width: auto; border-right: 0; border-bottom: 1px dashed #eaeef2; padding: 0 0 8px }
616
+ .pnext { margin-top: 1px }
617
+ }
618
+
619
+ /* 相关性筛掉的那些 —— 说一声它们还在, 但不占图的位置 (见 shared/world 的说明)。 */
620
+ .wmore {
621
+ padding: 5px 13px 9px; font-size: 10.5px; color: #b1bac4;
622
+ border-top: 1px dashed #eaeef2; margin-top: 2px;
623
+ }
624
+
625
+ /* ══ 关系视图 · 力导向布局 ═════════════════════════════════════════
626
+ 卡片布局之外的另一种读法: 节点只画 wizard, 聊天不再是容器而是节点身上的一
627
+ 道色条 + 一行群名。见 web/chat.js 的说明。 */
628
+ .wscroll.fx { overflow: hidden }
629
+ .wmap.force { position: absolute; inset: 0; padding: 0; min-width: 0 }
630
+ .fxn {
631
+ position: absolute; left: 0; top: 0; z-index: 2;
632
+ display: flex; align-items: center; gap: 7px; max-width: 210px;
633
+ padding: 5px 10px 5px 7px; border-radius: 10px;
634
+ background: #fff; border: 1px solid #d0d7de; border-left: 3px solid var(--c);
635
+ box-shadow: 0 1px 3px #1b1f2412; cursor: grab; touch-action: none;
636
+ user-select: none; -webkit-user-select: none;
637
+ transition: opacity .15s, box-shadow .12s;
638
+ will-change: transform;
639
+ }
640
+ .fxn .wav { width: 22px; height: 22px; border-radius: 7px; font-size: 12px }
641
+ .fxn .wav .live { width: 8px; height: 8px; right: -2px; bottom: -2px }
642
+ .fxb { min-width: 0; display: flex; flex-direction: column; gap: 0 }
643
+ .fxn .wname { font-size: 12px }
644
+ /* 群名压得比名字轻 —— 它是节点的属性, 不是节点本身。 */
645
+ .fxc {
646
+ font-size: 9.5px; color: var(--c); opacity: .85; overflow: hidden;
647
+ text-overflow: ellipsis; white-space: nowrap;
648
+ }
649
+ .fxn.self { background: #fffbe8 }
650
+ .fxn.cold { opacity: .72 }
651
+ .fxn.cold .wav { filter: grayscale(1); opacity: .55 }
652
+ .fxn.run .wname { color: #1a7f37 }
653
+ .fxn.dim { opacity: .22 }
654
+ .fxn.sel { box-shadow: 0 0 0 3px var(--c), 0 1px 3px #1b1f2412; }
655
+ .fxn.sel .fxc { opacity: 1 }
656
+ .fxn.drag { cursor: grabbing; box-shadow: 0 6px 16px #1b1f2426 }
657
+ /* 钉住 = 这个位置是人摆的, 力不再动它。 */
658
+ .fxn.pin::after {
659
+ content: "📌"; position: absolute; right: -5px; top: -7px; font-size: 9px;
660
+ opacity: .8; pointer-events: none;
661
+ }
662
+ /* 布局开关 —— 两种读法并列, 不是复选。 */
663
+ .wtools .wlay { display: inline-flex; border: 1px solid #d0d7de; border-radius: 7px; overflow: hidden }
664
+ .wtools .lb {
665
+ border: 0; background: #fff; cursor: pointer; padding: 3px 9px;
666
+ font-size: 11.5px; color: #57606a; font-family: inherit;
667
+ }
668
+ .wtools .lb + .lb { border-left: 1px solid #d0d7de }
669
+ .wtools .lb.on { background: #ddf4ff; color: #0550ae; font-weight: 600 }
670
+
671
+ /* ══ 关系 / 日程 ═══════════════════════════════════════════════════ */
672
+ /* 都是当前 role 的另一面, 侧栏 (它的会话) 留着 —— 点一个会话就回到对话。
673
+ 会话标题条与页脚总账只属于对话视图。 */
674
+ .pane { flex: 1; min-height: 0; display: flex; flex-direction: column; background: #fafbfc }
675
+
676
+ /* 回复卡片里的工具行是 nowrap 的 —— 不裁一刀会把整列撑出横向滚动。 */
677
+ .mb > .turn-group { overflow: hidden }