@wendongfly/myhi 1.3.193 → 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
@@ -294,6 +294,12 @@
294
294
  @media (pointer: coarse) { #cmd-input { font-size: 1rem; padding: 0.75rem 0.75rem 0.35rem; line-height: 1.55; } }
295
295
  #input-toolbar { display: flex; align-items: center; padding: 0.25rem 0.4rem 0.4rem; gap: 0.15rem; }
296
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); }
297
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; }
298
304
  .tb-btn:hover { background: var(--surface2); color: var(--text); }
299
305
  .tb-btn:active { transform: scale(0.92); }
@@ -448,6 +454,52 @@
448
454
  .dr-btn.take { background: var(--green); color: #fff; font-weight: 600; }
449
455
  .dr-btn:active { opacity: 0.75; }
450
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; }
451
503
  .dr-sess { font-size: 0.68rem; color: var(--blue2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
452
504
  .mem-file { margin-bottom: 0.5rem; border: 1px solid var(--surface2); border-radius: 8px; overflow: hidden; }
453
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; }
@@ -488,6 +540,44 @@
488
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; }
489
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; }
490
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
+ }
491
581
  </style>
492
582
  </head>
493
583
  <body>
@@ -567,6 +657,8 @@
567
657
  <button class="tb-btn" id="send-btn" onclick="sendCommand()" title="发送">
568
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>
569
659
  </button>
660
+ <!-- ⚡ 插话:AI 忙时把输入直接送进正在干的这一轮(不打断,AI 下一步看到);存草稿则等干完再发 -->
661
+ <button id="inject-btn" class="inj-btn" style="display:none" onclick="injectFromInput()" title="不打断 AI:马上把这条送过去,AI 在下一步就会看到并调整。&#10;适合补充、提醒、微调方向;方向完全错了请先点 ■ 停止">⚡ 插话</button>
570
662
  <!-- AI 忙时顶替发送键:这轮还没完(可能在跑子代理),先把想法存成草稿 -->
571
663
  <button class="tb-btn" id="draft-btn" style="display:none;color:var(--yellow)" onclick="saveDraftFromInput('busy')" title="AI 还在跑,先存为草稿,干完再发">
572
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>
@@ -880,44 +972,61 @@
880
972
  </div>
881
973
  </div>
882
974
 
883
- <!-- 灵感收件箱(草稿队列):会话正忙时先记下想法,干完再发。每条强制归属一个任务 -->
975
+ <!-- 灵感收件箱(草稿队列):会话正忙时先记下想法,干完再发。每条强制归属一个任务。
976
+ 本任务的草稿是一个有顺序的队列:可拖动 / ↑↓ 排序、就地编辑、多选合并 / 一起取回,开「依次自动发送」后每干完一轮发下一条 -->
884
977
  <div id="inbox-sheet" class="action-sheet">
885
978
  <div class="action-sheet-backdrop" onclick="closeInboxSheet()"></div>
886
- <div class="action-sheet-box" style="max-height:85vh;display:flex;flex-direction:column">
887
- <div class="action-sheet-title">📥 灵感收件箱 · 草稿</div>
888
- <div style="font-size:0.68rem;color:var(--muted);padding:0 0.2rem 0.5rem;line-height:1.55">
889
- 随手记想法:先存这儿,AI 干完后<b>取回</b>发送;勾「<b>自动接力</b>」则干完自动发出。只存本机,细节见各条目按钮。
890
- </div>
891
- <!-- 就地新增:不必先在消息框打字再"提取",直接在这儿写 -->
892
- <div style="border-top:1px solid var(--surface2);padding:0.5rem 0.2rem">
893
- <textarea id="inbox-new-text" rows="3" spellcheck="false" placeholder="写下临时想法…(截图可直接 Ctrl+V 粘进来)"
894
- 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>
895
- <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)">
896
- <span>📷</span><span id="inbox-new-img-name" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
897
- <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>
898
985
  </div>
899
- <div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.45rem;flex-wrap:wrap">
900
- <button class="dr-btn" onclick="document.getElementById('inbox-new-file').click()" title="也可以直接 Ctrl+V 粘贴截图">📷 选图片</button>
901
- <input type="file" id="inbox-new-file" accept="image/*" style="display:none"
902
- onchange="uploadInboxImage(this.files[0]); this.value='';">
903
- <label style="display:flex;align-items:center;gap:0.3rem;font-size:0.7rem;color:var(--muted);cursor:pointer">
904
- <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)"> ▶ 依次自动发送
905
994
  </label>
