@co0ontty/wand 1.32.0 → 1.32.1

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.
@@ -80,6 +80,33 @@
80
80
  var configPath = "${escapeHtml(configPath)}";
81
81
  var CHAT_EXPAND_STATE_STORAGE_KEY = "wand-chat-expand-state-v1";
82
82
 
83
+ // ===== 一次性 localStorage 迁移 =====
84
+ // 用 schema 版本号确保每个 migration 只跑一次。每加一项就 ++LS_SCHEMA_VERSION
85
+ // 并在 LS_MIGRATIONS append 一个函数。已升级用户的 wand-ls-schema 大于等于
86
+ // 当前长度时整段跳过;新用户首次加载会一口气把所有 migration 都跑完再写
87
+ // schema 号 —— 因此每个 migration 函数对「key 不存在」的输入也必须是无害的。
88
+ var LS_MIGRATIONS = [
89
+ // v1(2026-05)取消独立的「图钉」按钮,呼出侧栏即常驻。旧版残留的
90
+ // wand-sidebar-pinned=false 会让老用户继续走 drawer 模式看不到新行为,
91
+ // 这里直接清掉,让 state 初始化回退到默认 true。
92
+ function migrateSidebarPinDefault() {
93
+ try { localStorage.removeItem("wand-sidebar-pinned"); } catch (e) {}
94
+ }
95
+ ];
96
+ (function runLocalStorageMigrations() {
97
+ try {
98
+ var raw = localStorage.getItem("wand-ls-schema");
99
+ var applied = raw == null ? 0 : parseInt(raw, 10);
100
+ if (!(applied >= 0)) applied = 0;
101
+ for (var i = applied; i < LS_MIGRATIONS.length; i++) {
102
+ try { LS_MIGRATIONS[i](); } catch (e) {}
103
+ }
104
+ if (applied < LS_MIGRATIONS.length) {
105
+ localStorage.setItem("wand-ls-schema", String(LS_MIGRATIONS.length));
106
+ }
107
+ } catch (e) { /* localStorage 不可用就跳过,按默认行为运行 */ }
108
+ })();
109
+
83
110
  var state = {
84
111
  selectedId: (function() {
85
112
  try { return localStorage.getItem("wand-selected-session") || null; } catch (e) { return null; }
@@ -154,7 +181,12 @@
154
181
  bootstrapping: true,
155
182
  sessionsDrawerOpen: false,
156
183
  sidebarPinned: (function() {
157
- try { return localStorage.getItem("wand-sidebar-pinned") === "true"; } catch (e) { return false; }
184
+ // 新交互:桌面默认呼出即常驻;只有用户主动 X 关闭过才记 "false"
185
+ // 老用户的旧值("true"/"false")继续生效,没存过 key 时回退到 true。
186
+ try {
187
+ var v = localStorage.getItem("wand-sidebar-pinned");
188
+ return v === null ? true : v !== "false";
189
+ } catch (e) { return true; }
158
190
  })(),
159
191
  sidebarCollapsed: (function() {
160
192
  try { return localStorage.getItem("wand-sidebar-collapsed") === "true"; } catch (e) { return false; }
@@ -1380,6 +1412,36 @@
1380
1412
  });
1381
1413
  }
1382
1414
 
1415
+ // ===== 桌面:点 sidebar 外的空白处自动收起 =====
1416
+ // 旧版 drawer 模式下点 backdrop 关闭的便利性,在「呼出即常驻」之后用
1417
+ // document 级捕获 handler 续上。
1418
+ // - 仅 desktop + 全尺寸(非窄条)+ 已打开 时生效
1419
+ // - 窄条态不触发(窄条本来就是稳定常驻形态)
1420
+ // - 手机端由 .drawer-backdrop 元素自己接住点击,不在这里重复处理
1421
+ // - 各类弹层(modal / topbar-more / overflow 菜单 / 文件夹下拉等)不算
1422
+ // 「sidebar 外的空白」,否则点弹层会顺带把 sidebar 关掉
1423
+ // 用 capture 阶段是为了绕过下游按钮自己的 stopPropagation。
1424
+ document.addEventListener("click", function(e) {
1425
+ if (isMobileLayout()) return;
1426
+ if (!state.sidebarPinned) return;
1427
+ if (state.sidebarCollapsed) return;
1428
+ if (!state.sessionsDrawerOpen) return;
1429
+ var target = e.target;
1430
+ if (!target || !(target instanceof Element)) return;
1431
+ if (target.closest("#sessions-drawer")) return;
1432
+ if (target.closest("#sessions-toggle-button")) return;
1433
+ if (target.closest(".floating-sidebar-toggle")) return;
1434
+ if (target.closest(".sidebar-tile-bubble")) return;
1435
+ if (target.closest(
1436
+ ".modal-backdrop, .modal-overlay, .modal-container, " +
1437
+ "[role='dialog'], [role='menu'], " +
1438
+ ".topbar-more-menu, .sidebar-header-overflow, " +
1439
+ ".folder-picker-dropdown, .path-suggestions, " +
1440
+ ".permission-prompt-overlay, .restart-overlay"
1441
+ )) return;
1442
+ closeSessionsDrawer();
1443
+ }, true);
1444
+
1383
1445
  renderBootLoading();
1384
1446
  restoreLoginSession();
1385
1447
 
@@ -1604,9 +1666,6 @@
1604
1666
  '</button>' +
1605
1667
  '</div>' +
1606
1668
  '</div>' +
1607
- '<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
1608
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24z"/></svg>' +
1609
- '</button>' +
1610
1669
  '<button id="sidebar-collapse-btn" class="btn btn-ghost btn-sm sidebar-collapse-toggle' + (isCollapsed ? ' collapsed' : '') + '" type="button" title="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '" aria-label="' + (isCollapsed ? '展开侧栏' : '收起为窄条') + '">' +
1611
1670
  (isCollapsed
1612
1671
  ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="10 6 16 12 10 18"/><line x1="20" y1="5" x2="20" y2="19"/></svg>'
@@ -1841,13 +1900,8 @@
1841
1900
  '<button id="stop-button" class="btn-circle btn-circle-stop' + (state.selectedId ? "" : " hidden") + '" title="停止">' +
1842
1901
  '<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><rect x="3" y="3" width="10" height="10" rx="2"/></svg>' +
1843
1902
  '</button>' +
1844
- // 结构化模式且正在出 token 时显示:中断当前回复、立刻发送新输入。
1845
- // 默认走 #send-input-button → 排队;想插队的人显式按这颗。
1846
- // 用 pill 形态 + 文字 + 脉动,让用户一眼就看到「立即发送」这条快捷路径。
1847
- '<button id="interrupt-send-button" class="btn-pill btn-pill-interrupt hidden" type="button" title="中断当前回复并立即发送新输入(Cmd/Ctrl+Enter)" aria-label="立即发送">' +
1848
- '<svg class="btn-pill-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>' +
1849
- '<span class="btn-pill-label">立即</span>' +
1850
- '</button>' +
1903
+ // 「立即发送」按钮已下线 —— 默认行为永远是排队(气泡),想插队
1904
+ // 请点输入框上方那条气泡(chip)。Cmd/Ctrl+Enter 快捷键仍保留。
1851
1905
  '<button id="send-input-button" class="btn-circle btn-circle-send" title="发送">' +
1852
1906
  '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" 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>' +
1853
1907
  '</button>' +
@@ -5823,8 +5877,6 @@
5823
5877
  if (drawerBackdrop) drawerBackdrop.addEventListener("click", closeSessionsDrawer);
5824
5878
  var closeDrawerBtn = document.getElementById("close-drawer-button");
5825
5879
  if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
5826
- var pinBtn = document.getElementById("sidebar-pin-btn");
5827
- if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
5828
5880
  var collapseBtn = document.getElementById("sidebar-collapse-btn");
5829
5881
  if (collapseBtn) collapseBtn.addEventListener("click", toggleSidebarCollapsed);
5830
5882
  var sidebarMoreBtn = document.getElementById("sidebar-more-btn");
@@ -6092,11 +6144,6 @@
6092
6144
  closeSessionsDrawer();
6093
6145
  sendOrStart();
6094
6146
  });
6095
- var interruptSendBtn = document.getElementById("interrupt-send-button");
6096
- if (interruptSendBtn) interruptSendBtn.addEventListener("click", function() {
6097
- closeSessionsDrawer();
6098
- sendOrStart({ interrupt: true });
6099
- });
6100
6147
  var stopBtn = document.getElementById("stop-button");
6101
6148
  if (stopBtn) stopBtn.addEventListener("click", stopSession);
6102
6149
  var modeSelect = document.getElementById("chat-mode-select");
@@ -8159,14 +8206,17 @@
8159
8206
  // Keep placeholders short so they don't wrap on portrait mobile screens.
8160
8207
  // Only show informative state hints; drop the redundant "send to X" labels.
8161
8208
  if (terminalInteractive) return "终端交互中";
8162
- if (session && session.status !== "running") {
8209
+ // 只有真正进入终止态(exited / failed / stopped)才提示"会话已结束"
8210
+ // 结构化会话刚创建或一次回复结束后会回到 "idle"——那是等待下一条输入的
8211
+ // 正常状态,不应该被当成结束。
8212
+ if (session && (session.status === "exited" || session.status === "failed" || session.status === "stopped")) {
8163
8213
  if (canAutoResumeSession(session)) return "";
8164
8214
  return "会话已结束";
8165
8215
  }
8166
8216
  // 结构化会话在出 token 时,输入框仍然可用——告诉用户默认行为是排队,
8167
- // 想插队请按右侧的 » 按钮。短语保持单行不换行。
8217
+ // 想插队请直接点上面的气泡。短语尽量短,避免在窄屏手机上换行。
8168
8218
  if (isStructuredSession(session) && session.structuredState && session.structuredState.inFlight) {
8169
- return "回复中…Enter 排队 · 旁边的「» 立即」按钮中断并立即发送";
8219
+ return "回复中…Enter 排队 · 点气泡插队";
8170
8220
  }
8171
8221
  return "";
8172
8222
  }
@@ -9266,7 +9316,6 @@
9266
9316
  function updatePinState() {
9267
9317
  var drawer = document.getElementById("sessions-drawer");
9268
9318
  var mainLayout = document.querySelector(".main-layout");
9269
- var pinBtn = document.getElementById("sidebar-pin-btn");
9270
9319
  // 与 renderAppShell 保持一致:手机端只允许窄条形态 anchored。
9271
9320
  var isMobile = isMobileLayout();
9272
9321
  var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
@@ -9279,10 +9328,6 @@
9279
9328
  mainLayout.classList.toggle("sidebar-pinned", isAnchored);
9280
9329
  mainLayout.classList.toggle("sidebar-collapsed", isCollapsed);
9281
9330
  }
9282
- if (pinBtn) {
9283
- pinBtn.classList.toggle("pinned", state.sidebarPinned);
9284
- pinBtn.title = state.sidebarPinned ? "取消固定侧栏" : "固定侧栏";
9285
- }
9286
9331
  }
