@wendongfly/myhi 1.3.128 → 1.3.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/chat.html CHANGED
@@ -134,6 +134,25 @@
134
134
  .tool-body { display: none; background: var(--surface); border: 1px solid var(--surface2); border-radius: 6px; padding: 0.5rem; margin-top: 0.2rem; font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.75rem; line-height: 1.3; overflow-x: auto; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow-y: auto; }
135
135
  .tool-body.open { display: block; }
136
136
 
137
+ /* 子代理(Task)面板:内部活动按 parent_tool_use_id 归拢 + 状态灯,不平铺污染主流 */
138
+ .msg-subagent { border-left: 3px solid var(--accent); margin: 0.4rem 0 0.4rem 0.3rem; padding-left: 0.6rem; }
139
+ .sub-header { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; font-size: 0.8rem; color: var(--accent); padding: 0.28rem 0; user-select: none; flex-wrap: wrap; }
140
+ .sub-header:hover { filter: brightness(1.15); }
141
+ .sub-header .arrow { transition: transform 0.15s; font-size: 0.65rem; }
142
+ .sub-header .arrow.open { transform: rotate(90deg); }
143
+ .sub-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
144
+ .sub-dot.running { background: var(--blue); animation: subpulse 1s ease-in-out infinite; }
145
+ .sub-dot.done { background: var(--green); }
146
+ .sub-dot.fail { background: #e5484d; }
147
+ @keyframes subpulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.3;transform:scale(.65)} }
148
+ .sub-meta { color: var(--muted); font-size: 0.72rem; }
149
+ .sub-body { display: none; margin-top: 0.2rem; padding-left: 0.4rem; border-left: 1px dashed var(--surface2); }
150
+ .sub-body.open { display: block; }
151
+ .sub-desc { color: var(--muted); font-size: 0.74rem; padding: 0.2rem 0 0.35rem; white-space: pre-wrap; word-break: break-word; }
152
+ .sub-act { font-size: 0.75rem; padding: 0.15rem 0; color: var(--text); white-space: pre-wrap; word-break: break-word; }
153
+ .sub-act .k { color: var(--muted); }
154
+ .sub-summary { background: var(--surface); border: 1px solid var(--surface2); border-radius: 6px; padding: 0.4rem 0.5rem; margin-top: 0.35rem; font-size: 0.75rem; white-space: pre-wrap; word-break: break-word; max-height: 260px; overflow-y: auto; }
155
+
137
156
 
138
157
  /* Diff 视图 */
139
158
  .msg-diff { font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.78rem; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.6rem; overflow-x: auto; line-height: 1.35; }
@@ -1117,14 +1136,107 @@
1117
1136
  const TOOL_ICONS = { Read:'📄', Edit:'✏️', Write:'📝', Bash:'💻', Grep:'🔍', Glob:'📂', WebFetch:'🌐', WebSearch:'🔎', LSP:'🔧', Task:'🤖', Agent:'🤖' };
1118
1137
 
1119
1138
  // 子代理(Task 工具)渲染:把 subagent_type / description / prompt 拆开展示,
