@co0ontty/wand 1.10.0 → 1.14.2

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.
@@ -137,6 +137,7 @@
137
137
  notifBubble: (function() {
138
138
  try { var v = localStorage.getItem("wand-notif-bubble"); return v === null ? true : v === "true"; } catch (e) { return true; }
139
139
  })(),
140
+ toolContentCache: {},
140
141
  currentView: "terminal",
141
142
  terminalScale: (function() {
142
143
  try {
@@ -175,6 +176,8 @@
175
176
  lastRenderedMsgCount: 0,
176
177
  lastRenderedEmpty: null,
177
178
  renderPending: false,
179
+ chatPageSize: 20,
180
+ chatRenderedCount: 20,
178
181
  currentTask: null, // Current task title from Claude
179
182
  terminalInteractive: false,
180
183
  miniKeyboardVisible: false,
@@ -402,6 +405,35 @@
402
405
  updateChatJumpToBottomButton();
403
406
  }
404
407
 
408
+ /** Load older messages by expanding the visible window */
409
+ function loadMoreChatMessages() {
410
+ if (state.chatRenderedCount >= state.currentMessages.length) return;
411
+ state.chatRenderedCount += state.chatPageSize;
412
+ renderChat(true);
413
+ }
414
+
415
+ // Observe the "load more" sentinel for auto-loading when scrolled into view
416
+ var _loadMoreObserver = null;
417
+ function observeLoadMoreSentinel() {
418
+ if (_loadMoreObserver) { _loadMoreObserver.disconnect(); _loadMoreObserver = null; }
419
+ var sentinel = document.getElementById("chat-load-more-sentinel");
420
+ if (!sentinel) return;
421
+ // Click handler for the button
422
+ var btn = sentinel.querySelector(".chat-load-more-btn");
423
+ if (btn) btn.onclick = function() { loadMoreChatMessages(); };
424
+ // IntersectionObserver for auto-load on scroll
425
+ if (typeof IntersectionObserver === "undefined") return;
426
+ _loadMoreObserver = new IntersectionObserver(function(entries) {
427
+ for (var i = 0; i < entries.length; i++) {
428
+ if (entries[i].isIntersecting) {
429
+ loadMoreChatMessages();
430
+ break;
431
+ }
432
+ }
433
+ }, { root: getChatScrollElement(), rootMargin: "200px" });
434
+ _loadMoreObserver.observe(sentinel);
435
+ }
436
+
405
437
  // Helper function to persist selected session ID to localStorage
406
438
  function persistSelectedId() {
407
439
  try {
@@ -704,6 +736,7 @@
704
736
  state.lastRenderedMsgCount = 0;
705
737
  state.lastRenderedEmpty = null;
706
738
  state.renderPending = false;
739
+ state.chatRenderedCount = state.chatPageSize;
707
740
  state.askUserSelections = {};
708
741
  if (state.chatScrollElement && state.chatScrollHandler) {
709
742
  state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
@@ -1283,6 +1316,7 @@
1283
1316
  '<button class="settings-tab" data-tab="notifications">\u901a\u77e5</button>' +
1284
1317
  '<button class="settings-tab" data-tab="security">\u5b89\u5168</button>' +
1285
1318
  '<button class="settings-tab" data-tab="presets">\u547d\u4ee4\u9884\u8bbe</button>' +
1319
+ '<button class="settings-tab" data-tab="display">\u663e\u793a</button>' +
1286
1320
  '</div>' +
1287
1321
 
1288
1322
  // About tab
@@ -1305,6 +1339,31 @@
1305
1339
  '</div>' +
1306
1340
  '<p id="update-message" class="hint hidden"></p>' +
1307
1341
  '</div>' +
1342
+ '<div class="settings-update-section" id="android-apk-section">' +
1343
+ '<div id="android-apk-current-row" class="settings-about-row hidden">' +
1344
+ '<span class="settings-label">当前版本</span>' +
1345
+ '<span class="settings-value" id="settings-android-apk-current">-</span>' +
1346
+ '</div>' +
1347
+ '<div id="android-apk-github-row" class="settings-about-row hidden">' +
1348
+ '<span class="settings-label">线上版本</span>' +
1349
+ '<span class="settings-value" id="settings-android-apk-github" style="flex:1">-</span>' +
1350
+ '<button id="download-github-apk-btn" class="btn btn-ghost btn-sm hidden" type="button" style="margin-left:8px;flex-shrink:0">下载</button>' +
1351
+ '</div>' +
1352
+ '<div id="android-apk-local-row" class="settings-about-row hidden">' +
1353
+ '<span class="settings-label">本地版本</span>' +
1354
+ '<span class="settings-value" id="settings-android-apk-local" style="flex:1">-</span>' +
1355
+ '<button id="download-local-apk-btn" class="btn btn-ghost btn-sm hidden" type="button" style="margin-left:8px;flex-shrink:0">下载</button>' +
1356
+ '</div>' +
1357
+ '<p id="android-apk-message" class="hint hidden"></p>' +
1358
+ '</div>' +
1359
+ '<div class="settings-update-section" id="android-connect-section">' +
1360
+ '<div class="settings-section-title" style="margin-bottom:8px">App 连接码</div>' +
1361
+ '<div class="settings-connect-url-box">' +
1362
+ '<code id="android-connect-code" class="settings-connect-url-text" style="font-size:12px;word-break:break-all">-</code>' +
1363
+ '<button id="copy-connect-code-button" class="btn btn-ghost btn-sm" type="button" title="复制连接码">复制</button>' +
1364
+ '</div>' +
1365
+ '<p class="hint">复制此连接码粘贴到 Android App 即可自动连接,无需输入密码。修改密码后连接码自动失效。</p>' +
1366
+ '</div>' +
1308
1367
  '</div>' +
1309
1368
 
1310
1369
  // Notifications tab
@@ -1389,6 +1448,27 @@
1389
1448
  '<label class="field-label" for="cfg-shell">Shell</label>' +
1390
1449
  '<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
1391
1450
  '</div>' +
1451
+ (typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function" ?
1452
+ '<div style="margin-bottom:16px">' +
1453
+ '<div class="settings-section-title">应用图标</div>' +
1454
+ '<p class="hint" style="margin-top:-4px;margin-bottom:10px">选择 App 启动器图标,返回桌面后生效</p>' +
1455
+ '<div id="app-icon-picker" style="display:flex;gap:16px">' +
1456
+ '<div class="app-icon-option" data-icon="shorthair" style="cursor:pointer;text-align:center">' +
1457
+ '<div class="app-icon-preview" style="width:56px;height:56px;border-radius:12px;overflow:hidden;border:3px solid transparent;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center;margin-bottom:4px">' +
1458
+ PIXEL_AVATAR.user +
1459
+ '</div>' +
1460
+ '<span style="font-size:0.72rem;color:var(--text-secondary)">赛博虎妞</span>' +
1461
+ '</div>' +
1462
+ '<div class="app-icon-option" data-icon="garfield" style="cursor:pointer;text-align:center">' +
1463
+ '<div class="app-icon-preview" style="width:56px;height:56px;border-radius:12px;overflow:hidden;border:3px solid transparent;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center;margin-bottom:4px">' +
1464
+ PIXEL_AVATAR.assistant +
1465
+ '</div>' +
1466
+ '<span style="font-size:0.72rem;color:var(--text-secondary)">勤劳初二</span>' +
1467
+ '</div>' +
1468
+ '</div>' +
1469
+ '<p id="app-icon-message" class="hint hidden" style="margin-top:8px"></p>' +
1470
+ '</div>'
1471
+ : '') +
1392
1472
  '<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
1393
1473
  '<p id="config-message" class="hint hidden"></p>' +
1394
1474
  '</div>' +
@@ -1429,6 +1509,39 @@
1429
1509
  '<div class="settings-panel" id="settings-tab-presets">' +
1430
1510
  '<div id="presets-list" class="presets-list"></div>' +
1431
1511
  '</div>' +
1512
+
1513
+ // Display settings tab
1514
+ '<div class="settings-panel" id="settings-tab-display">' +
1515
+ '<div class="settings-section-title">卡片默认展开状态</div>' +
1516
+ '<p class="hint" style="margin-top:-4px;margin-bottom:12px">设置结构化聊天视图中各类卡片的默认展开/折叠状态。手动操作的展开状态优先于此默认设置。</p>' +
1517
+ '<div class="field field-inline">' +
1518
+ '<input id="cfg-card-edit" type="checkbox" class="field-checkbox" />' +
1519
+ '<label class="field-label" for="cfg-card-edit">编辑卡片 (Edit/Write)</label>' +
1520
+ '</div>' +
1521
+ '<p class="hint" style="margin-top:0;margin-bottom:10px">文件编辑和写入操作的 diff 视图</p>' +
1522
+ '<div class="field field-inline">' +
1523
+ '<input id="cfg-card-inline" type="checkbox" class="field-checkbox" />' +
1524
+ '<label class="field-label" for="cfg-card-inline">内联工具 (Read/Glob/Grep)</label>' +
1525
+ '</div>' +
1526
+ '<p class="hint" style="margin-top:0;margin-bottom:10px">文件读取、搜索等工具的结果</p>' +
1527
+ '<div class="field field-inline">' +
1528
+ '<input id="cfg-card-terminal" type="checkbox" class="field-checkbox" />' +
1529
+ '<label class="field-label" for="cfg-card-terminal">终端输出 (Bash)</label>' +
1530
+ '</div>' +
1531
+ '<p class="hint" style="margin-top:0;margin-bottom:10px">命令行执行结果</p>' +
1532
+ '<div class="field field-inline">' +
1533
+ '<input id="cfg-card-thinking" type="checkbox" class="field-checkbox" />' +
1534
+ '<label class="field-label" for="cfg-card-thinking">思考过程 (Thinking)</label>' +
1535
+ '</div>' +
1536
+ '<p class="hint" style="margin-top:0;margin-bottom:10px">Claude 的思考过程块</p>' +
1537
+ '<div class="field field-inline">' +
1538
+ '<input id="cfg-card-toolgroup" type="checkbox" class="field-checkbox" />' +
1539
+ '<label class="field-label" for="cfg-card-toolgroup">工具组</label>' +
1540
+ '</div>' +
1541
+ '<p class="hint" style="margin-top:0;margin-bottom:10px">连续同类工具调用的折叠组</p>' +
1542
+ '<button id="save-display-button" class="btn btn-primary btn-block">保存显示设置</button>' +
1543
+ '<p id="display-message" class="hint hidden"></p>' +
1544
+ '</div>' +
1432
1545
  '</div>' +
1433
1546
  '</div>' +
1434
1547
  '</section>';
@@ -1856,7 +1969,10 @@
1856
1969
  } catch (e) {}
1857
1970
  applyTerminalScale();
1858
1971
  updateScaleLabel();
1859
- scheduleTerminalResize();
1972
+ // Force refit: font size changed but container dimensions didn't,
1973
+ // so ensureTerminalFit (which resets viewport tracking) is needed
1974
+ // instead of scheduleTerminalResize (which skips when size unchanged).
1975
+ ensureTerminalFit();
1860
1976
  }
1861
1977
 
1862
1978
  function applyTerminalScale() {
@@ -2295,6 +2411,35 @@
2295
2411
  '</div>';
2296
2412
  }
2297
2413
 
2414
+ function timeAgo(isoString) {
2415
+ if (!isoString) return "";
2416
+ var now = Date.now();
2417
+ var then = new Date(isoString).getTime();
2418
+ var diff = Math.max(0, now - then);
2419
+ var seconds = Math.floor(diff / 1000);
2420
+ if (seconds < 60) return "刚刚";
2421
+ var minutes = Math.floor(seconds / 60);
2422
+ if (minutes < 60) return minutes + "分钟前";
2423
+ var hours = Math.floor(minutes / 60);
2424
+ if (hours < 24) return hours + "小时前";
2425
+ var days = Math.floor(hours / 24);
2426
+ if (days < 30) return days + "天前";
2427
+ return Math.floor(days / 30) + "个月前";
2428
+ }
2429
+
2430
+ function elapsedTime(isoString) {
2431
+ if (!isoString) return "";
2432
+ var now = Date.now();
2433
+ var then = new Date(isoString).getTime();
2434
+ var diff = Math.max(0, now - then);
2435
+ var seconds = Math.floor(diff / 1000);
2436
+ var minutes = Math.floor(seconds / 60);
2437
+ var hours = Math.floor(minutes / 60);
2438
+ if (hours > 0) return hours + "h" + (minutes % 60 > 0 ? (minutes % 60) + "m" : "");
2439
+ if (minutes > 0) return minutes + "m";
2440
+ return seconds + "s";
2441
+ }
2442
+
2298
2443
  function getSessionStatusLabel(session) {
2299
2444
  if (!session) return "";
2300
2445
  if (session.archived) return "已归档";
@@ -2317,29 +2462,55 @@
2317
2462
  return session.status || "";
2318
2463
  }
2319
2464
 
2465
+ /** Get a human-readable activity description for a running session */
2466
+ function getSessionActivityDesc(session) {
2467
+ if (!session) return "";
2468
+ if (session.permissionBlocked) return "等待你的授权";
2469
+ if (session.status !== "running") return "";
2470
+ // Check WebSocket-delivered currentTask first
2471
+ if (session.id === state.selectedId && state.currentTask && state.currentTask.title) {
2472
+ return state.currentTask.title;
2473
+ }
2474
+ // Fall back to snapshot-delivered currentTaskTitle
2475
+ if (session.currentTaskTitle) return session.currentTaskTitle;
2476
+ return "";
2477
+ }
2478
+
2479
+ /** Get the last meaningful assistant text from messages for notification/display */
2480
+ function getLastAssistantSummary(session) {
2481
+ var msgs = session && session.messages;
2482
+ if (!msgs || msgs.length === 0) return "";
2483
+ for (var i = msgs.length - 1; i >= 0; i--) {
2484
+ var msg = msgs[i];
2485
+ if (msg.role !== "assistant") continue;
2486
+ var blocks = msg.content || [];
2487
+ for (var j = 0; j < blocks.length; j++) {
2488
+ if (blocks[j].type === "text" && blocks[j].text && blocks[j].text.trim()) {
2489
+ var text = blocks[j].text.trim();
2490
+ // Strip markdown formatting for compact display
2491
+ text = text.replace(/^#+\s+/gm, "").replace(/\*\*/g, "").replace(/`/g, "");
2492
+ var firstLine = text.split("\n")[0].trim();
2493
+ return firstLine.slice(0, 100);
2494
+ }
2495
+ }
2496
+ }
2497
+ return "";
2498
+ }
2499
+
2320
2500
  function renderSessionItem(session) {
2321
2501
  var activeClass = session.id === state.selectedId ? " active" : "";
2322
2502
  var selectedClass = state.sessionsManageMode && state.selectedSessionIds[session.id] ? " selected" : "";
2323
2503
  var metaStatus = getSessionStatusLabel(session);
2324
2504
  var metaStatusClass = getSessionStatusClass(session);
2325
- var modeName = session.mode === "full-access" ? "全权限" : session.mode === "default" ? "默认" : session.mode === "native" ? "原生" : session.mode === "auto-edit" ? "自动编辑" : session.mode;
2326
2505
  var resumeButton = "";
2327
- var sessionIdDisplay = "";
2328
- var recoveryHint = "";
2329
2506
  var checkbox = renderManageCheckbox("sessions", session.id, "选择会话 " + session.command);
2330
2507
 
2331
2508
  if (session.provider === "claude" && session.claudeSessionId) {
2332
- var shortId = session.claudeSessionId.slice(0, 8);
2333
- sessionIdDisplay = '<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>';
2334
2509
  if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
2335
2510
  resumeButton = '<button class="session-action-btn" data-action="resume" data-session-id="' + session.id + '" type="button" aria-label="恢复会话" title="恢复 Claude 会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 105.64-11.36L3 10"/></svg></button>';
2336
2511
  }
2337
2512
  }
2338
2513
 
2339
- if (session.autoRecovered) {
2340
- recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
2341
- }
2342
-
2343
2514
  var canOpenMerge = !state.sessionsManageMode && session.worktreeEnabled && session.worktree && session.worktree.branch && session.worktree.path;
2344
2515
  var needsCleanup = session.worktreeMergeStatus === "merged" && session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false;
2345
2516
  var mergeDisabled = session.status === "running" || session.worktreeMergeStatus === "merging";
@@ -2350,23 +2521,50 @@
2350
2521
  ? '<button class="session-action-btn merge-btn" data-action="worktree-cleanup" data-session-id="' + session.id + '" type="button" aria-label="重试清理 worktree" title="重试清理 worktree"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>'
2351
2522
  : "";
2352
2523
  var deleteButton = state.sessionsManageMode ? '' : '<button class="session-action-btn delete-btn" data-action="delete-session" data-session-id="' + session.id + '" type="button" aria-label="删除会话" title="删除此会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
2353
- var modeBadge = renderSessionKindBadge(session);
2354
2524
  var actionsHtml = '<span class="session-actions">' + resumeButton + mergeButton + deleteButton + '</span>';
2355
2525
 
2526
+ // Title: summary or command
2527
+ var titleHtml = session.summary
2528
+ ? '<div class="session-title">' + escapeHtml(session.summary) + '</div>'
2529
+ : '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>';
2530
+
2531
+ // Activity description for running sessions
2532
+ var activityDesc = getSessionActivityDesc(session);
2533
+ var activityHtml = "";
2534
+ if (session.status === "running" && activityDesc) {
2535
+ activityHtml = '<div class="session-activity">' + escapeHtml(activityDesc) + '</div>';
2536
+ }
2537
+
2538
+ // Time display
2539
+ var timeDisplay = "";
2540
+ if (session.status === "running") {
2541
+ timeDisplay = '<span class="session-time" title="已运行 ' + escapeHtml(elapsedTime(session.startedAt)) + '">' + escapeHtml(elapsedTime(session.startedAt)) + '</span>';
2542
+ } else if (session.endedAt) {
2543
+ timeDisplay = '<span class="session-time" title="' + escapeHtml(new Date(session.endedAt).toLocaleString()) + '">' + escapeHtml(timeAgo(session.endedAt)) + '</span>';
2544
+ } else if (session.startedAt) {
2545
+ timeDisplay = '<span class="session-time" title="' + escapeHtml(new Date(session.startedAt).toLocaleString()) + '">' + escapeHtml(timeAgo(session.startedAt)) + '</span>';
2546
+ }
2547
+
2548
+ // Badges: worktree only (removed PTY/Structured and mode badges for cleaner look)
2549
+ var badgesHtml = renderWorktreeBadge(session);
2550
+
2551
+ // Recovery hint
2552
+ var recoveryHtml = session.autoRecovered ? '<span class="session-recovery-hint">自动恢复</span>' : '';
2553
+
2356
2554
  return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
2357
2555
  '<div class="session-item-content">' +
2358
2556
  '<div class="session-item-row">' +
2359
2557
  checkbox +
2360
2558
  '<div class="session-main">' +
2361
- (session.summary
2362
- ? '<div class="session-title">' + escapeHtml(session.summary) + '</div>'
2363
- : '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>') +
2559
+ '<div class="session-title-row">' +
2560
+ titleHtml +
2561
+ timeDisplay +
2562
+ '</div>' +
2563
+ activityHtml +
2364
2564
  '<div class="session-meta">' +
2365
- modeBadge +
2366
- '<span>' + escapeHtml(modeName) + '</span>' +
2367
2565
  '<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
2368
- sessionIdDisplay +
2369
- recoveryHint +
2566
+ badgesHtml +
2567
+ recoveryHtml +
2370
2568
  '</div>' +
2371
2569
  '</div>' +
2372
2570
  actionsHtml +
@@ -2525,6 +2723,8 @@
2525
2723
  '<p class="field-hint">留空则使用上方目录,支持路径自动补全。</p>' +
2526
2724
  '<div id="recent-paths-bubbles" class="recent-paths-bubbles"></div>' +
2527
2725
  '</div>' +
2726
+ '</div>' +
2727
+ '<div class="modal-footer">' +
2528
2728
  '<button id="run-button" class="btn btn-primary btn-block">启动会话</button>' +
2529
2729
  '<p id="modal-error" class="error-message hidden"></p>' +
2530
2730
  '</div>' +
@@ -2533,11 +2733,53 @@
2533
2733
  }
2534
2734
 
2535
2735
  // Global toggle function for tool card headers — called via onclick attribute
2736
+ // Lazy-load tool content for truncated results
2737
+ function __fetchToolContent(toolUseId, callback) {
2738
+ if (!state.selectedId || !toolUseId) return;
2739
+ var cacheKey = state.selectedId + ":" + toolUseId;
2740
+ if (state.toolContentCache[cacheKey]) {
2741
+ callback(null, state.toolContentCache[cacheKey]);
2742
+ return;
2743
+ }
2744
+ fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/tool-content/" + encodeURIComponent(toolUseId), { credentials: "same-origin" })
2745
+ .then(function(res) { return res.json(); })
2746
+ .then(function(data) {
2747
+ if (data.error) {
2748
+ callback(data.error, null);
2749
+ } else {
2750
+ state.toolContentCache[cacheKey] = data;
2751
+ callback(null, data);
2752
+ }
2753
+ })
2754
+ .catch(function() {
2755
+ callback("加载失败", null);
2756
+ });
2757
+ }
2758
+
2536
2759
  window.__tcToggle = function(e, headerEl) {
2537
2760
  var card = headerEl.closest(".tool-use-card");
2538
2761
  if (card) {
2762
+ var wasCollapsed = card.classList.contains("collapsed");
2539
2763
  card.classList.toggle("collapsed");
2540
2764
  persistElementExpandState(card, "tool-card");
2765
+ // Lazy-load truncated content on expand
2766
+ if (wasCollapsed && card.dataset.truncated === "true" && card.dataset.loaded !== "true") {
2767
+ var toolUseId = card.dataset.toolUseId;
2768
+ var resultDiv = card.querySelector(".tool-use-result");
2769
+ if (resultDiv) resultDiv.innerHTML = '<div class="tool-content-loading">加载中…</div>';
2770
+ card.dataset.loaded = "loading";
2771
+ __fetchToolContent(toolUseId, function(err, data) {
2772
+ if (err) {
2773
+ if (resultDiv) resultDiv.innerHTML = '<div class="tool-content-error" onclick="__tcToggle(null, card.querySelector(\'.tool-use-header\'))">加载失败,点击重试</div>';
2774
+ card.dataset.loaded = "";
2775
+ } else {
2776
+ card.dataset.truncated = "false";
2777
+ card.dataset.loaded = "true";
2778
+ var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
2779
+ if (resultDiv) resultDiv.innerHTML = '<pre class="tool-use-result-content">' + escapeHtml(content) + '</pre>';
2780
+ }
2781
+ });
2782
+ }
2541
2783
  }
2542
2784
  if (e) { e.preventDefault(); e.stopPropagation(); }
2543
2785
  };
@@ -2576,6 +2818,24 @@
2576
2818
  statusSpan.textContent = "✓";
2577
2819
  }
2578
2820
  }
2821
+ // Lazy-load truncated content on expand
2822
+ if (expanded && el.dataset.truncated === "true" && el.dataset.loaded !== "true") {
2823
+ var toolUseId = el.dataset.toolUseId;
2824
+ if (body) body.innerHTML = '<div class="tool-content-loading">加载中…</div>';
2825
+ el.dataset.loaded = "loading";
2826
+ __fetchToolContent(toolUseId, function(err, data) {
2827
+ if (err) {
2828
+ if (body) body.innerHTML = '<div class="tool-content-error">加载失败,点击重试</div>';
2829
+ el.dataset.loaded = "";
2830
+ } else {
2831
+ el.dataset.truncated = "false";
2832
+ el.dataset.loaded = "true";
2833
+ var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
2834
+ el.dataset.result = content;
2835
+ if (body) body.innerHTML = '<div class="inline-tool-result">' + formatInlineResult(content, "") + '</div>';
2836
+ }
2837
+ });
2838
+ }
2579
2839
  persistElementExpandState(el, "inline-tool");
2580
2840
  };