906
- <button class="dr-btn take" style="margin-left:auto;padding:0.35rem 1rem" onclick="addDraftHere()">+ 记下来</button>
907
995
  </div>
908
- <div style="margin-top:0.4rem">
909
- <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>
910
1024
  </div>
911
1025
  </div>
912
- <div style="font-size:0.66rem;color:var(--muted);padding:0.5rem 0.2rem 0.2rem;border-top:1px solid var(--surface2)">
913
- <b>本任务</b>(<span id="inbox-this-title"></span>)
914
- </div>
915
- <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>
916
- <div style="font-size:0.66rem;color:var(--muted);padding:0.4rem 0.2rem 0.2rem;border-top:1px solid var(--surface2)">
917
- <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>
918
1029
  </div>
919
- <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>
920
- <button class="action-sheet-cancel" onclick="closeInboxSheet()">关闭</button>
921
1030
  </div>
922
1031
  </div>
923
1032
 
@@ -1052,6 +1161,25 @@
1052
1161
  </div>
1053
1162
  </div>
1054
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
+
1055
1183
  <!-- 清除:先说清楚会发生什么,免得误以为出了故障 -->
1056
1184
  <div id="clear-sheet" class="action-sheet">
1057
1185
  <div class="action-sheet-backdrop" onclick="closeClearSheet()"></div>
@@ -1730,14 +1858,20 @@
1730
1858
  }
1731
1859
 
1732
1860
  // ── 消息渲染 ──────────────────────────────────
1733
- 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) {
1734
1869
  endStream(); endToolGroup();
1735
1870
  _userScrolledUp = false; hideNewMsgHint(); // 用户主动发消息 → 回到底部看回复
1736
- _streamedTextThisCall = false;
1737
- _textRenderedThisTurn = false;
1871
+ if (!inject) { _streamedTextThisCall = false; _textRenderedThisTurn = false; } // 插话并进当前这一轮,不算新一轮
1738
1872
  const msg = document.createElement('div');
1739
- msg.className = 'msg msg-input';
1740
- 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>`;
1741
1875
  chatArea.appendChild(msg);
1742
1876
  trimMessages();
1743
1877
  scrollToBottom();
@@ -2191,9 +2325,9 @@
2191
2325
  if (sel) { parent.appendChild(sel); return; }
2192
2326
  if (syntheticUserKind(msg.content)) return;
2193
2327
  const el = document.createElement('div');
2194
- el.className = 'msg msg-input';
2328
+ el.className = 'msg msg-input' + (msg.inject ? ' msg-inject' : '');
2195
2329
  const full = msg.timestamp ? new Date(msg.timestamp).toLocaleString('zh-CN') : '';
2196
- 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>`;
2197
2331
  parent.appendChild(el);
2198
2332
  return;
2199
2333
  }
@@ -2552,8 +2686,10 @@
2552
2686
  // busy 时发送键让位给「存草稿」键;回到 idle 再提示有没有待发草稿
2553
2687
  const draftBtn = document.getElementById('draft-btn');
2554
2688
  if (draftBtn) draftBtn.style.display = busy ? '' : 'none';
2689
+ try { updateInjectBtn(); } catch {}
2555
2690
  // 用 try 包住:这两个绑定声明在文件后段,初始化早期调用会撞 TDZ
2556
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 {}
2557
2693
  try { if (!busy) checkGuide(); else renderGuideTip(); } catch {}
2558
2694
  }
2559
2695
  _postStatus(state);
@@ -2668,7 +2804,7 @@
2668
2804
  const btn = document.createElement('button');
2669
2805
  btn.textContent = '返回首页';
2670
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';
2671
- btn.onclick = () => window.location.href = '/';
2807
+ btn.onclick = () => goHome();
2672
2808
  overlay.appendChild(btn);
2673
2809
  }
2674
2810
  }
@@ -2766,7 +2902,7 @@
2766
2902
  lastControlHolder = holder || null;
2767
2903
  });
2768
2904
  socket.on('control-denied', ({ reason }) => addStatusMessage('无法获取控制: ' + reason));
