wezard 0.3.3

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 (117) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +14 -0
  3. package/CLAUDE.md +83 -0
  4. package/LICENSE +21 -0
  5. package/README.md +410 -0
  6. package/cli/weclaude.sh +236 -0
  7. package/commands/audit.md +7 -0
  8. package/commands/wrc.md +7 -0
  9. package/config.example.jsonc +102 -0
  10. package/dist/cli/audit.js +156 -0
  11. package/dist/cli/audit.js.map +1 -0
  12. package/dist/cli/init.js +311 -0
  13. package/dist/cli/init.js.map +1 -0
  14. package/dist/cli/sync.js +175 -0
  15. package/dist/cli/sync.js.map +1 -0
  16. package/dist/daemon/approval.js +1301 -0
  17. package/dist/daemon/approval.js.map +1 -0
  18. package/dist/daemon/ask.js +101 -0
  19. package/dist/daemon/ask.js.map +1 -0
  20. package/dist/daemon/audit.js +168 -0
  21. package/dist/daemon/audit.js.map +1 -0
  22. package/dist/daemon/cc-bridge.js +204 -0
  23. package/dist/daemon/cc-bridge.js.map +1 -0
  24. package/dist/daemon/cfg-sync.js +240 -0
  25. package/dist/daemon/cfg-sync.js.map +1 -0
  26. package/dist/daemon/claim.js +95 -0
  27. package/dist/daemon/claim.js.map +1 -0
  28. package/dist/daemon/detail.js +153 -0
  29. package/dist/daemon/detail.js.map +1 -0
  30. package/dist/daemon/graph.js +176 -0
  31. package/dist/daemon/graph.js.map +1 -0
  32. package/dist/daemon/http.js +83 -0
  33. package/dist/daemon/http.js.map +1 -0
  34. package/dist/daemon/inbound.js +919 -0
  35. package/dist/daemon/inbound.js.map +1 -0
  36. package/dist/daemon/index.js +569 -0
  37. package/dist/daemon/index.js.map +1 -0
  38. package/dist/daemon/last-response.js +59 -0
  39. package/dist/daemon/last-response.js.map +1 -0
  40. package/dist/daemon/mirror-bridge.js +3512 -0
  41. package/dist/daemon/mirror-bridge.js.map +1 -0
  42. package/dist/daemon/mirror-store.js +39 -0
  43. package/dist/daemon/mirror-store.js.map +1 -0
  44. package/dist/daemon/outbound.js +42 -0
  45. package/dist/daemon/outbound.js.map +1 -0
  46. package/dist/daemon/peers.js +146 -0
  47. package/dist/daemon/peers.js.map +1 -0
  48. package/dist/daemon/pending.js +90 -0
  49. package/dist/daemon/pending.js.map +1 -0
  50. package/dist/daemon/quota.js +164 -0
  51. package/dist/daemon/quota.js.map +1 -0
  52. package/dist/daemon/redact.js +27 -0
  53. package/dist/daemon/redact.js.map +1 -0
  54. package/dist/daemon/session-cache.js +149 -0
  55. package/dist/daemon/session-cache.js.map +1 -0
  56. package/dist/daemon/session-scan.js +273 -0
  57. package/dist/daemon/session-scan.js.map +1 -0
  58. package/dist/daemon/sessions.js +35 -0
  59. package/dist/daemon/sessions.js.map +1 -0
  60. package/dist/daemon/spawn-tmux.js +213 -0
  61. package/dist/daemon/spawn-tmux.js.map +1 -0
  62. package/dist/daemon/tips.js +25 -0
  63. package/dist/daemon/tips.js.map +1 -0
  64. package/dist/daemon/topics.js +197 -0
  65. package/dist/daemon/topics.js.map +1 -0
  66. package/dist/daemon/usage.js +358 -0
  67. package/dist/daemon/usage.js.map +1 -0
  68. package/dist/daemon/wedoc.js +210 -0
  69. package/dist/daemon/wedoc.js.map +1 -0
  70. package/dist/daemon/ws.js +67 -0
  71. package/dist/daemon/ws.js.map +1 -0
  72. package/dist/mcp/server.js +385 -0
  73. package/dist/mcp/server.js.map +1 -0
  74. package/dist/shared/ansi.js +134 -0
  75. package/dist/shared/ansi.js.map +1 -0
  76. package/dist/shared/chat-http.js +195 -0
  77. package/dist/shared/chat-http.js.map +1 -0
  78. package/dist/shared/chat-render.js +53 -0
  79. package/dist/shared/chat-render.js.map +1 -0
  80. package/dist/shared/chat-view.js +116 -0
  81. package/dist/shared/chat-view.js.map +1 -0
  82. package/dist/shared/cli-backends.js +304 -0
  83. package/dist/shared/cli-backends.js.map +1 -0
  84. package/dist/shared/config-writer.js +39 -0
  85. package/dist/shared/config-writer.js.map +1 -0
  86. package/dist/shared/config.js +278 -0
  87. package/dist/shared/config.js.map +1 -0
  88. package/dist/shared/detail-render.js +836 -0
  89. package/dist/shared/detail-render.js.map +1 -0
  90. package/dist/shared/detail-store.js +205 -0
  91. package/dist/shared/detail-store.js.map +1 -0
  92. package/dist/shared/highlight.js +154 -0
  93. package/dist/shared/highlight.js.map +1 -0
  94. package/dist/shared/lan-ip.js +35 -0
  95. package/dist/shared/lan-ip.js.map +1 -0
  96. package/dist/shared/log.js +18 -0
  97. package/dist/shared/log.js.map +1 -0
  98. package/dist/shared/md-chunk.js +99 -0
  99. package/dist/shared/md-chunk.js.map +1 -0
  100. package/dist/shared/paths.js +12 -0
  101. package/dist/shared/paths.js.map +1 -0
  102. package/dist/shared/session-label.js +67 -0
  103. package/dist/shared/session-label.js.map +1 -0
  104. package/dist/shared/web-assets.js +51 -0
  105. package/dist/shared/web-assets.js.map +1 -0
  106. package/dist/svr/index.js +246 -0
  107. package/dist/svr/index.js.map +1 -0
  108. package/hooks/hooks.json +16 -0
  109. package/hooks/pre-tool-use.sh +167 -0
  110. package/launchd/com.weclaude.daemon.plist.template +45 -0
  111. package/package.json +97 -0
  112. package/scripts/install.sh +73 -0
  113. package/scripts/postinstall.sh +12 -0
  114. package/scripts/uninstall.sh +44 -0
  115. package/systemd/weclaude.service.template +17 -0
  116. package/web/chat.css +171 -0
  117. package/web/chat.js +367 -0
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "wezard",
3
+ "version": "0.3.3",
4
+ "description": "Bridge Claude Code and WeCom: PreToolUse approval cards + bidirectional tmux session mirror with image paste",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/guxi11/weclaude.git"
10
+ },
11
+ "homepage": "https://github.com/guxi11/weclaude#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/guxi11/weclaude/issues"
14
+ },
15
+ "keywords": [
16
+ "claude",
17
+ "claude-code",
18
+ "wecom",
19
+ "wechat-work",
20
+ "remote-control",
21
+ "approval",
22
+ "mcp"
23
+ ],
24
+ "bin": {
25
+ "weclaude": "./cli/weclaude.sh",
26
+ "weclaude-daemon": "./dist/daemon/index.js",
27
+ "weclaude-mcp": "./dist/mcp/server.js",
28
+ "weclaude-init": "./dist/cli/init.js",
29
+ "weclaude-svr": "./dist/svr/index.js"
30
+ },
31
+ "files": [
32
+ ".claude-plugin/marketplace.json",
33
+ ".claude-plugin/plugin.json",
34
+ "cli/weclaude.sh",
35
+ "dist/cli/init.js",
36
+ "dist/cli/init.js.map",
37
+ "dist/cli/sync.js",
38
+ "dist/cli/sync.js.map",
39
+ "dist/cli/audit.js",
40
+ "dist/cli/audit.js.map",
41
+ "dist/daemon/**/*.js",
42
+ "dist/daemon/**/*.js.map",
43
+ "dist/mcp/**/*.js",
44
+ "dist/mcp/**/*.js.map",
45
+ "dist/shared/**/*.js",
46
+ "dist/shared/**/*.js.map",
47
+ "dist/svr/**/*.js",
48
+ "dist/svr/**/*.js.map",
49
+ "web/chat.css",
50
+ "web/chat.js",
51
+ "hooks/pre-tool-use.sh",
52
+ "hooks/hooks.json",
53
+ "scripts/install.sh",
54
+ "scripts/uninstall.sh",
55
+ "scripts/postinstall.sh",
56
+ "launchd/com.weclaude.daemon.plist.template",
57
+ "systemd/weclaude.service.template",
58
+ "config.example.jsonc",
59
+ "commands/wrc.md",
60
+ "commands/audit.md",
61
+ "README.md",
62
+ "CLAUDE.md",
63
+ "LICENSE"
64
+ ],
65
+ "scripts": {
66
+ "build": "tsc -p tsconfig.json && chmod +x dist/cli/init.js dist/cli/audit.js dist/daemon/index.js dist/mcp/server.js dist/svr/index.js",
67
+ "dev:daemon": "tsx daemon/index.ts",
68
+ "dev:cli": "tsx cli/weclaude.ts",
69
+ "typecheck": "tsc --noEmit",
70
+ "prepare": "test -f dist/daemon/index.js || npm run build",
71
+ "prepublishOnly": "rm -rf dist && npm run build",
72
+ "prepack": "test -f dist/daemon/index.js || npm run build",
73
+ "postinstall": "sh scripts/postinstall.sh"
74
+ },
75
+ "dependencies": {
76
+ "@inquirer/prompts": "^7.2.0",
77
+ "@modelcontextprotocol/sdk": "^1.0.0",
78
+ "@wecom/aibot-node-sdk": "^1.0.6",
79
+ "diff": "^9.0.0",
80
+ "jsonc-parser": "^3.3.1",
81
+ "pino": "^9.5.0",
82
+ "zod": "^3.23.8"
83
+ },
84
+ "devDependencies": {
85
+ "@types/diff": "^7.0.2",
86
+ "@types/node": "^22.0.0",
87
+ "tsx": "^4.19.0",
88
+ "typescript": "^5.6.0"
89
+ },
90
+ "engines": {
91
+ "node": ">=20"
92
+ },
93
+ "os": [
94
+ "darwin",
95
+ "linux"
96
+ ]
97
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # Install resident daemon. Auto-detects macOS (launchd) vs Linux (systemd --user).
3
+ set -euo pipefail
4
+
5
+ REPO="$(cd "$(dirname "$0")/.." && pwd)"
6
+ NODE="$(command -v node)"
7
+ HOME_DIR="$HOME"
8
+ LABEL="com.weclaude.daemon"
9
+
10
+ [[ -x "$NODE" ]] || { echo "node not found in PATH"; exit 1; }
11
+
12
+ # Build if missing
13
+ if [[ ! -f "$REPO/dist/daemon/index.js" ]]; then
14
+ echo "[install] building..."
15
+ (cd "$REPO" && npm install --silent && npx tsc -p tsconfig.json)
16
+ fi
17
+
18
+ mkdir -p "$HOME_DIR/.weclaude"
19
+
20
+ OS="$(uname -s)"
21
+ case "$OS" in
22
+ Darwin)
23
+ LA_DIR="$HOME_DIR/Library/LaunchAgents"
24
+ PLIST_DST="$LA_DIR/${LABEL}.plist"
25
+ mkdir -p "$LA_DIR"
26
+ sed \
27
+ -e "s|__NODE__|$NODE|g" \
28
+ -e "s|__REPO__|$REPO|g" \
29
+ -e "s|__HOME__|$HOME_DIR|g" \
30
+ "$REPO/launchd/${LABEL}.plist.template" > "$PLIST_DST"
31
+
32
+ DOMAIN="gui/$(id -u)"
33
+ # Modern bootstrap/bootout API. Falls back to legacy load -w. load -w stderr
34
+ # is unreliable on recent macOS (sporadic "Failed to connect to bus: No
35
+ # medium found" / "Load failed: 5: I/O error" while the plist is in fact
36
+ # registered). Trust `launchctl list` for verification, not $?.
37
+ launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
38
+ # bootout returns synchronously but unload is async — bootstrapping too soon
39
+ # races into "Bootstrap failed: 5: I/O error". Poll until it's truly gone.
40
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
41
+ launchctl list "$LABEL" >/dev/null 2>&1 || break
42
+ sleep 0.2
43
+ done
44
+ launchctl bootstrap "$DOMAIN" "$PLIST_DST" 2>/dev/null \
45
+ || launchctl load -w "$PLIST_DST" 2>/dev/null \
46
+ || true
47
+ launchctl enable "$DOMAIN/$LABEL" 2>/dev/null || true
48
+
49
+ if launchctl list "$LABEL" >/dev/null 2>&1; then
50
+ echo "[install] launchd loaded: $PLIST_DST"
51
+ else
52
+ echo "[install] launchctl load failed — plist at $PLIST_DST not registered" >&2
53
+ exit 1
54
+ fi
55
+ ;;
56
+ Linux)
57
+ UNIT_DIR="$HOME_DIR/.config/systemd/user"
58
+ mkdir -p "$UNIT_DIR"
59
+ UNIT_DST="$UNIT_DIR/weclaude.service"
60
+ sed \
61
+ -e "s|__NODE__|$NODE|g" \
62
+ -e "s|__REPO__|$REPO|g" \
63
+ -e "s|__HOME__|$HOME_DIR|g" \
64
+ "$REPO/systemd/weclaude.service.template" > "$UNIT_DST"
65
+ systemctl --user daemon-reload
66
+ systemctl --user enable --now weclaude.service
67
+ echo "[install] systemd unit enabled: $UNIT_DST"
68
+ ;;
69
+ *)
70
+ echo "unsupported OS: $OS"; exit 1 ;;
71
+ esac
72
+
73
+ echo "[install] done. Logs at $HOME_DIR/.weclaude/daemon.{stdout,stderr,log}"
@@ -0,0 +1,12 @@
1
+ #!/bin/sh
2
+ # Refresh the Claude Code plugin clone after `npm install -g` so the
3
+ # marketplace copy at ~/.claude/plugins/marketplaces/weclaude-local/ stays
4
+ # in lockstep with the npm global install. Silent no-op in any irrelevant
5
+ # context (local dev install, claude not on PATH, marketplace never added).
6
+ # Must never fail npm install.
7
+
8
+ [ "$npm_config_global" = "true" ] || exit 0
9
+ command -v claude >/dev/null 2>&1 || exit 0
10
+
11
+ claude plugin marketplace update weclaude-local >/dev/null 2>&1 || true
12
+ exit 0
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ LABEL="com.weclaude.daemon"
4
+ HOME_DIR="$HOME"
5
+ OS="$(uname -s)"
6
+ REPO="$(cd "$(dirname "$0")/.." && pwd)"
7
+
8
+ # Best-effort: drop the Claude Code plugin + marketplace registration installed
9
+ # by `weclaude init`. Tries both `claude` and `claude-internal` since either
10
+ # could have been used at install time. All failures are non-fatal — uninstall
11
+ # must keep going even if claude binaries are gone.
12
+ for bin in claude claude-internal; do
13
+ command -v "$bin" >/dev/null 2>&1 || continue
14
+ "$bin" plugin uninstall weclaude@weclaude-local 2>/dev/null && \
15
+ echo "[uninstall] $bin: plugin uninstalled" || true
16
+ "$bin" plugin marketplace remove "$REPO" 2>/dev/null && \
17
+ echo "[uninstall] $bin: marketplace removed" || true
18
+ done
19
+
20
+ case "$OS" in
21
+ Darwin)
22
+ PLIST="$HOME_DIR/Library/LaunchAgents/${LABEL}.plist"
23
+ if [[ -f "$PLIST" ]]; then
24
+ launchctl unload "$PLIST" 2>/dev/null || true
25
+ rm -f "$PLIST"
26
+ echo "[uninstall] removed $PLIST"
27
+ fi
28
+ # Belt-and-braces: launchctl unload can leave a stray process if the plist
29
+ # was edited mid-flight. bootout the label, then SIGTERM any survivors.
30
+ launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
31
+ pkill -f "dist/daemon/index.js" 2>/dev/null || true
32
+ ;;
33
+ Linux)
34
+ UNIT="$HOME_DIR/.config/systemd/user/weclaude.service"
35
+ if [[ -f "$UNIT" ]]; then
36
+ systemctl --user disable --now weclaude.service 2>/dev/null || true
37
+ rm -f "$UNIT"
38
+ systemctl --user daemon-reload
39
+ echo "[uninstall] removed $UNIT"
40
+ fi
41
+ pkill -f "dist/daemon/index.js" 2>/dev/null || true
42
+ ;;
43
+ esac
44
+ echo "[uninstall] done."
@@ -0,0 +1,17 @@
1
+ [Unit]
2
+ Description=weclaude daemon (WeCom remote control for Claude Code)
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=__REPO__
9
+ ExecStart=__NODE__ __REPO__/dist/daemon/index.js
10
+ Restart=always
11
+ RestartSec=5
12
+ StandardOutput=append:__HOME__/.weclaude/daemon.stdout.log
13
+ StandardError=append:__HOME__/.weclaude/daemon.stderr.log
14
+ Environment=NODE_ENV=production
15
+
16
+ [Install]
17
+ WantedBy=default.target
package/web/chat.css ADDED
@@ -0,0 +1,171 @@
1
+ /* Chat 详情视图外壳。turn 正文的样式来自 detail-render 的 SHARED_CSS/TURN_CSS,
2
+ 由 /chat/app.css 拼在本文件之前 —— 这里只管三栏骨架、会话列表与页脚总账。 */
3
+
4
+ html, body { height: 100% }
5
+ body { overflow: hidden }
6
+
7
+ .app { display: flex; height: 100vh; height: 100dvh }
8
+
9
+ /* ── 侧栏: 会话列表 ─────────────────────────────────────────────── */
10
+ .side {
11
+ width: 288px; flex: none; background: #fff; border-right: 1px solid #d0d7de;
12
+ display: flex; flex-direction: column; min-height: 0;
13
+ }
14
+ .side-head { padding: 13px 16px 11px; border-bottom: 1px solid #eaeef2 }
15
+ .side-head .l1 { display: flex; align-items: baseline; gap: 8px }
16
+ .side-head .t { font-size: 14px; font-weight: 600 }
17
+ .side-head .n {
18
+ margin-left: auto; font-size: 11px; color: #8c959f;
19
+ font-variant-numeric: tabular-nums;
20
+ }
21
+ .side-head .s {
22
+ font-size: 11px; color: #8c959f; font-family: ui-monospace, Menlo, monospace;
23
+ word-break: break-all; margin-top: 3px;
24
+ }
25
+
26
+ .tags { flex: 1; min-height: 0; overflow-y: auto; padding: 6px 6px 12px }
27
+
28
+ .tag-row {
29
+ position: relative; display: flex; gap: 10px; align-items: flex-start;
30
+ padding: 8px 10px 8px 11px; border-radius: 8px; cursor: pointer;
31
+ border: 1px solid transparent; transition: background .12s;
32
+ }
33
+ .tag-row:hover { background: #f6f8fa }
34
+ /* 选中态用左侧色条而不是整块蓝底 —— 底色会和内部的 badge/预览抢注意力。 */
35
+ .tag-row.on { background: #f0f4f9 }
36
+ .tag-row.on::before {
37
+ content: ""; position: absolute; left: 2px; top: 9px; bottom: 9px;
38
+ width: 3px; border-radius: 2px; background: #0969da;
39
+ }
40
+
41
+ .tag-av {
42
+ width: 32px; height: 32px; flex: none; border-radius: 10px; background: #f1f3f6;
43
+ display: flex; align-items: center; justify-content: center; font-size: 17px;
44
+ position: relative; border: 1px solid #eaeef2;
45
+ }
46
+ .tag-av .live {
47
+ position: absolute; right: -3px; bottom: -3px; width: 10px; height: 10px;
48
+ border-radius: 50%; background: #1a7f37; border: 2px solid #fff;
49
+ animation: pulse 1.4s infinite;
50
+ }
51
+ @keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .3 } }
52
+
53
+ .tag-main { min-width: 0; flex: 1 }
54
+ .tag-l1 { display: flex; align-items: baseline; gap: 6px }
55
+ .tag-name {
56
+ font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden;
57
+ text-overflow: ellipsis; font-family: ui-monospace, Menlo, monospace;
58
+ }
59
+ .tag-row.running .tag-name { color: #1a7f37 }
60
+ .tag-ts {
61
+ margin-left: auto; flex: none; font-size: 11px; color: #8c959f;
62
+ font-family: ui-monospace, Menlo, monospace;
63
+ }
64
+ .tag-prev {
65
+ font-size: 12px; color: #656d76; margin-top: 2px; overflow: hidden;
66
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
67
+ line-height: 1.45;
68
+ }
69
+ /* 每行第三层: 轮数 / 工具数 / token —— 不点进去也能判断哪个会话在真干活。 */
70
+ .tag-meta {
71
+ display: flex; gap: 4px; align-items: center; margin-top: 4px;
72
+ font-size: 10.5px; color: #8c959f; font-family: ui-monospace, Menlo, monospace;
73
+ font-variant-numeric: tabular-nums;
74
+ }
75
+ .tag-meta .sep { opacity: .45 }
76
+ .tag-meta .hot { color: #9a6700 }
77
+ .tags .empty-side { padding: 28px 12px; text-align: center; color: #8c959f; font-size: 12px }
78
+
79
+ /* ── 主区 ───────────────────────────────────────────────────────── */
80
+ .main { flex: 1; min-width: 0; display: flex; flex-direction: column }
81
+ .topbar {
82
+ flex: none; display: flex; align-items: center; gap: 10px; padding: 11px 20px;
83
+ background: #fff; border-bottom: 1px solid #d0d7de;
84
+ }
85
+ .topbar .em { font-size: 18px }
86
+ .topbar .h { font-size: 15px; font-weight: 600; font-family: ui-monospace, Menlo, monospace }
87
+ .topbar .sub {
88
+ font-size: 11px; color: #8c959f; font-family: ui-monospace, Menlo, monospace;
89
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
90
+ }
91
+ .topbar .back { display: none; font-size: 18px; color: #656d76; cursor: pointer; padding: 0 4px }
92
+
93
+ .thread {
94
+ flex: 1; min-height: 0; overflow-y: auto; overflow-anchor: none;
95
+ padding: 16px 20px 24px; -webkit-overflow-scrolling: touch;
96
+ }
97
+ .thread-in { max-width: 920px; margin: 0 auto; display: flex; flex-direction: column; gap: 18px }
98
+ .empty { color: #8c959f; text-align: center; padding: 60px 0; font-size: 13px }
99
+ .more-btn {
100
+ align-self: center; background: #fff; border: 1px solid #d0d7de; border-radius: 16px;
101
+ padding: 5px 16px; font-size: 12px; color: #57606a; cursor: pointer; font-family: inherit;
102
+ }
103
+ .more-btn:hover { background: #f6f8fa; color: #0969da; border-color: #0969da55 }
104
+
105
+ /* ── turn 分组 ──────────────────────────────────────────────────── */
106
+ /* section 的默认样式给了白底+边框, 但没有内边距 —— 气泡/工具行会直接贴着卡片
107
+ 边缘, 所以这里补上一轮对话自己的呼吸空间。 */
108
+ .turn-group {
109
+ display: flex; flex-direction: column; gap: 10px;
110
+ padding: 14px 16px 16px; border-radius: 12px; border-color: #e1e6ec;
111
+ box-shadow: 0 1px 2px #1f23280a;
112
+ }
113
+ .tg-head {
114
+ display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
115
+ padding-bottom: 6px; border-bottom: 1px dashed #d8dee4;
116
+ }
117
+ .tg-dot { width: 7px; height: 7px; border-radius: 50%; background: #b1bac4; flex: none }
118
+ .tg-dot.live { background: #1a7f37; animation: pulse 1.4s infinite }
119
+ .tg-time { font-size: 11.5px; color: #8c959f; font-family: ui-monospace, Menlo, monospace }
120
+ .tg-tok {
121
+ font-size: 11px; color: #57606a; background: #f1f3f6; border-radius: 9px;
122
+ padding: 1px 7px; font-family: ui-monospace, Menlo, monospace;
123
+ }
124
+ .tg-link { margin-left: auto; color: #8c959f; text-decoration: none; font-size: 13px }
125
+ .tg-link:hover { color: #0969da }
126
+ .turn-group .bubbles { margin-top: 0 }
127
+
128
+ /* ── 页脚总账 ───────────────────────────────────────────────────── */
129
+ .statusbar {
130
+ flex: none; display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
131
+ padding: 7px 20px; background: #fff; border-top: 1px solid #d0d7de;
132
+ font-size: 11.5px; color: #57606a; font-family: ui-monospace, Menlo, monospace;
133
+ }
134
+ .statusbar .st { display: flex; align-items: baseline; gap: 5px }
135
+ .statusbar .st .k {
136
+ color: #8c959f; text-transform: uppercase; letter-spacing: .4px; font-size: 10px;
137
+ }
138
+ .statusbar .st .v { color: #1f2328; font-weight: 700; font-variant-numeric: tabular-nums }
139
+ /* 缺数据的指标压暗, 不再显示成有意义的 "0" —— 0 与「没采到」是两回事。 */
140
+ .statusbar .st.void .v { color: #b1bac4; font-weight: 400 }
141
+ .statusbar .pill { display: flex; align-items: center; gap: 5px; font-weight: 700 }
142
+ .statusbar .pill .d { width: 7px; height: 7px; border-radius: 50%; background: #8c959f }
143
+ .statusbar .pill.run { color: #1a7f37 }
144
+ .statusbar .pill.run .d { background: #1a7f37; animation: pulse 1.4s infinite }
145
+ .statusbar .model {
146
+ color: #57606a; background: #f1f3f6; border-radius: 9px; padding: 1px 7px;
147
+ max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
148
+ }
149
+ /* token I/O 堆叠条 + 图例: 色块必须有名字, 否则底部那条彩带谁也读不懂。 */
150
+ .sb-io { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0 }
151
+ .sb-io > .k { color: #8c959f; text-transform: uppercase; letter-spacing: .4px; font-size: 10px }
152
+ .sb-bar { display: flex; height: 6px; width: 120px; border-radius: 3px; overflow: hidden; background: #eaeef2 }
153
+ .sb-bar .seg { height: 100% }
154
+ .sb-leg { display: flex; align-items: center; gap: 9px; flex-wrap: wrap }
155
+ .sb-leg .lg { display: flex; align-items: center; gap: 4px; font-size: 10.5px; color: #8c959f }
156
+ .sb-leg .lg i { width: 8px; height: 8px; border-radius: 2px; flex: none }
157
+ .sb-leg .lg b { color: #1f2328; font-weight: 700; font-variant-numeric: tabular-nums }
158
+ .conn { margin-left: auto; display: flex; align-items: center; gap: 5px }
159
+ .conn .d { width: 7px; height: 7px; border-radius: 50%; background: #1a7f37 }
160
+ .conn.off .d { background: #cf222e }
161
+
162
+ @media (max-width: 760px) {
163
+ .app { flex-direction: column }
164
+ .side { width: auto; border-right: 0; border-bottom: 1px solid #d0d7de; max-height: 44vh }
165
+ .app.reading .side { display: none }
166
+ .app.reading .topbar .back { display: block }
167
+ .thread { padding: 12px }
168
+ .turn-group { padding: 12px }
169
+ .statusbar { gap: 10px; padding: 7px 12px }
170
+ .sb-io, .statusbar .model { display: none }
171
+ }