2581
2841
  // Toggle function for terminal tool blocks
@@ -2590,7 +2850,32 @@
2590
2850
  var toggleIcon = el.querySelector(".term-toggle-icon");
2591
2851
  if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "▶";
2592
2852
  persistElementExpandState(container, "terminal");
2593
- }
2853
+ // Lazy-load truncated content on expand
2854
+ if (isHidden && container.dataset.truncated === "true" && container.dataset.loaded !== "true") {
2855
+ var toolUseId = container.dataset.toolUseId;
2856
+ var termOutput = body.querySelector(".term-output");
2857
+ if (termOutput) termOutput.innerHTML = '<div class="tool-content-loading">加载中…</div>';
2858
+ container.dataset.loaded = "loading";
2859
+ __fetchToolContent(toolUseId, function(err, data) {
2860
+ if (err) {
2861
+ if (termOutput) termOutput.innerHTML = '<div class="tool-content-error">加载失败,点击重试</div>';
2862
+ container.dataset.loaded = "";
2863
+ } else {
2864
+ container.dataset.truncated = "false";
2865
+ container.dataset.loaded = "true";
2866
+ var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
2867
+ if (termOutput) {
2868
+ var lines = content.split("\n");
2869
+ var html = "";
2870
+ for (var i = 0; i < lines.length; i++) {
2871
+ if (!lines[i] && i === lines.length - 1) continue;
2872
+ html += '<div class="term-line">' + escapeHtml(lines[i]) + '</div>';
2873
+ }
2874
+ termOutput.innerHTML = html;
2875
+ }
2876
+ }
2877
+ });
2878
+ }
2594
2879
  };