2769
- socket.on('kicked', ({ reason }) => { addStatusMessage(reason || '你已被管理员踢出'); setTimeout(() => location.href = '/', 2000); });
2905
+ socket.on('kicked', ({ reason }) => { addStatusMessage(reason || '你已被管理员踢出'); setTimeout(() => goHome(), 2000); });
2770
2906
 
2771
2907
  // ── Agent 模式事件 ──────────────────────────────
2772
2908
  socket.on('agent:message', (msg) => {
@@ -2781,6 +2917,9 @@
2781
2917
  } else if (msg.subtype === 'interrupted') {
2782
2918
  addStatusMessage('查询已中断');
2783
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);
2784
2923
  } else if (msg.subtype === 'info' && msg.message) {
2785
2924
  addStatusMessage(msg.message);
2786
2925
  // 同步计划模式状态到按钮
@@ -2811,7 +2950,7 @@
2811
2950
  const sel = buildSyntheticUserEl(msg.content);
2812
2951
  if (sel) addStatusMessage(sel.textContent);
2813
2952
  } else if (!kind) {
2814
- addInputMessage(msg.content);
2953
+ addInputMessage(msg.content, msg.inject);
2815
2954
  }
2816
2955
  // caveat/noise:纯噪音,不渲染
2817
2956
  }
@@ -2870,6 +3009,8 @@
2870
3009
  // 关键:idle 放最后——渲染最终答复的 addAssistantMessage 会把状态置回 'working',
2871
3010
  // 若在其之前置 idle 会被顶回「执行中」+ 取消按钮不复位。渲染全部完成后再回 idle。
2872
3011
  setWorkState('idle');
3012
+ // 还有插话没被看到:AI 收尾后马上拿它开新一轮,界面别停在空闲
3013
+ if (msg.injectPending) { setWorkState('thinking'); showThinking(); }
2873
3014
  // 收尾水位提示:server 在 result 上附带了刚从 transcript 反查的精确水位
2874
3015
  // (ctxTokens——claude 2.1.292 起 stream 的 assistant usage 被清零,前端自己算不了)。
2875
3016
  // 旧 server 无此字段时退回 _lastCtxTokens(老 CLI 的 assistant 流仍带真实 usage)。
@@ -3340,6 +3481,49 @@
3340
3481
  cmdInput.focus();
3341
3482
  }
3342
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
+
3343
3527
  // ── @ 文件引用补全 ─────────────────────────────────────────
3344
3528
  // 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
3345
3529
  // 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
@@ -3601,6 +3785,7 @@
3601
3785
  window.toggleControl = function() { if (isController) socket.emit('release-control', { sessionId: SESSION_ID }); else if (canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID }); };
3602
3786
 
