@wendongfly/myhi 1.3.192 → 1.3.194

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); }
@@ -291,6 +294,12 @@
291
294
  @media (pointer: coarse) { #cmd-input { font-size: 1rem; padding: 0.75rem 0.75rem 0.35rem; line-height: 1.55; } }
292
295
  #input-toolbar { display: flex; align-items: center; padding: 0.25rem 0.4rem 0.4rem; gap: 0.15rem; }
293
296
  #input-toolbar .spacer { flex: 1; }
297
+ .inj-btn { flex-shrink: 0; height: 30px; border: 1px solid var(--yellow); background: rgba(210,153,34,0.12); color: var(--yellow); border-radius: 8px; padding: 0 0.6rem; font-size: 0.76rem; font-weight: 600; cursor: pointer; white-space: nowrap; align-self: center; }
298
+ .inj-btn:hover { background: rgba(210,153,34,0.22); }
299
+ .inj-tag { font-size: 0.66rem; margin-right: 0.35rem; color: var(--yellow); }
300
+ .inj-tag.seen { color: var(--green); }
301
+ .inj-tag.dropped { color: var(--red); }
302
+ .msg-inject .bubble { border: 1px dashed var(--yellow); }
294
303
  .tb-btn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent; color: var(--muted); cursor: pointer; transition: all 0.15s; flex-shrink: 0; }
295
304
  .tb-btn:hover { background: var(--surface2); color: var(--text); }
296
305
  .tb-btn:active { transform: scale(0.92); }