9287
9332
 
9288
9333
  function updateDrawerState() {
@@ -9306,9 +9351,26 @@
9306
9351
  }
9307
9352
 
9308
9353
  function toggleSessionsDrawer() {
9309
- if (state.sidebarPinned && !isMobileLayout()) return;
9354
+ var isMobile = isMobileLayout();
9355
+ if (!isMobile) {
9356
+ // 桌面:呼出 = 常驻全尺寸;再次点击 = 完全收起(floating-toggle 重新出现)。
9357
+ // 取消了独立的「图钉」概念,sidebarPinned 现在由呼出/关闭自动管理。
9358
+ var willOpen = !state.sidebarPinned;
9359
+ state.sidebarPinned = willOpen;
9360
+ state.sessionsDrawerOpen = willOpen;
9361
+ if (willOpen) {
9362
+ // 桌面重新呼出默认回到全尺寸;窄条形态需用户主动点 collapse 按钮切换。
9363
+ state.sidebarCollapsed = false;
9364
+ try { localStorage.setItem("wand-sidebar-collapsed", "false"); } catch (e) {}
9365
+ }
9366
+ try { localStorage.setItem("wand-sidebar-pinned", String(willOpen)); } catch (e) {}
9367
+ updateLayoutState();
9368
+ scheduleTerminalRefitAfterPaddingTransition();
9369
+ return;
9370
+ }
9371
+ // 手机端:保持原 drawer 行为。
9310
9372
  state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
9311
- if (state.sessionsDrawerOpen && isMobileLayout()) {
9373
+ if (state.sessionsDrawerOpen) {
9312
9374
  state.filePanelOpen = false;
9313
9375
  try {
9314
9376
  localStorage.setItem("wand-file-panel-open", "false");
@@ -9318,13 +9380,42 @@
9318
9380
  }
9319
9381
 
9320
9382
  function closeSessionsDrawer() {
9321
- if (state.sidebarPinned && !isMobileLayout()) return;
9383
+ var isMobile = isMobileLayout();
9384
+ if (!isMobile) {
9385
+ // 桌面:X 按钮 / backdrop 点击 = 完全收起,撤掉常驻状态,floating-toggle 重新出现。
9386
+ // 窄条状态下没有 X 按钮(CSS 隐藏),不会走到这里,因此无需特判 collapsed。
9387
+ if (!state.sidebarPinned && !state.sessionsDrawerOpen) return;
9388
+ closeSwipedItem();
9389
+ state.sidebarPinned = false;
9390
+ state.sessionsDrawerOpen = false;
9391
+ try { localStorage.setItem("wand-sidebar-pinned", "false"); } catch (e) {}
9392
+ updateLayoutState();
9393
+ scheduleTerminalRefitAfterPaddingTransition();
9394
+ return;
9395
+ }
9396
+ // 手机端:保持原 drawer 关闭行为。
9322
9397
  if (!state.sessionsDrawerOpen) return;
9323
9398
  closeSwipedItem();
9324
9399
  state.sessionsDrawerOpen = false;
9325
9400
  updateLayoutState();
9326
9401
  }
9327
9402
 
9403
+ // 桌面 padding-left transition 结束后重新拟合终端尺寸。
9404
+ // 抽出来给 toggleSessionsDrawer / closeSessionsDrawer / toggleSidebarCollapsed 复用。
9405
+ function scheduleTerminalRefitAfterPaddingTransition() {
9406
+ var mainLayout = document.querySelector(".main-layout");
9407
+ if (mainLayout) {
9408
+ var onEnd = function(e) {
9409
+ if (e.propertyName === "padding-left") {
9410
+ mainLayout.removeEventListener("transitionend", onEnd);
9411
+ scheduleTerminalResize(true);
9412
+ }
9413
+ };
9414
+ mainLayout.addEventListener("transitionend", onEnd);
9415
+ }
9416
+ setTimeout(function() { scheduleTerminalResize(true); }, 350);
9417
+ }
9418
+
9328
9419
  var collapsedTileBubbleEl = null;
9329
9420
  function ensureCollapsedTileBubble() {
9330
9421
  if (collapsedTileBubbleEl && document.body.contains(collapsedTileBubbleEl)) {
@@ -9390,8 +9481,7 @@
9390
9481
 
9391
9482
  function toggleSidebarCollapsed() {
9392
9483
  var isMobile = isMobileLayout();
9393
- // drawer 模式(未 pin)下点 collapse 视为「先固定、再收起为窄条」——
9394
- // 用户直觉是「点了就该看到窄条」,过去这里 early return 让按钮看上去没反应。
9484
+ // 任何形态下点窄条按钮都意味着「我要常驻」,确保 pinned 写上。
9395
9485
  if (!state.sidebarPinned) {
9396
9486
  state.sidebarPinned = true;
9397
9487
  try {
@@ -9415,47 +9505,13 @@
9415
9505
  localStorage.setItem("wand-sidebar-pinned", "false");
9416
9506
  } catch (e) {}
9417
9507
  } else {
9418
- // 桌面端展开窄条 → 300px 全栏固定,自动打开。
9508
+ // 桌面端展开窄条 → 300px 全栏常驻。
9419
9509
  state.sessionsDrawerOpen = true;
9420
9510
  }
9421
9511
  render();
9422
- var mainLayout = document.querySelector(".main-layout");
9423
- if (mainLayout) {
9424
- var onEnd = function(e) {
9425
- if (e.propertyName === "padding-left") {
9426
- mainLayout.removeEventListener("transitionend", onEnd);
9427
- scheduleTerminalResize(true);
9428
- }
9429
- };
9430
- mainLayout.addEventListener("transitionend", onEnd);
9431
- }
9432
- setTimeout(function() { scheduleTerminalResize(true); }, 350);
9512
+ scheduleTerminalRefitAfterPaddingTransition();
9433
9513
  }
9434
9514
 
9435
- function toggleSidebarPin() {
9436
- if (isMobileLayout()) return;
9437
- state.sidebarPinned = !state.sidebarPinned;
9438
- try {
9439
- localStorage.setItem("wand-sidebar-pinned", String(state.sidebarPinned));
9440
- } catch (e) {}
9441
- if (state.sidebarPinned) {
9442
- state.sessionsDrawerOpen = true;
9443
- }
9444
- updateLayoutState();
9445
- // Refit terminal after padding-left transition completes
9446
- var mainLayout = document.querySelector(".main-layout");
9447
- if (mainLayout) {
9448
- var onEnd = function(e) {
9449
- if (e.propertyName === "padding-left") {
9450
- mainLayout.removeEventListener("transitionend", onEnd);
9451
- scheduleTerminalResize(true);
9452
- }
9453
- };
9454
- mainLayout.addEventListener("transitionend", onEnd);
9455
- }
9456
- // Fallback refit in case transition doesn't fire
9457
- setTimeout(function() { scheduleTerminalResize(true); }, 350);
9458
- }
9459
9515
 
9460
9516
  // Store last focused element for focus trap
9461
9517
  var lastFocusedElement = null;
@@ -12257,9 +12313,10 @@
12257
12313
 
12258
12314
  function postStructuredInput(input, inputBox, session, opts) {
12259
12315
  opts = opts || {};
12260
- // 用户显式点击"立即发送"才会传 interrupt:true。普通 Enter / 点发送
12261
- // 在上一条还在流式时默认走 queue —— 后端 sendMessage(...) 会把它
12262
- // 追加到 queuedMessages,等当前 turn 结束自动 flush。
12316
+ // interrupt:true 现在只来自 Cmd/Ctrl+Enter 快捷键,或点队列气泡触发的
12317
+ // queueBarPromoteIndex()。普通 Enter / 点发送在上一条还在流式时默认走
12318
+ // queue —— 后端 sendMessage(...) 会把它追加到 queuedMessages,等当前 turn
12319
+ // 结束自动 flush;想插队就点输入框上方那条气泡。
12263
12320
  var requestedInterrupt = !!opts.interrupt;
12264
12321
  console.log("[WAND] postStructuredInput selectedId:", state.selectedId, "input:", input && input.substring(0, 50), "requestedInterrupt:", requestedInterrupt, "session:", session && { id: session.id, sessionKind: session.sessionKind, runner: session.runner, status: session.status, inFlight: session.structuredState && session.structuredState.inFlight });
12265
12322
  if (!state.selectedId || !input) return Promise.resolve();
@@ -12297,7 +12354,7 @@
12297
12354
  updateSessionSnapshot(optimisticPatch);
12298
12355
  var queueRefreshed = state.sessions.find(function(s) { return s.id === session.id; }) || session;
12299
12356
  state.currentMessages = buildMessagesForRender(queueRefreshed, getPreferredMessages(queueRefreshed, queueRefreshed.output, false));
12300
- updateInputHint("已加入排队,等待当前回复完成…");
12357
+ updateInputHint("已加入排队…");
12301
12358
  renderChat(true);
12302
12359
  updateStructuredQueueCounter();
12303
12360
  // 乐观 toast:原本只在 POST 完成后才提示,Claude 流式拖太久时用户根本
@@ -12458,8 +12515,9 @@
12458
12515
  // · 默认只展开队首(即下一个要发的那条),显示编号 + 文本 + × 删除
12459
12516
  // · 其他消息收起成一根小横杠(指示存在但不占空间)
12460
12517
  // · 鼠标悬到任意小横杠 → 该条展开、原本展开的那条收回小横杠
12461
- // · 悬停期间可以按住展开的那条向上 / 向下拖拽 换序
12462
- // 末尾跟一个 "立即" 按钮:中断当前回复、把队首作为新输入插队发出去。
12518
+ // · 点一下任意气泡中断当前回复、把这条作为新输入插队发出去
12519
+ // · 按住任意气泡向上 / 向下拖拽 → 换序
12520
+ // 末尾跟一个 ⚡ 按钮:等价于点队首气泡(保留作为快速插队的视觉提示)。
12463
12521
  // 数据源:session.queuedMessages(后端 WS + postStructuredInput 乐观更新)。
12464
12522
  // ──────────────────────────────────────────────────────────────────────────
12465
12523
 
@@ -12499,8 +12557,8 @@
12499
12557
  var itemClass = "queue-bar-item";
12500
12558
  if (isExpanded) itemClass += " expanded";
12501
12559
  if (single) itemClass += " queue-bar-item-single";
12502
- // 拖拽起手区是整个 chip,但 delete 按钮要独占点击。
12503
- var titleAttr = isExpanded ? raw + "(按住可拖动调整顺序)" : raw;
12560
+ // 整个 chip 既是拖拽起手区,也是"点一下立即发送"的触发点;delete 按钮单独占点击。
12561
+ var titleAttr = isExpanded ? raw + "(点一下立即发送 · 按住可拖动调序)" : raw + "(点一下立即发送)";
12504
12562
  chips +=
12505
12563
  '<li class="' + itemClass + '" data-index="' + i + '" data-action="drag"' +
12506
12564
  ' title="' + escapeHtml(titleAttr) + '">' +
@@ -12654,19 +12712,27 @@
12654
12712
  }
12655
12713
 
12656
12714
  function queueBarPromoteHead() {
12715
+ queueBarPromoteIndex(0);
12716
+ }
12717
+
12718
+ // 把队列里第 index 条剥下来,作为新的输入立刻发送出去。
12719
+ // - inFlight:interrupt + preserveQueue(中断当前回复,保留其它排队)
12720
+ // - 非 inFlight:当作普通新消息发出去
12721
+ // 用户路径:点输入框上方的气泡(chip)→ 这里。
12722
+ function queueBarPromoteIndex(index) {
12657
12723
  var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
12658
12724
  if (!session) return;
12659
12725
  var queue = Array.isArray(session.queuedMessages) ? session.queuedMessages.slice() : [];
12660
- if (queue.length === 0) return;
12661
- var head = queue[0];
12662
- var rest = queue.slice(1);
12726
+ if (index < 0 || index >= queue.length) return;
12727
+ var picked = queue[index];
12728
+ var rest = queue.slice(0, index).concat(queue.slice(index + 1));
12663
12729
  var prev = queue.slice();
12664
12730
  var inFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
12665
12731
 
12666
- // 乐观:剥掉队首;hover 下标随之收缩
12732
+ // 乐观:剥掉这一条;hover 下标随之收缩
12667
12733
  if (typeof state.queueBarHoverIndex === "number") {
12668
- if (state.queueBarHoverIndex === 0) state.queueBarHoverIndex = null;
12669
- else state.queueBarHoverIndex -= 1;
12734
+ if (state.queueBarHoverIndex === index) state.queueBarHoverIndex = null;
12735
+ else if (state.queueBarHoverIndex > index) state.queueBarHoverIndex -= 1;
12670
12736
  }
12671
12737
  updateSessionSnapshot({ id: session.id, queuedMessages: rest });
12672
12738
 
@@ -12674,14 +12740,14 @@
12674
12740
  ? crypto.randomUUID()
12675
12741
  : (Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10));
12676
12742
 
12677
- var body = { input: head, idempotencyKey: idempotencyKey };
12743
+ var body = { input: picked, idempotencyKey: idempotencyKey };
12678
12744
  if (inFlight) {
12679
12745
  // 中断 + 保留剩余队列
12680
12746
  body.interrupt = true;
12681
12747
  body.preserveQueue = true;
12682
12748
  }
12683
12749
  // 给一个乐观 toast,让用户瞬间知道点击生效了
12684
- showToast(inFlight ? "已请求中断当前回复,立即发送队首。" : "已立即发送队首消息。", "info");
12750
+ showToast(inFlight ? "已请求中断当前回复,立即发送这条。" : "已立即发送这条消息。", "info");
12685
12751
 
12686
12752
  fetch("/api/structured-sessions/" + session.id + "/messages", {
12687
12753
  method: "POST",
@@ -12715,6 +12781,9 @@
12715
12781
  }
12716
12782
 
12717
12783
  // ── 拖拽排序(Pointer Events + 真实高度的 sort/animate)──
12784
+ // 单条气泡的 pointerdown 也会进这里,但 queue.length <= 1 时直接返回,让
12785
+ // 系统 click 事件穿透到 #queue-bar-host 的 click delegate(那里再判断"点击
12786
+ // 气泡 → 立即发送")。
12718
12787
  function queueBarDragStart(ev, chipEl) {
12719
12788
  var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
12720
12789
  if (!session) return;
@@ -12746,6 +12815,7 @@
12746
12815
  startY: ev.clientY,
12747
12816
  gap: gap,
12748
12817
  queueSnapshot: queue,
12818
+ moved: false, // 没真正拖动过 → 抬手时按 tap 处理:promote 这条
12749
12819
  };
12750
12820
 
12751
12821
  chipEl.classList.add("dragging");
@@ -12793,6 +12863,8 @@
12793
12863
  if (!d || ev.pointerId !== d.pointerId) return;
12794
12864
  ev.preventDefault();
12795
12865
  var deltaY = ev.clientY - d.startY;
12866
+ // 4px 阈值过滤抖动 / 触屏轻微滑动;超过才算"真的在拖",否则抬手当 tap。
12867
+ if (Math.abs(deltaY) > 4) d.moved = true;
12796
12868
  d.itemEl.style.transform = "translateY(" + deltaY + "px)";
12797
12869
 
12798
12870
  // 拖动中心 Y 决定目标插入位置
@@ -12827,6 +12899,7 @@
12827
12899
  var origIndex = d.origIndex;
12828
12900
  var targetIndex = d.targetIndex;
12829
12901
  var queueSnapshot = d.queueSnapshot;
12902
+ var wasTap = !d.moved;
12830
12903
 
12831
12904
  // 清掉 inline transform 让 CSS 自然回位
12832
12905
  d.siblings.forEach(function(el) {
@@ -12838,8 +12911,9 @@
12838
12911
  state.queueBarDrag = null;
12839
12912
 
12840
12913
  if (origIndex === targetIndex) {
12841
- // 没动,光擦一下重渲就行
12914
+ // 没真的移动过 → 按 tap 处理:把这条剥下来插队发送。
12842
12915
  updateQueueBar();
12916
+ if (wasTap) queueBarPromoteIndex(origIndex);
12843
12917
  return;
12844
12918
  }
12845
12919
 
@@ -12886,7 +12960,16 @@
12886
12960
  var actionEl = ev.target && ev.target.closest ? ev.target.closest("[data-action]") : null;
12887
12961
  if (!actionEl || !host.contains(actionEl)) return;
12888
12962
  var action = actionEl.getAttribute("data-action");
12889
- if (action === "drag") return; // 拖拽由 pointerdown 处理,吞掉 click
12963
+ if (action === "drag") {
12964
+ // queue.length > 1 时 pointerdown 已经 preventDefault → click 不会到这;
12965
+ // queue.length === 1 时 drag-start 早退、click 会落到这里:当成 tap,
12966
+ // 把这条直接 promote 出去。
12967
+ ev.preventDefault();
12968
+ ev.stopPropagation();
12969
+ var idx = Number(actionEl.getAttribute("data-index"));
12970
+ queueBarPromoteIndex(idx);
12971
+ return;
12972
+ }
12890
12973
  ev.preventDefault();
12891
12974
  ev.stopPropagation();
12892
12975
  if (action === "promote") { queueBarPromoteHead(); return; }
@@ -13502,12 +13585,6 @@
13502
13585
  : (isCodex ? (isRunning ? "发送给 Codex" : "Codex 会话已结束") : (!selectedSession || isRunning || canResumeOnSend ? "发送" : "会话已结束")));
13503
13586
  sendBtn.classList.toggle("queue-mode", structuredInFlight);
13504
13587
  }
13505
- var interruptBtn = document.getElementById("interrupt-send-button");
13506
- if (interruptBtn) {
13507
- // 仅结构化 + inFlight 时显示。pty 会话有自己的 Ctrl+C / stop 按钮,
13508
- // 用不上这套语义。
13509
- interruptBtn.classList.toggle("hidden", !structuredInFlight);
13510
- }
13511
13588
  var container = document.getElementById("output");
13512
13589
  if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
13513
13590
  }
@@ -16993,24 +17070,13 @@
16993
17070
  return;
16994
17071
  }
16995
17072
 
16996
- // 当前 turn 已结束(结构化 inFlight=false 或 PTY 非 running)就把进度条
16997
- // 收起来——模型经常忘了发最后一条"全 completed" TodoWrite,让用户
16998
- // 对着 "5/6" 干瞪眼很别扭。allDone 那条分支保留,提前命中更快返回。
16999
- var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
17000
- var turnDone = false;
17001
- if (sel) {
17002
- if (isStructuredSession(sel)) {
17003
- turnDone = !(sel.structuredState && sel.structuredState.inFlight);
17004
- } else {
17005
- turnDone = sel.status !== "running";
17006
- }
17007
- }
17008
- if (turnDone) {
17009
- container.classList.add("hidden");
17010
- if (bodyEl) bodyEl.classList.add("hidden");
17011
- return;
17012
- }
17013
-
17073
+ // 之前这里在 turn 结束(结构化 inFlight=false 或 PTY 非 running)时
17074
+ // 把进度条收起来,理由是「模型经常忘了发最后一条全 completed
17075
+ // TodoWrite,让用户对着 5/6 干瞪眼很别扭」。但反馈是:在结构化模式下
17076
+ // inFlight 在流间隙会短暂置假,进度条因此跟着闪没;而且 turn 刚结束
17077
+ // 时用户其实想再看一眼最终进度。改回「只要当前 turn 里有 todos 就显
17078
+ // 示,allDone 时再隐藏」,跨 turn 残留交给开头那段「最后一条 user
17079
+ // 消息后才扫 TodoWrite」的 scoping 兜住。
17014
17080
  container.classList.remove("hidden");
17015
17081
  if (bodyEl) bodyEl.classList.remove("hidden");
17016
17082
 
@@ -515,33 +515,15 @@
515
515
  transition: width 0.3s var(--ease-out-expo), transform 0.35s var(--ease-out-expo), box-shadow 0.35s ease, opacity 0.25s ease;
516
516
  }
517
517
 
518
- /* 多塞一层 .sidebar-header-actions 把 specificity 抬到 0,4,0,
518
+ /* 窄条(collapsed)状态:不显示 X 关闭按钮。
519
+ 窄条只保留「展开」按钮,关闭操作必须先回到全尺寸。
520
+ 多塞一层 .sidebar-header-actions 把 specificity 抬到 0,4,0,
519
521
  压过文件后段 .sidebar-header-actions .btn-ghost.btn-sm
520
522
  与 .drawer-close-btn 的 display: inline-flex 重置规则。 */
521
- .sidebar.pinned .sidebar-header-actions .sidebar-close {
522
- display: none;
523
- }
524
-
525
- /* 窄条(collapsed)状态:明确不显示 X 关闭按钮。
526
- 虽然 collapsed 当前必然蕴含 pinned(见 isSidebarNarrow),
527
- 上一条 .sidebar.pinned 规则已经能把它藏住,但单独写一条更直观,
528
- 也防止以后允许 drawer 模式下 collapse 时漏掉。
529
- 展开(drawer 模式)下才显示关闭按钮供用户收起侧栏。 */
530
523
  .sidebar.collapsed .sidebar-header-actions .sidebar-close {
531
524
  display: none;
532
525
  }
533
526
 
534
- /* 与上方对称:drawer 模式(未 pin)下,X 关闭按钮已能把侧栏关掉,
535
- 再放一个「收起为窄条」按钮会和 X 视觉上重复,且它的行为
536
- (自动 pin + collapse)也容易让人误以为是关闭。
537
- 只在 pinned 模式保留这颗,drawer 模式下隐藏。
538
- 注意:必须用 .sidebar-header-actions 多嵌一层把 specificity 提到 0,4,0,
539
- 否则会和文件后段 .sidebar-header-actions .btn-ghost.btn-sm (0,3,0) 打平
540
- 而因后定义胜出,导致 drawer 模式下窄条按钮和 X 同时显示。 */
541
- .sidebar:not(.pinned) .sidebar-header-actions .sidebar-collapse-toggle {
542
- display: none;
543
- }
544
-
545
527
  /* ===== 侧栏窄条模式(仅 desktop pin 模式生效)===== */
546
528
  .sidebar.pinned.collapsed {
547
529
  width: var(--sidebar-collapsed-width);
@@ -553,7 +535,6 @@
553
535
  }
554
536
  .sidebar.pinned.collapsed .sidebar-header-main,
555
537
  .sidebar.pinned.collapsed .sidebar-header-more,
556
- .sidebar.pinned.collapsed .sidebar-pin-toggle,
557
538
  .sidebar.pinned.collapsed .sidebar-footer {
558
539
  display: none;
559
540
  }
@@ -700,15 +681,6 @@
700
681
  .sidebar-collapse-toggle.collapsed {
701
682
  color: var(--primary);
702
683
  }
703
- /* ===== 图钉按钮 ===== */
704
- .sidebar-pin-toggle {
705
- flex-shrink: 0;
706
- transition: transform 0.3s var(--ease-out-expo), color var(--transition-fast);
707
- }
708
- .sidebar-pin-toggle.pinned {
709
- color: var(--primary);
710
- transform: rotate(45deg);
711
- }
712
684
 
713
685
  /* ===== 侧边栏头部 ===== */
714
686
  .sidebar-header {
@@ -2617,6 +2589,11 @@
2617
2589
  flex: 1;
2618
2590
  display: flex;
2619
2591
  flex-direction: column;
2592
+ /* min-width: 0 是关键:作为 .main-layout(flex row)的子项,默认
2593
+ min-width:auto 会让 main-content 跟着内部最宽的不可断节点一起撑大,
2594
+ 窄屏上结果就是 chat 卡片把布局推宽 → 内容被 overflow:hidden 截掉
2595
+ 却又拖不动。这里强制可收缩,配合下游 .chat-* 的 break-word 才有效。 */
2596
+ min-width: 0;
2620
2597
  min-height: 0;
2621
2598
  overflow: hidden;
2622
2599
  background: transparent;
@@ -4062,14 +4039,21 @@
4062
4039
  color: var(--text-tertiary);
4063
4040
  }
4064
4041
 
4065
- /* ===== 消息气泡 ===== */
4042
+ /* ===== 消息气泡 =====
4043
+ 兜底约束:气泡不应超出聊天容器宽度。即便子节点(长 URL、不可断字符串、
4044
+ 嵌入元素)想撑宽,也由 max-width / overflow-x 截断或换行。 */
4066
4045
  .chat-message-bubble {
4067
4046
  padding: 12px 16px;
4068
4047
  border-radius: var(--radius-md);
4069
4048
  font-size: 0.875rem;
4070
4049
  line-height: var(--line-height-relaxed);
4071
4050
  word-wrap: break-word;
4051
+ overflow-wrap: anywhere;
4052
+ word-break: break-word;
4072
4053
  white-space: pre-wrap;
4054
+ max-width: 100%;
4055
+ min-width: 0;
4056
+ overflow-x: hidden;
4073
4057
  box-shadow: var(--shadow-sm);
4074
4058
  transition: box-shadow var(--transition-fast);
4075
4059
  }
@@ -4123,13 +4107,20 @@
4123
4107
  display: flex;
4124
4108
  flex-direction: column;
4125
4109
  gap: 2px;
4110
+ width: 100%;
4126
4111
  max-width: 100%;
4127
4112
  min-width: 0;
4113
+ /* overflow-x: hidden 兜底——内部 tool-use-card / inline-terminal /
4114
+ markdown 如果还有不可断超长 token,绝不让它把整张消息推宽。
4115
+ 结合 word-break + overflow-wrap,用户看到的永远是换行不是横滚条。 */
4116
+ overflow-x: hidden;
4128
4117
  font-size: 0.875rem;
4129
4118
  line-height: var(--line-height-relaxed);
4130
4119
  word-wrap: break-word;
4120
+ word-break: break-word;
4131
4121
  overflow-wrap: anywhere;
4132
4122
  color: var(--text-primary);
4123
+ box-sizing: border-box;
4133
4124
  }
4134
4125
 
4135
4126
  .chat-message-content > .thinking-inline,
@@ -4148,7 +4139,10 @@
4148
4139
  display: flex;
4149
4140
  flex-direction: column;
4150
4141
  gap: 6px;
4142
+ width: 100%;
4151
4143
  max-width: 100%;
4144
+ min-width: 0;
4145
+ box-sizing: border-box;
4152
4146
  }
4153
4147
  /* user turn 默认 align-self: flex-end(右对齐);多段渲染时强制
4154
4148
  容器内左对齐,否则 subagent 段会被推到右边,border-left 色条断裂。 */
@@ -4160,12 +4154,22 @@
4160
4154
  display: flex;
4161
4155
  flex-direction: column;
4162
4156
  align-self: flex-start;
4157
+ width: 100%;
4158
+ max-width: 100%;
4159
+ min-width: 0;
4160
+ box-sizing: border-box;
4163
4161
  }