2595
2880
  // Update streaming thinking content (called from WebSocket handler)
2596
2881
  function updateStreamingThinking(text) {
@@ -2860,6 +3145,30 @@
2860
3145
  }
2861
3146
  var saveConfigBtn = document.getElementById("save-config-button");
2862
3147
  if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
3148
+ var saveDisplayBtn = document.getElementById("save-display-button");
3149
+ if (saveDisplayBtn) saveDisplayBtn.addEventListener("click", saveDisplaySettings);
3150
+ // App icon picker (APK only)
3151
+ var appIconPicker = document.getElementById("app-icon-picker");
3152
+ if (appIconPicker) {
3153
+ var appIconOpts = appIconPicker.querySelectorAll(".app-icon-option");
3154
+ for (var ai = 0; ai < appIconOpts.length; ai++) {
3155
+ appIconOpts[ai].addEventListener("click", function() {
3156
+ var iconName = this.getAttribute("data-icon");
3157
+ if (!iconName || typeof WandNative === "undefined" || typeof WandNative.setAppIcon !== "function") return;
3158
+ try {
3159
+ WandNative.setAppIcon(iconName);
3160
+ _updateAppIconSelection(iconName);
3161
+ var msgEl = document.getElementById("app-icon-message");
3162
+ if (msgEl) {
3163
+ msgEl.textContent = "图标已切换,返回桌面后生效";
3164
+ msgEl.style.color = "var(--success)";
3165
+ msgEl.classList.remove("hidden");
3166
+ setTimeout(function() { msgEl.classList.add("hidden"); }, 3000);
3167
+ }
3168
+ } catch (_e) {}
3169
+ });
3170
+ }
3171
+ }
2863
3172
  var uploadCertBtn = document.getElementById("upload-cert-button");