3603
3787
  function updateControlUI(controlling, holderName) {
3788
+ try { updateInjectBtn(); } catch {}
3604
3789
  isController = controlling;
3605
3790
  const badge = document.getElementById('control-badge');
3606
3791
  if (controlling) {
@@ -3624,7 +3809,7 @@
3624
3809
  // ── 会话内使用说明(精简版;完整版在首页 ❓ 帮助)──────────────
3625
3810
  // 快捷栏按钮说明:帮助面板和鼠标悬停提示共用这一份,加按钮时在这里补一句
3626
3811
  const SK_HELP = {
3627
- draft: ['📥 草稿', 'AI 忙时先把想法存起来,干完再发;勾「自动接力」会在这一轮结束后自动发出。'],
3812
+ draft: ['📥 草稿', 'AI 忙时先把想法记下来排队:可拖动 / ↑↓ 排顺序、点文字直接改、多选合并或一起取回;开「依次自动发送」后每干完一轮自动发下一条;AI 忙时可「⚡ 插入」当前任务。'],
3628
3813
  folder: ['📂 文件夹', '在这台电脑上用资源管理器打开本任务目录(后台/服务模式下不可用)。'],
3629
3814
  model: ['模型', '切换 AI 模型。复杂任务用强模型,简单活换快模型更省;被安全检测误拦时也可换个模型再试。'],
3630
3815
  plan: ['计划', '先让 AI 只调研、出方案、不改文件;你审批后再执行。再点一次「退出计划」可按方案执行或仅退出。'],
@@ -3657,7 +3842,8 @@
3657
3842
  <b>发消息</b>:像聊天一样把需求说清楚即可;触屏回车=换行,用发送键发送。<br>
3658
3843
  <b>让它读文件</b>:打 <b>@</b> 从本项目逐层浏览、打字搜索;项目外打 <b>@D:</b>、<b>@/</b>、<b>@~/</b> 按路径逐层浏览;选中插路径;或点「文件」面板的 <b>@</b> 引用。<br>
3659
3844
  <b>工具/思考</b>:AI 干活的过程默认折叠,点「🔧 N 个工具调用」展开看命令与输出。<br>
3660
- <b>AI 忙时</b>:输入框变「存草稿」,先记着干完取回;勾自动接力会自动发出。<br>
3845
+ <b>AI 忙时</b>:想补充或提醒,点输入框右边「⚡ 插话」——不打断,AI 下一步就会看到并调整(气泡上显示「AI 已看到」);不急的就回车「存草稿」排队,草稿里可排顺序、编辑、合并,开「依次自动发送」后每干完一轮自动发下一条,AI 忙时也能把草稿「⚡ 插入」当前任务。<br>
3846
+ <b>看不懂就问</b>:在聊天里<b>选中</b>看不懂的词或一段话(手机长按选中),点「💡 解释」或「❓ 问一问」,答案在旁边小窗里,可以继续追问;这些问答<b>不会打扰正在干活的 AI</b>,想让它知道就点「👉 告诉 AI」。<br>
3661
3847
  <b>AI 提问 / 交方案</b>:AI 需要你拿主意时会弹出选项,选一个即可,也可「跳过,让 AI 自己决定」;收起后输入框上方「❓ AI 在等你回答」可再打开。<br>
3662
3848
  <b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。
3663
3849
  ${sec('顶栏')}
@@ -4277,7 +4463,10 @@
4277
4463
 
4278
4464
  // ── 辅助 ──────────────────────────────────────
4279
4465
  function updateViewers(count) { document.getElementById('viewer-count').textContent = count > 1 ? `${count} 人在线` : ''; }
4280
- 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(); };
4281
4470
 
4282
4471
  // ── 快速切换会话(免退出到首页)──────────────────────────
4283
4472
  function switchRelTime(ms) {
@@ -4356,7 +4545,7 @@
4356
4545
  const close = () => overlay.remove();
4357
4546
  overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
4358
4547
  box.querySelector('#sw-cancel').onclick = close;
4359
- box.querySelector('#sw-home').onclick = () => { window.location.href = '/'; };
4548
+ box.querySelector('#sw-home').onclick = () => { goHome(); };
4360
4549
  const bindRows = () => box.querySelectorAll('[data-sid]').forEach(row => row.onclick = () => {
4361
4550
  const sid = row.dataset.sid;
4362
4551
  if (sid && sid !== SESSION_ID) window.location.href = '/terminal/' + sid;
@@ -4641,6 +4830,7 @@
4641
4830
  });
4642
4831
  // 输入框上方的「AI 在等你」提示条:弹窗被收起后从这里再打开
4643
4832
  function renderPendingBar() {
4833
+ try { updateInjectBtn(); } catch {}
4644
4834
  const bar = document.getElementById('pending-bar');
4645
4835
  if (!bar) return;
4646
4836
  const txt = _planCtl ? '📋 AI 提交了方案,等你审批' : _askCtlId ? '❓ AI 在等你回答一个问题' : '';
@@ -5052,16 +5242,14 @@
5052
5242
  const cb = document.getElementById('inbox-new-autosend');
5053
5243
  if (cb) cb.checked = false;
5054
5244
  clearInboxNewImage();
5245
+ _ibSelMode = false; _ibSel.clear(); _ibEditing = null;
5246
+ ibTab('this');
5055
5247
  await refreshInbox(false);
5056
5248
  renderInbox();
5057
5249
  };
5058
5250
  window.closeInboxSheet = function() {
5059
5251
  document.getElementById('inbox-sheet').classList.remove('open');
5060
5252
  };
5061
- window.toggleInboxOthers = function() {
5062
- const box = document.getElementById('inbox-others');
5063
- box.style.display = box.style.display === 'none' ? '' : 'none';
5064
- };
5065
5253
 
5066
5254
  function draftPreview(d) {
5067
5255
  const t = (d.text || '').replace(/\s+/g, ' ').trim();
@@ -5086,48 +5274,228 @@
5086
5274
  return sameDay ? hm : `${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${hm}`;
5087
5275
  }
5088
5276
 
5089
- function renderInbox() {
5090
- const esc = (s) => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
5091
- const titleEl = document.getElementById('inbox-this-title');
5092
- 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]));
5093
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() {
5094
5292
  const mine = myDrafts();
5095
- document.getElementById('inbox-this').innerHTML = mine.length ? mine.map(d => `
5096
- <div class="dr-item">
5097
- <div class="dr-text">${esc(draftPreview(d))}</div>
5098
- <div class="dr-meta">
5099
- <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>
5100
5328
  ${d.source === 'busy' ? '<span class="dr-src">忙时自动存</span>' : ''}
5101
- <label style="cursor:pointer;color:${d.autoSend ? 'var(--yellow)' : 'var(--muted)'}"
5102
- title="勾上后:本轮干完、会话空闲时自动发出这条(不用你在页面前)。默认关。">
5103
- <input type="checkbox" ${d.autoSend ? 'checked' : ''} onchange="toggleAutoSend('${d.id}', this.checked)"
5104
- style="vertical-align:-1px"> 自动接力
5105
- </label>
5106
- <span class="dr-acts">
5107
- <button class="dr-btn take" onclick="useDraft('${d.id}')" title="取回到消息框,草稿随即删除">取回</button>
5108
- <button class="dr-btn" onclick="deleteDraft('${d.id}')">删</button>
5109
- </span>
5110
- </div>
5111
- </div>`).join('')
5112
- : '<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();
5113
5356
 
5357
+ // 其它任务:按任务分组
5114
5358
  const others = otherDrafts();
5115
- document.getElementById('inbox-others-count').textContent = others.length ? `(${others.length})` : '(0)';
5116
- document.getElementById('inbox-others').innerHTML = others.length ? others.map(d => `
5117
- <div class="dr-item">
5118
- <div class="dr-sess">${esc(d.sessionTitle || '(未知任务)')}${d.projectName ? ' · ' + esc(d.projectName) : ''}${d.sessionAlive ? '' : ' <span style="color:var(--muted)">(已结束)</span>'}</div>
5119
- <div class="dr-text" style="color:var(--text2)">${esc(draftPreview(d))}</div>
5120
- <div class="dr-meta">
5121
- <span>🕒 ${draftTime(d.createdAt)} · ${draftAge(d.createdAt)}</span>
5122
- <span class="dr-acts">
5123
- <button class="dr-btn" onclick="reassignDraft('${d.id}')" title="改投到本任务">改投</button>
5124
- <button class="dr-btn" onclick="deleteDraft('${d.id}')">删</button>
5125
- </span>
5126
- </div>
5127
- </div>`).join('')
5128
- : '<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
+ });
5129
5435
  }