4164
4162
  .chat-message.multi-agent .chat-message-segment.subagent {
4165
4163
  padding-left: 10px;
4166
4164
  margin-left: 6px;
4167
4165
  border-left: 2px solid var(--agent-color, var(--border-subtle));
4168
4166
  border-radius: 0 4px 4px 0;
4167
+ /* 父 .chat-message-segment 是 width:100%; box-sizing:border-box,padding
4168
+ 和 border 已被 border-box 吃掉,但 margin-left:6px 是外间距,会把
4169
+ 整段推出父容器 6px 撞屏幕右边。这里把宽度从父宽减掉同等 margin,
4170
+ 视觉上左缘内缩 6px、右缘对齐父容器,刚好不溢出。 */
4171
+ width: calc(100% - 6px);
4172
+ max-width: calc(100% - 6px);
4169
4173
  }
4170
4174
  .chat-message.multi-agent .chat-message-segment.subagent .chat-message-content {
4171
4175
  /* 子 agent 段内的气泡稍微弱化背景,区分主线 */
@@ -4179,13 +4183,19 @@
4179
4183
  color: var(--agent-color, var(--accent));
4180
4184
  }
4181
4185
 
4182
- /* "勤劳初二 ↳ 让 侦探猫 帮忙" 分隔提示 */
4186
+ /* "勤劳初二 ↳ 让 侦探猫 帮忙" 分隔提示
4187
+ 注:color-mix() 需要 Chrome/WebView 111+(2023.04)。Android APK 上常有更老
4188
+ 的 WebView,会把 color-mix(...) 当作 invalid 整条扔掉,导致背景/边框消失。
4189
+ 下面凡是 color-mix 的属性都先写一条 rgba()/纯色 fallback,再被 color-mix
4190
+ 覆盖;旧浏览器看到 fallback、新浏览器自动用 color-mix。同一文件里下方
4191
+ .chat-handoff-tag / .subagent-reply / .subagent-reply-cycle 等也是同思路。 */
4183
4192
  .chat-handoff {
4184
4193
  align-self: flex-start;
4185
4194
  margin: 4px 0 2px 4px;
4186
4195
  padding: 2px 10px;
4187
4196
  font-size: 0.7rem;
4188
4197
  color: var(--text-tertiary);
4198
+ background: rgba(197, 101, 61, 0.08);
4189
4199
  background: color-mix(in srgb, var(--agent-color, var(--accent)) 8%, transparent);
4190
4200
  border-left: 2px solid var(--agent-color, var(--accent));
4191
4201
  border-radius: 0 8px 8px 0;
@@ -4216,7 +4226,9 @@
4216
4226
  letter-spacing: 0.04em;
4217
4227
  text-transform: uppercase;
4218
4228
  color: var(--agent-color, var(--accent));
4229
+ background: rgba(197, 101, 61, 0.14);
4219
4230
  background: color-mix(in srgb, var(--agent-color, var(--accent)) 14%, transparent);
4231
+ border: 1px solid rgba(197, 101, 61, 0.32);
4220
4232
  border: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 32%, transparent);