2864
3173
  if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
2865
3174
  var checkUpdateBtn = document.getElementById("check-update-button");
@@ -2868,6 +3177,11 @@
2868
3177
  if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
2869
3178
  var doRestartBtn = document.getElementById("do-restart-button");
2870
3179
  if (doRestartBtn) doRestartBtn.addEventListener("click", performSettingsRestart);
3180
+ var copyConnectCodeBtn = document.getElementById("copy-connect-code-button");
3181
+ if (copyConnectCodeBtn) copyConnectCodeBtn.addEventListener("click", function() {
3182
+ var text = document.getElementById("android-connect-code");
3183
+ if (text) copyToClipboard(text.textContent, copyConnectCodeBtn);
3184
+ });
2871
3185
  // Notification preferences
2872
3186
  var notifSoundEl = document.getElementById("cfg-notif-sound");
2873
3187
  if (notifSoundEl) {
@@ -2890,7 +3204,13 @@
2890
3204
  // Browser notification section
2891
3205
  var notifRequestBtn = document.getElementById("notification-request-btn");
2892
3206
  if (notifRequestBtn) notifRequestBtn.addEventListener("click", function() {
2893
- if (typeof Notification !== "undefined") {
3207
+ if (_hasNativeBridge) {
3208
+ window._onNativePermissionResult = function() {
3209
+ updateNotificationStatus();
3210
+ delete window._onNativePermissionResult;
3211
+ };
3212
+ try { WandNative.requestPermission(); } catch (_e) {}
3213
+ } else if (typeof Notification !== "undefined") {
2894
3214
  Notification.requestPermission().then(function() { updateNotificationStatus(); });
2895
3215
  }
2896
3216
  });
@@ -4664,6 +4984,15 @@
4664
4984
  });
4665
4985
  }
4666
4986
 
4987
+ var _sessionListUpdateTimer = null;
4988
+ function scheduleSessionListUpdate() {
4989
+ if (_sessionListUpdateTimer) return;
4990
+ _sessionListUpdateTimer = setTimeout(function() {
4991
+ _sessionListUpdateTimer = null;
4992
+ updateSessionsList();
4993
+ }, 200);
4994
+ }
4995
+
4667
4996
  function updateSessionsList() {
4668
4997
  var listEl = document.getElementById("sessions-list");
4669
4998
  var countEl = document.getElementById("session-count");
@@ -4792,6 +5121,8 @@
4792
5121
  }
4793
5122
  state.selectedId = id;
4794
5123
  persistSelectedId();
5124
+ // Clear tool content cache on session switch
5125
+ state.toolContentCache = {};
4795
5126
  // Clear queued inputs from the previous session to prevent cross-session leaks
4796
5127
  state.messageQueue = [];
4797
5128
  state.pendingMessages = [];
@@ -5119,6 +5450,10 @@
5119
5450
  if (soundEl) soundEl.checked = state.notifSound;
5120
5451
  if (bubbleEl) bubbleEl.checked = state.notifBubble;
5121
5452
  updateNotificationStatus();
5453
+ // Load current app icon selection (APK only)
5454
+ if (typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function") {
5455
+ try { _updateAppIconSelection(WandNative.getAppIcon() || "shorthair"); } catch (_e) {}
5456
+ }
5122
5457
  }
5123
5458
  }
5124
5459
 
@@ -5210,6 +5545,46 @@
5210
5545
  }
5211
5546
  }
5212
5547
 
5548
+ function copyToClipboard(text, triggerBtn) {
5549
+ if (!text) return;
5550
+ navigator.clipboard.writeText(text).then(function() {
5551
+ if (triggerBtn) {
5552
+ var orig = triggerBtn.textContent;
5553
+ triggerBtn.textContent = "已复制";
5554
+ setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
5555
+ }
5556
+ }).catch(function() {
5557
+ // Fallback for non-secure contexts
5558
+ var ta = document.createElement("textarea");
5559
+ ta.value = text;
5560
+ ta.style.position = "fixed";
5561
+ ta.style.opacity = "0";
5562
+ document.body.appendChild(ta);
5563
+ ta.select();
5564
+ document.execCommand("copy");
5565
+ document.body.removeChild(ta);
5566
+ if (triggerBtn) {
5567
+ var orig = triggerBtn.textContent;
5568
+ triggerBtn.textContent = "已复制";
5569
+ setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
5570
+ }
5571
+ });
5572
+ }
5573
+
5574
+ function formatBytes(value) {
5575
+ if (typeof value !== "number" || !isFinite(value) || value < 0) return "-";
5576
+ if (value < 1024) return value + " B";
5577
+ var units = ["KB", "MB", "GB", "TB"];
5578
+ var size = value / 1024;
5579
+ var unitIndex = 0;
5580
+ while (size >= 1024 && unitIndex < units.length - 1) {
5581
+ size = size / 1024;
5582
+ unitIndex += 1;
5583
+ }
5584
+ var display = size >= 10 ? size.toFixed(0) : size.toFixed(1);
5585
+ return display + " " + units[unitIndex];
5586
+ }
5587
+
5213
5588
  function loadSettingsData() {
5214
5589
  fetch("/api/settings", { credentials: "same-origin" })
5215
5590
  .then(function(res) { return res.json(); })
@@ -5236,6 +5611,97 @@
5236
5611
  }
5237
5612
  }
5238
5613
 
5614
+ // ── Android APK version display ──
5615
+ var apkSection = document.getElementById("android-apk-section");
5616
+ var apkCurrentRow = document.getElementById("android-apk-current-row");
5617
+ var apkCurrentEl = document.getElementById("settings-android-apk-current");
5618
+ var apkGithubRow = document.getElementById("android-apk-github-row");
5619
+ var apkGithubEl = document.getElementById("settings-android-apk-github");
5620
+ var apkGithubBtn = document.getElementById("download-github-apk-btn");
5621
+ var apkLocalRow = document.getElementById("android-apk-local-row");
5622
+ var apkLocalEl = document.getElementById("settings-android-apk-local");
5623
+ var apkLocalBtn = document.getElementById("download-local-apk-btn");
5624
+ var apkMessageEl = document.getElementById("android-apk-message");
5625
+ var androidApk = data.androidApk || {};
5626
+ var isInApk = !!_apkVersion;
5627
+
5628
+ if (isInApk) {
5629
+ // ── APK 内模式:显示当前版本 + 线上版本 + 本地版本 ──
5630
+ if (apkCurrentRow && apkCurrentEl) {
5631
+ apkCurrentEl.textContent = "v" + _apkVersion;
5632
+ apkCurrentRow.classList.remove("hidden");
5633
+ }
5634
+ // 线上版本
5635
+ if (androidApk.github && apkGithubRow && apkGithubEl) {
5636
+ var ghLabel = androidApk.github.version ? ("v" + androidApk.github.version) : androidApk.github.fileName;
5637
+ if (typeof androidApk.github.size === "number") ghLabel += " · " + formatBytes(androidApk.github.size);
5638
+ apkGithubEl.textContent = ghLabel;
5639
+ apkGithubRow.classList.remove("hidden");
5640
+ if (apkGithubBtn) {
5641
+ apkGithubBtn.textContent = "下载安装";
5642
+ apkGithubBtn.classList.remove("hidden");
5643
+ apkGithubBtn.onclick = function() {
5644
+ try {
5645
+ WandNative.downloadUpdate(androidApk.github.downloadUrl, androidApk.github.fileName || "wand-update.apk", "github");
5646
+ } catch (e) {
5647
+ alert("调用下载失败: " + e.message);
5648
+ }
5649
+ };
5650
+ }
5651
+ }
5652
+ // 本地版本
5653
+ if (androidApk.local && apkLocalRow && apkLocalEl) {
5654
+ var lcLabel = androidApk.local.version ? ("v" + androidApk.local.version) : androidApk.local.fileName;
5655
+ if (typeof androidApk.local.size === "number") lcLabel += " · " + formatBytes(androidApk.local.size);
5656
+ apkLocalEl.textContent = lcLabel;
5657
+ apkLocalRow.classList.remove("hidden");
5658
+ if (apkLocalBtn) {
5659
+ apkLocalBtn.textContent = "下载安装";
5660
+ apkLocalBtn.classList.remove("hidden");
5661
+ apkLocalBtn.onclick = function() {
5662
+ try {
5663
+ WandNative.downloadUpdate(androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", "local");
5664
+ } catch (e) {
5665
+ alert("调用下载失败: " + e.message);
5666
+ }
5667
+ };
5668
+ }
5669
+ }
5670
+ // 都没有时
5671
+ if (!androidApk.github && !androidApk.local && apkMessageEl) {
5672
+ apkMessageEl.textContent = "暂无可用更新";
5673
+ apkMessageEl.classList.remove("hidden");
5674
+ }
5675
+ } else {
5676
+ // ── 浏览器模式:只显示线上版本 + 下载按钮 ──
5677
+ if (androidApk.github && apkGithubRow && apkGithubEl) {
5678
+ var ghLabel2 = androidApk.github.version ? ("v" + androidApk.github.version) : androidApk.github.fileName;
5679
+ if (typeof androidApk.github.size === "number") ghLabel2 += " · " + formatBytes(androidApk.github.size);
5680
+ apkGithubEl.textContent = ghLabel2;
5681
+ apkGithubRow.classList.remove("hidden");
5682
+ if (apkGithubBtn) {
5683
+ apkGithubBtn.textContent = "下载";
5684
+ apkGithubBtn.classList.remove("hidden");
5685
+ apkGithubBtn.onclick = function() {
5686
+ window.open(androidApk.github.downloadUrl, "_blank");
5687
+ };
5688
+ }
5689
+ } else if (apkMessageEl) {
5690
+ apkMessageEl.textContent = "暂未提供";
5691
+ apkMessageEl.classList.remove("hidden");
5692
+ }
5693
+ }
5694
+
5695
+ // App connect code (encrypted)
5696
+ var connectCodeEl = document.getElementById("android-connect-code");
5697
+ if (connectCodeEl) {
5698
+ connectCodeEl.textContent = "加载中...";
5699
+ fetch("/api/app-connect-code").then(function(r) { return r.json(); }).then(function(d) {
5700
+ if (d.code) connectCodeEl.textContent = d.code;
5701
+ else connectCodeEl.textContent = "生成失败";
5702
+ }).catch(function() { connectCodeEl.textContent = "获取失败"; });
5703
+ }
5704
+
5239
5705
  // Config fields
5240
5706
  var cfg = data.config || {};
5241
5707
  var hostEl = document.getElementById("cfg-host");
@@ -5274,6 +5740,19 @@
5274
5740
  if (!html) html = '<div class="empty-state-compact"><span class="empty-icon">\u2699</span><span>\u6ca1\u6709\u547d\u4ee4\u9884\u8bbe</span><span class="hint">\u5728 config.json \u7684 commandPresets \u4e2d\u914d\u7f6e</span></div>';
5275
5741
  presetsList.innerHTML = html;
5276
5742
  }
