@wendongfly/myhi 1.3.192 → 1.3.193

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
@@ -231,6 +231,9 @@
231
231
  /* ── 输入栏 ─────────────────────────────── */
232
232
  /* 输入区与对话同底色、不画分隔线:输入框自带圆角边框,浮在对话下方即可(原来一条贯穿的横线 + 浅色底带显得割裂) */
233
233
  #input-area { background: var(--bg); flex-shrink: 0; padding: 0.5rem 0.75rem 0.35rem; }
234
+ #pending-bar { margin: 0 0 0.45rem; padding: 0.45rem 0.7rem; border-radius: 9px; background: rgba(210,153,34,0.14); border: 1px solid rgba(210,153,34,0.45); color: var(--text); font-size: 0.8rem; cursor: pointer; }
235
+ .msg-plan { border: 1px solid var(--purple); border-radius: 10px; padding: 0.55rem 0.8rem !important; background: rgba(124,58,237,0.06); }
236
+ .msg-plan .plan-head { font-size: 0.78rem; font-weight: 600; color: var(--purple); margin-bottom: 0.25rem; }
234
237
  #input-area.no-shortcuts { padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); }
235
238
  #input-box { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; transition: border-color 0.2s; overflow: hidden; position: relative; }
236
239
  #img-preview { display: none; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--surface2); background: var(--surface); }
@@ -511,6 +514,7 @@
511
514
  <span class="sb-item" id="work-status"><span class="sb-dot idle"></span> 空闲</span>
512
515
  <span id="control-badge" class="readonly" style="display:none">只读</span>
513
516
  <span id="viewer-count" class="sb-item"></span>
517
+ <span id="guide-chip" class="sb-item" style="display:none;cursor:pointer" onclick="showMemory('project')"></span>
514
518
  <span id="update-badge" style="display:none;color:var(--yellow);cursor:pointer" onclick="doUpdate()">⬆ 有更新</span>
515
519
  <span id="claude-update-badge" style="display:none;color:var(--blue);cursor:pointer" onclick="doClaudeUpdate()">⬆ claude</span>
516
520
  </div>
@@ -518,6 +522,7 @@
518
522
  <div id="chat-area"></div>
519
523
 
520
524
  <div id="input-area">
525
+ <div id="pending-bar" style="display:none" onclick="reopenPending()"></div>
521
526
  <div id="readonly-overlay" style="display:none"></div>
522
527
  <div id="input-box">
523
528
  <!-- 待发草稿提示:本轮干完(idle)且收件箱里还有本任务的草稿时浮出来 -->
@@ -526,6 +531,14 @@
526
531
  <span id="draft-tip-text" style="flex:1;font-size:0.74rem;color:var(--yellow);overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
527
532
  <button class="img-remove" onclick="event.stopPropagation();hideDraftTip()" title="暂不处理">&times;</button>
528
533
  </div>
534
+ <!-- 建议生成项目说明:看出是长期项目(同目录多任务 / 聊了很多轮 / 说了「以后都…」)且还没有 CLAUDE.md 时浮出 -->
535
+ <div id="guide-tip" style="display:none;align-items:center;gap:0.5rem;padding:0.4rem 0.75rem;border-bottom:1px solid var(--surface2);background:rgba(46,160,67,0.10)">
536
+ <span style="font-size:0.95rem;flex-shrink:0">📘</span>
537
+ <span id="guide-tip-text" style="flex:1;min-width:0;font-size:0.74rem;color:var(--text2);line-height:1.45"></span>
538
+ <button onclick="guideTipGo()" style="flex-shrink:0;background:var(--green);color:#fff;border:none;border-radius:6px;padding:0.25rem 0.6rem;font-size:0.72rem;cursor:pointer">帮我生成</button>
539
+ <button onclick="guideTipOff()" style="flex-shrink:0;background:none;border:none;color:var(--muted);font-size:0.7rem;cursor:pointer;padding:0.2rem">不再提示</button>
540
+ <button class="img-remove" onclick="guideTipClose()" title="先不管">&times;</button>
541
+ </div>
529
542
  <div id="img-preview">
530
543
  <img id="img-preview-thumb" src="" alt="">
531
544
  <span class="img-info">已添加图片,输入文字后发送</span>
@@ -938,7 +951,7 @@
938
951
  <div id="memory-sheet" class="action-sheet">
939
952
  <div class="action-sheet-backdrop" onclick="closeMemorySheet()"></div>
940
953
  <div class="action-sheet-box" style="height:90vh;max-height:90vh;display:flex;flex-direction:column">
941
- <div class="action-sheet-title">Claude 记忆</div>
954
+ <div class="action-sheet-title">项目说明与记忆</div>
942
955
  <div id="memory-content" style="flex:1;overflow-y:auto;padding:0.3rem 0;font-size:0.78rem;color:var(--text2);scrollbar-width:thin">
943
956
  <div style="text-align:center;color:var(--muted);padding:2rem 0">加载中...</div>
944
957
  </div>
@@ -1012,7 +1025,47 @@
1012
1025
  </div>
1013
1026
  <button id="ask-sheet-submit-multi" class="action-sheet-cancel ask-submit-btn" style="display:none;background:var(--blue3);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="submitAskMulti()">提交回答</button>
1014
1027
  <button id="ask-sheet-submit-single" class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="submitAskReply()">发送自定义回答</button>
1015
- <button class="action-sheet-cancel" onclick="closeAskSheet()">取消</button>
1028
+ <button id="ask-sheet-skip" class="action-sheet-cancel" style="display:none" onclick="skipAsk()">⏭ 跳过,让 AI 自己决定</button>
1029
+ <button id="ask-sheet-cancel" class="action-sheet-cancel" onclick="closeAskSheet()">取消</button>
1030
+ </div>
1031
+ </div>
1032
+ <!-- 方案审批:计划模式下 AI 用 ExitPlanMode 提交方案 -->
1033
+ <div id="plan-sheet" class="action-sheet">
1034
+ <div class="action-sheet-backdrop" onclick="closePlanSheet()"></div>
1035
+ <div class="action-sheet-box" style="max-height:88vh;display:flex;flex-direction:column">
1036
+ <div class="action-sheet-title">📋 AI 提交了方案,请审批</div>
1037
+ <div class="msg-assistant" style="flex:1;min-height:0;overflow-y:auto;padding:0.2rem 0.4rem;scrollbar-width:thin"><div class="content" id="plan-sheet-body"></div></div>
1038
+ <textarea id="plan-sheet-fb" class="slash-inp" rows="2" placeholder="需要改的话写在这里,再点「按意见修改」(可选)" style="margin:0.5rem 0 0.4rem;resize:vertical"></textarea>
1039
+ <button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="approvePlan()">✅ 批准并执行</button>
1040
+ <button class="action-sheet-cancel" style="margin-bottom:0.4rem" onclick="revisePlan()">✏️ 按意见修改方案</button>
1041
+ <button class="action-sheet-cancel" onclick="closePlanSheet()">稍后再看</button>
1042
+ </div>
1043
+ </div>
1044
+ <!-- 退出计划:按方案执行 / 仅退出 -->
1045
+ <div id="planexit-sheet" class="action-sheet">
1046
+ <div class="action-sheet-backdrop" onclick="closePlanExit()"></div>
1047
+ <div class="action-sheet-box">
1048
+ <div class="action-sheet-title">退出计划模式</div>
1049
+ <button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="exitPlan(true)">✅ 退出并按方案执行</button>
1050
+ <button class="action-sheet-cancel" style="margin-bottom:0.4rem" onclick="exitPlan(false)">仅退出计划模式</button>
1051
+ <button class="action-sheet-cancel" onclick="closePlanExit()">取消</button>
1052
+ </div>
1053
+ </div>
1054
+
1055
+ <!-- 清除:先说清楚会发生什么,免得误以为出了故障 -->
1056
+ <div id="clear-sheet" class="action-sheet">
1057
+ <div class="action-sheet-backdrop" onclick="closeClearSheet()"></div>
1058
+ <div class="action-sheet-box">
1059
+ <div class="action-sheet-title">清除对话,从头开始?</div>
1060
+ <div style="font-size:0.82rem;color:var(--muted);line-height:1.65;padding:0 0.4rem 0.7rem">
1061
+ · AI 会<b>忘掉本任务之前聊过的内容</b>,下次提问是全新对话<br>
1062
+ · 界面上的聊天记录一并清空<br>
1063
+ · <b>项目文件、已做的修改都不受影响</b><br>
1064
+ 适合换新话题、或 AI 被旧内容带偏时用。只是嫌对话太长,用「压缩」更合适——会保留要点。
1065
+ </div>
1066
+ <button class="action-sheet-cancel" style="background:var(--red);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="doClear(true)">清除并开始新对话</button>
1067
+ <button class="action-sheet-cancel" style="margin-bottom:0.4rem" onclick="closeClearSheet();doSlashCmd('/compact')">改用压缩(保留要点)</button>
1068
+ <button class="action-sheet-cancel" onclick="closeClearSheet()">取消</button>
1016
1069
  </div>
1017
1070
  </div>
1018
1071
 
@@ -2153,6 +2206,8 @@
2153
2206
  if (block.type === 'thinking' && block.thinking) thinkings.push(block.thinking);
2154
2207
  else if (block.type === 'text' && block.text) texts.push(block.text);
2155
2208
  else if (block.type === 'tool_use') {
2209
+ const planTxt = planTextOf(block.name, block.input);
2210
+ if (planTxt !== null) { addPlanCard(planTxt); continue; }
2156
2211
  // Task 在翻页(fragment)模式下退化为普通工具卡——面板逻辑挂在聊天区,
2157
2212
  // 塞进 fragment 会错位;整本回放仍用面板
2158
2213
  if (block.name === 'Task' && !_replayParent) {
@@ -2499,6 +2554,7 @@
2499
2554
  if (draftBtn) draftBtn.style.display = busy ? '' : 'none';
2500
2555
  // 用 try 包住:这两个绑定声明在文件后段,初始化早期调用会撞 TDZ
2501
2556
  try { if (!busy) _draftTipDismissed = false; renderDraftTip(); } catch {}
2557
+ try { if (!busy) checkGuide(); else renderGuideTip(); } catch {}
2502
2558
  }
2503
2559
  _postStatus(state);
2504
2560
  }
@@ -2658,6 +2714,7 @@
2658
2714
  currentSession = session;
2659
2715
  overlay.classList.add('hidden');
2660
2716
  document.title = session.title + ' — myhi';
2717
+ setTimeout(() => { try { checkGuide(); } catch {} }, 0);
2661
2718
  document.getElementById('session-title').textContent = session.title;
2662
2719
  if (session.cwd) document.getElementById('session-cwd').textContent = session.cwd;
2663
2720
  updateViewers(session.viewers);
@@ -2782,9 +2839,9 @@
2782
2839
  _textRenderedThisTurn = true;
2783
2840
  }
2784
2841
  } else if (block.type === 'tool_use') {
2785
- if (block.name === 'AskUserQuestion' && block.input) {
2786
- openAskSheet(block.input);
2787
- }
2842
+ if (block.name === 'AskUserQuestion' && block.input) legacyAsk(block.id, block.input);
2843
+ const planTxt = planTextOf(block.name, block.input);
2844
+ if (planTxt !== null) { addPlanCard(planTxt); continue; }
2788
2845
  if (block.name === 'Task') {
2789
2846
  subaCreatePanel(block.id, subagentToolInfo(block.input || {}));
2790
2847
  } else {
@@ -2858,9 +2915,8 @@
2858
2915
  break;
2859
2916
  case 'tool_use':
2860
2917
  // 独立工具调用消息
2861
- if (msg.name === 'AskUserQuestion' && msg.input) {
2862
- openAskSheet(msg.input);
2863
- }
2918
+ if (msg.name === 'AskUserQuestion' && msg.input) legacyAsk(msg.id, msg.input);
2919
+ if (planTextOf(msg.name, msg.input) !== null) { addPlanCard(planTextOf(msg.name, msg.input)); break; }
2864
2920
  if (msg.name === 'Task') {
2865
2921
  subaCreatePanel(msg.id, subagentToolInfo(msg.input || {}));
2866
2922
  } else {
@@ -3231,6 +3287,7 @@
3231
3287
 
3232
3288
  function sendCommand() {
3233
3289
  const text = cmdInput.value.trim();
3290
+ if (GUIDE_REMEMBER_RE.test(text)) _guideRemember = true; // 「以后都… / 记住…」:这一轮干完建议写进项目说明
3234
3291
  if (!text && !_pendingImage && !_pendingFile) return;
3235
3292
 
3236
3293
  // AI 还在跑这一轮(可能正开着子代理,整轮 result 未到,服务端 isBusy 会拒收):
@@ -3565,22 +3622,56 @@
3565
3622
 
3566
3623
  // ── 设置面板 ──────────────────────────────────
3567
3624
  // ── 会话内使用说明(精简版;完整版在首页 ❓ 帮助)──────────────
3625
+ // 快捷栏按钮说明:帮助面板和鼠标悬停提示共用这一份,加按钮时在这里补一句
3626
+ const SK_HELP = {
3627
+ draft: ['📥 草稿', 'AI 忙时先把想法存起来,干完再发;勾「自动接力」会在这一轮结束后自动发出。'],
3628
+ folder: ['📂 文件夹', '在这台电脑上用资源管理器打开本任务目录(后台/服务模式下不可用)。'],
3629
+ model: ['模型', '切换 AI 模型。复杂任务用强模型,简单活换快模型更省;被安全检测误拦时也可换个模型再试。'],
3630
+ plan: ['计划', '先让 AI 只调研、出方案、不改文件;你审批后再执行。再点一次「退出计划」可按方案执行或仅退出。'],
3631
+ compact: ['压缩', '对话太长时把前面内容压成摘要,AI 保留要点、继续干活,上下文水位下降。不会丢文件。'],
3632
+ resume: ['恢复', '翻看本任务以前的对话,选一段接着聊。'],
3633
+ clear: ['清除', '让 AI 忘掉之前聊过的内容、从头开始新对话,聊天记录也清空;项目文件不受影响。只嫌太长用「压缩」。'],
3634
+ secrets: ['🔑 密钥', '保存 token / 密码等,命令里用 $名字 引用,值不会进聊天和模型。新增后清除或重开会话生效。'],
3635
+ guard: ['🛡 护栏', '发给 AI 前把姓名、证件号、手机号等换成代号,回来再还原;映射只留在本机。'],
3636
+ followup: ['🔄 跟进', '长任务交出去后,AI 每说完一轮,隔一段时间自动替你问进度、让它接着干;判断全部完成就停(最多 5 次,每次都算一轮模型调用)。'],
3637
+ git: ['提交', '查看改动并 git 提交 / 推送。'],
3638
+ memory: ['记忆', '查看、编辑「项目说明」和 AI 的长期记忆,每次新对话都会带上;长期项目可一键让 AI 生成项目说明。'],
3639
+ skills: ['技能', '管理常用指令模板(/命令),一点就发;可自建。'],
3640
+ cost: ['费用', '查看本次会话的 token 用量与花费。'],
3641
+ };
3642
+ document.querySelectorAll('.sk[data-sk]').forEach(b => { const h = SK_HELP[b.dataset.sk]; if (h && !b.title) b.title = h[1]; });
3643
+
3568
3644
  window.openChatHelp = function() {
3569
3645
  overlay.innerHTML = '';
3570
3646
  overlay.classList.remove('hidden');
3571
3647
  const wrap = document.createElement('div');
3572
3648
  wrap.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:1rem;background:rgba(0,0,0,0.55)';
3573
3649
  const box = document.createElement('div');
3574
- box.style.cssText = 'max-width:520px;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:1.1rem;font-size:0.82rem;line-height:1.75;color:var(--text);max-height:80vh;overflow-y:auto';
3650
+ box.style.cssText = 'max-width:560px;width:100%;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:1.1rem;font-size:0.82rem;line-height:1.75;color:var(--text);max-height:85vh;overflow-y:auto';
3651
+ const sec = (t) => `<div style="font-size:0.86rem;font-weight:700;margin:0.8rem 0 0.3rem">${t}</div>`;
3652
+ const row = (k, v) => `<div style="display:flex;gap:0.6rem;padding:0.28rem 0;border-top:1px solid var(--border)"><span style="flex:0 0 5.2em;font-weight:600">${k}</span><span style="flex:1;color:var(--text2,var(--text))">${v}</span></div>`;
3653
+ const shown = new Set([...document.querySelectorAll('#sk-claude-agent .sk[data-sk]')].map(b => b.dataset.sk));
3654
+ const skRows = Object.entries(SK_HELP).filter(([k]) => currentSession?.mode !== 'agent' || shown.has(k)).map(([, h]) => row(h[0], h[1])).join('');
3575
3655
  box.innerHTML = `
3576
- <div style="font-size:1rem;font-weight:700;margin-bottom:0.5rem">💬 会话怎么用</div>
3656
+ <div style="font-size:1rem;font-weight:700;margin-bottom:0.3rem">💬 会话怎么用</div>
3577
3657
  <b>发消息</b>:像聊天一样把需求说清楚即可;触屏回车=换行,用发送键发送。<br>
3578
3658
  <b>让它读文件</b>:打 <b>@</b> 从本项目逐层浏览、打字搜索;项目外打 <b>@D:</b>、<b>@/</b>、<b>@~/</b> 按路径逐层浏览;选中插路径;或点「文件」面板的 <b>@</b> 引用。<br>
3579
3659
  <b>工具/思考</b>:AI 干活的过程默认折叠,点「🔧 N 个工具调用」展开看命令与输出。<br>
3580
3660
  <b>AI 忙时</b>:输入框变「存草稿」,先记着干完取回;勾自动接力会自动发出。<br>
3581
- <b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。<br>
3582
- <b>常用按钮</b>:模型/计划/压缩/恢复/清除/密钥/护栏/提交——普通用户可在设置→快捷栏精简。<br>
3583
- <span style="color:var(--muted)">更多说明见首页顶栏 ❓ 帮助。</span>`;
3661
+ <b>AI 提问 / 交方案</b>:AI 需要你拿主意时会弹出选项,选一个即可,也可「跳过,让 AI 自己决定」;收起后输入框上方「❓ AI 在等你回答」可再打开。<br>
3662
+ <b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。
3663
+ ${sec('顶栏')}
3664
+ ${row('▦ 分屏', '电脑上多个任务并排看、并排干。')}
3665
+ ${row('?', '就是这份说明。')}
3666
+ ${row('⚙ 设置', '聊天显示(思考 / 工具输出是否展开)、快捷栏显示哪些按钮、本任务的选项。')}
3667
+ ${row('◈ 水位', 'AI 的「记忆容量」用了多少。快满时 AI 容易忘事,点「压缩」腾空间。')}
3668
+ ${row('文件', '浏览、编辑、上传、下载本任务目录里的文件。')}
3669
+ ${row('切换', '跳到其他任务。')}
3670
+ ${row('›', '返回首页。')}
3671
+ ${row('📘', '状态栏出现「已带项目说明」表示 AI 每次会先读本项目的项目说明;变黄「偏长」时建议精简。点它查看 / 编辑。')}
3672
+ ${sec('快捷栏(输入框下方)')}
3673
+ ${skRows}
3674
+ <div style="color:var(--muted);margin-top:0.5rem">按钮太多?到 ⚙ 设置 → 快捷栏 关掉用不到的。鼠标停在按钮上也能看到说明。更多见首页 ❓ 帮助。</div>`;
3584
3675
  const closeBtn = document.createElement('button');
3585
3676
  closeBtn.textContent = '知道了';
3586
3677
  closeBtn.style.cssText = 'display:block;margin:0.8rem auto 0;background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.5rem 1.4rem;font-size:0.85rem;cursor:pointer';
@@ -4443,10 +4534,7 @@
4443
4534
  askMultiState.answers[qIdx] = { question: qText, label };
4444
4535
  refreshAskMultiSubmit();
4445
4536
  } else {
4446
- _clearAsk();
4447
- addInputMessage(label);
4448
- socket.emit('agent:query', { prompt: label });
4449
- showThinking();
4537
+ answerAsk({ [qText]: label }, label);
4450
4538
  }
4451
4539
  };
4452
4540
  block.appendChild(item);
@@ -4456,6 +4544,8 @@
4456
4544
  });
4457
4545
 
4458
4546
  if (isMulti) refreshAskMultiSubmit();
4547
+ document.getElementById('ask-sheet-skip').style.display = _askCtlId ? '' : 'none';
4548
+ document.getElementById('ask-sheet-cancel').textContent = _askCtlId ? '稍后回答' : '取消';
4459
4549
  sheet.classList.add('open');
4460
4550
  if (!isMulti) inp.focus();
4461
4551
  }
@@ -4483,21 +4573,125 @@
4483
4573
  const answers = askMultiState.answers.slice(0, n);
4484
4574
  if (answers.some((a) => !a)) return;
4485
4575
  const reply = answers.map((a, i) => 'Q' + (i + 1) + ': ' + a.question + '\nA: ' + a.label).join('\n\n');
4576
+ const map = {}; answers.forEach((a) => { map[a.question] = a.label; });
4577
+ answerAsk(map, reply);
4578
+ };
4579
+ // 交互通道下「取消」只是先收起,问题还挂着(输入框上方的提示条可再打开);旧模式照旧清掉
4580
+ window.closeAskSheet = function() {
4581
+ if (_askCtlId) { document.getElementById('ask-sheet').classList.remove('open'); renderPendingBar(); return; }
4486
4582
  _clearAsk();
4487
- addInputMessage(reply);
4488
- socket.emit('agent:query', { prompt: reply });
4489
- showThinking();
4490
4583
  };
4491
- window.closeAskSheet = function() { _clearAsk(); };
4492
4584
  window.submitAskReply = function() {
4493
4585
  const inp = document.getElementById('ask-sheet-input');
4494
4586
  const text = inp.value.trim();
4495
4587
  if (!text) return;
4588
+ answerAsk({ [_askQuestions[0]?.question || '']: text }, text);
4589
+ };
4590
+
4591
+ // ── 交互通道(1.3.193):AI 的提问 / 方案审批由 claude 发起、等 myhi 回答 ──
4592
+ // 新 claude:服务端推 agent:interaction(带 id),答案用 agent:respond 交回 claude 正在等待的那个调用,
4593
+ // 模型拿到的是「对自己问题的回答」,上下文连贯;刷新 / 换设备时服务端会补发,先答先算。
4594
+ // 老 claude(没有交互通道):沿用旧做法——看到 tool_use 就弹窗,答案当新消息发出去。
4595
+ let _askCtlId = null; // 当前问答弹窗对应的交互 id(null = 旧模式)
4596
+ let _planCtl = null; // 待审批的方案 { id, plan }
4597
+ const _ctlToolUseIds = new Set(); // 已由交互通道接管的 tool_use,旧路径别再弹一次
4598
+ function ensureControl() { if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID }); }
4599
+ function answerAsk(map, display) {
4600
+ if (_askCtlId) {
4601
+ ensureControl();
4602
+ socket.emit('agent:respond', { id: _askCtlId, decision: { answers: map } });
4603
+ _askCtlId = null; _clearAsk(); renderPendingBar(); showThinking();
4604
+ return;
4605
+ }
4496
4606
  _clearAsk();
4497
- addInputMessage(text);
4498
- socket.emit('agent:query', { prompt: text });
4607
+ addInputMessage(display);
4608
+ socket.emit('agent:query', { prompt: display });
4499
4609
  showThinking();
4610
+ }
4611
+ window.skipAsk = function() {
4612
+ if (!_askCtlId) return _clearAsk();
4613
+ ensureControl();
4614
+ socket.emit('agent:respond', { id: _askCtlId, decision: { skip: true } });
4615
+ _askCtlId = null; _clearAsk(); renderPendingBar();
4616
+ };
4617
+ // 旧路径:tool_use 一出现就弹。交互通道开着时先等一下,服务端的正式请求到了就以它为准
4618
+ function legacyAsk(toolUseId, input) {
4619
+ if (!currentSession?.interactive) return openAskSheet(input);
4620
+ setTimeout(() => { if (!_ctlToolUseIds.has(toolUseId) && !_askCtlId) openAskSheet(input); }, 1500);
4621
+ }
4622
+ socket.on('agent:interaction', (v) => {
4623
+ if (!v?.id) return;
4624
+ if (v.toolUseId) _ctlToolUseIds.add(v.toolUseId);
4625
+ if (v.kind === 'ask') {
4626
+ if (_askCtlId === v.id) return; // 重连补发的同一题
4627
+ _clearAsk(); _askCtlId = v.id;
4628
+ openAskSheet({ questions: v.questions || [] });
4629
+ } else if (v.kind === 'plan') {
4630
+ if (_planCtl?.id === v.id) return;
4631
+ _planCtl = { id: v.id, plan: v.plan || '' };
4632
+ openPlanSheet();
4633
+ _postStatus('waiting');
4634
+ }
4635
+ renderPendingBar();
4636
+ });
4637
+ socket.on('agent:interaction-closed', ({ id } = {}) => {
4638
+ if (_askCtlId === id) { _askCtlId = null; _clearAsk(); }
4639
+ if (_planCtl?.id === id) { _planCtl = null; closePlanSheet(); }
4640
+ renderPendingBar();
4641
+ });
4642
+ // 输入框上方的「AI 在等你」提示条:弹窗被收起后从这里再打开
4643
+ function renderPendingBar() {
4644
+ const bar = document.getElementById('pending-bar');
4645
+ if (!bar) return;
4646
+ const txt = _planCtl ? '📋 AI 提交了方案,等你审批' : _askCtlId ? '❓ AI 在等你回答一个问题' : '';
4647
+ bar.textContent = txt ? txt + ' · 点这里查看' : '';
4648
+ bar.style.display = txt ? '' : 'none';
4649
+ }
4650
+ window.reopenPending = function() {
4651
+ if (_planCtl) return openPlanSheet();
4652
+ if (_askCtlId) document.getElementById('ask-sheet').classList.add('open');
4653
+ };
4654
+ // ── 方案审批 ──
4655
+ window.openPlanSheet = function() {
4656
+ if (!_planCtl) return;
4657
+ document.getElementById('plan-sheet-body').innerHTML = renderMarkdown(_planCtl.plan || '(方案为空)');
4658
+ document.getElementById('plan-sheet').classList.add('open');
4500
4659
  };
4660
+ window.closePlanSheet = function() { document.getElementById('plan-sheet').classList.remove('open'); renderPendingBar(); };
4661
+ window.approvePlan = function() {
4662
+ if (!_planCtl) return;
4663
+ ensureControl();
4664
+ socket.emit('agent:respond', { id: _planCtl.id, decision: { approve: true } });
4665
+ _planCtl = null; closePlanSheet(); showThinking();
4666
+ };
4667
+ window.revisePlan = function() {
4668
+ if (!_planCtl) return;
4669
+ const fb = document.getElementById('plan-sheet-fb').value.trim();
4670
+ if (!fb) { document.getElementById('plan-sheet-fb').focus(); document.getElementById('plan-sheet-fb').placeholder = '先写下要怎么改,再点这个按钮'; return; }
4671
+ ensureControl();
4672
+ socket.emit('agent:respond', { id: _planCtl.id, decision: { approve: false, feedback: fb } });
4673
+ document.getElementById('plan-sheet-fb').value = '';
4674
+ _planCtl = null; closePlanSheet(); showThinking();
4675
+ };
4676
+ // ── 聊天里的方案卡片:ExitPlanMode 提交的方案、写进 ~/.claude/plans/*.md 的方案文件(原来只是个折叠的「写文件」,看不到内容)──
4677
+ function planTextOf(name, input) {
4678
+ if (!input) return null;
4679
+ if (name === 'ExitPlanMode') return String(input.plan || '');
4680
+ if (name === 'Write' && /[\\/]\.claude[\\/]plans[\\/][^\\/]+\.md$/.test(input.file_path || '')) return String(input.content || '');
4681
+ return null;
4682
+ }
4683
+ let _lastPlanText = '';
4684
+ function addPlanCard(text) {
4685
+ const t = String(text || '').trim();
4686
+ if (!t || t === _lastPlanText) return; // 先写方案文件、再 ExitPlanMode 提交同一份:只显示一次
4687
+ _lastPlanText = t;
4688
+ const el = document.createElement('div');
4689
+ el.className = 'msg msg-assistant msg-plan';
4690
+ el.innerHTML = '<div class="plan-head">📋 方案</div><div class="content"></div>';
4691
+ el.querySelector('.content').innerHTML = renderMarkdown(t);
4692
+ (_replayParent || chatArea).appendChild(el);
4693
+ if (!_replayParent) { trimMessages(); scrollToBottom(); }
4694
+ }
4501
4695
  document.getElementById('ask-sheet-input').addEventListener('keydown', (e) => {
4502
4696
  if (e.key === 'Enter') { e.preventDefault(); submitAskReply(); }
4503
4697
  });
@@ -4609,15 +4803,22 @@
4609
4803
  };
4610
4804
 
4611
4805
  // ── 清除会话 ──────────────────────────────────
4612
- window.doClear = function() {
4806
+ window.closeClearSheet = function() { document.getElementById('clear-sheet').classList.remove('open'); };
4807
+ window.doClear = function(confirmed) {
4808
+ // Agent 模式先弹说明确认:清除不可撤销,且用户常不清楚它会让 AI 忘掉上下文
4809
+ if (currentSession?.mode === 'agent' && !confirmed) {
4810
+ document.getElementById('clear-sheet').classList.add('open');
4811
+ return;
4812
+ }
4813
+ closeClearSheet();
4814
+ if (workState !== 'idle' && !confirm('AI 还在执行,清除会中断这一轮。确定清除?')) return;
4613
4815
  // 清除聊天区 UI
4614
4816
  chatArea.innerHTML = '';
4615
4817
  endStream();
4616
4818
  // Agent 模式:发送 /clear 清除 Claude 上下文
4617
4819
  if (currentSession?.mode === 'agent') {
4618
4820
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
4619
- socket.emit('agent:query', { prompt: '/clear' });
4620
- addStatusMessage('已清除对话和 Claude 上下文');
4821
+ socket.emit('agent:query', { prompt: '/clear' }); // 服务端回一条「已开始新对话」,所有在看的人都能看到
4621
4822
  _ctxNudgeLevel = 0;
4622
4823
  _lastCtxTokens = 0;
4623
4824
  ctxUpdate(0); // 上下文已清空,水位指示复位
@@ -4793,6 +4994,55 @@
4793
4994
  `本任务有 ${n} 条待发草稿 —— 点这里取回`;
4794
4995
  tip.style.display = 'flex';
4795
4996
  }
4997
+ // ── 项目说明(CLAUDE.md):状态标记 + 适时建议生成 ─────────────
4998
+ // 新用户不知道 CLAUDE.md,等看出是长期项目时再提:同目录还有别的任务 / 本任务聊了很多轮 / 说了「以后都…」。
4999
+ // 每个目录可「不再提示」(存本机);× 只关本页。
5000
+ const GUIDE_REMEMBER_RE = /以后(都|每次|一律)|每次都(要|得|先)|记住|总是要|别再忘|下次(也|还)要/;
5001
+ const GUIDE_TURNS = 15;
5002
+ let _guide = null, _guideRemember = false, _guideTipClosed = false;
5003
+ const guideOffKey = 'myhi_guide_tip_off';
5004
+ const guideOffList = () => { try { return JSON.parse(localStorage.getItem(guideOffKey) || '[]'); } catch { return []; } };
5005
+ async function checkGuide() {
5006
+ if (currentSession?.mode !== 'agent') return;
5007
+ try { const r = await fetch('/api/session/' + SESSION_ID + '/guide'); if (r.ok) _guide = await r.json(); } catch {}
5008
+ renderGuideChip(); renderGuideTip();
5009
+ }
5010
+ function renderGuideChip() {
5011
+ const c = document.getElementById('guide-chip');
5012
+ if (!c) return;
5013
+ if (!_guide?.exists) { c.style.display = 'none'; return; }
5014
+ c.style.display = '';
5015
+ c.style.color = _guide.long ? 'var(--yellow)' : 'var(--muted)';
5016
+ c.textContent = _guide.long ? '📘 项目说明偏长' : '📘 已带项目说明';
5017
+ c.title = _guide.long
5018
+ ? `项目说明(CLAUDE.md)有 ${_guide.lines} 行,每轮都会占用 AI 的记忆容量。建议精简到要点(可以让 AI 帮你精简)。点击查看`
5019
+ : `AI 每次开始工作前会先读本项目的项目说明(CLAUDE.md,${_guide.lines} 行)。点击查看 / 编辑`;
5020
+ }
5021
+ function guideTipReason() {
5022
+ if (!_guide || _guide.exists) return '';
5023
+ if (_guideRemember) return '想让 AI 以后每次都记得?可以写进「项目说明」,每次新对话 AI 都会先读,不用再重复交代。';
5024
+ if (_guide.siblings > 0) return `这个项目目录还有 ${_guide.siblings} 个其他任务在用。让 AI 把项目情况和你的要求写成「项目说明」,以后每个任务、每次对话都会先读。`;
5025
+ if (_guide.turns >= GUIDE_TURNS) return '这个项目你常来。让 AI 把项目情况和你的要求写成「项目说明」,以后每次新对话不用再重复交代。';
5026
+ return '';
5027
+ }
5028
+ window.renderGuideTip = function() {
5029
+ const tip = document.getElementById('guide-tip');
5030
+ if (!tip) return;
5031
+ const why = guideTipReason();
5032
+ const off = currentSession?.cwd && guideOffList().includes(currentSession.cwd);
5033
+ if (!why || off || _guideTipClosed || workState !== 'idle' || !canTakeControl()) { tip.style.display = 'none'; return; }
5034
+ document.getElementById('guide-tip-text').textContent = why;
5035
+ tip.style.display = 'flex';
5036
+ };
5037
+ window.guideTipGo = function() { _guideTipClosed = true; renderGuideTip(); genProjectGuide(); };
5038
+ window.guideTipClose = function() { _guideTipClosed = true; _guideRemember = false; renderGuideTip(); };
5039
+ window.guideTipOff = function() {
5040
+ const l = guideOffList();
5041
+ if (currentSession?.cwd && !l.includes(currentSession.cwd)) { l.push(currentSession.cwd); localStorage.setItem(guideOffKey, JSON.stringify(l.slice(-200))); }
5042
+ renderGuideTip();
5043
+ addStatusMessage('本项目不再提示生成项目说明(需要时在快捷栏「记忆」里生成)');
5044
+ };
5045
+
4796
5046
  window.hideDraftTip = function() { _draftTipDismissed = true; renderDraftTip(); };
4797
5047
 
4798
5048
  window.openInboxSheet = async function() {
@@ -5215,16 +5465,28 @@
5215
5465
  const isPlan = currentSession?.permissionMode === 'plan';
5216
5466
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
5217
5467
  if (isPlan) {
5218
- // 直接切回 bypassPermissions,不走 /plan 命令(因为那要等 agent 响应)
5219
- socket.emit('set-mode', { sessionId: SESSION_ID, mode: 'bypassPermissions' });
5220
- if (currentSession) currentSession.permissionMode = 'bypassPermissions';
5221
- updatePlanButton();
5222
- addStatusMessage('已退出计划模式,恢复到自动执行');
5468
+ // AI 已经提交了方案在等审批:直接打开审批(批准即在原进程里切到执行,不打断)
5469
+ if (_planCtl) return openPlanSheet();
5470
+ document.getElementById('planexit-sheet').classList.add('open');
5223
5471
  } else {
5224
5472
  // 进入计划:弹窗输入计划描述
5225
5473
  doSlashCmd('/plan');
5226
5474
  }
5227
5475
  };
5476
+ window.closePlanExit = function() { document.getElementById('planexit-sheet').classList.remove('open'); };
5477
+ // 手动退出计划:切回自动执行会重启 claude 进程——AI 还在规划时先确认,免得这一轮被悄悄打断
5478
+ window.exitPlan = function(run) {
5479
+ closePlanExit();
5480
+ if (workState !== 'idle' && !confirm('AI 还在规划,现在退出会中断这一轮。确定退出?')) return;
5481
+ socket.emit('set-mode', { sessionId: SESSION_ID, mode: 'bypassPermissions' });
5482
+ if (currentSession) currentSession.permissionMode = 'bypassPermissions';
5483
+ updatePlanButton();
5484
+ addStatusMessage('已退出计划模式,恢复到自动执行');
5485
+ if (run) setTimeout(() => {
5486
+ const t = '按刚才的方案开始执行。';
5487
+ addInputMessage(t); socket.emit('agent:query', { prompt: t }); showThinking();
5488
+ }, 600);
5489
+ };
5228
5490
  socket.on('mode-changed', ({ sessionId, mode }) => {
5229
5491
  if (sessionId !== SESSION_ID) return;
5230
5492
  if (currentSession) currentSession.permissionMode = mode;
@@ -5756,10 +6018,22 @@
5756
6018
  container.innerHTML = '';
5757
6019
  container.appendChild(makeBar());
5758
6020
 
6021
+ // 本项目还没有项目说明(CLAUDE.md):讲清它是什么、适合什么项目,给一键生成入口——新用户不必知道这个文件名
6022
+ if (_memScope === 'project' && !files.some(f => f.name === 'CLAUDE.md')) {
6023
+ const intro = document.createElement('div');
6024
+ intro.style.cssText = 'border:1px dashed var(--border);border-radius:10px;padding:0.75rem 0.85rem;margin-bottom:0.6rem;line-height:1.7';
6025
+ intro.innerHTML = '<div style="font-weight:700;color:var(--text);font-size:0.86rem">📘 本项目还没有「项目说明」</div>'
6026
+ + '<div>项目说明是 AI <b>每次开始工作前都会先读</b>的一页项目规矩:项目是做什么的、关键目录、怎么构建测试、你的固定要求(如「回复用中文」「改完先跑测试」)。有了它,每次新对话不用再重复交代。</div>'
6027
+ + '<div style="color:var(--muted);margin-top:0.2rem">适合会反复来做的长期项目;一次性的小任务不需要。</div>'
6028
+ + '<button onclick="genProjectGuide()" style="margin-top:0.55rem;width:100%;padding:0.5rem;border-radius:8px;cursor:pointer;font-size:0.8rem;border:none;background:var(--green);color:#fff;font-weight:600">📘 让 AI 生成项目说明</button>'
6029
+ + '<div style="color:var(--muted);font-size:0.7rem;margin-top:0.3rem">AI 会先看一遍项目、写出草稿给你确认,确认后才保存(文件名 CLAUDE.md,放在项目目录里)。</div>';
6030
+ container.appendChild(intro);
6031
+ }
6032
+
5759
6033
  if (files.length === 0) {
5760
6034
  const e = document.createElement('div');
5761
6035
  e.style.cssText = 'text-align:center;color:var(--muted);padding:1.5rem 0';
5762
- e.textContent = _memScope === 'project' ? '本项目暂无记忆文件' : '暂无记忆文件';
6036
+ e.textContent = _memScope === 'project' ? '还没有其他记忆(AI 在对话中记下的经验会出现在这里)' : '暂无记忆文件';
5763
6037
  container.appendChild(e);
5764
6038
  return;
5765
6039
  }
@@ -5771,7 +6045,9 @@
5771
6045
  const dm = m[1].match(/^description:\s*(.+)$/m);
5772
6046
  return { desc: dm ? dm[1].trim() : '', body: content.slice(m[0].length) };
5773
6047
  };
5774
- const cleanName = (n) => n.replace(/^.*\//, '').replace(/\.md$/i, '');
6048
+ // CLAUDE.md 用大白话称呼(文件名放进描述里给懂行的人看)
6049
+ const NICE = { 'CLAUDE.md': ['📘 项目说明', 'CLAUDE.md · AI 每次开始工作前先读的项目规矩'], '~/.claude/CLAUDE.md': ['🌐 全局说明', '~/.claude/CLAUDE.md · 对所有项目都生效'] };
6050
+ const cleanName = (n) => NICE[n] ? NICE[n][0] : n.replace(/^.*\//, '').replace(/\.md$/i, '');
5775
6051
 
5776
6052
  // 搜索框
5777
6053
  const search = document.createElement('input');
@@ -5789,7 +6065,7 @@
5789
6065
  for (const f of files) {
5790
6066
  if (kw && !(f.name.toLowerCase().includes(kw) || (f.content || '').toLowerCase().includes(kw))) continue;
5791
6067
  shown++;
5792
- const { desc, body: md } = parseFM(f.content || '');
6068
+ const fm = parseFM(f.content || ''); const md = fm.body; const desc = fm.desc || (NICE[f.name] ? NICE[f.name][1] : '');
5793
6069
  const deletable = f.name.startsWith('memory/') || f.name.startsWith('~/.claude/memory/');
5794
6070
  const editable = deletable || f.name.endsWith('CLAUDE.md') || f.name.endsWith('settings.json');
5795
6071
  const card = document.createElement('div');
@@ -5837,6 +6113,26 @@
5837
6113
  };
5838
6114
 
5839
6115
  // 让 AI 按规则过一遍记忆(只分析给建议,不改文件)
6116
+ // 一键生成项目说明(CLAUDE.md):AI 先调研、出草稿,用户确认后才写文件——项目说明会改变 AI 之后的行为,不能静默写入
6117
+ window.genProjectGuide = function() {
6118
+ closeMemorySheet();
6119
+ if (currentSession?.mode !== 'agent') return addStatusMessage('该功能仅在 agent(Claude) 会话可用');
6120
+ if (workState !== 'idle') return addStatusMessage('AI 正在干活,等这一轮结束再生成项目说明');
6121
+ const prompt = [
6122
+ '请为当前项目目录写一份项目说明(项目根目录的 CLAUDE.md,以后每次对话开始你都会先读它)。',
6123
+ '步骤:1) 先快速了解项目:目录结构、README、构建/依赖配置(如 package.json、pyproject、Makefile 等),不用逐个细读源码;',
6124
+ '2) 结合本次对话里我交代过的要求和习惯;',
6125
+ '3) 用中文写一份精简草稿(建议 30~60 行,只写要点):项目是做什么的、关键目录/文件、怎么运行/构建/测试、约定与注意事项、我的固定要求;',
6126
+ '不要写任何密码、token、密钥等敏感值。',
6127
+ '4) 先把完整草稿贴出来让我确认,问我是否保存(可以直接保存 / 我要修改);我确认后再写入 CLAUDE.md。在我确认前不要创建或修改任何文件。',
6128
+ '如果项目根目录已经有 CLAUDE.md,就在它基础上补充,并说明改了哪些。',
6129
+ ].join('');
6130
+ if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
6131
+ addInputMessage('📘 让 AI 生成项目说明');
6132
+ socket.emit('agent:query', { prompt });
6133
+ showThinking();
6134
+ };
6135
+
5840
6136
  window.analyzeMemory = function() {
5841
6137
  const scopeTxt = _memScope === 'all' ? '所有项目 + 全局' : '本项目';
5842
6138
  const prompt = [