@@ -445,6 +454,52 @@
445
454
  .dr-btn.take { background: var(--green); color: #fff; font-weight: 600; }
446
455
  .dr-btn:active { opacity: 0.75; }
447
456
  .dr-src { color: var(--blue2); }
457
+ /* 草稿面板(队列) */
458
+ .ib-box { max-width: 640px !important; max-height: min(86vh, 760px); min-height: min(420px, 80vh); display: flex; flex-direction: column; padding-top: 0.6rem !important; }
459
+ @media (min-width: 769px) { #inbox-sheet.open { align-items: center; } #inbox-sheet .ib-box { border-radius: 14px; } }
460
+ .ib-head { display: flex; align-items: center; gap: 0.6rem; padding: 0.1rem 0.1rem 0.55rem; border-bottom: 1px solid var(--surface2); }
461
+ .ib-head > b { font-size: 0.92rem; white-space: nowrap; }
462
+ .ib-tabs { display: flex; gap: 2px; background: var(--surface2); border-radius: 8px; padding: 2px; }
463
+ .ib-tabs button { border: none; background: none; color: var(--muted); font-size: 0.74rem; padding: 0.25rem 0.65rem; border-radius: 6px; cursor: pointer; white-space: nowrap; }
464
+ .ib-tabs button.on { background: var(--surface); color: var(--text); font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,0.12); }
465
+ .ib-x { margin-left: auto; background: none; border: none; color: var(--muted); font-size: 1rem; cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 6px; }
466
+ .ib-x:hover { background: var(--surface2); color: var(--text); }
467
+ .ib-pane { flex: 1; min-height: 0; display: flex; flex-direction: column; }
468
+ .ib-bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.1rem 0.35rem; flex-wrap: wrap; }
469
+ .ib-hint { flex: 1; min-width: 8em; font-size: 0.68rem; color: var(--muted); line-height: 1.45; }
470
+ .ib-autoall { display: flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; color: var(--text2); cursor: pointer; white-space: nowrap; }
471
+ .ib-autoall input { width: 0.95rem; height: 0.95rem; }
472
+ .ib-paused { display: flex; align-items: center; gap: 0.5rem; font-size: 0.7rem; color: var(--yellow); background: rgba(210,153,34,0.10); border-radius: 8px; padding: 0.35rem 0.55rem; margin-bottom: 0.3rem; }
473
+ .ib-paused span { flex: 1; }
474
+ .ib-list { flex: 1; min-height: 3rem; overflow-y: auto; scrollbar-width: thin; }
475
+ .ib-item { display: flex; gap: 0.45rem; align-items: flex-start; padding: 0.5rem 0.3rem; border-bottom: 1px solid var(--surface2); border-radius: 6px; }
476
+ .ib-item.sel { background: rgba(46,160,67,0.08); }
477
+ .ib-item.drag-over-top { box-shadow: inset 0 2px 0 var(--green); }
478
+ .ib-item.drag-over-bottom { box-shadow: inset 0 -2px 0 var(--green); }
479
+ .ib-item.dragging { opacity: 0.4; }
480
+ .ib-handle { cursor: grab; color: var(--dim, var(--muted)); font-size: 0.8rem; padding-top: 0.1rem; user-select: none; letter-spacing: -2px; }
481
+ @media (hover: none) { .ib-handle { display: none; } }
482
+ .ib-no { flex-shrink: 0; min-width: 1.35rem; height: 1.35rem; border-radius: 50%; background: var(--surface2); color: var(--text2); font-size: 0.68rem; font-weight: 700; display: flex; align-items: center; justify-content: center; }
483
+ .ib-no.auto { background: var(--green); color: #fff; }
484
+ .ib-item input[type=checkbox] { width: 1.05rem; height: 1.05rem; margin-top: 0.15rem; flex-shrink: 0; }
485
+ .ib-main { flex: 1; min-width: 0; }
486
+ .ib-text { color: var(--text); font-size: 0.82rem; line-height: 1.5; word-break: break-word; white-space: pre-wrap; cursor: text; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; border-radius: 4px; }
487
+ .ib-text:hover { background: var(--surface2); }
488
+ .ib-edit textarea { width: 100%; box-sizing: border-box; min-height: 5.5em; background: var(--bg); color: var(--text); border: 1px solid var(--green); border-radius: 7px; padding: 0.4rem 0.5rem; font: inherit; font-size: 0.82rem; resize: vertical; outline: none; }
489
+ .ib-edit div { display: flex; gap: 0.4rem; justify-content: flex-end; margin-top: 0.3rem; align-items: center; }
490
+ .ib-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 0.3rem 0.5rem; margin-top: 0.3rem; font-size: 0.66rem; color: var(--muted); }
491
+ .ib-chip { border: 1px solid var(--border); background: none; color: var(--muted); border-radius: 10px; padding: 0.05rem 0.45rem; font-size: 0.64rem; cursor: pointer; }
492
+ .ib-chip.on { border-color: var(--green); color: var(--green); font-weight: 600; }
493
+ .ib-acts { display: flex; gap: 0.25rem; margin-left: auto; }
494
+ .ib-ic { border: none; background: var(--surface2); color: var(--text2); border-radius: 6px; font-size: 0.7rem; padding: 0.22rem 0.45rem; cursor: pointer; }
495
+ .ib-ic:disabled { opacity: 0.35; cursor: default; }
496
+ .ib-selbar { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; padding: 0.45rem 0.2rem; border-top: 1px solid var(--surface2); font-size: 0.74rem; }
497
+ .ib-selbar span { margin-right: auto; color: var(--text2); }
498
+ .ib-compose { border-top: 1px solid var(--surface2); padding: 0.5rem 0.1rem 0; }
499
+ .ib-compose textarea { width: 100%; box-sizing: border-box; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem 0.55rem; font-size: 0.82rem; resize: vertical; min-height: 2.6em; }
500
+ .ib-compose-row { display: flex; align-items: center; gap: 0.55rem; margin-top: 0.4rem; flex-wrap: wrap; }
501
+ .ib-link { color: var(--blue2); cursor: pointer; font-size: 0.7rem; }
502
+ .ib-grp { font-size: 0.7rem; color: var(--blue2); font-weight: 600; padding: 0.55rem 0.2rem 0.15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
448
503
  .dr-sess { font-size: 0.68rem; color: var(--blue2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
449
504
  .mem-file { margin-bottom: 0.5rem; border: 1px solid var(--surface2); border-radius: 8px; overflow: hidden; }
450
505
  .mem-file-header { display: flex; align-items: center; gap: 0.35rem; padding: 0.4rem 0.6rem; background: var(--surface2); cursor: pointer; font-size: 0.75rem; color: var(--blue); user-select: none; }
@@ -485,6 +540,44 @@
485
540
  #qr-box .close-btn { margin-top: 1rem; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 1.5rem; cursor: pointer; font-size: 0.9rem; }
486
541
  #status-overlay { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(13,17,23,0.85); color: var(--muted); font-size: 0.9rem; z-index: 50; }
487
542
  #status-overlay.hidden { display: none; }
543
+
544
+ /* ── 💡 问一问:选中文字浮出的小工具条 + 旁边的问答小窗(不进当前会话) ── */
545
+ #ask-pop { position: fixed; z-index: 60; display: none; gap: 2px; padding: 3px; background: var(--surface); border: 1px solid var(--border2, var(--border)); border-radius: 9px; box-shadow: 0 6px 20px rgba(0,0,0,0.25); }
546
+ #ask-pop.show { display: flex; }
547
+ #ask-pop button { background: none; border: none; color: var(--text); font-size: 0.78rem; padding: 0.35rem 0.6rem; border-radius: 6px; cursor: pointer; white-space: nowrap; }
548
+ #ask-pop button:hover { background: var(--surface2); }
549
+ #ask-pop button.pri { color: var(--green); font-weight: 600; }
550
+ #ask-backdrop { display: none; position: fixed; inset: 0; z-index: 69; background: rgba(0,0,0,0.4); }
551
+ #ask-panel { position: fixed; z-index: 70; top: 0; right: 0; bottom: 0; width: min(420px, 100vw); background: var(--surface); border-left: 1px solid var(--border); box-shadow: -8px 0 30px rgba(0,0,0,0.18);
552
+ display: flex; flex-direction: column; transform: translateX(105%); transition: transform 0.2s ease; }
553
+ #ask-panel.open { transform: none; }
554
+ .ask-head { display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.8rem 0.9rem 0.6rem; border-bottom: 1px solid var(--border); }
555
+ .ask-head b { font-size: 0.95rem; }
556
+ .ask-sub { font-size: 0.7rem; color: var(--muted); margin-top: 0.15rem; }
557
+ .ask-x { margin-left: auto; background: none; border: none; color: var(--muted); font-size: 1rem; cursor: pointer; padding: 0.1rem 0.35rem; border-radius: 6px; }
558
+ .ask-x:hover { background: var(--surface2); color: var(--text); }
559
+ #ask-body { flex: 1; overflow-y: auto; padding: 0.7rem 0.9rem; font-size: 0.84rem; line-height: 1.7; }
560
+ .ask-quote { border-left: 3px solid var(--green); background: var(--bg); padding: 0.4rem 0.6rem; border-radius: 0 6px 6px 0; color: var(--text2, var(--text)); font-size: 0.78rem; white-space: pre-wrap; word-break: break-word; max-height: 8.5em; overflow-y: auto; margin-bottom: 0.7rem; }
561
+ .ask-q { margin: 0.7rem 0 0.3rem; text-align: right; }
562
+ .ask-q span { display: inline-block; background: var(--surface2); border-radius: 10px; padding: 0.3rem 0.65rem; font-size: 0.8rem; text-align: left; white-space: pre-wrap; }
563
+ .ask-a { padding: 0.1rem 0; }
564
+ .ask-a p { margin: 0.3rem 0; } .ask-a ul, .ask-a ol { margin: 0.3rem 0; padding-left: 1.2rem; }
565
+ .ask-a h1, .ask-a h2, .ask-a h3 { font-size: 0.88rem; margin: 0.5rem 0 0.3rem; }
566
+ .ask-a code { background: var(--surface2); padding: 0.05rem 0.3rem; border-radius: 4px; font-size: 0.78rem; font-family: 'SF Mono','Consolas',monospace; }
567
+ .ask-a pre { background: var(--bg); border: 1px solid var(--border); padding: 0.5rem 0.6rem; border-radius: 6px; overflow-x: auto; } .ask-a pre code { background: none; padding: 0; }
568
+ .ask-a.err { color: var(--red); font-size: 0.8rem; }
569
+ .ask-acts { display: flex; gap: 0.4rem; margin: 0.35rem 0 0.2rem; }
570
+ .ask-acts button { background: var(--bg); border: 1px solid var(--border); color: var(--text2, var(--text)); border-radius: 7px; padding: 0.22rem 0.6rem; font-size: 0.74rem; cursor: pointer; }
571
+ .ask-acts button.pri { border-color: var(--green); color: var(--green); font-weight: 600; }
572
+ .ask-foot { border-top: 1px solid var(--border); padding: 0.55rem 0.7rem calc(0.55rem + env(safe-area-inset-bottom)); display: flex; gap: 0.45rem; align-items: flex-end; }
573
+ #ask-input { flex: 1; resize: none; max-height: 7em; min-height: 2.3em; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 9px; padding: 0.45rem 0.6rem; font: inherit; font-size: 0.84rem; outline: none; }
574
+ #ask-input:focus { border-color: var(--green); }
575
+ #ask-send { flex-shrink: 0; background: var(--green); color: #fff; border: none; border-radius: 9px; padding: 0.5rem 0.8rem; font-size: 0.8rem; cursor: pointer; }
576
+ #ask-send.stop { background: var(--surface2); color: var(--text); }
577
+ @media (max-width: 768px) {
578
+ #ask-panel { top: auto; left: 0; width: auto; height: 78vh; border-left: none; border-top: 1px solid var(--border); border-radius: 16px 16px 0 0; transform: translateY(105%); }
579
+ #ask-panel.open + #ask-backdrop, #ask-backdrop.show { display: block; }
580
+ }
488
581
  </style>
489
582
  </head>
490
583
  <body>
@@ -511,6 +604,7 @@
511
604
  <span class="sb-item" id="work-status"><span class="sb-dot idle"></span> 空闲</span>
512
605
  <span id="control-badge" class="readonly" style="display:none">只读</span>
513
606
  <span id="viewer-count" class="sb-item"></span>
607
+ <span id="guide-chip" class="sb-item" style="display:none;cursor:pointer" onclick="showMemory('project')"></span>
514
608
  <span id="update-badge" style="display:none;color:var(--yellow);cursor:pointer" onclick="doUpdate()">⬆ 有更新</span>
515
609
  <span id="claude-update-badge" style="display:none;color:var(--blue);cursor:pointer" onclick="doClaudeUpdate()">⬆ claude</span>
516
610
  </div>
@@ -518,6 +612,7 @@
518
612
  <div id="chat-area"></div>
519
613
 
520
614
  <div id="input-area">
615
+ <div id="pending-bar" style="display:none" onclick="reopenPending()"></div>
521
616
  <div id="readonly-overlay" style="display:none"></div>
522
617
  <div id="input-box">
523
618
  <!-- 待发草稿提示:本轮干完(idle)且收件箱里还有本任务的草稿时浮出来 -->
@@ -526,6 +621,14 @@
526
621
  <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
622
  <button class="img-remove" onclick="event.stopPropagation();hideDraftTip()" title="暂不处理">&times;</button>
528
623
  </div>
624
+ <!-- 建议生成项目说明:看出是长期项目(同目录多任务 / 聊了很多轮 / 说了「以后都…」)且还没有 CLAUDE.md 时浮出 -->
625
+ <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)">
626
+ <span style="font-size:0.95rem;flex-shrink:0">📘</span>
627
+ <span id="guide-tip-text" style="flex:1;min-width:0;font-size:0.74rem;color:var(--text2);line-height:1.45"></span>
628
+ <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>
629
+ <button onclick="guideTipOff()" style="flex-shrink:0;background:none;border:none;color:var(--muted);font-size:0.7rem;cursor:pointer;padding:0.2rem">不再提示</button>
630
+ <button class="img-remove" onclick="guideTipClose()" title="先不管">&times;</button>
631
+ </div>
529
632
  <div id="img-preview">
530
633
  <img id="img-preview-thumb" src="" alt="">
531
634
  <span class="img-info">已添加图片,输入文字后发送</span>
@@ -554,6 +657,8 @@
554
657
  <button class="tb-btn" id="send-btn" onclick="sendCommand()" title="发送">
555
658
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
556
659
  </button>
660
+ <!-- ⚡ 插话:AI 忙时把输入直接送进正在干的这一轮(不打断,AI 下一步看到);存草稿则等干完再发 -->
661
+ <button id="inject-btn" class="inj-btn" style="display:none" onclick="injectFromInput()" title="不打断 AI:马上把这条送过去,AI 在下一步就会看到并调整。&#10;适合补充、提醒、微调方向;方向完全错了请先点 ■ 停止">⚡ 插话</button>
557
662
  <!-- AI 忙时顶替发送键:这轮还没完(可能在跑子代理),先把想法存成草稿 -->
558
663
  <button class="tb-btn" id="draft-btn" style="display:none;color:var(--yellow)" onclick="saveDraftFromInput('busy')" title="AI 还在跑,先存为草稿,干完再发">
559
664
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
@@ -867,44 +972,61 @@
867
972
  </div>
868
973
  </div>
869
974
 
870
- <!-- 灵感收件箱(草稿队列):会话正忙时先记下想法,干完再发。每条强制归属一个任务 -->
975
+ <!-- 灵感收件箱(草稿队列):会话正忙时先记下想法,干完再发。每条强制归属一个任务。
976
+ 本任务的草稿是一个有顺序的队列:可拖动 / ↑↓ 排序、就地编辑、多选合并 / 一起取回,开「依次自动发送」后每干完一轮发下一条 -->
871
977
  <div id="inbox-sheet" class="action-sheet">
872
978
  <div class="action-sheet-backdrop" onclick="closeInboxSheet()"></div>
873
- <div class="action-sheet-box" style="max-height:85vh;display:flex;flex-direction:column">
874
- <div class="action-sheet-title">📥 灵感收件箱 · 草稿</div>
875
- <div style="font-size:0.68rem;color:var(--muted);padding:0 0.2rem 0.5rem;line-height:1.55">
876
- 随手记想法:先存这儿,AI 干完后<b>取回</b>发送;勾「<b>自动接力</b>」则干完自动发出。只存本机,细节见各条目按钮。
877
- </div>
878
- <!-- 就地新增:不必先在消息框打字再"提取",直接在这儿写 -->
879
- <div style="border-top:1px solid var(--surface2);padding:0.5rem 0.2rem">
880
- <textarea id="inbox-new-text" rows="3" spellcheck="false" placeholder="写下临时想法…(截图可直接 Ctrl+V 粘进来)"
881
- style="width:100%;box-sizing:border-box;background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:0.5rem;font-size:0.82rem;resize:vertical"></textarea>
882
- <div id="inbox-new-img" style="display:none;align-items:center;gap:0.4rem;margin-top:0.35rem;font-size:0.7rem;color:var(--text2)">
883
- <span>📷</span><span id="inbox-new-img-name" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
884
- <button class="dr-btn" onclick="clearInboxNewImage()">移除</button>
979
+ <div class="action-sheet-box ib-box">
980
+ <div class="ib-head">
981
+ <b>📥 草稿</b>
982
+ <div class="ib-tabs">
983
+ <button data-ibt="this" class="on" onclick="ibTab('this')">本任务 <span id="ib-n-this"></span></button>
984
+ <button data-ibt="others" onclick="ibTab('others')">其它任务 <span id="inbox-others-count"></span></button>
885
985
  </div>
886
- <div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.45rem;flex-wrap:wrap">
887
- <button class="dr-btn" onclick="document.getElementById('inbox-new-file').click()" title="也可以直接 Ctrl+V 粘贴截图">📷 选图片</button>
888
- <input type="file" id="inbox-new-file" accept="image/*" style="display:none"
889
- onchange="uploadInboxImage(this.files[0]); this.value='';">
890
- <label style="display:flex;align-items:center;gap:0.3rem;font-size:0.7rem;color:var(--muted);cursor:pointer">
891
- <input type="checkbox" id="inbox-new-autosend" style="width:1rem;height:1rem"> 自动接力
986
+ <button class="ib-x" onclick="closeInboxSheet()" title="关闭">✕</button>
987
+ </div>
988
+ <div id="ib-pane-this" class="ib-pane">
989
+ <div class="ib-bar">
990
+ <span id="ib-hint" class="ib-hint"></span>
991
+ <button id="ib-sel-btn" class="dr-btn" onclick="ibToggleSelect()" title="勾选几条后合并、一起取回或删除">☑ 多选</button>
992
+ <label class="ib-autoall" title="勾上后:每干完一轮,自动按顺序发出下一条(每条都是一轮真实的模型调用)。上一轮被中断或出错时会暂停,等你确认再继续。">
993
+ <input type="checkbox" id="ib-auto-all" onchange="ibAutoAll(this.checked)"> ▶ 依次自动发送
892
994
  </label>
893
- <button class="dr-btn take" style="margin-left:auto;padding:0.35rem 1rem" onclick="addDraftHere()">+ 记下来</button>
894
995
  </div>
895
- <div style="margin-top:0.4rem">
896
- <a onclick="saveDraftFromInput('manual')" style="color:var(--blue2);cursor:pointer;font-size:0.7rem">或:把下方消息框里已写的内容存为草稿 ↓</a>
996
+ <div id="ib-paused" class="ib-paused" style="display:none">
997
+ <span>⏸ 自动发送暂停中:上一轮被中断或出错时不会自动往下发</span>
998
+ <button class="dr-btn take" onclick="ibResume()">继续发送</button>
999
+ </div>
1000
+ <div id="inbox-this" class="ib-list"></div>
1001
+ <div id="ib-selbar" class="ib-selbar" style="display:none">
1002
+ <span>已选 <b id="ib-sel-n">0</b> 条</span>
1003
+ <button class="dr-btn" onclick="ibMergeSelected(false)" title="按列表顺序拼成一条(自动编号),可再编辑;之后也能撤销合并">🔗 合并</button>
1004
+ <button class="dr-btn take" onclick="ibMergeSelected(true)" title="合并后直接放进消息框">一起取回</button>
1005
+ <button class="dr-btn" onclick="ibDeleteSelected()" style="color:var(--red)">删除</button>
1006
+ <button class="dr-btn" onclick="ibToggleSelect(false)">取消</button>
1007
+ </div>
1008
+ <!-- 就地新增:不必先在消息框打字再"提取",直接在这儿写 -->
1009
+ <div class="ib-compose">
1010
+ <textarea id="inbox-new-text" rows="2" spellcheck="false" placeholder="记下一个想法…(截图可直接 Ctrl+V 粘进来)"></textarea>
1011
+ <div id="inbox-new-img" style="display:none;align-items:center;gap:0.4rem;margin-top:0.3rem;font-size:0.7rem;color:var(--text2)">
1012
+ <span>📷</span><span id="inbox-new-img-name" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
1013
+ <button class="dr-btn" onclick="clearInboxNewImage()">移除</button>
1014
+ </div>
1015
+ <div class="ib-compose-row">
1016
+ <button class="dr-btn" onclick="document.getElementById('inbox-new-file').click()" title="也可以直接 Ctrl+V 粘贴截图">📷</button>
1017
+ <input type="file" id="inbox-new-file" accept="image/*" style="display:none" onchange="uploadInboxImage(this.files[0]); this.value='';">
1018
+ <label style="display:flex;align-items:center;gap:0.3rem;font-size:0.7rem;color:var(--muted);cursor:pointer" title="这条也参与自动发送">
1019
+ <input type="checkbox" id="inbox-new-autosend" style="width:0.95rem;height:0.95rem"> ⚡ 自动
1020
+ </label>
1021
+ <a onclick="saveDraftFromInput('manual')" class="ib-link" title="把下方消息框里已写的内容存为草稿">存消息框内容</a>
1022
+ <button class="dr-btn take" style="margin-left:auto;padding:0.35rem 0.95rem" onclick="addDraftHere()">+ 记下来</button>
1023
+ </div>
897
1024
  </div>
898
1025
  </div>
899
- <div style="font-size:0.66rem;color:var(--muted);padding:0.5rem 0.2rem 0.2rem;border-top:1px solid var(--surface2)">
900
- <b>本任务</b>(<span id="inbox-this-title"></span>)
901
- </div>
902
- <div id="inbox-this" style="overflow-y:auto;padding:0.2rem 0;font-size:0.8rem;scrollbar-width:thin;min-height:3rem;max-height:34vh"></div>
903
- <div style="font-size:0.66rem;color:var(--muted);padding:0.4rem 0.2rem 0.2rem;border-top:1px solid var(--surface2)">
904
- <a onclick="toggleInboxOthers()" style="color:var(--blue2);cursor:pointer">▸ 其它任务的草稿 <span id="inbox-others-count"></span></a>
1026
+ <div id="ib-pane-others" class="ib-pane" style="display:none">
1027
+ <div class="ib-hint" style="padding:0.1rem 0.2rem 0.4rem">其它任务里记的草稿。点「改投」挪到本任务。</div>
1028
+ <div id="inbox-others" class="ib-list"></div>
905
1029
  </div>
906
- <div id="inbox-others" style="display:none;overflow-y:auto;padding:0.2rem 0;font-size:0.8rem;scrollbar-width:thin;max-height:26vh"></div>
907
- <button class="action-sheet-cancel" onclick="closeInboxSheet()">关闭</button>
908
1030
  </div>
909
1031
  </div>
910
1032
 
@@ -938,7 +1060,7 @@
938
1060
  <div id="memory-sheet" class="action-sheet">
939
1061
  <div class="action-sheet-backdrop" onclick="closeMemorySheet()"></div>
940
1062
  <div class="action-sheet-box" style="height:90vh;max-height:90vh;display:flex;flex-direction:column">
941
- <div class="action-sheet-title">Claude 记忆</div>
1063
+ <div class="action-sheet-title">项目说明与记忆</div>
942
1064
  <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
1065
  <div style="text-align:center;color:var(--muted);padding:2rem 0">加载中...</div>
944
1066
  </div>
@@ -1012,7 +1134,66 @@
1012
1134
  </div>
1013
1135
  <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
1136
  <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>
1137
+ <button id="ask-sheet-skip" class="action-sheet-cancel" style="display:none" onclick="skipAsk()">⏭ 跳过,让 AI 自己决定</button>
1138
+ <button id="ask-sheet-cancel" class="action-sheet-cancel" onclick="closeAskSheet()">取消</button>
1139
+ </div>
1140
+ </div>
1141
+ <!-- 方案审批:计划模式下 AI 用 ExitPlanMode 提交方案 -->
1142
+ <div id="plan-sheet" class="action-sheet">
1143
+ <div class="action-sheet-backdrop" onclick="closePlanSheet()"></div>
1144
+ <div class="action-sheet-box" style="max-height:88vh;display:flex;flex-direction:column">
1145
+ <div class="action-sheet-title">📋 AI 提交了方案,请审批</div>
1146
+ <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>
1147
+ <textarea id="plan-sheet-fb" class="slash-inp" rows="2" placeholder="需要改的话写在这里,再点「按意见修改」(可选)" style="margin:0.5rem 0 0.4rem;resize:vertical"></textarea>
1148
+ <button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="approvePlan()">✅ 批准并执行</button>
1149
+ <button class="action-sheet-cancel" style="margin-bottom:0.4rem" onclick="revisePlan()">✏️ 按意见修改方案</button>
1150
+ <button class="action-sheet-cancel" onclick="closePlanSheet()">稍后再看</button>
1151
+ </div>
1152
+ </div>
1153
+ <!-- 退出计划:按方案执行 / 仅退出 -->
1154
+ <div id="planexit-sheet" class="action-sheet">
1155
+ <div class="action-sheet-backdrop" onclick="closePlanExit()"></div>
1156
+ <div class="action-sheet-box">
1157
+ <div class="action-sheet-title">退出计划模式</div>
1158
+ <button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="exitPlan(true)">✅ 退出并按方案执行</button>
1159
+ <button class="action-sheet-cancel" style="margin-bottom:0.4rem" onclick="exitPlan(false)">仅退出计划模式</button>
1160
+ <button class="action-sheet-cancel" onclick="closePlanExit()">取消</button>
1161
+ </div>
1162
+ </div>
1163
+
1164
+ <!-- 💡 问一问:选中文字后的小工具条 + 问答小窗。问答不进当前会话,不打扰正在干活的 AI -->
1165
+ <div id="ask-pop">
1166
+ <button class="pri" onmousedown="event.preventDefault()" onclick="askFromSelection(false)">💡 解释</button>
1167
+ <button onmousedown="event.preventDefault()" onclick="askFromSelection(true)">❓ 问一问…</button>
1168
+ <button onmousedown="event.preventDefault()" onclick="askCopySelection()">📋 复制</button>
1169
+ </div>
1170
+ <div id="ask-panel" role="dialog" aria-label="问一问">
1171
+ <div class="ask-head">
1172
+ <div><b>💡 问一问</b><div class="ask-sub">这里的问答不会打扰正在干活的 AI,放心问</div></div>
1173
+ <button class="ask-x" onclick="closeAsk()" title="关闭 (Esc)">✕</button>
1174
+ </div>
1175
+ <div id="ask-body"></div>
1176
+ <div class="ask-foot">
1177
+ <textarea id="ask-input" rows="1" placeholder="继续问…(Enter 发送)"></textarea>
1178
+ <button id="ask-send" onclick="askSendOrStop()">发送</button>
1179
+ </div>
1180
+ </div>
1181
+ <div id="ask-backdrop" onclick="closeAsk()"></div>
1182
+
1183
+ <!-- 清除:先说清楚会发生什么,免得误以为出了故障 -->
1184
+ <div id="clear-sheet" class="action-sheet">
1185
+ <div class="action-sheet-backdrop" onclick="closeClearSheet()"></div>
1186
+ <div class="action-sheet-box">
1187
+ <div class="action-sheet-title">清除对话,从头开始?</div>
1188
+ <div style="font-size:0.82rem;color:var(--muted);line-height:1.65;padding:0 0.4rem 0.7rem">
1189
+ · AI 会<b>忘掉本任务之前聊过的内容</b>,下次提问是全新对话<br>
1190
+ · 界面上的聊天记录一并清空<br>
1191
+ · <b>项目文件、已做的修改都不受影响</b><br>
1192
+ 适合换新话题、或 AI 被旧内容带偏时用。只是嫌对话太长,用「压缩」更合适——会保留要点。
1193
+ </div>
1194
+ <button class="action-sheet-cancel" style="background:var(--red);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="doClear(true)">清除并开始新对话</button>
1195
+ <button class="action-sheet-cancel" style="margin-bottom:0.4rem" onclick="closeClearSheet();doSlashCmd('/compact')">改用压缩(保留要点)</button>
1196
+ <button class="action-sheet-cancel" onclick="closeClearSheet()">取消</button>
1016
1197
  </div>
1017
1198
  </div>
1018
1199
 
@@ -1677,14 +1858,20 @@
1677
1858
  }