1120
- // 而不是甩一坨 input JSON。返回 { label, body } 交给 addToolMessage。
1139
+ // 而不是甩一坨 input JSON。返回 { label, body, desc } 交给面板/工具卡。
1121
1140
  function subagentToolInfo(input) {
1122
1141
  const type = (input && input.subagent_type) || 'general-purpose';
1123
1142
  const desc = (input && input.description) || '';
1124
1143
  const prompt = (input && input.prompt) || '';
1125
1144
  const label = `子代理 · ${type}`;
1126
1145
  const body = (desc ? `📋 ${desc}\n\n` : '') + (prompt || JSON.stringify(input || {}, null, 2));
1127
- return { label, body };
1146
+ return { label, body, desc };
1147
+ }
1148
+
1149
+ // ── 子代理(Task)面板:子代理内部活动带 parent_tool_use_id 冒泡到主 stream(见 agent.js),
1150
+ // 这里按 taskId(=Task 的 tool_use id) 归拢到一张可折叠面板 + 状态灯,主流不再被平铺污染 ──
1151
+ let _subagents = {}; // taskId → { el, bodyEl, dotEl, metaEl, toolCount, state }
1152
+ function subaResetAll() { _subagents = {}; }
1153
+
1154
+ // 生成一个可折叠的内部工具项(复用工具卡样式),放进子代理面板 body
1155
+ function subaMakeItem(name, payload) {
1156
+ const icon = TOOL_ICONS[name] || '🔧';
1157
+ const item = document.createElement('div'); item.className = 'msg-tool';
1158
+ const h = document.createElement('div'); h.className = 'tool-header';
1159
+ h.innerHTML = `<span class="arrow">▶</span> ${icon} ${escHtml(name || '工具')}`;
1160
+ const b = document.createElement('div'); b.className = 'tool-body';
1161
+ b.innerHTML = renderAnsiHtml(typeof payload === 'string' ? payload : JSON.stringify(payload || {}, null, 2));
1162
+ h.onclick = () => { b.classList.toggle('open'); h.querySelector('.arrow').classList.toggle('open'); };
1163
+ item.appendChild(h); item.appendChild(b);
1164
+ return item;
1165
+ }
1166
+
1167
+ function subaCreatePanel(taskId, info, opts) {
1168
+ if (!taskId) return null;
1169
+ if (_subagents[taskId]) return _subagents[taskId];
1170
+ endStream(); removeThinking(); endToolGroup();
1171
+ const el = document.createElement('div'); el.className = 'msg msg-subagent';
1172
+ const header = document.createElement('div'); header.className = 'sub-header';
1173
+ const arrow = document.createElement('span'); arrow.className = 'arrow'; arrow.textContent = '▶';
1174
+ const dot = document.createElement('span'); dot.className = 'sub-dot running';
1175
+ const title = document.createElement('span'); title.innerHTML = `🤖 ${escHtml((info && info.label) || '子代理')}`;
1176
+ const meta = document.createElement('span'); meta.className = 'sub-meta'; meta.textContent = '运行中…';
1177
+ header.appendChild(arrow); header.appendChild(dot); header.appendChild(title); header.appendChild(meta);
1178
+ const bodyEl = document.createElement('div'); bodyEl.className = 'sub-body';
1179
+ if (info && info.desc) { const d = document.createElement('div'); d.className = 'sub-desc'; d.textContent = '📋 ' + info.desc; bodyEl.appendChild(d); }
1180
+ header.onclick = () => { bodyEl.classList.toggle('open'); arrow.classList.toggle('open'); };
1181
+ el.appendChild(header); el.appendChild(bodyEl);
1182
+ chatArea.appendChild(el); trimMessages();
1183
+ const rec = { el, bodyEl, dotEl: dot, metaEl: meta, toolCount: 0, state: 'running' };
1184
+ _subagents[taskId] = rec;
1185
+ if (opts && opts.done) subaMarkDone(taskId, null);
1186
+ scrollToBottom();
1187
+ return rec;
1188
+ }
1189
+
1190
+ // 把一条带 parent_tool_use_id 的消息拆成活动,追加进对应面板。命中返回 true。
1191
+ function subaRoute(msg) {
1192
+ const pid = msg && msg.parent_tool_use_id;
1193
+ const rec = pid && _subagents[pid];
1194
+ if (!rec) return false;
1195
+ const blocks = msg.message && msg.message.content;
1196
+ if (Array.isArray(blocks)) {
1197
+ for (const bl of blocks) {
1198
+ if (bl.type === 'tool_use') {
1199
+ rec.bodyEl.appendChild(subaMakeItem(bl.name === 'Task' ? '子代理' : bl.name, bl.input));
1200
+ rec.toolCount++;
1201
+ } else if (bl.type === 'text' && bl.text && bl.text.trim()) {
1202
+ const t = document.createElement('div'); t.className = 'sub-act';
1203
+ t.innerHTML = `<span class="k">💬</span> ${escHtml(bl.text.trim())}`;
1204
+ rec.bodyEl.appendChild(t);
1205
+ } else if (bl.type === 'tool_result') {
1206
+ const c = typeof bl.content === 'string' ? bl.content
1207
+ : Array.isArray(bl.content) ? bl.content.map(x => x.text || '').join('\n') : JSON.stringify(bl.content);
1208
+ if (c && c.trim()) rec.bodyEl.appendChild(subaMakeItem('↳ 结果', c));
1209
+ }
1210
+ }
1211
+ } else if (typeof msg.content === 'string' && msg.content.trim()) {
1212
+ const t = document.createElement('div'); t.className = 'sub-act';
1213
+ t.innerHTML = `<span class="k">↳</span> ${escHtml(msg.content.trim())}`;
1214
+ rec.bodyEl.appendChild(t);
1215
+ }
1216
+ if (rec.state === 'running') rec.metaEl.textContent = `运行中… ${rec.toolCount} 步`;
1217
+ scrollToBottom();
1218
+ return true;
1219
+ }
1220
+
1221
+ function subaMarkDone(taskId, summary) {
1222
+ const rec = _subagents[taskId];
1223
+ if (!rec || rec.state === 'done') return;
1224
+ rec.state = 'done';
1225
+ rec.dotEl.className = 'sub-dot done';
1226
+ rec.metaEl.textContent = `✅ 完成 · ${rec.toolCount} 步`;
1227
+ if (summary && String(summary).trim()) {
1228
+ const s = document.createElement('div'); s.className = 'sub-summary';
1229
+ s.textContent = String(summary).trim();
1230
+ rec.bodyEl.appendChild(s);
1231
+ }
1232
+ }
1233
+ function subaMarkAllDone() { for (const id in _subagents) subaMarkDone(id, null); }
1234
+
1235
+ // 主线里 Task 的 tool_result(tool_use_id == taskId)→ 面板转完成 + 摘要。命中返回 true。
1236
+ function subaTryComplete(toolUseId, content) {
1237
+ if (!toolUseId || !_subagents[toolUseId]) return false;
1238
+ subaMarkDone(toolUseId, content);
1239
+ return true;
1128
1240
  }