5130
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
+
5131
5499
  // ── 抽屉内就地新增草稿(不必先在消息框打字再"提取")──────────
5132
5500
  let _inboxNewImage = null; // { path, name }
5133
5501
 
@@ -5215,13 +5583,16 @@
5215
5583
  window.useDraft = async function(id) {
5216
5584
  const d = _inboxCache.drafts.find(x => x.id === id);
5217
5585
  if (!d) return;
5218
- 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') : '');
5219
5590
  cmdInput.style.height = 'auto';
5220
5591
  cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
5221
5592
  clearPendingImage();
5222
5593
  clearPendingFile();
5223
5594
  if (d.images?.length) setPendingImage(d.images[0].path, '');
5224
- if (d.files?.length) {
5595
+ else if (d.files?.length) {
5225
5596
  _pendingFile = { path: d.files[0].path, name: d.files[0].name };
5226
5597
  const fp = document.getElementById('file-preview');
5227
5598
  const nameEl = document.getElementById('file-preview-name');
@@ -6133,6 +6504,185 @@
6133
6504
  showThinking();
6134
6505
  };
6135
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
+
6136
6686
  window.analyzeMemory = function() {
6137
6687
  const scopeTxt = _memScope === 'all' ? '所有项目 + 全局' : '本项目';
6138
6688
  const prompt = [