1678
1859
 
1679
1860
  // ── 消息渲染 ──────────────────────────────────
1680
- function addInputMessage(text) {
1861
+ // ⚡ 插话气泡上的小标记:等 AI 下一步看到 → AI 已看到 / 未送达
1862
+ function injectTagHtml(inj) {
1863
+ if (!inj) return '';
1864
+ const st = inj.state || 'pending';
1865
+ const txt = st === 'seen' ? '⚡ 插话 · AI 已看到' : st === 'dropped' ? '⚡ 插话 · 未送达' + (inj.reason ? ':' + inj.reason : '') : '⚡ 插话 · 等 AI 下一步看到';
1866
+ return `<span class="inj-tag ${st}" data-inject-tag="${escHtml(inj.id || '')}">${escHtml(txt)}</span>`;
1867
+ }
1868
+ function addInputMessage(text, inject) {
1681
1869
  endStream(); endToolGroup();
1682
1870
  _userScrolledUp = false; hideNewMsgHint(); // 用户主动发消息 → 回到底部看回复
1683
- _streamedTextThisCall = false;
1684
- _textRenderedThisTurn = false;
1871
+ if (!inject) { _streamedTextThisCall = false; _textRenderedThisTurn = false; } // 插话并进当前这一轮,不算新一轮
1685
1872
  const msg = document.createElement('div');
1686
- msg.className = 'msg msg-input';
1687
- msg.innerHTML = `<div><div class="bubble">${escHtml(text)}</div><div class="meta">${formatTime(new Date())}</div></div>`;
1873
+ msg.className = 'msg msg-input' + (inject ? ' msg-inject' : '');
1874
+ msg.innerHTML = `<div><div class="bubble">${escHtml(text)}</div><div class="meta">${injectTagHtml(inject)}${formatTime(new Date())}</div></div>`;
1688
1875
  chatArea.appendChild(msg);
1689
1876
  trimMessages();
1690
1877
  scrollToBottom();
@@ -2138,9 +2325,9 @@
2138
2325
  if (sel) { parent.appendChild(sel); return; }
2139
2326
  if (syntheticUserKind(msg.content)) return;
2140
2327
  const el = document.createElement('div');
2141
- el.className = 'msg msg-input';
2328
+ el.className = 'msg msg-input' + (msg.inject ? ' msg-inject' : '');
2142
2329
  const full = msg.timestamp ? new Date(msg.timestamp).toLocaleString('zh-CN') : '';
2143
- el.innerHTML = `<div><div class="bubble">${escHtml(msg.content)}</div><div class="meta" title="${escHtml(full)}">${msg.timestamp ? formatTime(new Date(msg.timestamp)) : ''}</div></div>`;
2330
+ el.innerHTML = `<div><div class="bubble">${escHtml(msg.content)}</div><div class="meta" title="${escHtml(full)}">${injectTagHtml(msg.inject)}${msg.timestamp ? formatTime(new Date(msg.timestamp)) : ''}</div></div>`;
2144
2331
  parent.appendChild(el);
2145
2332
  return;
2146
2333
  }
@@ -2153,6 +2340,8 @@
2153
2340
  if (block.type === 'thinking' && block.thinking) thinkings.push(block.thinking);
2154
2341
  else if (block.type === 'text' && block.text) texts.push(block.text);
2155
2342
  else if (block.type === 'tool_use') {
2343
+ const planTxt = planTextOf(block.name, block.input);
2344
+ if (planTxt !== null) { addPlanCard(planTxt); continue; }
2156
2345
  // Task 在翻页(fragment)模式下退化为普通工具卡——面板逻辑挂在聊天区,
2157
2346
  // 塞进 fragment 会错位;整本回放仍用面板
2158
2347
  if (block.name === 'Task' && !_replayParent) {
@@ -2497,8 +2686,11 @@
2497
2686
  // busy 时发送键让位给「存草稿」键;回到 idle 再提示有没有待发草稿
2498
2687
  const draftBtn = document.getElementById('draft-btn');
2499
2688
  if (draftBtn) draftBtn.style.display = busy ? '' : 'none';
2689
+ try { updateInjectBtn(); } catch {}
2500
2690
  // 用 try 包住:这两个绑定声明在文件后段,初始化早期调用会撞 TDZ
2501
2691
  try { if (!busy) _draftTipDismissed = false; renderDraftTip(); } catch {}
2692
+ try { if (!busy) _ibIdleSince = Date.now(); if (document.getElementById('inbox-sheet').classList.contains('open')) renderInbox(); } catch {}
2693
+ try { if (!busy) checkGuide(); else renderGuideTip(); } catch {}
2502
2694
  }
2503
2695
  _postStatus(state);
2504
2696
  }
@@ -2612,7 +2804,7 @@
2612
2804
  const btn = document.createElement('button');
2613
2805
  btn.textContent = '返回首页';
2614
2806
  btn.style.cssText = 'margin-top:1rem;padding:0.5rem 1.5rem;border:1px solid var(--muted);border-radius:8px;background:transparent;color:var(--text2);cursor:pointer;font-size:0.9rem';
2615
- btn.onclick = () => window.location.href = '/';
2807
+ btn.onclick = () => goHome();
2616
2808
  overlay.appendChild(btn);
2617
2809
  }
2618
2810
  }
@@ -2658,6 +2850,7 @@
2658
2850
  currentSession = session;
2659
2851
  overlay.classList.add('hidden');
2660
2852
  document.title = session.title + ' — myhi';
2853
+ setTimeout(() => { try { checkGuide(); } catch {} }, 0);
2661
2854
  document.getElementById('session-title').textContent = session.title;
2662
2855
  if (session.cwd) document.getElementById('session-cwd').textContent = session.cwd;
2663
2856
  updateViewers(session.viewers);
@@ -2709,7 +2902,7 @@
2709
2902
  lastControlHolder = holder || null;
2710
2903
  });
2711
2904
  socket.on('control-denied', ({ reason }) => addStatusMessage('无法获取控制: ' + reason));
2712
- socket.on('kicked', ({ reason }) => { addStatusMessage(reason || '你已被管理员踢出'); setTimeout(() => location.href = '/', 2000); });
2905
+ socket.on('kicked', ({ reason }) => { addStatusMessage(reason || '你已被管理员踢出'); setTimeout(() => goHome(), 2000); });
2713
2906
 
2714
2907
  // ── Agent 模式事件 ──────────────────────────────
2715
2908
  socket.on('agent:message', (msg) => {
@@ -2724,6 +2917,9 @@
2724
2917
  } else if (msg.subtype === 'interrupted') {
2725
2918
  addStatusMessage('查询已中断');
2726
2919
  setWorkState('idle');
2920
+ } else if (msg.subtype === 'inject_state') {
2921
+ document.querySelectorAll(`[data-inject-tag="${CSS.escape(msg.id || '')}"]`).forEach(t => { t.outerHTML = injectTagHtml({ id: msg.id, state: msg.state, reason: msg.reason }); });
2922
+ if (msg.state === 'dropped' && msg.reason) addStatusMessage('⚡ ' + msg.reason);
2727
2923
  } else if (msg.subtype === 'info' && msg.message) {
2728
2924
  addStatusMessage(msg.message);
2729
2925
  // 同步计划模式状态到按钮
@@ -2754,7 +2950,7 @@
2754
2950
  const sel = buildSyntheticUserEl(msg.content);
2755
2951
  if (sel) addStatusMessage(sel.textContent);
2756
2952
  } else if (!kind) {
2757
- addInputMessage(msg.content);
2953
+ addInputMessage(msg.content, msg.inject);
2758
2954
  }
2759
2955
  // caveat/noise:纯噪音,不渲染
2760
2956
  }
@@ -2782,9 +2978,9 @@
2782
2978
  _textRenderedThisTurn = true;
2783
2979
  }