4221
4233
  border-radius: 4px;
4222
4234
  vertical-align: 1px;
@@ -4226,7 +4238,9 @@
4226
4238
 
4227
4239
  /* subagent 回复气泡(群聊角色完成任务后的发言) */
4228
4240
  .subagent-reply {
4241
+ background: rgba(197, 101, 61, 0.06);
4229
4242
  background: color-mix(in srgb, var(--agent-color, var(--accent)) 6%, var(--bg-surface));
4243
+ border: 1px solid rgba(197, 101, 61, 0.20);
4230
4244
  border: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 20%, transparent);
4231
4245
  border-radius: 10px;
4232
4246
  padding: 10px 14px;
@@ -4236,6 +4250,10 @@
4236
4250
  color: var(--text-primary);
4237
4251
  word-break: break-word;
4238
4252
  overflow-wrap: anywhere;
4253
+ /* box-sizing: border-box 让 padding(14px*2)+border(1px*2) 算进 width:100%,
4254
+ 不再外鼓 30px 撑出 subagent 段右边缘。 */
4255
+ box-sizing: border-box;
4256
+ width: 100%;
4239
4257
  max-width: 100%;
4240
4258
  min-width: 0;
4241
4259
  overflow-x: hidden;
@@ -4245,7 +4263,9 @@
4245
4263
  .subagent-reply > :last-child { margin-bottom: 0; }