1129
1241
 
1130
1242
  // ── 消息渲染 ──────────────────────────────────
@@ -1798,6 +1910,8 @@
1798
1910
  // ── Agent 模式事件 ──────────────────────────────
1799
1911
  socket.on('agent:message', (msg) => {
1800
1912
  if (!msg) return;
1913
+ // 子代理内部活动(带 parent_tool_use_id)归入对应面板,不平铺进主流
1914
+ if (subaRoute(msg)) { setWorkState('working'); return; }
1801
1915
  switch (msg.type) {
1802
1916
  case 'system':
1803
1917
  if (msg.subtype === 'init') {
@@ -1836,15 +1950,14 @@
1836
1950
  if (block.name === 'AskUserQuestion' && block.input) {
1837
1951
  openAskSheet(block.input);
1838
1952
  }
1839
- if (block.name === 'Task' && block.input) {
1840
- const info = subagentToolInfo(block.input);
1841
- addToolMessage(info.body, info.label);
1953
+ if (block.name === 'Task') {
1954
+ subaCreatePanel(block.id, subagentToolInfo(block.input || {}));
1842
1955
  } else {
1843
1956
  addToolMessage(JSON.stringify(block.input || {}, null, 2), block.name || '工具');
1844
1957
  }
1845
1958
  } else if (block.type === 'tool_result') {
1846
1959
  const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
1847
- addRawOutput(content);
1960
+ if (!subaTryComplete(block.tool_use_id, content)) addRawOutput(content);
1848
1961
  }
1849
1962
  }
1850
1963
  }
@@ -1860,6 +1973,7 @@
1860
1973
  // 轮次结束时确保闹钟 chip 被扫描(防止最后一条消息未经过 endStream)
1861
1974
  endStream();
1862
1975
  const cost = msg.total_cost_usd ? ` ($${msg.total_cost_usd.toFixed(4)})` : '';
1976
+ subaMarkAllDone(); // 轮结束:仍在"运行中"的子代理面板一律收尾为完成(兜底)
1863
1977
  addStatusMessage(`完成${cost}`);
1864
1978
  // 关键:idle 放最后——渲染最终答复的 addAssistantMessage 会把状态置回 'working',
1865
1979
  // 若在其之前置 idle 会被顶回「执行中」+ 取消按钮不复位。渲染全部完成后再回 idle。
@@ -1903,9 +2017,8 @@
1903
2017
  if (msg.name === 'AskUserQuestion' && msg.input) {
1904
2018
  openAskSheet(msg.input);
1905
2019
  }
1906
- if (msg.name === 'Task' && msg.input) {
1907
- const info = subagentToolInfo(msg.input);
1908
- addToolMessage(info.body, info.label);
2020
+ if (msg.name === 'Task') {
2021
+ subaCreatePanel(msg.id, subagentToolInfo(msg.input || {}));
1909
2022
  } else {
1910
2023
  addToolMessage(JSON.stringify(msg.input || {}, null, 2), msg.name || '工具');
1911
2024
  }
@@ -1919,7 +2032,7 @@
1919
2032
  const text = typeof msg.content === 'string' ? msg.content
1920
2033
  : Array.isArray(msg.content) ? msg.content.map(b => b.text || '').join('\n')
1921
2034
  : JSON.stringify(msg.content);
1922
- if (text.trim()) addRawOutput(text);
2035
+ if (text.trim() && !subaTryComplete(msg.tool_use_id, text)) addRawOutput(text);
1923
2036
  }
1924
2037
  setWorkState('working');
1925
2038
  break;
@@ -1950,8 +2063,10 @@
1950
2063
  _historyHasMore = true;
1951
2064
  _userScrolledUp = false;
1952
2065
 
2066
+ subaResetAll();
1953
2067
  for (const msg of history) {
1954
2068
  endStream(); // 每条历史消息都是独立的,不要流式合并
2069
+ if (subaRoute(msg)) continue; // 子代理内部历史消息归入对应面板
1955
2070
  if (msg.type === 'user' && msg.content) {
1956
2071
  addInputMessage(msg.content);
1957
2072
  } else if (msg.type === 'assistant' && msg.message?.content) {
@@ -1960,7 +2075,10 @@
1960
2075
  for (const block of msg.message.content) {
1961
2076
  if (block.type === 'thinking' && block.thinking) { showThinking(block.thinking); removeThinking(); }
1962
2077
  else if (block.type === 'text' && block.text) texts.push(block.text);
1963
- else if (block.type === 'tool_use') addToolMessage(JSON.stringify(block.input || {}, null, 2), block.name || '工具');
2078
+ else if (block.type === 'tool_use') {
2079
+ if (block.name === 'Task') subaCreatePanel(block.id, subagentToolInfo(block.input || {}), { done: false });
2080
+ else addToolMessage(JSON.stringify(block.input || {}, null, 2), block.name || '工具');
2081
+ }
1964
2082
  }
1965
2083
  if (texts.length) {
1966
2084
  const combined = texts.join('\n\n');
@@ -1977,6 +2095,7 @@
1977
2095
  addStatusMessage(`完成${cost}`);
1978
2096
  }
1979
2097
  }
2098
+ subaMarkAllDone(); // 历史都是过去式:剩余"运行中"的子代理面板收尾为完成
1980
2099
 
1981
2100
  // 还原断点处的流式气泡,让后续 live delta 接着拼
1982
2101
  if (inflightText) {
@@ -72,15 +72,39 @@ try {
72
72
 
73
73
  # ---------- 2b) Claude Code CLI(myhi 的 AI 引擎,@anthropic-ai/claude-code)----------
74
74
  if (-not $SkipClaude) {
75
- Info "安装/更新 Claude CLI(npm i -g @anthropic-ai/claude-code@latest,稍候)..."
76
75
  $eaC = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
76
+ # 停掉可能在跑的 claude.exe(升级时它锁着 ~250MB 原生二进制会 EBUSY),并清 .claude-code-<hash>
77
+ # 残留(防 npm 原子替换 ENOTEMPTY)——与前面停 myhi/清 .myhi-* 同款。
78
+ taskkill /IM claude.exe /F /T 2>$null | Out-Null
79
+ $npmRc = (npm.cmd root -g | Select-Object -Last 1).ToString().Trim()
80
+ Get-ChildItem (Join-Path $npmRc '@anthropic-ai') -Filter '.claude-code-*' -Directory -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
81
+ Info "安装/更新 Claude CLI(npm i -g @anthropic-ai/claude-code@latest,稍候)..."
77
82
  npm.cmd i -g '@anthropic-ai/claude-code@latest' --allow-scripts=@anthropic-ai/claude-code
78
83
  if ($LASTEXITCODE -ne 0) { npm.cmd i -g '@anthropic-ai/claude-code@latest' }
79
- $claudeExit = $LASTEXITCODE
84
+
85
+ # 校验 claude 真能跑:claude-code 2.x 是平台原生二进制,其平台包(optionalDependencies,
86
+ # 如 @anthropic-ai/claude-code-win32-x64)可能在安装时【静默失败】没装上 → bin/claude 只剩
87
+ # 500B 报错占位 → 用时才发现坏。故装完立即验证,坏了【自动补装对应平台包】自愈。
88
+ function Test-ClaudeVer {
89
+ try { $v = (& claude --version 2>&1 | Select-Object -First 1); if ("$v" -match '\d+\.\d+\.\d+') { return "$v" } } catch {}
90
+ return $null
91
+ }
92
+ $cv = Test-ClaudeVer
93
+ if (-not $cv) {
94
+ Warn "Claude 原生二进制缺失(平台包静默失败),补装平台包..."
95
+ $npmRootC = (npm.cmd root -g | Select-Object -Last 1).ToString().Trim()
96
+ $ccPkg = Join-Path $npmRootC '@anthropic-ai\claude-code\package.json'
97
+ $ccVer = if (Test-Path $ccPkg) { (Get-Content $ccPkg -Raw | ConvertFrom-Json).version } else { 'latest' }
98
+ $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
99
+ # 清 .claude-code-<hash> 残留,防补装时 ENOTEMPTY
100
+ Get-ChildItem (Join-Path $npmRootC '@anthropic-ai') -Filter '.claude-code-*' -Directory -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
101
+ npm.cmd i -g "@anthropic-ai/claude-code-win32-$arch@$ccVer"
102
+ $cv = Test-ClaudeVer
103
+ }
80
104
  $ErrorActionPreference = $eaC
81
105
  # 不因 claude 装失败而中断整体部署(可事后手动补)
82
- if ($claudeExit -eq 0) { Ok "Claude CLI 已安装" }
83
- else { Warn "Claude CLI 安装失败(exit $claudeExit),可稍后手动:npm i -g @anthropic-ai/claude-code" }
106
+ if ($cv) { Ok "Claude CLI 可用:$cv" }
107
+ else { Warn "Claude CLI 仍不可用,请手动排查:claude --version(或补装 @anthropic-ai/claude-code-win32-x64)" }
84
108
  }
85
109
  $pkgDir = Join-Path $npmRoot '@wendongfly\myhi'
86
110
  $daemon = Join-Path $pkgDir 'bin\daemon.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendongfly/myhi",
3
- "version": "1.3.128",
3
+ "version": "1.3.130",
4
4
  "description": "Web-based terminal sharing with chat UI — control your terminal from phone via LAN/Tailscale",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",