2784
2980
  } else if (block.type === 'tool_use') {
2785
- if (block.name === 'AskUserQuestion' && block.input) {
2786
- openAskSheet(block.input);
2787
- }
2981
+ if (block.name === 'AskUserQuestion' && block.input) legacyAsk(block.id, block.input);
2982
+ const planTxt = planTextOf(block.name, block.input);
2983
+ if (planTxt !== null) { addPlanCard(planTxt); continue; }
2788
2984
  if (block.name === 'Task') {
2789
2985
  subaCreatePanel(block.id, subagentToolInfo(block.input || {}));
2790
2986
  } else {
@@ -2813,6 +3009,8 @@
2813
3009
  // 关键:idle 放最后——渲染最终答复的 addAssistantMessage 会把状态置回 'working',
2814
3010
  // 若在其之前置 idle 会被顶回「执行中」+ 取消按钮不复位。渲染全部完成后再回 idle。
2815
3011
  setWorkState('idle');
3012
+ // 还有插话没被看到:AI 收尾后马上拿它开新一轮,界面别停在空闲
3013
+ if (msg.injectPending) { setWorkState('thinking'); showThinking(); }
2816
3014
  // 收尾水位提示:server 在 result 上附带了刚从 transcript 反查的精确水位
2817
3015
  // (ctxTokens——claude 2.1.292 起 stream 的 assistant usage 被清零,前端自己算不了)。
2818
3016
  // 旧 server 无此字段时退回 _lastCtxTokens(老 CLI 的 assistant 流仍带真实 usage)。
@@ -2858,9 +3056,8 @@
2858
3056
  break;
2859
3057
  case 'tool_use':
2860
3058
  // 独立工具调用消息
2861
- if (msg.name === 'AskUserQuestion' && msg.input) {
2862
- openAskSheet(msg.input);
2863
- }
3059
+ if (msg.name === 'AskUserQuestion' && msg.input) legacyAsk(msg.id, msg.input);
3060
+ if (planTextOf(msg.name, msg.input) !== null) { addPlanCard(planTextOf(msg.name, msg.input)); break; }
2864
3061
  if (msg.name === 'Task') {
2865
3062
  subaCreatePanel(msg.id, subagentToolInfo(msg.input || {}));
2866
3063
  } else {
@@ -3231,6 +3428,7 @@
3231
3428
 
3232
3429
  function sendCommand() {
3233
3430
  const text = cmdInput.value.trim();
3431
+ if (GUIDE_REMEMBER_RE.test(text)) _guideRemember = true; // 「以后都… / 记住…」:这一轮干完建议写进项目说明
3234
3432
  if (!text && !_pendingImage && !_pendingFile) return;
3235
3433
 
3236
3434
  // AI 还在跑这一轮(可能正开着子代理,整轮 result 未到,服务端 isBusy 会拒收):
@@ -3283,6 +3481,49 @@
3283
3481
  cmdInput.focus();
3284
3482
  }
3285
3483
 
3484
+ // ── ⚡ 插话 ─────────────────────────────────────────────
3485
+ // AI 忙时把输入(或一条草稿)直接送进正在干的这一轮:claude 在下一步(两次工具调用之间)看到它并调整,
3486
+ // 不打断、不重启。服务端靠 claude 的回显确认「AI 已看到」,气泡上的标记随之更新。
3487
+ function canInjectNow() {
3488
+ return currentSession?.mode === 'agent' && !!currentSession.injectable && workState !== 'idle' && canTakeControl() && !_askCtlId && !_planCtl;
3489
+ }
3490
+ window.updateInjectBtn = function() {
3491
+ const b = document.getElementById('inject-btn');
3492
+ if (b) b.style.display = canInjectNow() ? '' : 'none';
3493
+ };
3494
+ function injectEmit(payload, onOk) {
3495
+ if (!isController) {
3496
+ if (canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
3497
+ else { addStatusMessage('其他用户控制中,无法插话'); return; }
3498
+ }
3499
+ socket.emit('agent:inject', payload, (r) => {
3500
+ if (!r?.ok) { addStatusMessage('⚡ 插话没发出去:' + (r?.error || '未知原因')); return; }
3501
+ onOk && onOk(r);
3502
+ });
3503
+ }
3504
+ window.injectFromInput = function() {
3505
+ const text = cmdInput.value.trim();
3506
+ if (!text && !_pendingImage && !_pendingFile) { addStatusMessage('先在输入框里写下要补充的话,再点 ⚡ 插话'); cmdInput.focus(); return; }
3507
+ if (!canInjectNow()) { sendCommand(); return; }
3508
+ const imgPath = _pendingImage?.path || null;
3509
+ const filePath = _pendingFile?.path || null;
3510
+ const fileName = _pendingFile?.name || '';
3511
+ const display = imgPath ? `📷 ${text || '分析图片'}` : filePath ? `📎 ${fileName}${text ? ' ' + text : ''}` : text;
3512
+ const prompt = imgPath
3513
+ ? `用户上传了一张图片,路径是 ${imgPath},请用 Read 工具读取这张图片。用户的要求是:${text || '描述图片内容'}`
3514
+ : filePath
3515
+ ? `用户上传了一个临时文件用于本次分析(不在项目文件树里),路径是 ${filePath}(原文件名 ${fileName}),请用合适的工具(如 Read)读取并分析它。用户的要求是:${text || '分析这个文件'}`
3516
+ : text;
3517
+ injectEmit({ prompt, display }, () => {
3518
+ cmdInput.value = ''; cmdInput.style.height = 'auto';
3519
+ clearPendingImage(); clearPendingFile();
3520
+ cmdInput.focus();
3521
+ });
3522
+ };
3523
+ window.injectDraft = function(id) {
3524
+ injectEmit({ draftId: id }, async () => { closeInboxSheet(); await refreshInbox(); });
3525
+ };
3526
+
3286
3527
  // ── @ 文件引用补全 ─────────────────────────────────────────
3287
3528
  // 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
3288
3529
  // 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
@@ -3544,6 +3785,7 @@
3544
3785
  window.toggleControl = function() { if (isController) socket.emit('release-control', { sessionId: SESSION_ID }); else if (canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID }); };
3545
3786
 
3546
3787
  function updateControlUI(controlling, holderName) {
3788
+ try { updateInjectBtn(); } catch {}
3547
3789
  isController = controlling;
3548
3790
  const badge = document.getElementById('control-badge');
3549
3791
  if (controlling) {
@@ -3565,22 +3807,57 @@
3565
3807
 
3566
3808
  // ── 设置面板 ──────────────────────────────────
3567
3809
  // ── 会话内使用说明(精简版;完整版在首页 ❓ 帮助)──────────────
3810
+ // 快捷栏按钮说明:帮助面板和鼠标悬停提示共用这一份,加按钮时在这里补一句
3811
+ const SK_HELP = {
3812
+ draft: ['📥 草稿', 'AI 忙时先把想法记下来排队:可拖动 / ↑↓ 排顺序、点文字直接改、多选合并或一起取回;开「依次自动发送」后每干完一轮自动发下一条;AI 忙时可「⚡ 插入」当前任务。'],
3813
+ folder: ['📂 文件夹', '在这台电脑上用资源管理器打开本任务目录(后台/服务模式下不可用)。'],
3814
+ model: ['模型', '切换 AI 模型。复杂任务用强模型,简单活换快模型更省;被安全检测误拦时也可换个模型再试。'],
3815
+ plan: ['计划', '先让 AI 只调研、出方案、不改文件;你审批后再执行。再点一次「退出计划」可按方案执行或仅退出。'],
3816
+ compact: ['压缩', '对话太长时把前面内容压成摘要,AI 保留要点、继续干活,上下文水位下降。不会丢文件。'],
3817
+ resume: ['恢复', '翻看本任务以前的对话,选一段接着聊。'],
3818
+ clear: ['清除', '让 AI 忘掉之前聊过的内容、从头开始新对话,聊天记录也清空;项目文件不受影响。只嫌太长用「压缩」。'],
3819
+ secrets: ['🔑 密钥', '保存 token / 密码等,命令里用 $名字 引用,值不会进聊天和模型。新增后清除或重开会话生效。'],
3820
+ guard: ['🛡 护栏', '发给 AI 前把姓名、证件号、手机号等换成代号,回来再还原;映射只留在本机。'],
3821
+ followup: ['🔄 跟进', '长任务交出去后,AI 每说完一轮,隔一段时间自动替你问进度、让它接着干;判断全部完成就停(最多 5 次,每次都算一轮模型调用)。'],
3822
+ git: ['提交', '查看改动并 git 提交 / 推送。'],
3823
+ memory: ['记忆', '查看、编辑「项目说明」和 AI 的长期记忆,每次新对话都会带上;长期项目可一键让 AI 生成项目说明。'],
3824
+ skills: ['技能', '管理常用指令模板(/命令),一点就发;可自建。'],
3825
+ cost: ['费用', '查看本次会话的 token 用量与花费。'],
3826
+ };
3827
+ document.querySelectorAll('.sk[data-sk]').forEach(b => { const h = SK_HELP[b.dataset.sk]; if (h && !b.title) b.title = h[1]; });
3828
+
3568
3829
  window.openChatHelp = function() {
3569
3830
  overlay.innerHTML = '';
3570
3831
  overlay.classList.remove('hidden');
3571
3832
  const wrap = document.createElement('div');
3572
3833
  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
3834
  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';
3835
+ 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';
3836
+ const sec = (t) => `<div style="font-size:0.86rem;font-weight:700;margin:0.8rem 0 0.3rem">${t}</div>`;
3837
+ 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>`;
3838
+ const shown = new Set([...document.querySelectorAll('#sk-claude-agent .sk[data-sk]')].map(b => b.dataset.sk));
3839
+ const skRows = Object.entries(SK_HELP).filter(([k]) => currentSession?.mode !== 'agent' || shown.has(k)).map(([, h]) => row(h[0], h[1])).join('');
3575
3840
  box.innerHTML = `
3576
- <div style="font-size:1rem;font-weight:700;margin-bottom:0.5rem">💬 会话怎么用</div>
3841
+ <div style="font-size:1rem;font-weight:700;margin-bottom:0.3rem">💬 会话怎么用</div>
3577
3842
  <b>发消息</b>:像聊天一样把需求说清楚即可;触屏回车=换行,用发送键发送。<br>
3578
3843
  <b>让它读文件</b>:打 <b>@</b> 从本项目逐层浏览、打字搜索;项目外打 <b>@D:</b>、<b>@/</b>、<b>@~/</b> 按路径逐层浏览;选中插路径;或点「文件」面板的 <b>@</b> 引用。<br>
3579
3844
  <b>工具/思考</b>:AI 干活的过程默认折叠,点「🔧 N 个工具调用」展开看命令与输出。<br>
3580
- <b>AI 忙时</b>:输入框变「存草稿」,先记着干完取回;勾自动接力会自动发出。<br>
3581
- <b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。<br>
3582
- <b>常用按钮</b>:模型/计划/压缩/恢复/清除/密钥/护栏/提交——普通用户可在设置→快捷栏精简。<br>
3583
- <span style="color:var(--muted)">更多说明见首页顶栏 ❓ 帮助。</span>`;
3845
+ <b>AI 忙时</b>:想补充或提醒,点输入框右边「⚡ 插话」——不打断,AI 下一步就会看到并调整(气泡上显示「AI 已看到」);不急的就回车「存草稿」排队,草稿里可排顺序、编辑、合并,开「依次自动发送」后每干完一轮自动发下一条,AI 忙时也能把草稿「⚡ 插入」当前任务。<br>
3846
+ <b>看不懂就问</b>:在聊天里<b>选中</b>看不懂的词或一段话(手机长按选中),点「💡 解释」或「❓ 问一问」,答案在旁边小窗里,可以继续追问;这些问答<b>不会打扰正在干活的 AI</b>,想让它知道就点「👉 告诉 AI」。<br>
3847
+ <b>AI 提问 / 交方案</b>:AI 需要你拿主意时会弹出选项,选一个即可,也可「跳过,让 AI 自己决定」;收起后输入框上方「❓ AI 在等你回答」可再打开。<br>
3848
+ <b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。
3849
+ ${sec('顶栏')}
3850
+ ${row('▦ 分屏', '电脑上多个任务并排看、并排干。')}
3851
+ ${row('?', '就是这份说明。')}
3852
+ ${row('⚙ 设置', '聊天显示(思考 / 工具输出是否展开)、快捷栏显示哪些按钮、本任务的选项。')}
3853
+ ${row('◈ 水位', 'AI 的「记忆容量」用了多少。快满时 AI 容易忘事,点「压缩」腾空间。')}
3854
+ ${row('文件', '浏览、编辑、上传、下载本任务目录里的文件。')}
3855
+ ${row('切换', '跳到其他任务。')}
3856
+ ${row('›', '返回首页。')}
3857
+ ${row('📘', '状态栏出现「已带项目说明」表示 AI 每次会先读本项目的项目说明;变黄「偏长」时建议精简。点它查看 / 编辑。')}
3858
+ ${sec('快捷栏(输入框下方)')}
3859
+ ${skRows}
3860
+ <div style="color:var(--muted);margin-top:0.5rem">按钮太多?到 ⚙ 设置 → 快捷栏 关掉用不到的。鼠标停在按钮上也能看到说明。更多见首页 ❓ 帮助。</div>`;
3584
3861
  const closeBtn = document.createElement('button');
3585
3862
  closeBtn.textContent = '知道了';
3586
3863
  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';
@@ -4186,7 +4463,10 @@
4186
4463
 
4187
4464
  // ── 辅助 ──────────────────────────────────────
4188
4465
  function updateViewers(count) { document.getElementById('viewer-count').textContent = count > 1 ? `${count} 人在线` : ''; }
4189
- window.goBack = function() { window.location.href = '/'; };
4466
+ // 回首页走顶层窗口:电脑工作台里会话页是嵌在首页右侧的,只在嵌入框里跳 '/' 会把整个首页套进右侧
4467
+ function goHome() { try { (window.top || window).location.href = '/'; } catch { window.location.href = '/'; } }
4468
+ window.goHome = goHome;
4469
+ window.goBack = function() { goHome(); };
4190
4470
 
4191
4471
  // ── 快速切换会话(免退出到首页)──────────────────────────
4192
4472
  function switchRelTime(ms) {
@@ -4265,7 +4545,7 @@
4265
4545
  const close = () => overlay.remove();
4266
4546
  overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
4267
4547
  box.querySelector('#sw-cancel').onclick = close;
4268
- box.querySelector('#sw-home').onclick = () => { window.location.href = '/'; };
4548
+ box.querySelector('#sw-home').onclick = () => { goHome(); };
4269
4549
  const bindRows = () => box.querySelectorAll('[data-sid]').forEach(row => row.onclick = () => {
4270
4550
  const sid = row.dataset.sid;
4271
4551
  if (sid && sid !== SESSION_ID) window.location.href = '/terminal/' + sid;
@@ -4443,10 +4723,7 @@
4443
4723
  askMultiState.answers[qIdx] = { question: qText, label };
4444
4724
  refreshAskMultiSubmit();
4445
4725
  } else {
4446
- _clearAsk();
4447
- addInputMessage(label);
4448
- socket.emit('agent:query', { prompt: label });
4449
- showThinking();
4726
+ answerAsk({ [qText]: label }, label);
4450
4727
  }
4451
4728
  };
4452
4729
  block.appendChild(item);
@@ -4456,6 +4733,8 @@
4456
4733
  });
4457
4734
 
4458
4735
  if (isMulti) refreshAskMultiSubmit();
4736
+ document.getElementById('ask-sheet-skip').style.display = _askCtlId ? '' : 'none';
4737
+ document.getElementById('ask-sheet-cancel').textContent = _askCtlId ? '稍后回答' : '取消';
4459
4738
  sheet.classList.add('open');
4460
4739
  if (!isMulti) inp.focus();
4461
4740
  }
@@ -4483,21 +4762,126 @@
4483
4762
  const answers = askMultiState.answers.slice(0, n);
4484
4763
  if (answers.some((a) => !a)) return;
4485
4764
  const reply = answers.map((a, i) => 'Q' + (i + 1) + ': ' + a.question + '\nA: ' + a.label).join('\n\n');
4765
+ const map = {}; answers.forEach((a) => { map[a.question] = a.label; });
4766
+ answerAsk(map, reply);
4767
+ };
4768
+ // 交互通道下「取消」只是先收起,问题还挂着(输入框上方的提示条可再打开);旧模式照旧清掉
4769
+ window.closeAskSheet = function() {
4770
+ if (_askCtlId) { document.getElementById('ask-sheet').classList.remove('open'); renderPendingBar(); return; }
4486
4771
  _clearAsk();
4487
- addInputMessage(reply);
4488
- socket.emit('agent:query', { prompt: reply });
4489
- showThinking();
4490
4772
  };
4491
- window.closeAskSheet = function() { _clearAsk(); };
4492
4773
  window.submitAskReply = function() {
4493
4774
  const inp = document.getElementById('ask-sheet-input');
4494
4775
  const text = inp.value.trim();
4495
4776
  if (!text) return;
4777
+ answerAsk({ [_askQuestions[0]?.question || '']: text }, text);
4778
+ };
4779
+
4780
+ // ── 交互通道(1.3.193):AI 的提问 / 方案审批由 claude 发起、等 myhi 回答 ──
4781
+ // 新 claude:服务端推 agent:interaction(带 id),答案用 agent:respond 交回 claude 正在等待的那个调用,
4782
+ // 模型拿到的是「对自己问题的回答」,上下文连贯;刷新 / 换设备时服务端会补发,先答先算。
4783
+ // 老 claude(没有交互通道):沿用旧做法——看到 tool_use 就弹窗,答案当新消息发出去。
4784
+ let _askCtlId = null; // 当前问答弹窗对应的交互 id(null = 旧模式)
4785
+ let _planCtl = null; // 待审批的方案 { id, plan }
4786
+ const _ctlToolUseIds = new Set(); // 已由交互通道接管的 tool_use,旧路径别再弹一次
4787
+ function ensureControl() { if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID }); }
4788
+ function answerAsk(map, display) {
4789
+ if (_askCtlId) {
4790
+ ensureControl();
4791
+ socket.emit('agent:respond', { id: _askCtlId, decision: { answers: map } });
4792
+ _askCtlId = null; _clearAsk(); renderPendingBar(); showThinking();
4793
+ return;
4794
+ }
4496
4795
  _clearAsk();
4497
- addInputMessage(text);
4498
- socket.emit('agent:query', { prompt: text });
4796
+ addInputMessage(display);
4797
+ socket.emit('agent:query', { prompt: display });
4499
4798
  showThinking();
4799
+ }
4800
+ window.skipAsk = function() {
4801
+ if (!_askCtlId) return _clearAsk();
4802
+ ensureControl();
4803
+ socket.emit('agent:respond', { id: _askCtlId, decision: { skip: true } });
4804
+ _askCtlId = null; _clearAsk(); renderPendingBar();
4805
+ };
4806
+ // 旧路径:tool_use 一出现就弹。交互通道开着时先等一下,服务端的正式请求到了就以它为准
4807
+ function legacyAsk(toolUseId, input) {
4808
+ if (!currentSession?.interactive) return openAskSheet(input);
4809
+ setTimeout(() => { if (!_ctlToolUseIds.has(toolUseId) && !_askCtlId) openAskSheet(input); }, 1500);
4810
+ }
4811
+ socket.on('agent:interaction', (v) => {
4812
+ if (!v?.id) return;
4813
+ if (v.toolUseId) _ctlToolUseIds.add(v.toolUseId);
4814
+ if (v.kind === 'ask') {
4815
+ if (_askCtlId === v.id) return; // 重连补发的同一题
4816
+ _clearAsk(); _askCtlId = v.id;
4817
+ openAskSheet({ questions: v.questions || [] });
4818
+ } else if (v.kind === 'plan') {
4819
+ if (_planCtl?.id === v.id) return;
4820
+ _planCtl = { id: v.id, plan: v.plan || '' };
4821
+ openPlanSheet();
4822
+ _postStatus('waiting');
4823
+ }
4824
+ renderPendingBar();
4825
+ });
4826
+ socket.on('agent:interaction-closed', ({ id } = {}) => {
4827
+ if (_askCtlId === id) { _askCtlId = null; _clearAsk(); }
4828
+ if (_planCtl?.id === id) { _planCtl = null; closePlanSheet(); }
4829
+ renderPendingBar();
4830
+ });
4831
+ // 输入框上方的「AI 在等你」提示条:弹窗被收起后从这里再打开
4832
+ function renderPendingBar() {
4833
+ try { updateInjectBtn(); } catch {}
4834
+ const bar = document.getElementById('pending-bar');
4835
+ if (!bar) return;
4836
+ const txt = _planCtl ? '📋 AI 提交了方案,等你审批' : _askCtlId ? '❓ AI 在等你回答一个问题' : '';
4837
+ bar.textContent = txt ? txt + ' · 点这里查看' : '';
4838
+ bar.style.display = txt ? '' : 'none';
4839
+ }
4840
+ window.reopenPending = function() {
4841
+ if (_planCtl) return openPlanSheet();
4842
+ if (_askCtlId) document.getElementById('ask-sheet').classList.add('open');
4843
+ };
4844
+ // ── 方案审批 ──
4845
+ window.openPlanSheet = function() {
4846
+ if (!_planCtl) return;
4847
+ document.getElementById('plan-sheet-body').innerHTML = renderMarkdown(_planCtl.plan || '(方案为空)');
4848
+ document.getElementById('plan-sheet').classList.add('open');
4849
+ };
4850
+ window.closePlanSheet = function() { document.getElementById('plan-sheet').classList.remove('open'); renderPendingBar(); };
4851
+ window.approvePlan = function() {
4852
+ if (!_planCtl) return;
4853
+ ensureControl();
4854
+ socket.emit('agent:respond', { id: _planCtl.id, decision: { approve: true } });
4855
+ _planCtl = null; closePlanSheet(); showThinking();
4856
+ };
4857
+ window.revisePlan = function() {
4858
+ if (!_planCtl) return;
4859
+ const fb = document.getElementById('plan-sheet-fb').value.trim();
4860
+ if (!fb) { document.getElementById('plan-sheet-fb').focus(); document.getElementById('plan-sheet-fb').placeholder = '先写下要怎么改,再点这个按钮'; return; }
4861
+ ensureControl();
4862
+ socket.emit('agent:respond', { id: _planCtl.id, decision: { approve: false, feedback: fb } });
4863
+ document.getElementById('plan-sheet-fb').value = '';
4864
+ _planCtl = null; closePlanSheet(); showThinking();
4500
4865
  };
4866
+ // ── 聊天里的方案卡片:ExitPlanMode 提交的方案、写进 ~/.claude/plans/*.md 的方案文件(原来只是个折叠的「写文件」,看不到内容)──
4867
+ function planTextOf(name, input) {
4868
+ if (!input) return null;
4869
+ if (name === 'ExitPlanMode') return String(input.plan || '');
4870
+ if (name === 'Write' && /[\\/]\.claude[\\/]plans[\\/][^\\/]+\.md$/.test(input.file_path || '')) return String(input.content || '');
4871
+ return null;
4872
+ }
4873
+ let _lastPlanText = '';
4874
+ function addPlanCard(text) {
4875
+ const t = String(text || '').trim();
4876
+ if (!t || t === _lastPlanText) return; // 先写方案文件、再 ExitPlanMode 提交同一份:只显示一次
4877
+ _lastPlanText = t;
4878
+ const el = document.createElement('div');
4879
+ el.className = 'msg msg-assistant msg-plan';
4880
+ el.innerHTML = '<div class="plan-head">📋 方案</div><div class="content"></div>';
4881
+ el.querySelector('.content').innerHTML = renderMarkdown(t);
4882
+ (_replayParent || chatArea).appendChild(el);
4883
+ if (!_replayParent) { trimMessages(); scrollToBottom(); }
4884
+ }
4501
4885
  document.getElementById('ask-sheet-input').addEventListener('keydown', (e) => {
4502
4886
  if (e.key === 'Enter') { e.preventDefault(); submitAskReply(); }
4503
4887
  });
@@ -4609,15 +4993,22 @@
4609
4993
  };