4246
4264
 
4247
4265
  .subagent-reply.error {
4266
+ background: rgba(226, 87, 76, 0.08);
4248
4267
  background: color-mix(in srgb, var(--danger, #e2574c) 8%, var(--bg-surface));
4268
+ border-color: rgba(226, 87, 76, 0.35);
4249
4269
  border-color: color-mix(in srgb, var(--danger, #e2574c) 35%, transparent);
4250
4270
  }
4251
4271
 
@@ -4336,8 +4356,11 @@
4336
4356
  font-size: 0.7rem;
4337
4357
  font-weight: 500;
4338
4358
  line-height: 1.4;
4359
+ color: var(--accent);
4339
4360
  color: var(--agent-color, var(--accent));
4361
+ background: rgba(255, 248, 240, 0.95);
4340
4362
  background: color-mix(in srgb, var(--agent-color, var(--accent)) 8%, var(--bg-surface, #fff));
4363
+ border: 1px solid rgba(197, 101, 61, 0.28);
4341
4364
  border: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 28%, transparent);
4342
4365
  border-radius: 999px;
4343
4366
  cursor: pointer;
@@ -4345,7 +4368,9 @@
4345
4368
  transition: background 0.15s ease, transform 0.1s ease, border-color 0.15s ease;
4346
4369
  }
4347
4370
  .subagent-reply-cycle:hover {
4371
+ background: rgba(248, 226, 210, 1);
4348
4372
  background: color-mix(in srgb, var(--agent-color, var(--accent)) 16%, var(--bg-surface, #fff));
4373
+ border-color: rgba(197, 101, 61, 0.45);
4349
4374
  border-color: color-mix(in srgb, var(--agent-color, var(--accent)) 45%, transparent);
4350
4375
  }
4351
4376
  .subagent-reply-cycle:active {
@@ -4362,6 +4387,7 @@
4362
4387
  height: 6px;
4363
4388
  }
4364
4389
  .subagent-reply-scroll::-webkit-scrollbar-thumb {
4390
+ background: rgba(197, 101, 61, 0.35);
4365
4391
  background: color-mix(in srgb, var(--agent-color, var(--accent)) 35%, transparent);
4366
4392
  border-radius: 3px;
4367
4393
  }
@@ -4442,10 +4468,15 @@
4442
4468
  font-size: 0.7rem;
4443
4469
  color: var(--text-secondary);
4444
4470
  white-space: pre-wrap;
4471
+ overflow-wrap: anywhere;
4445
4472
  word-break: break-all;
4446
4473
  max-height: 240px;
4474
+ max-width: 100%;
4475
+ min-width: 0;
4476
+ overflow-x: hidden;
4447
4477
  overflow-y: auto;
4448
4478
  border-top: 1px dashed var(--border-subtle);
4479
+ box-sizing: border-box;
4449
4480
  }
4450
4481
  .unknown-block.collapsed .unknown-block-body {
4451
4482
  display: none;
@@ -4601,6 +4632,9 @@
4601
4632
  overflow: hidden;
4602
4633
  width: 100%;
4603
4634
  max-width: var(--chat-card-max-width, 720px);
4635
+ /* min-width: 0 让卡片不被内部 <pre>/长路径撑出父容器;缺这一行时
4636
+ 手机 APK 上长命令 / 长 JSON 会把整张卡片推宽到屏幕外被截掉。 */
4637
+ min-width: 0;
4604
4638
  box-sizing: border-box;
4605
4639
  background: linear-gradient(180deg, rgba(255, 251, 245, 0.88) 0%, rgba(255, 246, 234, 0.72) 100%);
4606
4640
  box-shadow: 0 1px 2px rgba(89, 58, 32, 0.04);
@@ -4780,8 +4814,12 @@
4780
4814
  font-family: var(--font-mono);
4781
4815
  color: var(--text-secondary);
4782
4816
  white-space: pre-wrap;
4783
- overflow-wrap: break-word;
4817
+ overflow-wrap: anywhere;
4818
+ word-break: break-word;
4784
4819
  max-height: 400px;
4820
+ max-width: 100%;
4821
+ min-width: 0;
4822
+ overflow-x: hidden;
4785
4823
  overflow-y: auto;
4786
4824
  line-height: 1.55;
4787
4825
  }
@@ -4796,13 +4834,20 @@
4796
4834
  font-family: var(--font-mono);
4797
4835
  color: var(--text-secondary);
4798
4836
  white-space: pre-wrap;
4799
- overflow-wrap: break-word;
4837
+ /* anywhere + break-all 兜底长 token / 长路径 / 长 URL:手机 APK 上
4838
+ 遇到不可断字符串时换行,绝不横向溢出。 */
4839
+ overflow-wrap: anywhere;
4840
+ word-break: break-all;
4800
4841
  max-height: 320px;
4842
+ max-width: 100%;
4843
+ min-width: 0;
4844
+ overflow-x: hidden;
4801
4845
  overflow-y: auto;
4802
4846
  line-height: 1.55;
4803
4847
  background: rgba(125, 91, 57, 0.035);
4804
4848
  border-radius: var(--radius-xs);
4805
4849
  padding: 8px 10px;
4850
+ box-sizing: border-box;
4806
4851
  }
4807
4852
  .tool-use-result-empty {
4808
4853
  font-size: 0.75rem;
@@ -5046,7 +5091,8 @@
5046
5091
  }
5047
5092
  .thinking-inline.expanded .thinking-inline-preview {
5048
5093
  white-space: pre-wrap;
5049
- overflow-wrap: break-word;
5094
+ overflow-wrap: anywhere;
5095
+ word-break: break-word;
5050
5096
  }
5051
5097
  .thinking-inline-action {
5052
5098
  flex-shrink: 0;
@@ -5120,6 +5166,8 @@
5120
5166
  margin: 4px 0;
5121
5167
  width: 100%;
5122
5168
  max-width: var(--chat-card-max-width, 720px);
5169
+ /* 同 .tool-use-card:内部嵌套 inline-tool 长路径不能撑爆窄屏父容器。 */
5170
+ min-width: 0;
5123
5171
  box-sizing: border-box;
5124
5172
  border-radius: var(--radius-sm);
5125
5173
  border: 1px solid var(--border-subtle);
@@ -5248,6 +5296,8 @@
5248
5296
  margin: 1px 0;
5249
5297
  width: 100%;
5250
5298
  max-width: var(--chat-card-max-width, 720px);
5299
+ /* 同 .tool-use-card:长路径 / 长查询不能撑爆窄屏父容器。 */
5300
+ min-width: 0;
5251
5301
  box-sizing: border-box;
5252
5302
  border-radius: var(--radius-xs);
5253
5303
  cursor: pointer;
@@ -5267,6 +5317,8 @@
5267
5317
  font-family: var(--font-mono);
5268
5318
  line-height: 1.45;
5269
5319
  border-radius: var(--radius-xs);
5320
+ max-width: 100%;
5321
+ min-width: 0;
5270
5322
  transition: background 0.16s ease, color 0.16s ease;
5271
5323
  }
5272
5324
  .inline-tool:hover .inline-tool-row {
@@ -5339,6 +5391,9 @@
5339
5391
  padding: 10px 12px;
5340
5392
  margin-top: 6px;
5341
5393
  max-height: 320px;
5394
+ max-width: 100%;
5395
+ min-width: 0;
5396
+ overflow-x: hidden;
5342
5397
  overflow-y: auto;
5343
5398
  scrollbar-width: thin;
5344
5399
  scrollbar-color: rgba(125, 91, 57, 0.2) transparent;
@@ -5359,7 +5414,10 @@
5359
5414
  line-height: 1.55;
5360
5415
  color: var(--text-secondary);
5361
5416
  white-space: pre-wrap;
5417
+ overflow-wrap: anywhere;
5362
5418
  word-break: break-all;
5419
+ max-width: 100%;
5420
+ min-width: 0;
5363
5421
  margin: 0;
5364
5422
  }
5365
5423
  .inline-tool-error-inline {
@@ -5442,6 +5500,8 @@
5442
5500
  margin: 4px 0;
5443
5501
  width: 100%;
5444
5502
  max-width: var(--chat-card-max-width, 720px);
5503
+ /* 同 .tool-use-card:长命令 / 长输出不能撑爆窄屏父容器。 */
5504
+ min-width: 0;
5445
5505
  box-sizing: border-box;
5446
5506
  border: 1px solid rgba(15, 12, 9, 0.6);
5447
5507
  border-radius: var(--radius-sm);
@@ -5471,6 +5531,8 @@
5471
5531
  cursor: pointer;
5472
5532
  user-select: none;
5473
5533
  position: relative;
5534
+ max-width: 100%;
5535
+ min-width: 0;
5474
5536
  }
5475
5537
  .term-header:hover {
5476
5538
  background:
@@ -5550,7 +5612,10 @@
5550
5612
  line-height: 1.55;
5551
5613
  margin-bottom: 6px;
5552
5614
  white-space: pre-wrap;
5615
+ overflow-wrap: anywhere;
5553
5616
  word-break: break-all;
5617
+ max-width: 100%;
5618
+ min-width: 0;
5554
5619
  }
5555
5620
  .term-prompt {
5556
5621
  color: #6ee09a;
@@ -5563,6 +5628,9 @@
5563
5628
  padding-top: 8px;
5564
5629
  border-top: 1px dashed rgba(255, 255, 255, 0.06);
5565
5630
  max-height: 360px;
5631
+ max-width: 100%;
5632
+ min-width: 0;
5633
+ overflow-x: hidden;
5566
5634
  overflow-y: auto;
5567
5635
  scrollbar-width: thin;
5568
5636
  scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
@@ -5583,7 +5651,10 @@
5583
5651
  color: #d4c2a3;
5584
5652
  line-height: 1.55;
5585
5653
  white-space: pre-wrap;
5654
+ overflow-wrap: anywhere;
5586
5655
  word-break: break-all;
5656
+ max-width: 100%;
5657
+ min-width: 0;
5587
5658
  }
5588
5659
  .term-exit {
5589
5660
  margin-top: 10px;
@@ -5605,6 +5676,8 @@
5605
5676
  margin: 4px 0;
5606
5677
  width: 100%;
5607
5678
  max-width: var(--chat-card-max-width, 720px);
5679
+ /* 同 .tool-use-card:长路径 / 长 diff 不能撑爆窄屏父容器。 */
5680
+ min-width: 0;
5608
5681
  box-sizing: border-box;
5609
5682
  border: 1px solid var(--border-subtle);
5610
5683
  border-radius: var(--radius-sm);
@@ -5626,6 +5699,8 @@
5626
5699
  font-size: 0.8125rem;
5627
5700
  cursor: pointer;
5628
5701
  transition: background 0.18s ease;
5702
+ max-width: 100%;
5703
+ min-width: 0;
5629
5704
  }
5630
5705
  .diff-header:hover {
5631
5706
  background: rgba(197, 101, 61, 0.04);
@@ -5701,11 +5776,15 @@
5701
5776
  display: flex;
5702
5777
  gap: 1px;
5703
5778
  background: rgba(125, 91, 57, 0.08);
5779
+ max-width: 100%;
5780
+ min-width: 0;
5704
5781
  }
5705
5782
  .diff-col {
5706
- flex: 1;
5783
+ flex: 1 1 0;
5707
5784
  min-width: 0;
5785
+ max-width: 100%;
5708
5786
  background: rgba(255, 253, 248, 0.85);
5787
+ overflow: hidden;
5709
5788
  }
5710
5789
  .diff-col-half {
5711
5790
  flex: 1;
@@ -5731,7 +5810,12 @@
5731
5810
  font-size: 0.75rem;
5732
5811
  line-height: 1.55;
5733
5812
  white-space: pre-wrap;
5813
+ /* 长 token / 长 URL 必须按字符断,否则不可断字符串会撑爆 .diff-col。 */
5814
+ overflow-wrap: anywhere;
5734
5815
  word-break: break-all;
5816
+ max-width: 100%;
5817
+ min-width: 0;
5818
+ box-sizing: border-box;
5735
5819
  }
5736
5820
  .diff-add {
5737
5821
  background: rgba(79, 122, 88, 0.08);
@@ -5821,12 +5905,20 @@
5821
5905
  margin: 0;
5822
5906
  font-size: 0.7rem;
5823
5907
  max-height: 600px;
5908
+ max-width: 100%;
5909
+ min-width: 0;
5910
+ overflow-x: hidden;
5824
5911
  overflow-y: auto;
5912
+ white-space: pre-wrap;
5913
+ overflow-wrap: anywhere;
5914
+ word-break: break-all;
5825
5915
  }
5826
5916
  .tool-result-content code {
5827
5917
  font-family: var(--font-mono);
5828
5918
  white-space: pre-wrap;
5919
+ overflow-wrap: anywhere;
5829
5920
  word-break: break-all;
5921
+ max-width: 100%;
5830
5922
  }
5831
5923
 
5832
5924
  /* Markdown Content */
@@ -5902,8 +5994,14 @@
5902
5994
  overflow-wrap: anywhere;
5903
5995
  word-break: break-all;
5904
5996
  }
5997
+ /* 表格容器:先让 table-layout:fixed 强制按 100% 收缩,单元格内
5998
+ overflow-wrap:anywhere 会负责换行;只有在内容真的塞不下(比如
5999
+ 超长不可断字符串撑爆 fixed 布局)才退回横向滚动,这样大多数情况
6000
+ 下用户看到的是换行而不是横滚条。 */
5905
6001
  .markdown-content .md-table-wrap {
5906
6002
  margin: 12px 0;
6003
+ max-width: 100%;
6004
+ min-width: 0;
5907
6005
  overflow-x: auto;
5908
6006
  border: 1px solid var(--border);
5909
6007
  border-radius: var(--radius-sm);
@@ -6452,10 +6550,10 @@
6452
6550
  }
6453
6551
  .queue-bar-item.expanded:active { cursor: grabbing; }
6454
6552
 
6455
- /* 单条排队时不必拖动 */
6456
- .queue-bar-item-single { cursor: default; }
6457
- .queue-bar-item-single.expanded { cursor: default; }
6458
- .queue-bar-item-single.expanded:active { cursor: default; }
6553
+ /* 单条排队时不能拖排序,但仍可点击 → 立即发送 */
6554
+ .queue-bar-item-single { cursor: pointer; }
6555
+ .queue-bar-item-single.expanded { cursor: pointer; }
6556
+ .queue-bar-item-single.expanded:active { cursor: pointer; }
6459
6557
 
6460
6558
  /* sibling 在被拖期间平滑回位 */
6461
6559
  .queue-bar-item-sliding { transition: transform 160ms cubic-bezier(0.2, 0.7, 0.2, 1); }
@@ -6615,11 +6713,15 @@
6615
6713
  }
6616
6714
 
6617
6715
  .input-hint {
6618
- font-size: 0.5625rem;
6716
+ font-size: 0.5rem; /* 8px — 让位给同一行的按钮,桌面端仍清晰可读 */
6717
+ line-height: 1.2;
6619
6718
  color: var(--text-muted);
6620
6719
  white-space: nowrap;
6621
6720
  user-select: none;
6622
6721
  opacity: 0.7;
6722
+ max-width: 38ch; /* 防止突然变长的状态文案撑爆右侧区域 */
6723
+ overflow: hidden;
6724
+ text-overflow: ellipsis;
6623
6725
  }
6624
6726
 
6625
6727
  /* Session info bar at bottom of input composer */
@@ -7519,40 +7621,8 @@
7519
7621
  .btn-pill-label {
7520
7622
  display: inline;
7521
7623
  }
7522
- .btn-pill-interrupt {
7523
- background: linear-gradient(180deg, rgba(197, 101, 61, 0.18) 0%, rgba(197, 101, 61, 0.10) 100%);
7524
- color: var(--accent);
7525
- margin-right: 4px;
7526
- box-shadow: 0 0 0 1px rgba(197, 101, 61, 0.30) inset;
7527
- animation: interruptPulse 1.8s ease-in-out infinite;
7528
- }
7529
- .btn-pill-interrupt:hover {
7530
- background: linear-gradient(180deg, var(--accent) 0%, #a8522f 100%);
7531
- color: #fff;
7532
- box-shadow: 0 3px 10px rgba(197, 101, 61, 0.35);
7533
- transform: translateY(-1px);
7534
- animation: none;
7535
- }
7536
- .btn-pill-interrupt:active {
7537
- transform: translateY(0) scale(0.97);
7538
- }
7539
- .btn-pill-interrupt.hidden {
7540
- display: none;
7541
- }
7542
- @keyframes interruptPulse {
7543
- 0%, 100% { box-shadow: 0 0 0 1px rgba(197, 101, 61, 0.30) inset, 0 0 0 0 rgba(197, 101, 61, 0.0); }
7544
- 50% { box-shadow: 0 0 0 1px rgba(197, 101, 61, 0.45) inset, 0 0 0 4px rgba(197, 101, 61, 0.12); }
7545
- }
7546
- /* 屏宽 < 480 时把"立即"文字藏掉,只剩 » 图标,避免和 send 一起把行挤爆。
7547
- hover/focus 期间临时回显,让长按移动端用户也能确认按钮含义。 */
7548
- @media (max-width: 480px) {
7549
- .btn-pill-interrupt {
7550
- padding: 0 8px;
7551
- }
7552
- .btn-pill-interrupt .btn-pill-label {
7553
- display: none;
7554
- }
7555
- }
7624
+ /* .btn-pill-interrupt 与 @keyframes interruptPulse 已下线:
7625
+ 默认行为永远是"排队(气泡)",想插队请点输入框上方那条气泡。 */
7556
7626
 
7557
7627
  /* send 按钮在「排队模式」下退到次要色,让相邻的"立即发送"成为视觉重点。
7558
7628
  不动尺寸 / 圆形,避免 layout shift。 */
@@ -8944,9 +9014,8 @@
8944
9014
  line-height: 1.5;
8945
9015
  }
8946
9016
 
8947
- /* Sidebar header icon buttons — pin / more / close — uniform 32×32 ghost */
9017
+ /* Sidebar header icon buttons — collapse / more / close — uniform 32×32 ghost */
8948
9018
  .sidebar-header-actions .btn-ghost.btn-sm,
8949
- .sidebar-pin-toggle,
8950
9019
  .drawer-close-btn {
8951
9020
  width: 32px;
8952
9021
  height: 32px;
@@ -8965,16 +9034,10 @@
8965
9034
  color 0.16s ease,
8966
9035
  transform 0.22s cubic-bezier(0.34, 1.4, 0.64, 1);
8967
9036
  }
8968
- .sidebar-header-actions .btn-ghost.btn-sm:hover,
8969
- .sidebar-pin-toggle:hover {
9037
+ .sidebar-header-actions .btn-ghost.btn-sm:hover {
8970
9038
  background: rgba(125, 91, 57, 0.08);
8971
9039
  color: var(--text-primary);
8972
9040
  }
8973
- .sidebar-pin-toggle.pinned {
8974
- background: rgba(197, 101, 61, 0.12);
8975
- color: var(--accent);
8976
- transform: rotate(45deg);
8977
- }
8978
9041
 
8979
9042
  /* Drawer close — same pattern as modal-close (rotate + danger tint) */
8980
9043
  .drawer-close-btn {
@@ -9705,9 +9768,6 @@
9705
9768
 
9706
9769
  /* 平板适配 */
9707
9770
  @media (max-width: 768px) {
9708
- /* 手机隐藏「固定侧栏」按钮:手机上完整 300px 常驻太占地,
9709
- 只保留「收起为窄条」按钮,让用户能切到 56px 窄条形态。 */
9710
- .sidebar-pin-toggle { display: none; }
9711
9771
  /* drawer 模式(pinned 但非窄条)下若未打开,则隐藏到屏幕左侧。
9712
9772
  窄条模式(pinned + collapsed)由 .sidebar.pinned.collapsed 的 width:56px
9713
9773
  规则常驻显示,不进入这条隐藏分支。 */
@@ -10065,8 +10125,12 @@
10065
10125
  padding: 0 14px 0 4px;
10066
10126
  border-radius: 5px;
10067
10127
  }
10128
+ /* 窄屏(手机)保留提示,但用更小字号 + 更紧的宽度上限,避免把发送按钮挤出行外。
10129
+ 状态切换("思考中…" / "已加入排队…")能截断成省略号也比整条消失友好。 */
10068
10130
  .input-hint {
10069
- display: none;
10131
+ font-size: 0.45rem;
10132
+ max-width: 18ch;
10133
+ opacity: 0.6;
10070
10134
  }
10071
10135
 
10072
10136
  /* 移动端内联快捷键 - 折叠为展开按钮,展开到独立第二行 */
@@ -10501,6 +10565,43 @@
10501
10565
  .markdown-content pre,
10502
10566
  .markdown-content .code-block pre,
10503
10567
  .subagent-reply pre { padding: 10px; }
10568
+ /* diff 卡片:窄屏下两列 side-by-side 每列只剩 ~160px,旧/新被挤成
10569
+ 单字一行的"窄条"几乎不可读。改成垂直堆叠(旧在上,新在下),
10570
+ 单列占满屏宽,长行靠 word-break 自然换行,杜绝横向溢出。 */
10571
+ .diff-columns {
10572
+ flex-direction: column;
10573
+ }
10574
+ .diff-col,
10575
+ .diff-col-half,
10576
+ .diff-col-full {
10577
+ flex: 1 1 auto;
10578
+ width: 100%;
10579
+ max-width: 100%;
10580
+ }
10581
+ /* 终端命令头部也放开 wrap,长命令在 preview 里仍 ellipsis,但 dot/toggle
10582
+ 之类不会把卡片撑宽。 */
10583
+ .term-header {
10584
+ flex-wrap: wrap;
10585
+ row-gap: 4px;
10586
+ }
10587
+ .diff-header {
10588
+ flex-wrap: wrap;
10589
+ row-gap: 4px;
10590
+ }
10591
+ .diff-path {
10592
+ flex-basis: 100%;
10593
+ order: 99;
10594
+ }
10595
+ /* inline-tool meta(搜索路径等)窄屏给整行,不再贴右挤标题。 */
10596
+ .inline-tool-row {
10597
+ flex-wrap: wrap;
10598
+ row-gap: 2px;
10599
+ }
10600
+ .inline-tool-meta {
10601
+ max-width: 100%;
10602
+ flex-basis: 100%;
10603
+ padding-left: 22px;
10604
+ }
10504
10605
  }
10505
10606
 
10506
10607
  /* Blank chat mobile optimization */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.32.0",
3
+ "version": "1.32.1",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {