wezard 1.4.2 → 2.0.1

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