5743
+
5744
+ // Card expand defaults
5745
+ var cd = cfg.cardDefaults || {};
5746
+ var cdEditEl = document.getElementById("cfg-card-edit");
5747
+ var cdInlineEl = document.getElementById("cfg-card-inline");
5748
+ var cdTerminalEl = document.getElementById("cfg-card-terminal");
5749
+ var cdThinkingEl = document.getElementById("cfg-card-thinking");
5750
+ var cdToolgroupEl = document.getElementById("cfg-card-toolgroup");
5751
+ if (cdEditEl) cdEditEl.checked = cd.editCards === true;
5752
+ if (cdInlineEl) cdInlineEl.checked = cd.inlineTools === true;
5753
+ if (cdTerminalEl) cdTerminalEl.checked = cd.terminal === true;
5754
+ if (cdThinkingEl) cdThinkingEl.checked = cd.thinking === true;
5755
+ if (cdToolgroupEl) cdToolgroupEl.checked = cd.toolGroup === true;
5277
5756
  })
5278
5757
  .catch(function() {});
5279
5758
  }
@@ -5320,6 +5799,52 @@
5320
5799
  });
5321
5800
  }
5322
5801
 
5802
+ function saveDisplaySettings() {
5803
+ var msgEl = document.getElementById("display-message");
5804
+ if (msgEl) { msgEl.classList.add("hidden"); msgEl.textContent = ""; }
5805
+
5806
+ var body = {
5807
+ cardDefaults: {
5808
+ editCards: !!(document.getElementById("cfg-card-edit") || {}).checked,
5809
+ inlineTools: !!(document.getElementById("cfg-card-inline") || {}).checked,
5810
+ terminal: !!(document.getElementById("cfg-card-terminal") || {}).checked,
5811
+ thinking: !!(document.getElementById("cfg-card-thinking") || {}).checked,
5812
+ toolGroup: !!(document.getElementById("cfg-card-toolgroup") || {}).checked,
5813
+ }
5814
+ };
5815
+
5816
+ fetch("/api/settings/config", {
5817
+ method: "POST",
5818
+ headers: { "Content-Type": "application/json" },
5819
+ credentials: "same-origin",
5820
+ body: JSON.stringify(body)
5821
+ })
5822
+ .then(function(res) { return res.json(); })
5823
+ .then(function(data) {
5824
+ if (msgEl) {
5825
+ if (data.error) {
5826
+ msgEl.textContent = data.error;
5827
+ msgEl.style.color = "var(--error)";
5828
+ } else {
5829
+ msgEl.textContent = "显示设置已保存";
5830
+ msgEl.style.color = "var(--success)";
5831
+ }
5832
+ msgEl.classList.remove("hidden");
5833
+ }
5834
+ // Update local config so card defaults take effect immediately
5835
+ if (!data.error && state.config) {
5836
+ state.config.cardDefaults = body.cardDefaults;
5837
+ }
5838
+ })
5839
+ .catch(function() {
5840
+ if (msgEl) {
5841
+ msgEl.textContent = "保存失败。";
5842
+ msgEl.style.color = "var(--error)";
5843
+ msgEl.classList.remove("hidden");
5844
+ }
5845
+ });
5846
+ }
5847
+
5323
5848
  function uploadCertificates() {
5324
5849
  var keyFile = document.getElementById("cert-key-file");
5325
5850
  var certFile = document.getElementById("cert-cert-file");
@@ -5459,6 +5984,16 @@
5459
5984
 
5460
5985
  // ── Notification Settings Helpers ──
5461
5986
 
5987
+ function _updateAppIconSelection(activeIcon) {
5988
+ var opts = document.querySelectorAll(".app-icon-option");
5989
+ for (var i = 0; i < opts.length; i++) {
5990
+ var preview = opts[i].querySelector(".app-icon-preview");
5991
+ if (preview) {
5992
+ preview.style.borderColor = opts[i].getAttribute("data-icon") === activeIcon ? "var(--accent)" : "transparent";
5993
+ }
5994
+ }
5995
+ }
5996
+
5462
5997
  function updateNotificationStatus() {
5463
5998
  var statusEl = document.getElementById("notification-permission-status");
5464
5999
  var requestBtn = document.getElementById("notification-request-btn");
@@ -5466,15 +6001,20 @@
5466
6001
  var testMsgEl = document.getElementById("notification-test-message");
5467
6002
  if (!statusEl) return;
5468
6003
 
5469
- if (typeof Notification === "undefined") {
5470
- statusEl.textContent = "\u4e0d\u652f\u6301";
5471
- statusEl.style.color = "var(--fg-muted)";
5472
- if (requestBtn) requestBtn.classList.add("hidden");
5473
- if (resetBtn) resetBtn.classList.add("hidden");
5474
- return;
6004
+ // Determine permission state: native bridge or browser API
6005
+ var perm = _getNativePermission();
6006
+ if (perm === null) {
6007
+ // No native bridge — fall back to browser Notification API
6008
+ if (typeof Notification === "undefined") {
6009
+ statusEl.textContent = "\u4e0d\u652f\u6301";
6010
+ statusEl.style.color = "var(--fg-muted)";
6011
+ if (requestBtn) requestBtn.classList.add("hidden");
6012
+ if (resetBtn) resetBtn.classList.add("hidden");
6013
+ return;
6014
+ }
6015
+ perm = Notification.permission;
5475
6016
  }
5476
6017
 
5477
- var perm = Notification.permission;
5478
6018
  if (perm === "granted") {
5479
6019
  statusEl.textContent = "\u5df2\u6388\u6743 \u2713";
5480
6020
  statusEl.style.color = "var(--success)";
@@ -5495,6 +6035,28 @@
5495
6035
 
5496
6036
  function resetNotificationPermission() {
5497
6037
  var testMsgEl = document.getElementById("notification-test-message");
6038
+
6039
+ // Native bridge path — trigger Android system permission dialog
6040
+ if (_hasNativeBridge) {
6041
+ // Listen for permission result callback from native
6042
+ window._onNativePermissionResult = function(result) {
6043
+ updateNotificationStatus();
6044
+ if (testMsgEl) {
6045
+ if (result === "granted") {
6046
+ testMsgEl.textContent = "\u2713 \u5df2\u6388\u6743";
6047
+ testMsgEl.style.color = "var(--success)";
6048
+ } else {
6049
+ testMsgEl.textContent = "\u2717 \u672a\u6388\u6743\uff0c\u8bf7\u5728\u7cfb\u7edf\u8bbe\u7f6e\u4e2d\u5f00\u542f Wand \u7684\u901a\u77e5\u6743\u9650";
6050
+ testMsgEl.style.color = "var(--danger)";
6051
+ }
6052
+ testMsgEl.classList.remove("hidden");
6053
+ }
6054
+ delete window._onNativePermissionResult;
6055
+ };
6056
+ try { WandNative.requestPermission(); } catch (_e) {}
6057
+ return;
6058
+ }
6059
+
5498
6060
  if (typeof Notification === "undefined") return;
5499
6061
 
5500
6062
  // Always call requestPermission — this triggers the browser's native
@@ -5550,9 +6112,44 @@
5550
6112
  });
5551
6113
  results.push(bubbleEnabled ? "\u2713 \u5e94\u7528\u5185\u6c14\u6ce1" : "\u2013 \u5e94\u7528\u5185\u6c14\u6ce1\uff08\u5df2\u5173\u95ed\uff09");
5552
6114
 
5553
- // 3. Test browser notification
6115
+ // 3. Test system notification (native bridge or browser API)
6116
+ if (_hasNativeBridge) {
6117
+ var nativePerm = _getNativePermission();
6118
+ if (nativePerm === "granted") {
6119
+ try {
6120
+ WandNative.sendNotification("Wand \u6d4b\u8bd5\u901a\u77e5", "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002", "wand-test");
6121
+ results.push("\u2713 \u7cfb\u7edf\u901a\u77e5");
6122
+ } catch (_e) {
6123
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff09");
6124
+ }
6125
+ } else if (nativePerm === "denied") {
6126
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff0c\u8bf7\u5728\u7cfb\u7edf\u8bbe\u7f6e\u4e2d\u5f00\u542f\uff09");
6127
+ } else {
6128
+ // "default" — request permission, then report
6129
+ window._onNativePermissionResult = function(result) {
6130
+ updateNotificationStatus();
6131
+ if (result === "granted") {
6132
+ try {
6133
+ WandNative.sendNotification("Wand \u6d4b\u8bd5\u901a\u77e5", "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002", "wand-test");
6134
+ results.push("\u2713 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
6135
+ } catch (_e2) {
6136
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff09");
6137
+ }
6138
+ } else {
6139
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
6140
+ }
6141
+ showTestResults(testMsgEl, results);
6142
+ delete window._onNativePermissionResult;
6143
+ };
6144
+ try { WandNative.requestPermission(); } catch (_e) {}
6145
+ return; // async — results shown in callback
6146
+ }
6147
+ showTestResults(testMsgEl, results);
6148
+ return;
6149
+ }
6150
+
5554
6151
  if (typeof Notification === "undefined") {
5555
- results.push("\u2013 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u4e0d\u652f\u6301\uff09");
6152
+ results.push("\u2013 \u7cfb\u7edf\u901a\u77e5\uff08\u4e0d\u652f\u6301\uff09");
5556
6153
  showTestResults(testMsgEl, results);
5557
6154
  return;
5558
6155
  }
@@ -5561,27 +6158,27 @@
5561
6158
  if (perm === "granted") {
5562
6159
  try {
5563
6160
  var n = new Notification("Wand \u6d4b\u8bd5\u901a\u77e5", {
5564
- body: "\u6d4f\u89c8\u5668\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
6161
+ body: "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
5565
6162
  icon: "/favicon.ico",
5566
6163
  tag: "wand-test",
5567
6164
  });
5568
6165
  setTimeout(function() { n.close(); }, 5000);
5569
- results.push("\u2713 \u6d4f\u89c8\u5668\u901a\u77e5");
6166
+ results.push("\u2713 \u7cfb\u7edf\u901a\u77e5");
5570
6167
  } catch (_e) {
5571
- results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS\uff09");
6168
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS\uff09");
5572
6169
  }
5573
6170
  showTestResults(testMsgEl, results);
5574
6171
  } else if (perm === "denied") {
5575
- results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff09");
6172
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff09");
5576
6173
  showTestResults(testMsgEl, results);
5577
6174
  } else {
5578
6175
  // "default" — try requesting
5579
6176
  Notification.requestPermission().then(function(result) {
5580
6177
  updateNotificationStatus();
5581
6178
  if (result === "granted") {
5582
- results.push("\u2713 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
6179
+ results.push("\u2713 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
5583
6180
  } else {
5584
- results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
6181
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
5585
6182
  }
5586
6183
  showTestResults(testMsgEl, results);
5587
6184
  });
@@ -7837,6 +8434,13 @@
7837
8434
  resetInputPanelViewportSpacing();
7838
8435
  setTimeout(function() {
7839
8436
  window.scrollTo(0, 0);
8437
+ // On mobile, force terminal refit + scroll after keyboard dismissal.
8438
+ // The container height restores but xterm needs an explicit refit to
8439
+ // fill the expanded space, and the scroll position needs resetting.
8440
+ if (isTouchDevice()) {
8441
+ ensureTerminalFit();
8442
+ maybeScrollTerminalToBottom("force");
8443
+ }
7840
8444
  }, 100);
7841
8445
  }
7842
8446
 
@@ -8562,6 +9166,15 @@
8562
9166
  syncInputBoxScroll(inputBox);
8563
9167
  }
8564
9168
 
9169
+ // Keyboard just closed — force terminal refit and scroll to bottom
9170
+ // after a delay so the keyboard dismiss animation and layout settle.
9171
+ if (keyboardOpen && !isKeyboardOpen) {
9172
+ setTimeout(function() {
9173
+ ensureTerminalFit();
9174
+ maybeScrollTerminalToBottom("force");
9175
+ }, 200);
9176
+ }
9177
+
8565
9178
  keyboardOpen = isKeyboardOpen;
8566
9179
  lastHeight = vv.height;
8567
9180
  }
@@ -8666,6 +9279,11 @@
8666
9279
  }
8667
9280
  state.resizeHandler = function() { scheduleTerminalResize(true); };
8668
9281
  window.addEventListener("resize", state.resizeHandler);
9282
+ // Also listen to visualViewport resize for pinch-zoom / browser zoom
9283
+ if (window.visualViewport) {
9284
+ state.visualViewportHandler = function() { scheduleTerminalResize(true); };
9285
+ window.visualViewport.addEventListener("resize", state.visualViewportHandler);
9286
+ }
8669
9287
  requestAnimationFrame(function() { scheduleTerminalResize(true); });
8670
9288
  }
8671
9289
 
@@ -8682,6 +9300,10 @@
8682
9300
  window.removeEventListener("resize", state.resizeHandler);
8683
9301
  state.resizeHandler = null;
8684
9302
  }
9303
+ if (state.visualViewportHandler && window.visualViewport) {
9304
+ window.visualViewport.removeEventListener("resize", state.visualViewportHandler);
9305
+ state.visualViewportHandler = null;
9306
+ }
8685
9307
  [["mousemove", "resizeMouseMove"], ["mouseup", "resizeMouseUp"],
8686
9308
  ["touchmove", "resizeTouchMove"], ["touchend", "resizeTouchEnd"]
8687
9309
  ].forEach(function(pair) {
@@ -8814,6 +9436,12 @@
8814
9436
  state.pollTimer = setInterval(refreshAll, 1600);
8815
9437
  }
8816
9438
 
9439
+ // Periodically refresh session time displays (30s)
9440
+ setInterval(function() {
9441
+ var timeEls = document.querySelectorAll(".session-time");
9442
+ if (timeEls.length > 0) scheduleSessionListUpdate();
9443
+ }, 30000);
9444
+
8817
9445
  function initWebSocket() {
8818
9446
  if (!window.WebSocket) return false;
8819
9447
 
@@ -8952,14 +9580,26 @@
8952
9580
  // Trigger status bar completion animation
8953
9581
  scheduleChatRender(true);
8954
9582
  }
8955
- // Notify user when a session completes (browser + in-app if backgrounded or not viewing)
9583
+ // Notify user when a session completes show what was accomplished
8956
9584
  var endedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
8957
- var endedName = endedSession ? (endedSession.label || endedSession.command || msg.sessionId) : msg.sessionId;
8958
9585
  var endedExitCode = msg.data && msg.data.exitCode;
8959
9586
  var endedIsError = endedExitCode !== null && endedExitCode !== undefined && endedExitCode !== 0;
9587
+ // Build meaningful notification body
9588
+ var endedTaskSummary = endedSession ? (endedSession.summary || "") : "";
9589
+ var endedLastReply = endedSession ? getLastAssistantSummary(endedSession) : "";
9590
+ var endedNotifTitle = endedIsError ? "任务异常结束" : "任务已完成";
9591
+ var endedNotifBody = "";
9592
+ if (endedTaskSummary) {
9593
+ endedNotifBody = endedTaskSummary;
9594
+ if (endedLastReply && !endedIsError) {
9595
+ endedNotifBody += "\n" + endedLastReply;
9596
+ }
9597
+ } else {
9598
+ endedNotifBody = endedSession ? (endedSession.command || msg.sessionId) : msg.sessionId;
9599
+ }
8960
9600
  sendBrowserNotification(
8961
- endedIsError ? "\u4f1a\u8bdd\u5f02\u5e38\u7ed3\u675f" : "\u4f1a\u8bdd\u5df2\u5b8c\u6210",
8962
- endedName,
9601
+ endedNotifTitle,
9602
+ endedNotifBody,
8963
9603
  {
8964
9604
  tag: "wand-ended-" + msg.sessionId,
8965
9605
  onClick: function() {
@@ -8969,8 +9609,8 @@
8969
9609
  );
8970
9610
  if (msg.sessionId !== state.selectedId || document.hidden) {
8971
9611
  showNotificationBubble({
8972
- title: endedIsError ? "\u4f1a\u8bdd\u5f02\u5e38\u7ed3\u675f" : "\u4f1a\u8bdd\u5df2\u5b8c\u6210",
8973
- body: endedName,
9612
+ title: endedNotifTitle,
9613
+ body: endedNotifBody,
8974
9614
  type: endedIsError ? "warning" : "success",
8975
9615
  icon: endedIsError ? "!" : "\u2713",
8976
9616
  duration: 6000,
@@ -9052,6 +9692,8 @@
9052
9692
  state.currentTask = msg.data || null;
9053
9693
  updateTaskDisplay();
9054
9694
  }
9695
+ // Update session list to reflect current activity (debounced)
9696
+ scheduleSessionListUpdate();
9055
9697
  break;
9056
9698
  case 'status':
9057
9699
  if (msg.sessionId && msg.data) {
@@ -9087,10 +9729,18 @@
9087
9729
  };
9088
9730
  // Browser notification for permission waiting (background tab)
9089
9731
  var permSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
9090
- var permSessionName = permSession ? (permSession.label || permSession.command || msg.sessionId) : msg.sessionId;
9732
+ var permTaskName = permSession ? (permSession.summary || permSession.command || msg.sessionId) : msg.sessionId;
9733
+ var permDetail = msg.data.permissionRequest.prompt || "需要权限审批";
9734
+ var permTarget = msg.data.permissionRequest.target;
9735
+ var permBody = permTaskName;
9736
+ if (permTarget) {
9737
+ permBody += "\n" + permDetail + " · " + permTarget;
9738
+ } else {
9739
+ permBody += "\n" + permDetail;
9740
+ }
9091
9741
  sendBrowserNotification(
9092
- "\u4f1a\u8bdd\u7b49\u5f85\u6388\u6743",
9093
- permSessionName + " \u2014 " + (msg.data.permissionRequest.prompt || "\u9700\u8981\u6743\u9650\u5ba1\u6279"),
9742
+ "需要你的授权",
9743
+ permBody,
9094
9744
  {
9095
9745
  tag: "wand-perm-" + msg.sessionId,
9096
9746
  onClick: function() {
@@ -9103,12 +9753,12 @@
9103
9753
  // In-app bubble if not currently viewing this session
9104
9754
  if (msg.sessionId !== state.selectedId) {
9105
9755
  showNotificationBubble({
9106
- title: "\u4f1a\u8bdd\u7b49\u5f85\u6388\u6743",
9107
- body: permSessionName + " \u2014 " + (msg.data.permissionRequest.prompt || "\u9700\u8981\u6743\u9650\u5ba1\u6279"),
9756
+ title: "需要你的授权",
9757
+ body: permBody,
9108
9758
  type: "warning",
9109
9759
  icon: "!",
9110
9760
  duration: 0,
9111
- actionLabel: "\u67e5\u770b",
9761
+ actionLabel: "去处理",
9112
9762
  action: function() {
9113
9763
  selectSession(msg.sessionId);
9114
9764
  }
@@ -9542,9 +10192,9 @@
9542
10192
  return;
9543
10193
  }
9544
10194
 
9545
- var messages = state.currentMessages;
10195
+ var allMessages = state.currentMessages;
9546
10196
 
9547
- if (messages.length === 0) {
10197
+ if (allMessages.length === 0) {
9548
10198
  if (state.lastRenderedEmpty !== "empty") {
9549
10199
  renderChatEmptyState(chatOutput, '<div class="empty-state"><strong>对话已开始</strong><br>在下方输入框发送消息,Claude 会自动回复。</div>');
9550
10200
  state.lastRenderedEmpty = "empty";
@@ -9553,6 +10203,16 @@
9553
10203
  return;
9554
10204
  }
9555
10205
 
10206
+ // Lazy loading: only render the most recent chatRenderedCount messages.
10207
+ // Auto-expand when new messages arrive during active streaming to avoid hiding them.
10208
+ var totalMsgCount = allMessages.length;
10209
+ if (totalMsgCount > state.chatRenderedCount && state.chatAutoFollow) {
10210
+ state.chatRenderedCount = totalMsgCount;
10211
+ }
10212
+ var visibleOffset = Math.max(0, totalMsgCount - state.chatRenderedCount);
10213
+ var messages = visibleOffset > 0 ? allMessages.slice(visibleOffset) : allMessages;
10214
+ var hasOlderMessages = visibleOffset > 0;
10215
+
9556
10216
  // Check if messages actually changed
9557
10217
  var msgCount = messages.length;
9558
10218
  var outputHash = selectedSession.output ? selectedSession.output.length : 0;
@@ -9613,16 +10273,17 @@
9613
10273
  // Build HTML with system info cards interleaved
9614
10274
  var html = '';
9615
10275
  var reversedMessages = messages.slice().reverse();
9616
- var msgCount = messages.length;
10276
+ var visibleCount = messages.length;
9617
10277
 
9618
10278
  for (var i = 0; i < reversedMessages.length; i++) {
9619
10279
  var msg = reversedMessages[i];
9620
- var originalIndex = msgCount - 1 - i; // Original index in messages array
10280
+ var localIndex = visibleCount - 1 - i; // Index within visible slice
10281
+ var originalIndex = localIndex + visibleOffset; // Index in full messages array
9621
10282
 
9622
10283
  // Find system info for this message position
9623
10284
  var sysInfo = null;
9624
10285
  for (var j = 0; j < systemInfo.length; j++) {
9625
- if (systemInfo[j].beforeMessage === originalIndex) {
10286
+ if (systemInfo[j].beforeMessage === localIndex) {
9626
10287
  sysInfo = systemInfo[j];
9627
10288
  break;
9628
10289
  }
@@ -9642,6 +10303,13 @@
9642
10303
  html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null, originalIndex);
9643
10304
  }
9644
10305
 
10306
+ // Add sentinel for loading older messages (DOM end = visual top in column-reverse)
10307
+ if (hasOlderMessages) {
10308
+ html += '<div class="chat-load-more" id="chat-load-more-sentinel">' +
10309
+ '<button class="chat-load-more-btn" type="button">加载更早的 ' + Math.min(state.chatPageSize, visibleOffset) + ' 条消息</button>' +
10310
+ '</div>';
10311
+ }
10312
+
9645
10313
  chatMessages.innerHTML = html;
9646
10314
  attachAllCopyHandlers(chatMessages);
9647
10315
  bindChatScrollListener();
@@ -9667,6 +10335,7 @@
9667
10335
  // Scroll to bottom (newest message) - column-reverse: scrollTop=0 is visual bottom
9668
10336
  requestAnimationFrame(function() {
9669
10337
  smartScrollToBottom(chatMessages);
10338
+ observeLoadMoreSentinel();
9670
10339
  });
9671
10340
  }
9672
10341
 
@@ -9687,15 +10356,15 @@
9687
10356
  });
9688
10357
  }
9689
10358
 
9690
- // Pre-compute per-round cumulative usage.
10359
+ // Pre-compute per-round cumulative usage using original (full array) indices.
9691
10360
  // A "round" starts at a user message and includes all subsequent assistant turns
9692
10361
  // until the next user message. Only the last assistant in each round shows the total.
9693
10362
  var roundUsageByIndex = {};
9694
10363
  (function() {
9695
10364
  var acc = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, totalCostUsd: 0 };
9696
10365
  var lastAssistantIdx = -1;
9697
- for (var mi = 0; mi < messages.length; mi++) {
9698
- var m = messages[mi];
10366
+ for (var mi = 0; mi < allMessages.length; mi++) {
10367
+ var m = allMessages[mi];
9699
10368
  if (m.role === "user") {
9700
10369
  if (lastAssistantIdx >= 0 && (acc.inputTokens > 0 || acc.outputTokens > 0 || acc.totalCostUsd > 0)) {
9701
10370
  roundUsageByIndex[lastAssistantIdx] = {
@@ -9739,7 +10408,7 @@
9739
10408
  var insertedEls = [];
9740
10409
  for (var i = 0; i < newMessages.length; i++) {
9741
10410
  var div = document.createElement("div");
9742
- var nmOrigIdx = existingCount + (newMessages.length - 1 - i);
10411
+ var nmOrigIdx = visibleOffset + existingCount + (newMessages.length - 1 - i);
9743
10412
  div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null, nmOrigIdx);
9744
10413
  var el = div.firstElementChild;
9745
10414
  if (el) {
@@ -9771,7 +10440,7 @@
9771
10440
  for (var mi = 0; mi < MAX_STREAMING_SCAN; mi++) {
9772
10441
  var currentEl = existingEls[mi];
9773
10442
  var tmpWrap = document.createElement("div");
9774
- var srOrigIdx = reversedMessages.length - 1 - mi;
10443
+ var srOrigIdx = visibleOffset + reversedMessages.length - 1 - mi;
9775
10444
  tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null, srOrigIdx);
9776
10445
  var replacementEl = tmpWrap.firstElementChild;
9777
10446
  if (!replacementEl) continue;
@@ -9817,7 +10486,7 @@
9817
10486
  renderStructuredStatusBar(chatMessages, selectedSession);
9818
10487
 
9819
10488
  // Update todo progress bar from latest messages
9820
- updateTodoProgress(messages);
10489
+ updateTodoProgress(allMessages);
9821
10490
  }
9822
10491
 
9823
10492
  // Smart scroll: only auto-scroll if user is near bottom
@@ -10890,7 +11559,8 @@
10890
11559
  // Thinking card (deep thought) — from PTY parsing
10891
11560
  if (msg.role === "thinking") {
10892
11561
  var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
10893
- var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
11562
+ var thinkingPersisted = getPersistedExpandState(thinkingKey);
11563
+ var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
10894
11564
  return '<div class="chat-message thinking">' +
10895
11565
  '<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
10896
11566
  '<span class="thinking-inline-icon">⦿</span>' +
@@ -11028,7 +11698,7 @@
11028
11698
  var summaryText = parts.join(" · ");
11029
11699
  var groupKey = buildExpandKey("tool-group", [messageKey, items[0] && items[0].index, items.length]);
11030
11700
  var persistedExpanded = getPersistedExpandState(groupKey);
11031
- var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11701
+ var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.toolGroup) : persistedExpanded;
11032
11702
 
11033
11703
  // Render each item's inline-tool card
11034
11704
  var innerHtml = "";
@@ -11139,7 +11809,8 @@
11139
11809
  '</div>';
11140
11810
  }
11141
11811
  var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
11142
- var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
11812
+ var thinkingPersisted = getPersistedExpandState(thinkingKey);
11813
+ var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
11143
11814
  return '<div class="thinking-inline ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="' + escapeHtml(thinkingText) + '" onclick="__thinkingToggle(this)">' +
11144
11815
  '<span class="thinking-inline-icon">⦿</span>' +
11145
11816
  '<span class="thinking-inline-preview">' + escapeHtml(thinkingExpanded ? thinkingText : preview) + '</span>' +
@@ -11236,7 +11907,7 @@
11236
11907
  var fullResult = resultContent;
11237
11908
 
11238
11909
  var expandedHtml = "";
11239
- var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11910
+ var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.inlineTools) : persistedExpanded;
11240
11911
  if (hasResult) {
11241
11912
  expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
11242
11913
  '<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
@@ -11248,16 +11919,23 @@
11248
11919
  expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';"><div class="inline-tool-loading">等待响应…</div></div>';
11249
11920
  }
11250
11921
 
11922
+ var isTruncated = toolResult && toolResult._truncated === true;
11923
+
11251
11924
  var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
11252
11925
  var extraClass = isError ? 'inline-tool-error-inline' : '';
11253
11926
  if (shouldExpand) extraClass += ' inline-tool-open';
11254
11927
 
11928
+ var truncatedAttrs = isTruncated
11929
+ ? 'data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '" '
11930
+ : '';
11931
+
11255
11932
  return '<div class="inline-tool ' + extraClass + '" ' +
11256
11933
  'data-expand-kind="inline-tool" ' +
11257
11934
  'data-expand-key="' + escapeHtml(expandKey) + '" ' +
11258
11935
  'data-result="' + escapeHtml(fullResult) + '" ' +
11259
11936
  'data-preview="' + previewDataAttr + '" ' +
11260
11937
  'data-status="' + (isError ? 'error' : (hasResult ? 'done' : 'pending')) + '" ' +
11938
+ truncatedAttrs +
11261
11939
  'onclick="__inlineToolToggle(this)">' +
11262
11940
  '<div class="inline-tool-row">' +
11263
11941
  '<span class="inline-tool-status">' + statusIcon + '</span>' +
@@ -11314,9 +11992,14 @@
11314
11992
 
11315
11993
  // Show command preview in header (truncate long commands)
11316
11994
  var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
11317
- var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11995
+ var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.terminal) : persistedExpanded;
11318
11996
 
11319
- return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '">' +
11997
+ var termTruncated = toolResult && toolResult._truncated === true;
11998
+ var termTruncAttrs = termTruncated
11999
+ ? ' data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '"'
12000
+ : '';
12001
+
12002
+ return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '"' + termTruncAttrs + '>' +
11320
12003
  '<div class="term-header" onclick="__terminalExpand(this)">' +
11321
12004
  statusDot +
11322
12005
  '<span class="term-cmd-preview"><span class="term-prompt">$</span> ' + escapeHtml(cmdPreview) + '</span>' +
@@ -11596,10 +12279,12 @@
11596
12279
 
11597
12280
  var expandKey = buildExpandKey("tool-card", [messageKey, toolId]);
11598
12281
  var persistedExpanded = getPersistedExpandState(expandKey);
11599
- var shouldExpand = persistedExpanded === null ? statusClass === "loading" : persistedExpanded;
12282
+ var cardDefaultExpand = !!(state.config && state.config.cardDefaults && state.config.cardDefaults.editCards);
12283
+ var shouldExpand = persistedExpanded === null ? (statusClass === "loading" || cardDefaultExpand) : persistedExpanded;
12284
+ var tcTruncated = toolResult && toolResult._truncated === true;
11600
12285
  var collapsedClass = shouldExpand ? "" : " collapsed";
11601
12286
  var toggleHtml = '<span class="tool-use-toggle">▼</span>';
11602
- return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-expand-kind="tool-card" data-expand-key="' + escapeHtml(expandKey) + '" data-tool-use-id="' + escapeHtml(toolId) + '">' +
12287
+ return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-expand-kind="tool-card" data-expand-key="' + escapeHtml(expandKey) + '" data-tool-use-id="' + escapeHtml(toolId) + '"' + (tcTruncated ? ' data-truncated="true"' : '') + '>' +
11603
12288
  '<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
11604
12289
  '<span class="tool-use-icon">' + headerIcon + '</span>' +
11605
12290
  '<span class="tool-use-name">' + escapeHtml(titleText) + '</span>' +
@@ -12059,7 +12744,7 @@
12059
12744
  '</div>';
12060
12745
 
12061
12746
  var bodyHtml = opts.body
12062
- ? '<div class="notification-bubble-body">' + escapeHtml(opts.body) + '</div>'
12747
+ ? '<div class="notification-bubble-body">' + escapeHtml(opts.body).replace(/\n/g, '<br>') + '</div>'
12063
12748
  : '';
12064
12749
 
12065
12750
  var actionsHtml = opts.actionLabel
@@ -12125,13 +12810,43 @@
12125
12810
 
12126
12811
  // ── Browser Notification API ──
12127
12812
 
12813
+ // Detect Android APK native bridge
12814
+ var _hasNativeBridge = typeof WandNative !== "undefined" && typeof WandNative.sendNotification === "function";
12815
+ // Detect if running inside APK and extract installed version from User-Agent
12816
+ var _apkVersionMatch = navigator.userAgent.match(/WandApp\/([^\s]+)/);
12817
+ var _apkVersion = _apkVersionMatch ? _apkVersionMatch[1] : null;
12818
+
12819
+ function _getNativePermission() {
12820
+ if (_hasNativeBridge && typeof WandNative.getPermission === "function") {
12821
+ try { return WandNative.getPermission(); } catch (_e) {}
12822
+ }
12823
+ return null;
12824
+ }
12825
+
12128
12826
  function requestNotificationPermission() {
12827
+ if (_hasNativeBridge) {
12828
+ var perm = _getNativePermission();
12829
+ if (perm === "default" || perm === "denied") {
12830
+ try { WandNative.requestPermission(); } catch (_e) {}
12831
+ }
12832
+ return;
12833
+ }
12129
12834
  if (typeof Notification !== "undefined" && Notification.permission === "default") {
12130
12835
  Notification.requestPermission();
12131
12836
  }
12132
12837
  }
12133
12838
 
12134
12839
  function sendBrowserNotification(title, body, opts) {
12840
+ // Native Android bridge path
12841
+ if (_hasNativeBridge) {
12842
+ var perm = _getNativePermission();
12843
+ if (perm !== "granted") return;
12844
+ try {
12845
+ WandNative.sendNotification(title || "Wand", body || "", (opts && opts.tag) || "");
12846
+ } catch (_e) {}
12847
+ return;
12848
+ }
12849
+ // Browser Notification API path
12135
12850
  if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
12136
12851
  if (!document.hidden) return; // Only notify when tab is in background
12137
12852
  try {