4610
4994
 
4611
4995
  // ── 清除会话 ──────────────────────────────────
4612
- window.doClear = function() {
4996
+ window.closeClearSheet = function() { document.getElementById('clear-sheet').classList.remove('open'); };
4997
+ window.doClear = function(confirmed) {
4998
+ // Agent 模式先弹说明确认:清除不可撤销,且用户常不清楚它会让 AI 忘掉上下文
4999
+ if (currentSession?.mode === 'agent' && !confirmed) {
5000
+ document.getElementById('clear-sheet').classList.add('open');
5001
+ return;
5002
+ }
5003
+ closeClearSheet();
5004
+ if (workState !== 'idle' && !confirm('AI 还在执行,清除会中断这一轮。确定清除?')) return;
4613
5005
  // 清除聊天区 UI
4614
5006
  chatArea.innerHTML = '';
4615
5007
  endStream();
4616
5008
  // Agent 模式:发送 /clear 清除 Claude 上下文
4617
5009
  if (currentSession?.mode === 'agent') {
4618
5010
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
4619
- socket.emit('agent:query', { prompt: '/clear' });
4620
- addStatusMessage('已清除对话和 Claude 上下文');
5011
+ socket.emit('agent:query', { prompt: '/clear' }); // 服务端回一条「已开始新对话」,所有在看的人都能看到
4621
5012
  _ctxNudgeLevel = 0;
4622
5013
  _lastCtxTokens = 0;
4623
5014
  ctxUpdate(0); // 上下文已清空,水位指示复位
@@ -4793,6 +5184,55 @@
4793
5184
  `本任务有 ${n} 条待发草稿 —— 点这里取回`;
4794
5185
  tip.style.display = 'flex';
4795
5186
  }
5187
+ // ── 项目说明(CLAUDE.md):状态标记 + 适时建议生成 ─────────────
5188
+ // 新用户不知道 CLAUDE.md,等看出是长期项目时再提:同目录还有别的任务 / 本任务聊了很多轮 / 说了「以后都…」。
5189
+ // 每个目录可「不再提示」(存本机);× 只关本页。
5190
+ const GUIDE_REMEMBER_RE = /以后(都|每次|一律)|每次都(要|得|先)|记住|总是要|别再忘|下次(也|还)要/;
5191
+ const GUIDE_TURNS = 15;
5192
+ let _guide = null, _guideRemember = false, _guideTipClosed = false;
5193
+ const guideOffKey = 'myhi_guide_tip_off';
5194
+ const guideOffList = () => { try { return JSON.parse(localStorage.getItem(guideOffKey) || '[]'); } catch { return []; } };
5195
+ async function checkGuide() {
5196
+ if (currentSession?.mode !== 'agent') return;
5197
+ try { const r = await fetch('/api/session/' + SESSION_ID + '/guide'); if (r.ok) _guide = await r.json(); } catch {}
5198
+ renderGuideChip(); renderGuideTip();
5199
+ }
5200
+ function renderGuideChip() {
5201
+ const c = document.getElementById('guide-chip');
5202
+ if (!c) return;
5203
+ if (!_guide?.exists) { c.style.display = 'none'; return; }
5204
+ c.style.display = '';
5205
+ c.style.color = _guide.long ? 'var(--yellow)' : 'var(--muted)';
5206
+ c.textContent = _guide.long ? '📘 项目说明偏长' : '📘 已带项目说明';
5207
+ c.title = _guide.long
5208
+ ? `项目说明(CLAUDE.md)有 ${_guide.lines} 行,每轮都会占用 AI 的记忆容量。建议精简到要点(可以让 AI 帮你精简)。点击查看`
5209
+ : `AI 每次开始工作前会先读本项目的项目说明(CLAUDE.md,${_guide.lines} 行)。点击查看 / 编辑`;
5210
+ }
5211
+ function guideTipReason() {
5212
+ if (!_guide || _guide.exists) return '';
5213
+ if (_guideRemember) return '想让 AI 以后每次都记得?可以写进「项目说明」,每次新对话 AI 都会先读,不用再重复交代。';
5214
+ if (_guide.siblings > 0) return `这个项目目录还有 ${_guide.siblings} 个其他任务在用。让 AI 把项目情况和你的要求写成「项目说明」,以后每个任务、每次对话都会先读。`;
5215
+ if (_guide.turns >= GUIDE_TURNS) return '这个项目你常来。让 AI 把项目情况和你的要求写成「项目说明」,以后每次新对话不用再重复交代。';
5216
+ return '';
5217
+ }
5218
+ window.renderGuideTip = function() {
5219
+ const tip = document.getElementById('guide-tip');
5220
+ if (!tip) return;
5221
+ const why = guideTipReason();
5222
+ const off = currentSession?.cwd && guideOffList().includes(currentSession.cwd);
5223
+ if (!why || off || _guideTipClosed || workState !== 'idle' || !canTakeControl()) { tip.style.display = 'none'; return; }
5224
+ document.getElementById('guide-tip-text').textContent = why;
5225
+ tip.style.display = 'flex';
5226
+ };
5227
+ window.guideTipGo = function() { _guideTipClosed = true; renderGuideTip(); genProjectGuide(); };
5228
+ window.guideTipClose = function() { _guideTipClosed = true; _guideRemember = false; renderGuideTip(); };
5229
+ window.guideTipOff = function() {
5230
+ const l = guideOffList();
5231
+ if (currentSession?.cwd && !l.includes(currentSession.cwd)) { l.push(currentSession.cwd); localStorage.setItem(guideOffKey, JSON.stringify(l.slice(-200))); }
5232
+ renderGuideTip();
5233
+ addStatusMessage('本项目不再提示生成项目说明(需要时在快捷栏「记忆」里生成)');
5234
+ };
5235
+
4796
5236
  window.hideDraftTip = function() { _draftTipDismissed = true; renderDraftTip(); };
4797
5237
 
4798
5238
  window.openInboxSheet = async function() {
@@ -4802,16 +5242,14 @@
4802
5242
  const cb = document.getElementById('inbox-new-autosend');
4803
5243
  if (cb) cb.checked = false;
4804
5244
  clearInboxNewImage();
5245
+ _ibSelMode = false; _ibSel.clear(); _ibEditing = null;
5246
+ ibTab('this');
4805
5247
  await refreshInbox(false);
4806
5248
  renderInbox();
4807
5249
  };
4808
5250
  window.closeInboxSheet = function() {
4809
5251
  document.getElementById('inbox-sheet').classList.remove('open');
4810
5252
  };
4811
- window.toggleInboxOthers = function() {
4812
- const box = document.getElementById('inbox-others');
4813
- box.style.display = box.style.display === 'none' ? '' : 'none';
4814
- };
4815
5253
 
4816
5254
  function draftPreview(d) {
4817
5255
  const t = (d.text || '').replace(/\s+/g, ' ').trim();
@@ -4836,48 +5274,228 @@
4836
5274
  return sameDay ? hm : `${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${hm}`;
4837
5275
  }
4838
5276
 
4839
- function renderInbox() {
4840
- const esc = (s) => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
4841
- const titleEl = document.getElementById('inbox-this-title');
4842
- if (titleEl) titleEl.textContent = currentSession?.title || '本会话';
5277
+ // ── 草稿面板(队列)─────────────────────────────
5278
+ let _ibSelMode = false; // 多选模式
5279
+ const _ibSel = new Set(); // 选中的草稿 id
5280
+ let _ibEditing = null; // 正在就地编辑的草稿 id
5281
+ let _ibIdleSince = Date.now(); // 最近一次变空闲的时刻:刚干完的几秒里自动接力还没发,别急着报「暂停」
5282
+ let _ibPauseTimer = null;
5283
+ const ibEsc = (s) => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
4843
5284
 
5285
+ window.ibTab = function(t) {
5286
+ document.querySelectorAll('.ib-tabs button').forEach(b => b.classList.toggle('on', b.dataset.ibt === t));
5287
+ document.getElementById('ib-pane-this').style.display = t === 'this' ? '' : 'none';
5288
+ document.getElementById('ib-pane-others').style.display = t === 'others' ? '' : 'none';
5289
+ };
5290
+
5291
+ function renderInbox() {
4844
5292
  const mine = myDrafts();
4845
- document.getElementById('inbox-this').innerHTML = mine.length ? mine.map(d => `
4846
- <div class="dr-item">
4847
- <div class="dr-text">${esc(draftPreview(d))}</div>
4848
- <div class="dr-meta">
4849
- <span title="${esc(new Date(d.createdAt).toLocaleString())}">🕒 ${draftTime(d.createdAt)} · ${draftAge(d.createdAt)}</span>
5293
+ for (const id of [..._ibSel]) if (!mine.some(d => d.id === id)) _ibSel.delete(id);
5294
+ document.getElementById('ib-n-this').textContent = mine.length ? `(${mine.length})` : '';
5295
+ const autoN = mine.filter(d => d.autoSend).length;
5296
+ const allAuto = mine.length > 0 && autoN === mine.length;
5297
+ const autoAll = document.getElementById('ib-auto-all');
5298
+ autoAll.checked = allAuto;
5299
+ autoAll.indeterminate = autoN > 0 && !allAuto;
5300
+ document.getElementById('ib-hint').textContent = !mine.length ? '还没有草稿。AI 忙时想到什么,先记在这里。'
5301
+ : autoN ? `共 ${mine.length} 条,${autoN} 条会按顺序自动发送(绿色序号),每干完一轮发下一条`
5302
+ : `共 ${mine.length} 条,按顺序排队。AI 干完后点「取回」发送`;
5303
+ const selBtn = document.getElementById('ib-sel-btn');
5304
+ selBtn.textContent = _ibSelMode ? '完成' : '☑ 多选';
5305
+ selBtn.style.display = mine.length > 1 || _ibSelMode ? '' : 'none';
5306
+
5307
+ // 暂停提示:有待自动发送的草稿,会话却已空闲一阵(上一轮被中断 / 出错时不会自动往下发)
5308
+ clearTimeout(_ibPauseTimer);
5309
+ const idleFor = Date.now() - _ibIdleSince;
5310
+ const paused = autoN > 0 && workState === 'idle' && idleFor > 6000;
5311
+ if (autoN > 0 && workState === 'idle' && !paused) _ibPauseTimer = setTimeout(() => { if (document.getElementById('inbox-sheet').classList.contains('open')) renderInbox(); }, 6100 - idleFor);
5312
+ document.getElementById('ib-paused').style.display = paused ? 'flex' : 'none';
5313
+
5314
+ const n = mine.length;
5315
+ document.getElementById('inbox-this').innerHTML = n ? mine.map((d, i) => {
5316
+ const editing = _ibEditing === d.id;
5317
+ const att = [d.images?.length ? `📷${d.images.length > 1 ? '×' + d.images.length : ''}` : '', d.files?.length ? `📎${d.files.length > 1 ? '×' + d.files.length : ''}` : ''].filter(Boolean).join(' ');
5318
+ const lead = _ibSelMode
5319
+ ? `<input type="checkbox" ${_ibSel.has(d.id) ? 'checked' : ''} onchange="ibPick('${d.id}', this.checked)">`
5320
+ : `<span class="ib-handle" title="拖动排序" onmousedown="ibArmDrag(this)">⋮⋮</span>`;
5321
+ const body = editing
5322
+ ? `<div class="ib-edit"><textarea id="ib-edit-ta" spellcheck="false">${ibEsc(d.text || '')}</textarea>
5323
+ <div><span style="font-size:0.64rem;color:var(--muted);margin-right:auto">Ctrl+Enter 保存 · Esc 取消</span>
5324
+ <button class="dr-btn" onclick="ibEditCancel()">取消</button><button class="dr-btn take" onclick="ibEditSave('${d.id}')">保存</button></div></div>`
5325
+ : `<div class="ib-text" onclick="${_ibSelMode ? `ibPick('${d.id}')` : `ibEdit('${d.id}')`}" title="${_ibSelMode ? '点选' : '点击编辑'}">${ibEsc((att ? att + ' ' : '') + ((d.text || '').trim() || '(仅附件)'))}</div>`;
5326
+ const meta = editing ? '' : `<div class="ib-meta">
5327
+ <span title="${ibEsc(new Date(d.createdAt).toLocaleString())}">🕒 ${draftTime(d.createdAt)}</span>
4850
5328
  ${d.source === 'busy' ? '<span class="dr-src">忙时自动存</span>' : ''}
4851
- <label style="cursor:pointer;color:${d.autoSend ? 'var(--yellow)' : 'var(--muted)'}"
4852
- title="勾上后:本轮干完、会话空闲时自动发出这条(不用你在页面前)。默认关。">
4853
- <input type="checkbox" ${d.autoSend ? 'checked' : ''} onchange="toggleAutoSend('${d.id}', this.checked)"
4854
- style="vertical-align:-1px"> 自动接力
4855
- </label>
4856
- <span class="dr-acts">
4857
- <button class="dr-btn take" onclick="useDraft('${d.id}')" title="取回到消息框,草稿随即删除">取回</button>
4858
- <button class="dr-btn" onclick="deleteDraft('${d.id}')">删</button>
4859
- </span>
4860
- </div>
4861
- </div>`).join('')
4862
- : '<div style="color:var(--muted);font-size:0.75rem;padding:0.5rem">(本任务暂无草稿)</div>';
5329
+ ${d.mergedFrom?.length ? `<span class="dr-src">由 ${d.mergedFrom.length} 条合并 · <a style="cursor:pointer;text-decoration:underline" onclick="ibUnmerge('${d.id}')">撤销合并</a></span>` : ''}
5330
+ <button class="ib-chip ${d.autoSend ? 'on' : ''}" onclick="toggleAutoSend('${d.id}', ${!d.autoSend})" title="${d.autoSend ? '这条会在轮到它时自动发出。点一下取消' : '点一下:轮到这条时自动发出'}">⚡ ${d.autoSend ? '自动' : '手动'}</button>
5331
+ ${_ibSelMode ? '' : `<span class="ib-acts">
5332
+ <button class="ib-ic" ${i === 0 ? 'disabled' : ''} onclick="ibMove('${d.id}', -1)" title="上移">↑</button>
5333
+ <button class="ib-ic" ${i === n - 1 ? 'disabled' : ''} onclick="ibMove('${d.id}', 1)" title="下移">↓</button>
5334
+ <button class="ib-ic" onclick="ibDelete('${d.id}')" title="删除">🗑</button>
5335
+ ${canInjectNow() ? `<button class="dr-btn" style="color:var(--yellow);border:1px solid var(--yellow);background:rgba(210,153,34,0.12)" onclick="injectDraft('${d.id}')" title="AI 正在干活:不打断,马上把这条送过去,AI 下一步就会看到">⚡ 插入</button>` : ''}
5336
+ <button class="dr-btn take" onclick="useDraft('${d.id}')" title="放回消息框,草稿随即出队">取回</button>
5337
+ </span>`}
5338
+ </div>`;
5339
+ return `<div class="ib-item ${_ibSel.has(d.id) ? 'sel' : ''}" data-id="${d.id}">${lead}<span class="ib-no ${d.autoSend ? 'auto' : ''}" title="${d.autoSend ? '第 ' + (i + 1) + ' 条 · 自动发送' : '第 ' + (i + 1) + ' 条'}">${i + 1}</span><div class="ib-main">${body}${meta}</div></div>`;
5340
+ }).join('') : '<div style="color:var(--muted);font-size:0.75rem;padding:1.2rem 0.5rem;text-align:center">本任务暂无草稿</div>';
5341
+
5342
+ const selbar = document.getElementById('ib-selbar');
5343
+ selbar.style.display = _ibSelMode ? 'flex' : 'none';
5344
+ document.getElementById('ib-sel-n').textContent = _ibSel.size;
5345
+ selbar.querySelectorAll('button').forEach((b, k) => { if (k < 3) b.disabled = k < 2 ? _ibSel.size < (k === 0 ? 2 : 1) : !_ibSel.size; });
5346
+
5347
+ if (_ibEditing) {
5348
+ const ta = document.getElementById('ib-edit-ta');
5349
+ if (ta && document.activeElement !== ta) { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); }
5350
+ if (ta) ta.onkeydown = (e) => {
5351
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); ibEditCancel(); }
5352
+ else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); ibEditSave(_ibEditing); }
5353
+ };
5354
+ }
5355
+ ibBindDrag();
4863
5356
 
5357
+ // 其它任务:按任务分组
4864
5358
  const others = otherDrafts();
4865
- document.getElementById('inbox-others-count').textContent = others.length ? `(${others.length})` : '(0)';
4866
- document.getElementById('inbox-others').innerHTML = others.length ? others.map(d => `
4867
- <div class="dr-item">
4868
- <div class="dr-sess">${esc(d.sessionTitle || '(未知任务)')}${d.projectName ? ' · ' + esc(d.projectName) : ''}${d.sessionAlive ? '' : ' <span style="color:var(--muted)">(已结束)</span>'}</div>
4869
- <div class="dr-text" style="color:var(--text2)">${esc(draftPreview(d))}</div>
4870
- <div class="dr-meta">
4871
- <span>🕒 ${draftTime(d.createdAt)} · ${draftAge(d.createdAt)}</span>
4872
- <span class="dr-acts">
4873
- <button class="dr-btn" onclick="reassignDraft('${d.id}')" title="改投到本任务">改投</button>
4874
- <button class="dr-btn" onclick="deleteDraft('${d.id}')">删</button>
4875
- </span>
4876
- </div>
4877
- </div>`).join('')
4878
- : '<div style="color:var(--muted);font-size:0.75rem;padding:0.5rem">(没有其它任务的草稿)</div>';
5359
+ document.getElementById('inbox-others-count').textContent = others.length ? `(${others.length})` : '';
5360
+ const groups = new Map();
5361
+ for (const d of others) { const k = d.sessionId; if (!groups.has(k)) groups.set(k, []); groups.get(k).push(d); }
5362
+ document.getElementById('inbox-others').innerHTML = others.length ? [...groups.values()].map(g => `
5363
+ <div class="ib-grp">${ibEsc(g[0].sessionTitle || '(未知任务)')}${g[0].projectName ? ' · ' + ibEsc(g[0].projectName) : ''}${g[0].sessionAlive ? '' : ' <span style="color:var(--muted);font-weight:400">(已结束)</span>'}</div>
5364
+ ${g.map(d => `<div class="ib-item"><div class="ib-main">
5365
+ <div class="ib-text" style="cursor:default;color:var(--text2)">${ibEsc(draftPreview(d))}</div>
5366
+ <div class="ib-meta"><span>🕒 ${draftTime(d.createdAt)} · ${draftAge(d.createdAt)}</span>
5367
+ <span class="ib-acts"><button class="ib-ic" onclick="deleteDraft('${d.id}')" title="删除">🗑</button>
5368
+ <button class="dr-btn" onclick="reassignDraft('${d.id}')" title="挪到本任务,排到队尾">改投到本任务</button></span></div>
5369
+ </div></div>`).join('')}`).join('')
5370
+ : '<div style="color:var(--muted);font-size:0.75rem;padding:1.2rem 0.5rem;text-align:center">没有其它任务的草稿</div>';
5371
+ }
5372
+
5373
+ async function ibReload() { await refreshInbox(); renderInbox(); }
5374
+ async function ibPost(url, body) {
5375
+ const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
5376
+ const d = await r.json().catch(() => ({}));
5377
+ if (!r.ok || d.error) throw new Error(d.error || ('HTTP ' + r.status));
5378
+ return d;
5379
+ }
5380
+
5381
+ // 就地编辑
5382
+ window.ibEdit = function(id) { _ibEditing = id; renderInbox(); };
5383
+ window.ibEditCancel = function() { _ibEditing = null; renderInbox(); };
5384
+ window.ibEditSave = async function(id) {
5385
+ const ta = document.getElementById('ib-edit-ta');
5386
+ const text = ta ? ta.value : '';
5387
+ const d = myDrafts().find(x => x.id === id);
5388
+ if (!text.trim() && !d?.images?.length && !d?.files?.length) { addStatusMessage('草稿不能为空;不要了可以删除'); return; }
5389
+ try {
5390
+ await fetch('/api/inbox/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) });
5391
+ } catch { addStatusMessage('保存失败,请重试'); return; }
5392
+ _ibEditing = null;
5393
+ await ibReload();
5394
+ };
5395
+
5396
+ // 排序:↑↓(手机)/ 拖动(电脑)都汇成「新顺序」提交
5397
+ async function ibSaveOrder(ids) {
5398
+ const byId = new Map(_inboxCache.drafts.map(d => [d.id, d]));
5399
+ // 先本地生效再提交,拖动 / 连点 ↑↓ 不卡
5400
+ const mine = ids.map(id => byId.get(id)).filter(Boolean);
5401
+ _inboxCache.drafts = _inboxCache.drafts.filter(d => d.sessionId !== SESSION_ID).concat(mine);
5402
+ renderInbox();
5403
+ try { await ibPost('/api/inbox/reorder', { sessionId: SESSION_ID, ids }); } catch (e) { addStatusMessage('保存顺序失败:' + e.message); }
5404
+ await ibReload();
5405
+ }
5406
+ window.ibMove = function(id, dir) {
5407
+ const ids = myDrafts().map(d => d.id);
5408
+ const i = ids.indexOf(id), j = i + dir;
5409
+ if (i < 0 || j < 0 || j >= ids.length) return;
5410
+ [ids[i], ids[j]] = [ids[j], ids[i]];
5411
+ ibSaveOrder(ids);
5412
+ };
5413
+ window.ibArmDrag = function(h) { const it = h.closest('.ib-item'); if (it) it.draggable = true; };
5414
+ let _ibDragId = null;
5415
+ function ibBindDrag() {
5416
+ document.querySelectorAll('#inbox-this .ib-item').forEach(it => {
5417
+ it.ondragstart = (e) => { _ibDragId = it.dataset.id; it.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', _ibDragId); } catch {} };
5418
+ it.ondragend = () => { it.draggable = false; it.classList.remove('dragging'); _ibDragId = null; document.querySelectorAll('.ib-item').forEach(x => x.classList.remove('drag-over-top', 'drag-over-bottom')); };
5419
+ it.ondragover = (e) => {
5420
+ if (!_ibDragId || it.dataset.id === _ibDragId) return;
5421
+ e.preventDefault();
5422
+ const r = it.getBoundingClientRect(), top = e.clientY < r.top + r.height / 2;
5423
+ it.classList.toggle('drag-over-top', top); it.classList.toggle('drag-over-bottom', !top);
5424
+ };
5425
+ it.ondragleave = () => it.classList.remove('drag-over-top', 'drag-over-bottom');
5426
+ it.ondrop = (e) => {
5427
+ e.preventDefault();
5428
+ if (!_ibDragId || it.dataset.id === _ibDragId) return;
5429
+ const top = it.classList.contains('drag-over-top');
5430
+ const ids = myDrafts().map(d => d.id).filter(x => x !== _ibDragId);
5431
+ ids.splice(ids.indexOf(it.dataset.id) + (top ? 0 : 1), 0, _ibDragId);
5432
+ ibSaveOrder(ids);
5433
+ };
5434
+ });
4879
5435
  }
4880
5436
 
5437
+ // 多选
5438
+ window.ibToggleSelect = function(on) {
5439
+ _ibSelMode = typeof on === 'boolean' ? on : !_ibSelMode;
5440
+ _ibSel.clear(); _ibEditing = null;
5441
+ renderInbox();
5442
+ };
5443
+ window.ibPick = function(id, on) {
5444
+ if (on === undefined) on = !_ibSel.has(id);
5445
+ on ? _ibSel.add(id) : _ibSel.delete(id);
5446
+ renderInbox();
5447
+ };
5448
+ window.ibMergeSelected = async function(take) {
5449
+ const ids = myDrafts().map(d => d.id).filter(id => _ibSel.has(id)); // 按列表顺序
5450
+ if (!ids.length) return;
5451
+ if (ids.length === 1) { if (take) { _ibSelMode = false; _ibSel.clear(); return useDraft(ids[0]); } return; }
5452
+ let merged;
5453
+ try { merged = (await ibPost('/api/inbox/merge', { ids })).draft; }
5454
+ catch (e) { addStatusMessage('合并失败:' + e.message); return; }
5455
+ _ibSelMode = false; _ibSel.clear();
5456
+ await refreshInbox();
5457
+ if (take) return useDraft(merged.id);
5458
+ _ibEditing = merged.id;
5459
+ renderInbox();
5460
+ addStatusMessage(`📥 已把 ${ids.length} 条合并成一条,可以直接改;不满意可点「撤销合并」`);
5461
+ };
5462
+ window.ibDeleteSelected = async function() {
5463
+ const ids = [..._ibSel];
5464
+ if (!ids.length || !confirm(`删除选中的 ${ids.length} 条草稿?`)) return;
5465
+ for (const id of ids) { try { await fetch('/api/inbox/' + id, { method: 'DELETE' }); } catch {} }
5466
+ _ibSelMode = false; _ibSel.clear();
5467
+ await ibReload();
5468
+ };
5469
+ window.ibDelete = async function(id) {
5470
+ const d = myDrafts().find(x => x.id === id);
5471
+ if (d && (d.text || '').trim().length > 20 && !confirm('删除这条草稿?\n\n' + d.text.trim().slice(0, 80))) return;
5472
+ await deleteDraft(id);
5473
+ };
5474
+ window.ibUnmerge = async function(id) {
5475
+ try { await ibPost('/api/inbox/' + id + '/unmerge'); } catch (e) { addStatusMessage('撤销合并失败:' + e.message); return; }
5476
+ _ibEditing = null;
5477
+ await ibReload();
5478
+ addStatusMessage('📥 已撤销合并,原来的几条已放回');
5479
+ };
5480
+
5481
+ // 依次自动发送:整队开 / 关
5482
+ window.ibAutoAll = async function(on) {
5483
+ const mine = myDrafts();
5484
+ const targets = mine.filter(d => !!d.autoSend !== on);
5485
+ if (on && !confirm(`按顺序自动发送 ${mine.length} 条草稿?\n\n每干完一轮自动发下一条,共约 ${mine.length} 轮模型调用。\n上一轮被中断或出错时会暂停,等你确认再继续。`)) { renderInbox(); return; }
5486
+ for (const d of targets) {
5487
+ try { await fetch('/api/inbox/' + d.id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ autoSend: on }) }); } catch {}
5488
+ }
5489
+ await ibReload();
5490
+ addStatusMessage(on ? (workState === 'idle' ? '📥 已开启依次自动发送,马上发出第 1 条' : '📥 已开启依次自动发送:本轮干完后从第 1 条开始') : '📥 已关闭自动发送');
5491
+ };
5492
+ window.ibResume = async function() {
5493
+ try { await ibPost('/api/inbox/resume', { sessionId: SESSION_ID }); } catch (e) { addStatusMessage('继续失败:' + e.message); return; }
5494
+ _ibIdleSince = Date.now();
5495
+ renderInbox();
5496
+ addStatusMessage('📥 继续按顺序发送草稿');
5497
+ };
5498
+
4881
5499
  // ── 抽屉内就地新增草稿(不必先在消息框打字再"提取")──────────
4882
5500
  let _inboxNewImage = null; // { path, name }
4883
5501
 
@@ -4965,13 +5583,16 @@
4965
5583
  window.useDraft = async function(id) {
4966
5584
  const d = _inboxCache.drafts.find(x => x.id === id);
4967
5585
  if (!d) return;
4968
- cmdInput.value = d.text || '';
5586
+ // 消息框一次只能挂一张图 / 一个文件:合并来的多余附件以路径写进文字里,AI 照样会读
5587
+ const extra = [...(d.images || []).slice(1).map(a => `另附图片 ${a.path},请用 Read 工具读取。`),
5588
+ ...(d.files || []).slice(d.images?.length ? 0 : 1).map(a => `另附文件 ${a.path}(原文件名 ${a.name}),请读取分析。`)];
5589
+ cmdInput.value = (d.text || '') + (extra.length ? '\n' + extra.join('\n') : '');
4969
5590
  cmdInput.style.height = 'auto';
4970
5591
  cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
4971
5592
  clearPendingImage();
4972
5593
  clearPendingFile();
4973
5594
  if (d.images?.length) setPendingImage(d.images[0].path, '');
4974
- if (d.files?.length) {
5595
+ else if (d.files?.length) {
4975
5596
  _pendingFile = { path: d.files[0].path, name: d.files[0].name };
4976
5597
  const fp = document.getElementById('file-preview');
4977
5598
  const nameEl = document.getElementById('file-preview-name');
@@ -5215,16 +5836,28 @@
5215
5836
  const isPlan = currentSession?.permissionMode === 'plan';
5216
5837
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
5217
5838
  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('已退出计划模式,恢复到自动执行');
5839
+ // AI 已经提交了方案在等审批:直接打开审批(批准即在原进程里切到执行,不打断)
5840
+ if (_planCtl) return openPlanSheet();
5841
+ document.getElementById('planexit-sheet').classList.add('open');
5223
5842
  } else {
5224
5843
  // 进入计划:弹窗输入计划描述
5225
5844
  doSlashCmd('/plan');
5226
5845
  }
5227
5846
  };
5847
+ window.closePlanExit = function() { document.getElementById('planexit-sheet').classList.remove('open'); };
5848
+ // 手动退出计划:切回自动执行会重启 claude 进程——AI 还在规划时先确认,免得这一轮被悄悄打断
5849
+ window.exitPlan = function(run) {
5850
+ closePlanExit();
5851
+ if (workState !== 'idle' && !confirm('AI 还在规划,现在退出会中断这一轮。确定退出?')) return;
5852
+ socket.emit('set-mode', { sessionId: SESSION_ID, mode: 'bypassPermissions' });
5853
+ if (currentSession) currentSession.permissionMode = 'bypassPermissions';
5854
+ updatePlanButton();
5855
+ addStatusMessage('已退出计划模式,恢复到自动执行');
5856
+ if (run) setTimeout(() => {
5857
+ const t = '按刚才的方案开始执行。';
5858
+ addInputMessage(t); socket.emit('agent:query', { prompt: t }); showThinking();
5859
+ }, 600);
5860
+ };
5228
5861
  socket.on('mode-changed', ({ sessionId, mode }) => {
5229
5862
  if (sessionId !== SESSION_ID) return;
5230
5863
  if (currentSession) currentSession.permissionMode = mode;
@@ -5756,10 +6389,22 @@
5756
6389
  container.innerHTML = '';
5757
6390
  container.appendChild(makeBar());
5758
6391
 
6392
+ // 本项目还没有项目说明(CLAUDE.md):讲清它是什么、适合什么项目,给一键生成入口——新用户不必知道这个文件名
6393
+ if (_memScope === 'project' && !files.some(f => f.name === 'CLAUDE.md')) {
6394
+ const intro = document.createElement('div');
6395
+ intro.style.cssText = 'border:1px dashed var(--border);border-radius:10px;padding:0.75rem 0.85rem;margin-bottom:0.6rem;line-height:1.7';
6396
+ intro.innerHTML = '<div style="font-weight:700;color:var(--text);font-size:0.86rem">📘 本项目还没有「项目说明」</div>'
6397
+ + '<div>项目说明是 AI <b>每次开始工作前都会先读</b>的一页项目规矩:项目是做什么的、关键目录、怎么构建测试、你的固定要求(如「回复用中文」「改完先跑测试」)。有了它,每次新对话不用再重复交代。</div>'
6398
+ + '<div style="color:var(--muted);margin-top:0.2rem">适合会反复来做的长期项目;一次性的小任务不需要。</div>'
6399
+ + '<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>'
6400
+ + '<div style="color:var(--muted);font-size:0.7rem;margin-top:0.3rem">AI 会先看一遍项目、写出草稿给你确认,确认后才保存(文件名 CLAUDE.md,放在项目目录里)。</div>';
6401
+ container.appendChild(intro);
6402
+ }
6403
+
5759
6404
  if (files.length === 0) {
5760
6405
  const e = document.createElement('div');
5761
6406
  e.style.cssText = 'text-align:center;color:var(--muted);padding:1.5rem 0';
5762
- e.textContent = _memScope === 'project' ? '本项目暂无记忆文件' : '暂无记忆文件';
6407
+ e.textContent = _memScope === 'project' ? '还没有其他记忆(AI 在对话中记下的经验会出现在这里)' : '暂无记忆文件';
5763
6408
  container.appendChild(e);
5764
6409
  return;
5765
6410
  }
@@ -5771,7 +6416,9 @@
5771
6416
  const dm = m[1].match(/^description:\s*(.+)$/m);
5772
6417
  return { desc: dm ? dm[1].trim() : '', body: content.slice(m[0].length) };
5773
6418
  };
5774
- const cleanName = (n) => n.replace(/^.*\//, '').replace(/\.md$/i, '');
6419
+ // CLAUDE.md 用大白话称呼(文件名放进描述里给懂行的人看)
6420
+ const NICE = { 'CLAUDE.md': ['📘 项目说明', 'CLAUDE.md · AI 每次开始工作前先读的项目规矩'], '~/.claude/CLAUDE.md': ['🌐 全局说明', '~/.claude/CLAUDE.md · 对所有项目都生效'] };
6421
+ const cleanName = (n) => NICE[n] ? NICE[n][0] : n.replace(/^.*\//, '').replace(/\.md$/i, '');
5775
6422
 
5776
6423
  // 搜索框
5777
6424
  const search = document.createElement('input');
@@ -5789,7 +6436,7 @@
5789
6436
  for (const f of files) {
5790
6437
  if (kw && !(f.name.toLowerCase().includes(kw) || (f.content || '').toLowerCase().includes(kw))) continue;
5791
6438
  shown++;
5792
- const { desc, body: md } = parseFM(f.content || '');
6439
+ const fm = parseFM(f.content || ''); const md = fm.body; const desc = fm.desc || (NICE[f.name] ? NICE[f.name][1] : '');
5793
6440
  const deletable = f.name.startsWith('memory/') || f.name.startsWith('~/.claude/memory/');
5794
6441
  const editable = deletable || f.name.endsWith('CLAUDE.md') || f.name.endsWith('settings.json');
5795
6442
  const card = document.createElement('div');
@@ -5837,6 +6484,205 @@
5837
6484
  };
5838
6485
 
5839
6486
  // 让 AI 按规则过一遍记忆(只分析给建议,不改文件)
6487
+ // 一键生成项目说明(CLAUDE.md):AI 先调研、出草稿,用户确认后才写文件——项目说明会改变 AI 之后的行为,不能静默写入
6488
+ window.genProjectGuide = function() {
6489
+ closeMemorySheet();
6490
+ if (currentSession?.mode !== 'agent') return addStatusMessage('该功能仅在 agent(Claude) 会话可用');
6491
+ if (workState !== 'idle') return addStatusMessage('AI 正在干活,等这一轮结束再生成项目说明');
6492
+ const prompt = [
6493
+ '请为当前项目目录写一份项目说明(项目根目录的 CLAUDE.md,以后每次对话开始你都会先读它)。',
6494
+ '步骤:1) 先快速了解项目:目录结构、README、构建/依赖配置(如 package.json、pyproject、Makefile 等),不用逐个细读源码;',
6495
+ '2) 结合本次对话里我交代过的要求和习惯;',
6496
+ '3) 用中文写一份精简草稿(建议 30~60 行,只写要点):项目是做什么的、关键目录/文件、怎么运行/构建/测试、约定与注意事项、我的固定要求;',
6497
+ '不要写任何密码、token、密钥等敏感值。',
6498
+ '4) 先把完整草稿贴出来让我确认,问我是否保存(可以直接保存 / 我要修改);我确认后再写入 CLAUDE.md。在我确认前不要创建或修改任何文件。',
6499
+ '如果项目根目录已经有 CLAUDE.md,就在它基础上补充,并说明改了哪些。',
6500
+ ].join('');
6501
+ if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
6502
+ addInputMessage('📘 让 AI 生成项目说明');
6503
+ socket.emit('agent:query', { prompt });
6504
+ showThinking();
6505
+ };
6506
+
6507
+ // ── 💡 问一问 ─────────────────────────────────────
6508
+ // 在聊天里选中一段文字 → 浮出「解释 / 问一问 / 复制」;回答在旁边小窗里流式出现,可以继续追问。
6509
+ // 服务端为每次提问起一个一次性的 claude(不带工具、不落会话),正在干活的 AI 看不到这些问答;
6510
+ // 想让它知道就点「👉 告诉 AI」,把要点放进输入框由用户确认后发送。
6511
+ const askPop = document.getElementById('ask-pop');
6512
+ const askPanel = document.getElementById('ask-panel');
6513
+ const askBody = document.getElementById('ask-body');
6514
+ const askInput = document.getElementById('ask-input');
6515
+ const askSendBtn = document.getElementById('ask-send');
6516
+ const _askTouch = window.matchMedia('(hover: none)');
6517
+ if (_askTouch.matches) askInput.placeholder = '继续问…';
6518
+ let _askSel = null; // 当前选区 { text, msgEl }
6519
+ let _askThread = null; // { selection, context, userAsk, items: [{ q, a, err, done }] }
6520
+ let _askReq = null; // 正在等回答的 reqId
6521
+ const askClip = (s, n) => { s = String(s || '').trim(); return s.length > n ? s.slice(0, n) + '…' : s; };
6522
+
6523
+ function askHidePop() { askPop.classList.remove('show'); _askSel = null; }
6524
+ function askCheckSelection() {
6525
+ if (currentSession?.mode !== 'agent') return askHidePop();
6526
+ const sel = window.getSelection();
6527
+ const text = sel && !sel.isCollapsed ? sel.toString().trim() : '';
6528
+ if (!text || !sel.rangeCount) return askHidePop();
6529
+ const node = sel.anchorNode && (sel.anchorNode.nodeType === 1 ? sel.anchorNode : sel.anchorNode.parentElement);
6530
+ const msgEl = node && node.closest && node.closest('#chat-area .msg');
6531
+ if (!msgEl) return askHidePop();
6532
+ _askSel = { text, msgEl };
6533
+ askPop.classList.add('show');
6534
+ const pw = askPop.offsetWidth, ph = askPop.offsetHeight;
6535
+ if (_askTouch.matches) {
6536
+ // 手机:系统自带的选择菜单在选区上方,工具条放到输入框上方,互不遮挡
6537
+ const ia = document.getElementById('input-area').getBoundingClientRect();
6538
+ askPop.style.left = Math.max(8, (window.innerWidth - pw) / 2) + 'px';
6539
+ askPop.style.top = Math.max(8, ia.top - ph - 10) + 'px';
6540
+ } else {
6541
+ const r = sel.getRangeAt(0).getBoundingClientRect();
6542
+ let top = r.top - ph - 8; if (top < 56) top = r.bottom + 8;
6543
+ askPop.style.left = Math.min(window.innerWidth - pw - 8, Math.max(8, r.left + r.width / 2 - pw / 2)) + 'px';
6544
+ askPop.style.top = top + 'px';
6545
+ }
6546
+ }
6547
+ let _askSelTimer = null;
6548
+ document.addEventListener('selectionchange', () => { clearTimeout(_askSelTimer); _askSelTimer = setTimeout(askCheckSelection, 250); });
6549
+ chatArea.addEventListener('scroll', () => { if (askPop.classList.contains('show')) askCheckSelection(); }, { passive: true });
6550
+
6551
+ // 选区所在消息的上下文:消息原文(太长就截选区附近)+ 用户在它之前说的那句需求
6552
+ function askContextOf(msgEl, selText) {
6553
+ let ctx = (msgEl.innerText || '').trim();
6554
+ if (ctx.length > 3000) {
6555
+ const i = Math.max(0, ctx.indexOf(selText.slice(0, 50)));
6556
+ ctx = ctx.slice(Math.max(0, i - 1500), i + 1500);
6557
+ }
6558
+ let userAsk = '';
6559
+ for (let el = msgEl.previousElementSibling; el; el = el.previousElementSibling) {
6560
+ if (el.classList.contains('msg-input')) { userAsk = (el.innerText || '').trim(); break; }
6561
+ }
6562
+ return { context: ctx, userAsk };
6563
+ }
6564
+
6565
+ window.askFromSelection = function(custom) {
6566
+ if (!_askSel) return;
6567
+ const { text, msgEl } = _askSel;
6568
+ const { context, userAsk } = askContextOf(msgEl, text);
6569
+ askHidePop();
6570
+ try { window.getSelection().removeAllRanges(); } catch {}
6571
+ askCancel();
6572
+ _askThread = { selection: text, context, userAsk, items: [] };
6573
+ openAsk();
6574
+ if (custom) { renderAsk(); askInput.focus(); }
6575
+ else askSend('');
6576
+ };
6577
+ window.askCopySelection = function() {
6578
+ if (!_askSel) return;
6579
+ const t = _askSel.text;
6580
+ (navigator.clipboard?.writeText(t) || Promise.reject()).then(() => addStatusMessage('已复制'), () => {
6581
+ const ta = document.createElement('textarea'); ta.value = t; document.body.appendChild(ta); ta.select();
6582
+ try { document.execCommand('copy'); addStatusMessage('已复制'); } catch {} ta.remove();
6583
+ });
6584
+ askHidePop();
6585
+ };
6586
+
6587
+ function openAsk() {
6588
+ askPanel.classList.add('open');
6589
+ document.getElementById('ask-backdrop').classList.toggle('show', window.innerWidth <= 768);
6590
+ }
6591
+ window.closeAsk = function() {
6592
+ askPanel.classList.remove('open');
6593
+ document.getElementById('ask-backdrop').classList.remove('show');
6594
+ };
6595
+ function askCancel() {
6596
+ if (_askReq) { socket.emit('aside:cancel'); _askReq = null; }
6597
+ if (_askThread) _askThread.items.forEach(it => { if (!it.done) { it.done = true; if (!it.a) it.err = '已停止'; } });
6598
+ }
6599
+
6600
+ function renderAsk() {
6601
+ const t = _askThread;
6602
+ if (!t) { askBody.innerHTML = ''; return; }
6603
+ let html = `<div class="ask-quote">${escHtml(askClip(t.selection, 600))}</div>`;
6604
+ t.items.forEach((it, i) => {
6605
+ html += `<div class="ask-q"><span>${escHtml(it.q || '解释一下这段是什么意思')}</span></div>`;
6606
+ if (it.err && !it.a) html += `<div class="ask-a err">⚠️ ${escHtml(it.err)}</div>`;
6607
+ else html += `<div class="ask-a md" data-i="${i}">${it.a ? renderMarkdown(it.a) : '<span style="color:var(--muted)">思考中…</span>'}</div>`;
6608
+ if (it.err && it.a) html += `<div class="ask-a err">⚠️ ${escHtml(it.err)}</div>`;
6609
+ if (it.done && it.a) {
6610
+ html += `<div class="ask-acts">${canTakeControl() ? `<button class="pri" onclick="askTellAI(${i})" title="把这段问答的要点放进输入框,你确认后再发给正在干活的 AI">👉 告诉 AI</button>` : ''}`
6611
+ + `<button onclick="askCopyAnswer(${i})">📋 复制</button></div>`;
6612
+ }
6613
+ });
6614
+ if (!t.items.length) html += '<div style="color:var(--muted);font-size:0.78rem">在下面输入你想问的,比如「这是什么意思?」「为什么要这样做?」「和 XX 有什么区别?」</div>';
6615
+ askBody.innerHTML = html;
6616
+ askBody.scrollTop = askBody.scrollHeight;
6617
+ const busy = !!_askReq;
6618
+ askSendBtn.textContent = busy ? '停止' : '发送';
6619
+ askSendBtn.classList.toggle('stop', busy);
6620
+ }
6621
+
6622
+ function askSend(q) {
6623
+ const t = _askThread;
6624
+ if (!t) return;
6625
+ const history = t.items.filter(it => it.done && it.a).map(it => ({ q: it.q || '解释一下这段是什么意思', a: it.a }));
6626
+ const it = { q, a: '', err: '', done: false };
6627
+ t.items.push(it);
6628
+ _askReq = 'a' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
6629
+ it.reqId = _askReq;
6630
+ socket.emit('aside:ask', { reqId: _askReq, selection: t.selection, context: t.context, userAsk: t.userAsk, question: q, history });
6631
+ renderAsk();
6632
+ }
6633
+ window.askSendOrStop = function() {
6634
+ if (_askReq) { askCancel(); renderAsk(); return; }
6635
+ const q = askInput.value.trim();
6636
+ if (!q) { askInput.focus(); return; }
6637
+ if (!_askThread) _askThread = { selection: '', context: '', userAsk: '', items: [] };
6638
+ askInput.value = ''; askInput.style.height = '';
6639
+ askSend(q);
6640
+ };
6641
+ askInput.addEventListener('keydown', (e) => {
6642
+ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !_askTouch.matches) { e.preventDefault(); askSendOrStop(); }
6643
+ else if (e.key === 'Escape') { e.preventDefault(); closeAsk(); }
6644
+ });
6645
+ askInput.addEventListener('input', () => { askInput.style.height = 'auto'; askInput.style.height = Math.min(askInput.scrollHeight, 150) + 'px'; });
6646
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && askPanel.classList.contains('open') && document.activeElement !== askInput) closeAsk(); });
6647
+
6648
+ const askItemOf = (reqId) => _askThread?.items.find(it => it.reqId === reqId);
6649
+ socket.on('aside:delta', ({ reqId, text }) => {
6650
+ const it = askItemOf(reqId);
6651
+ if (!it || it.done) return;
6652
+ it.a = text;
6653
+ const el = askBody.querySelector(`.ask-a[data-i="${_askThread.items.indexOf(it)}"]`);
6654
+ if (el) { const atBottom = askBody.scrollHeight - askBody.scrollTop - askBody.clientHeight < 40; el.innerHTML = renderMarkdown(text); if (atBottom) askBody.scrollTop = askBody.scrollHeight; }
6655
+ else renderAsk();
6656
+ });
6657
+ socket.on('aside:done', ({ reqId, text, error, partial }) => {
6658
+ const it = askItemOf(reqId);
6659
+ if (_askReq === reqId) _askReq = null;
6660
+ if (!it || it.done) { renderAsk(); return; }
6661
+ it.done = true;
6662
+ if (text) it.a = text;
6663
+ else { it.a = partial || it.a; it.err = error || '没有拿到回答,稍后再试'; }
6664
+ renderAsk();
6665
+ });
6666
+
6667
+ // 👉 告诉 AI:把问答要点放进输入框(不直接发),用户补上自己的想法后再发给干活的 AI
6668
+ window.askTellAI = function(i) {
6669
+ const t = _askThread, it = t?.items[i];
6670
+ if (!it) return;
6671
+ const firstPara = askClip(String(it.a).split(/\n\s*\n/)[0].replace(/[#*`>]/g, ''), 200);
6672
+ const about = t.selection ? `关于你刚才说的「${askClip(t.selection.replace(/\s+/g, ' '), 60)}」,` : '';
6673
+ const asked = it.q ? `我问了一下「${askClip(it.q, 60)}」,` : '';
6674
+ cmdInput.value = `${about}${asked}了解到:${firstPara}\n我的想法是:`;
6675
+ cmdInput.dispatchEvent(new Event('input'));
6676
+ if (window.innerWidth <= 768) closeAsk();
6677
+ cmdInput.focus();
6678
+ cmdInput.setSelectionRange(cmdInput.value.length, cmdInput.value.length);
6679
+ addStatusMessage('已放进输入框,补上你的想法后发送给 AI');
6680
+ };
6681
+ window.askCopyAnswer = function(i) {
6682
+ const a = _askThread?.items[i]?.a;
6683
+ if (a) navigator.clipboard?.writeText(a).then(() => addStatusMessage('已复制回答'), () => {});
6684
+ };
6685
+
5840
6686
  window.analyzeMemory = function() {
5841
6687
  const scopeTxt = _memScope === 'all' ? '所有项目 + 全局' : '本项目';
5842
6688
  const prompt = [