@co0ontty/wand 1.10.0 → 1.14.3

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,56 @@
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="switch-card-list">' +
1518
+ '<label class="switch-card" for="cfg-card-edit">' +
1519
+ '<div class="switch-card-header">' +
1520
+ '<span class="switch-card-title">编辑卡片 (Edit/Write)</span>' +
1521
+ '<input id="cfg-card-edit" type="checkbox" class="switch-toggle" />' +
1522
+ '<span class="switch-slider"></span>' +
1523
+ '</div>' +
1524
+ '<div class="switch-card-desc">文件编辑和写入操作的 diff 视图</div>' +
1525
+ '</label>' +
1526
+ '<label class="switch-card" for="cfg-card-inline">' +
1527
+ '<div class="switch-card-header">' +
1528
+ '<span class="switch-card-title">内联工具 (Read/Glob/Grep)</span>' +
1529
+ '<input id="cfg-card-inline" type="checkbox" class="switch-toggle" />' +
1530
+ '<span class="switch-slider"></span>' +
1531
+ '</div>' +
1532
+ '<div class="switch-card-desc">文件读取、搜索等工具的结果</div>' +
1533
+ '</label>' +
1534
+ '<label class="switch-card" for="cfg-card-terminal">' +
1535
+ '<div class="switch-card-header">' +
1536
+ '<span class="switch-card-title">终端输出 (Bash)</span>' +
1537
+ '<input id="cfg-card-terminal" type="checkbox" class="switch-toggle" />' +
1538
+ '<span class="switch-slider"></span>' +
1539
+ '</div>' +
1540
+ '<div class="switch-card-desc">命令行执行结果</div>' +
1541
+ '</label>' +
1542
+ '<label class="switch-card" for="cfg-card-thinking">' +
1543
+ '<div class="switch-card-header">' +
1544
+ '<span class="switch-card-title">思考过程 (Thinking)</span>' +
1545
+ '<input id="cfg-card-thinking" type="checkbox" class="switch-toggle" />' +
1546
+ '<span class="switch-slider"></span>' +
1547
+ '</div>' +
1548
+ '<div class="switch-card-desc">Claude 的思考过程块</div>' +
1549
+ '</label>' +
1550
+ '<label class="switch-card" for="cfg-card-toolgroup">' +
1551
+ '<div class="switch-card-header">' +
1552
+ '<span class="switch-card-title">工具组</span>' +
1553
+ '<input id="cfg-card-toolgroup" type="checkbox" class="switch-toggle" />' +
1554
+ '<span class="switch-slider"></span>' +
1555
+ '</div>' +
1556
+ '<div class="switch-card-desc">连续同类工具调用的折叠组</div>' +
1557
+ '</label>' +
1558
+ '</div>' +
1559
+ '<button id="save-display-button" class="btn btn-primary btn-block" style="margin-top:16px">保存显示设置</button>' +
1560
+ '<p id="display-message" class="hint hidden"></p>' +
1561
+ '</div>' +
1432
1562
  '</div>' +
1433
1563
  '</div>' +
1434
1564
  '</section>';
@@ -1856,7 +1986,10 @@
1856
1986
  } catch (e) {}
1857
1987
  applyTerminalScale();
1858
1988
  updateScaleLabel();
1859
- scheduleTerminalResize();
1989
+ // Force refit: font size changed but container dimensions didn't,
1990
+ // so ensureTerminalFit (which resets viewport tracking) is needed
1991
+ // instead of scheduleTerminalResize (which skips when size unchanged).
1992
+ ensureTerminalFit();
1860
1993
  }
1861
1994
 
1862
1995
  function applyTerminalScale() {
@@ -2295,6 +2428,35 @@
2295
2428
  '</div>';
2296
2429
  }
2297
2430
 
2431
+ function timeAgo(isoString) {
2432
+ if (!isoString) return "";
2433
+ var now = Date.now();
2434
+ var then = new Date(isoString).getTime();
2435
+ var diff = Math.max(0, now - then);
2436
+ var seconds = Math.floor(diff / 1000);
2437
+ if (seconds < 60) return "刚刚";
2438
+ var minutes = Math.floor(seconds / 60);
2439
+ if (minutes < 60) return minutes + "分钟前";
2440
+ var hours = Math.floor(minutes / 60);
2441
+ if (hours < 24) return hours + "小时前";
2442
+ var days = Math.floor(hours / 24);
2443
+ if (days < 30) return days + "天前";
2444
+ return Math.floor(days / 30) + "个月前";
2445
+ }
2446
+
2447
+ function elapsedTime(isoString) {
2448
+ if (!isoString) return "";
2449
+ var now = Date.now();
2450
+ var then = new Date(isoString).getTime();
2451
+ var diff = Math.max(0, now - then);
2452
+ var seconds = Math.floor(diff / 1000);
2453
+ var minutes = Math.floor(seconds / 60);
2454
+ var hours = Math.floor(minutes / 60);
2455
+ if (hours > 0) return hours + "h" + (minutes % 60 > 0 ? (minutes % 60) + "m" : "");
2456
+ if (minutes > 0) return minutes + "m";
2457
+ return seconds + "s";
2458
+ }
2459
+
2298
2460
  function getSessionStatusLabel(session) {
2299
2461
  if (!session) return "";
2300
2462
  if (session.archived) return "已归档";
@@ -2317,29 +2479,55 @@
2317
2479
  return session.status || "";
2318
2480
  }
2319
2481
 
2482
+ /** Get a human-readable activity description for a running session */
2483
+ function getSessionActivityDesc(session) {
2484
+ if (!session) return "";
2485
+ if (session.permissionBlocked) return "等待你的授权";
2486
+ if (session.status !== "running") return "";
2487
+ // Check WebSocket-delivered currentTask first
2488
+ if (session.id === state.selectedId && state.currentTask && state.currentTask.title) {
2489
+ return state.currentTask.title;
2490
+ }
2491
+ // Fall back to snapshot-delivered currentTaskTitle
2492
+ if (session.currentTaskTitle) return session.currentTaskTitle;
2493
+ return "";
2494
+ }
2495
+
2496
+ /** Get the last meaningful assistant text from messages for notification/display */
2497
+ function getLastAssistantSummary(session) {
2498
+ var msgs = session && session.messages;
2499
+ if (!msgs || msgs.length === 0) return "";
2500
+ for (var i = msgs.length - 1; i >= 0; i--) {
2501
+ var msg = msgs[i];
2502
+ if (msg.role !== "assistant") continue;
2503
+ var blocks = msg.content || [];
2504
+ for (var j = 0; j < blocks.length; j++) {
2505
+ if (blocks[j].type === "text" && blocks[j].text && blocks[j].text.trim()) {
2506
+ var text = blocks[j].text.trim();
2507
+ // Strip markdown formatting for compact display
2508
+ text = text.replace(/^#+\s+/gm, "").replace(/\*\*/g, "").replace(/`/g, "");
2509
+ var firstLine = text.split("\n")[0].trim();
2510
+ return firstLine.slice(0, 100);
2511
+ }
2512
+ }
2513
+ }
2514
+ return "";
2515
+ }
2516
+
2320
2517
  function renderSessionItem(session) {
2321
2518
  var activeClass = session.id === state.selectedId ? " active" : "";
2322
2519
  var selectedClass = state.sessionsManageMode && state.selectedSessionIds[session.id] ? " selected" : "";
2323
2520
  var metaStatus = getSessionStatusLabel(session);
2324
2521
  var metaStatusClass = getSessionStatusClass(session);
2325
- var modeName = session.mode === "full-access" ? "全权限" : session.mode === "default" ? "默认" : session.mode === "native" ? "原生" : session.mode === "auto-edit" ? "自动编辑" : session.mode;
2326
2522
  var resumeButton = "";
2327
- var sessionIdDisplay = "";
2328
- var recoveryHint = "";
2329
2523
  var checkbox = renderManageCheckbox("sessions", session.id, "选择会话 " + session.command);
2330
2524
 
2331
2525
  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
2526
  if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
2335
2527
  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
2528
  }
2337
2529
  }
2338
2530
 
2339
- if (session.autoRecovered) {
2340
- recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
2341
- }
2342
-
2343
2531
  var canOpenMerge = !state.sessionsManageMode && session.worktreeEnabled && session.worktree && session.worktree.branch && session.worktree.path;
2344
2532
  var needsCleanup = session.worktreeMergeStatus === "merged" && session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false;
2345
2533
  var mergeDisabled = session.status === "running" || session.worktreeMergeStatus === "merging";
@@ -2350,23 +2538,50 @@
2350
2538
  ? '<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
2539
  : "";
2352
2540
  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
2541
  var actionsHtml = '<span class="session-actions">' + resumeButton + mergeButton + deleteButton + '</span>';
2355
2542
 
2543
+ // Title: summary or command
2544
+ var titleHtml = session.summary
2545
+ ? '<div class="session-title">' + escapeHtml(session.summary) + '</div>'
2546
+ : '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>';
2547
+
2548
+ // Activity description for running sessions
2549
+ var activityDesc = getSessionActivityDesc(session);
2550
+ var activityHtml = "";
2551
+ if (session.status === "running" && activityDesc) {
2552
+ activityHtml = '<div class="session-activity">' + escapeHtml(activityDesc) + '</div>';
2553
+ }
2554
+
2555
+ // Time display
2556
+ var timeDisplay = "";
2557
+ if (session.status === "running") {
2558
+ timeDisplay = '<span class="session-time" title="已运行 ' + escapeHtml(elapsedTime(session.startedAt)) + '">' + escapeHtml(elapsedTime(session.startedAt)) + '</span>';
2559
+ } else if (session.endedAt) {
2560
+ timeDisplay = '<span class="session-time" title="' + escapeHtml(new Date(session.endedAt).toLocaleString()) + '">' + escapeHtml(timeAgo(session.endedAt)) + '</span>';
2561
+ } else if (session.startedAt) {
2562
+ timeDisplay = '<span class="session-time" title="' + escapeHtml(new Date(session.startedAt).toLocaleString()) + '">' + escapeHtml(timeAgo(session.startedAt)) + '</span>';
2563
+ }
2564
+
2565
+ // Badges: worktree only (removed PTY/Structured and mode badges for cleaner look)
2566
+ var badgesHtml = renderWorktreeBadge(session);
2567
+
2568
+ // Recovery hint
2569
+ var recoveryHtml = session.autoRecovered ? '<span class="session-recovery-hint">自动恢复</span>' : '';
2570
+
2356
2571
  return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
2357
2572
  '<div class="session-item-content">' +
2358
2573
  '<div class="session-item-row">' +
2359
2574
  checkbox +
2360
2575
  '<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>') +
2576
+ '<div class="session-title-row">' +
2577
+ titleHtml +
2578
+ timeDisplay +
2579
+ '</div>' +
2580
+ activityHtml +
2364
2581
  '<div class="session-meta">' +
2365
- modeBadge +
2366
- '<span>' + escapeHtml(modeName) + '</span>' +
2367
2582
  '<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
2368
- sessionIdDisplay +
2369
- recoveryHint +
2583
+ badgesHtml +
2584
+ recoveryHtml +
2370
2585
  '</div>' +
2371
2586
  '</div>' +
2372
2587
  actionsHtml +
@@ -2525,6 +2740,8 @@
2525
2740
  '<p class="field-hint">留空则使用上方目录,支持路径自动补全。</p>' +
2526
2741
  '<div id="recent-paths-bubbles" class="recent-paths-bubbles"></div>' +
2527
2742
  '</div>' +
2743
+ '</div>' +
2744
+ '<div class="modal-footer">' +
2528
2745
  '<button id="run-button" class="btn btn-primary btn-block">启动会话</button>' +
2529
2746
  '<p id="modal-error" class="error-message hidden"></p>' +
2530
2747
  '</div>' +
@@ -2533,11 +2750,53 @@
2533
2750
  }
2534
2751
 
2535
2752
  // Global toggle function for tool card headers — called via onclick attribute
2753
+ // Lazy-load tool content for truncated results
2754
+ function __fetchToolContent(toolUseId, callback) {
2755
+ if (!state.selectedId || !toolUseId) return;
2756
+ var cacheKey = state.selectedId + ":" + toolUseId;
2757
+ if (state.toolContentCache[cacheKey]) {
2758
+ callback(null, state.toolContentCache[cacheKey]);
2759
+ return;
2760
+ }
2761
+ fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/tool-content/" + encodeURIComponent(toolUseId), { credentials: "same-origin" })
2762
+ .then(function(res) { return res.json(); })
2763
+ .then(function(data) {
2764
+ if (data.error) {
2765
+ callback(data.error, null);
2766
+ } else {
2767
+ state.toolContentCache[cacheKey] = data;
2768
+ callback(null, data);
2769
+ }
2770
+ })
2771
+ .catch(function() {
2772
+ callback("加载失败", null);
2773
+ });
2774
+ }
2775
+
2536
2776
  window.__tcToggle = function(e, headerEl) {
2537
2777
  var card = headerEl.closest(".tool-use-card");
2538
2778
  if (card) {
2779
+ var wasCollapsed = card.classList.contains("collapsed");
2539
2780
  card.classList.toggle("collapsed");
2540
2781
  persistElementExpandState(card, "tool-card");
2782
+ // Lazy-load truncated content on expand
2783
+ if (wasCollapsed && card.dataset.truncated === "true" && card.dataset.loaded !== "true") {
2784
+ var toolUseId = card.dataset.toolUseId;
2785
+ var resultDiv = card.querySelector(".tool-use-result");
2786
+ if (resultDiv) resultDiv.innerHTML = '<div class="tool-content-loading">加载中…</div>';
2787
+ card.dataset.loaded = "loading";
2788
+ __fetchToolContent(toolUseId, function(err, data) {
2789
+ if (err) {
2790
+ if (resultDiv) resultDiv.innerHTML = '<div class="tool-content-error" onclick="__tcToggle(null, card.querySelector(\'.tool-use-header\'))">加载失败,点击重试</div>';
2791
+ card.dataset.loaded = "";
2792
+ } else {
2793
+ card.dataset.truncated = "false";
2794
+ card.dataset.loaded = "true";
2795
+ var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
2796
+ if (resultDiv) resultDiv.innerHTML = '<pre class="tool-use-result-content">' + escapeHtml(content) + '</pre>';
2797
+ }
2798
+ });
2799
+ }
2541
2800
  }
2542
2801
  if (e) { e.preventDefault(); e.stopPropagation(); }
2543
2802
  };
@@ -2576,6 +2835,24 @@
2576
2835
  statusSpan.textContent = "✓";
2577
2836
  }
2578
2837
  }
2838
+ // Lazy-load truncated content on expand
2839
+ if (expanded && el.dataset.truncated === "true" && el.dataset.loaded !== "true") {
2840
+ var toolUseId = el.dataset.toolUseId;
2841
+ if (body) body.innerHTML = '<div class="tool-content-loading">加载中…</div>';
2842
+ el.dataset.loaded = "loading";
2843
+ __fetchToolContent(toolUseId, function(err, data) {
2844
+ if (err) {
2845
+ if (body) body.innerHTML = '<div class="tool-content-error">加载失败,点击重试</div>';
2846
+ el.dataset.loaded = "";
2847
+ } else {
2848
+ el.dataset.truncated = "false";
2849
+ el.dataset.loaded = "true";
2850
+ var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
2851
+ el.dataset.result = content;
2852
+ if (body) body.innerHTML = '<div class="inline-tool-result">' + formatInlineResult(content, "") + '</div>';
2853
+ }
2854
+ });
2855
+ }
2579
2856
  persistElementExpandState(el, "inline-tool");
2580
2857
  };
2581
2858
  // Toggle function for terminal tool blocks
@@ -2590,6 +2867,32 @@
2590
2867
  var toggleIcon = el.querySelector(".term-toggle-icon");
2591
2868
  if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "▶";
2592
2869
  persistElementExpandState(container, "terminal");
2870
+ // Lazy-load truncated content on expand
2871
+ if (isHidden && container.dataset.truncated === "true" && container.dataset.loaded !== "true") {
2872
+ var toolUseId = container.dataset.toolUseId;
2873
+ var termOutput = body.querySelector(".term-output");
2874
+ if (termOutput) termOutput.innerHTML = '<div class="tool-content-loading">加载中…</div>';
2875
+ container.dataset.loaded = "loading";
2876
+ __fetchToolContent(toolUseId, function(err, data) {
2877
+ if (err) {
2878
+ if (termOutput) termOutput.innerHTML = '<div class="tool-content-error">加载失败,点击重试</div>';
2879
+ container.dataset.loaded = "";
2880
+ } else {
2881
+ container.dataset.truncated = "false";
2882
+ container.dataset.loaded = "true";
2883
+ var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
2884
+ if (termOutput) {
2885
+ var lines = content.split("\n");
2886
+ var html = "";
2887
+ for (var i = 0; i < lines.length; i++) {
2888
+ if (!lines[i] && i === lines.length - 1) continue;
2889
+ html += '<div class="term-line">' + escapeHtml(lines[i]) + '</div>';
2890
+ }
2891
+ termOutput.innerHTML = html;
2892
+ }
2893
+ }
2894
+ });
2895
+ }
2593
2896
  }
2594
2897
  };
2595
2898
  // Update streaming thinking content (called from WebSocket handler)
@@ -2860,6 +3163,30 @@
2860
3163
  }
2861
3164
  var saveConfigBtn = document.getElementById("save-config-button");
2862
3165
  if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
3166
+ var saveDisplayBtn = document.getElementById("save-display-button");
3167
+ if (saveDisplayBtn) saveDisplayBtn.addEventListener("click", saveDisplaySettings);
3168
+ // App icon picker (APK only)
3169
+ var appIconPicker = document.getElementById("app-icon-picker");
3170
+ if (appIconPicker) {
3171
+ var appIconOpts = appIconPicker.querySelectorAll(".app-icon-option");
3172
+ for (var ai = 0; ai < appIconOpts.length; ai++) {
3173
+ appIconOpts[ai].addEventListener("click", function() {
3174
+ var iconName = this.getAttribute("data-icon");
3175
+ if (!iconName || typeof WandNative === "undefined" || typeof WandNative.setAppIcon !== "function") return;
3176
+ try {
3177
+ WandNative.setAppIcon(iconName);
3178
+ _updateAppIconSelection(iconName);
3179
+ var msgEl = document.getElementById("app-icon-message");
3180
+ if (msgEl) {
3181
+ msgEl.textContent = "图标已切换,返回桌面后生效";
3182
+ msgEl.style.color = "var(--success)";
3183
+ msgEl.classList.remove("hidden");
3184
+ setTimeout(function() { msgEl.classList.add("hidden"); }, 3000);
3185
+ }
3186
+ } catch (_e) {}
3187
+ });
3188
+ }
3189
+ }
2863
3190
  var uploadCertBtn = document.getElementById("upload-cert-button");
2864
3191
  if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
2865
3192
  var checkUpdateBtn = document.getElementById("check-update-button");
@@ -2868,6 +3195,11 @@
2868
3195
  if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
2869
3196
  var doRestartBtn = document.getElementById("do-restart-button");
2870
3197
  if (doRestartBtn) doRestartBtn.addEventListener("click", performSettingsRestart);
3198
+ var copyConnectCodeBtn = document.getElementById("copy-connect-code-button");
3199
+ if (copyConnectCodeBtn) copyConnectCodeBtn.addEventListener("click", function() {
3200
+ var text = document.getElementById("android-connect-code");
3201
+ if (text) copyToClipboard(text.textContent, copyConnectCodeBtn);
3202
+ });
2871
3203
  // Notification preferences
2872
3204
  var notifSoundEl = document.getElementById("cfg-notif-sound");
2873
3205
  if (notifSoundEl) {
@@ -2890,7 +3222,13 @@
2890
3222
  // Browser notification section
2891
3223
  var notifRequestBtn = document.getElementById("notification-request-btn");
2892
3224
  if (notifRequestBtn) notifRequestBtn.addEventListener("click", function() {
2893
- if (typeof Notification !== "undefined") {
3225
+ if (_hasNativeBridge) {
3226
+ window._onNativePermissionResult = function() {
3227
+ updateNotificationStatus();
3228
+ delete window._onNativePermissionResult;
3229
+ };
3230
+ try { WandNative.requestPermission(); } catch (_e) {}
3231
+ } else if (typeof Notification !== "undefined") {
2894
3232
  Notification.requestPermission().then(function() { updateNotificationStatus(); });
2895
3233
  }
2896
3234
  });
@@ -4664,6 +5002,15 @@
4664
5002
  });
4665
5003
  }
4666
5004
 
5005
+ var _sessionListUpdateTimer = null;
5006
+ function scheduleSessionListUpdate() {
5007
+ if (_sessionListUpdateTimer) return;
5008
+ _sessionListUpdateTimer = setTimeout(function() {
5009
+ _sessionListUpdateTimer = null;
5010
+ updateSessionsList();
5011
+ }, 200);
5012
+ }
5013
+
4667
5014
  function updateSessionsList() {
4668
5015
  var listEl = document.getElementById("sessions-list");
4669
5016
  var countEl = document.getElementById("session-count");
@@ -4792,6 +5139,8 @@
4792
5139
  }
4793
5140
  state.selectedId = id;
4794
5141
  persistSelectedId();
5142
+ // Clear tool content cache on session switch
5143
+ state.toolContentCache = {};
4795
5144
  // Clear queued inputs from the previous session to prevent cross-session leaks
4796
5145
  state.messageQueue = [];
4797
5146
  state.pendingMessages = [];
@@ -5119,6 +5468,10 @@
5119
5468
  if (soundEl) soundEl.checked = state.notifSound;
5120
5469
  if (bubbleEl) bubbleEl.checked = state.notifBubble;
5121
5470
  updateNotificationStatus();
5471
+ // Load current app icon selection (APK only)
5472
+ if (typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function") {
5473
+ try { _updateAppIconSelection(WandNative.getAppIcon() || "shorthair"); } catch (_e) {}
5474
+ }
5122
5475
  }
5123
5476
  }
5124
5477
 
@@ -5210,6 +5563,46 @@
5210
5563
  }
5211
5564
  }
5212
5565
 
5566
+ function copyToClipboard(text, triggerBtn) {
5567
+ if (!text) return;
5568
+ navigator.clipboard.writeText(text).then(function() {
5569
+ if (triggerBtn) {
5570
+ var orig = triggerBtn.textContent;
5571
+ triggerBtn.textContent = "已复制";
5572
+ setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
5573
+ }
5574
+ }).catch(function() {
5575
+ // Fallback for non-secure contexts
5576
+ var ta = document.createElement("textarea");
5577
+ ta.value = text;
5578
+ ta.style.position = "fixed";
5579
+ ta.style.opacity = "0";
5580
+ document.body.appendChild(ta);
5581
+ ta.select();
5582
+ document.execCommand("copy");
5583
+ document.body.removeChild(ta);
5584
+ if (triggerBtn) {
5585
+ var orig = triggerBtn.textContent;
5586
+ triggerBtn.textContent = "已复制";
5587
+ setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
5588
+ }
5589
+ });
5590
+ }
5591
+
5592
+ function formatBytes(value) {
5593
+ if (typeof value !== "number" || !isFinite(value) || value < 0) return "-";
5594
+ if (value < 1024) return value + " B";
5595
+ var units = ["KB", "MB", "GB", "TB"];
5596
+ var size = value / 1024;
5597
+ var unitIndex = 0;
5598
+ while (size >= 1024 && unitIndex < units.length - 1) {
5599
+ size = size / 1024;
5600
+ unitIndex += 1;
5601
+ }
5602
+ var display = size >= 10 ? size.toFixed(0) : size.toFixed(1);
5603
+ return display + " " + units[unitIndex];
5604
+ }
5605
+
5213
5606
  function loadSettingsData() {
5214
5607
  fetch("/api/settings", { credentials: "same-origin" })
5215
5608
  .then(function(res) { return res.json(); })
@@ -5236,6 +5629,97 @@
5236
5629
  }
5237
5630
  }
5238
5631
 
5632
+ // ── Android APK version display ──
5633
+ var apkSection = document.getElementById("android-apk-section");
5634
+ var apkCurrentRow = document.getElementById("android-apk-current-row");
5635
+ var apkCurrentEl = document.getElementById("settings-android-apk-current");
5636
+ var apkGithubRow = document.getElementById("android-apk-github-row");
5637
+ var apkGithubEl = document.getElementById("settings-android-apk-github");
5638
+ var apkGithubBtn = document.getElementById("download-github-apk-btn");
5639
+ var apkLocalRow = document.getElementById("android-apk-local-row");
5640
+ var apkLocalEl = document.getElementById("settings-android-apk-local");
5641
+ var apkLocalBtn = document.getElementById("download-local-apk-btn");
5642
+ var apkMessageEl = document.getElementById("android-apk-message");
5643
+ var androidApk = data.androidApk || {};
5644
+ var isInApk = !!_apkVersion;
5645
+
5646
+ if (isInApk) {
5647
+ // ── APK 内模式:显示当前版本 + 线上版本 + 本地版本 ──
5648
+ if (apkCurrentRow && apkCurrentEl) {
5649
+ apkCurrentEl.textContent = "v" + _apkVersion;
5650
+ apkCurrentRow.classList.remove("hidden");
5651
+ }
5652
+ // 线上版本
5653
+ if (androidApk.github && apkGithubRow && apkGithubEl) {
5654
+ var ghLabel = androidApk.github.version ? ("v" + androidApk.github.version) : androidApk.github.fileName;
5655
+ if (typeof androidApk.github.size === "number") ghLabel += " · " + formatBytes(androidApk.github.size);
5656
+ apkGithubEl.textContent = ghLabel;
5657
+ apkGithubRow.classList.remove("hidden");
5658
+ if (apkGithubBtn) {
5659
+ apkGithubBtn.textContent = "下载安装";
5660
+ apkGithubBtn.classList.remove("hidden");
5661
+ apkGithubBtn.onclick = function() {
5662
+ try {
5663
+ WandNative.downloadUpdate(androidApk.github.downloadUrl, androidApk.github.fileName || "wand-update.apk", "github");
5664
+ } catch (e) {
5665
+ alert("调用下载失败: " + e.message);
5666
+ }
5667
+ };
5668
+ }
5669
+ }
5670
+ // 本地版本
5671
+ if (androidApk.local && apkLocalRow && apkLocalEl) {
5672
+ var lcLabel = androidApk.local.version ? ("v" + androidApk.local.version) : androidApk.local.fileName;
5673
+ if (typeof androidApk.local.size === "number") lcLabel += " · " + formatBytes(androidApk.local.size);
5674
+ apkLocalEl.textContent = lcLabel;
5675
+ apkLocalRow.classList.remove("hidden");
5676
+ if (apkLocalBtn) {
5677
+ apkLocalBtn.textContent = "下载安装";
5678
+ apkLocalBtn.classList.remove("hidden");
5679
+ apkLocalBtn.onclick = function() {
5680
+ try {
5681
+ WandNative.downloadUpdate(androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", "local");
5682
+ } catch (e) {
5683
+ alert("调用下载失败: " + e.message);
5684
+ }
5685
+ };
5686
+ }
5687
+ }
5688
+ // 都没有时
5689
+ if (!androidApk.github && !androidApk.local && apkMessageEl) {
5690
+ apkMessageEl.textContent = "暂无可用更新";
5691
+ apkMessageEl.classList.remove("hidden");
5692
+ }
5693
+ } else {
5694
+ // ── 浏览器模式:只显示线上版本 + 下载按钮 ──
5695
+ if (androidApk.github && apkGithubRow && apkGithubEl) {
5696
+ var ghLabel2 = androidApk.github.version ? ("v" + androidApk.github.version) : androidApk.github.fileName;
5697
+ if (typeof androidApk.github.size === "number") ghLabel2 += " · " + formatBytes(androidApk.github.size);
5698
+ apkGithubEl.textContent = ghLabel2;
5699
+ apkGithubRow.classList.remove("hidden");
5700
+ if (apkGithubBtn) {
5701
+ apkGithubBtn.textContent = "下载";
5702
+ apkGithubBtn.classList.remove("hidden");
5703
+ apkGithubBtn.onclick = function() {
5704
+ window.open(androidApk.github.downloadUrl, "_blank");
5705
+ };
5706
+ }
5707
+ } else if (apkMessageEl) {
5708
+ apkMessageEl.textContent = "暂未提供";
5709
+ apkMessageEl.classList.remove("hidden");
5710
+ }
5711
+ }
5712
+
5713
+ // App connect code (encrypted)
5714
+ var connectCodeEl = document.getElementById("android-connect-code");
5715
+ if (connectCodeEl) {
5716
+ connectCodeEl.textContent = "加载中...";
5717
+ fetch("/api/app-connect-code").then(function(r) { return r.json(); }).then(function(d) {
5718
+ if (d.code) connectCodeEl.textContent = d.code;
5719
+ else connectCodeEl.textContent = "生成失败";
5720
+ }).catch(function() { connectCodeEl.textContent = "获取失败"; });
5721
+ }
5722
+
5239
5723
  // Config fields
5240
5724
  var cfg = data.config || {};
5241
5725
  var hostEl = document.getElementById("cfg-host");
@@ -5274,6 +5758,19 @@
5274
5758
  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
5759
  presetsList.innerHTML = html;
5276
5760
  }
5761
+
5762
+ // Card expand defaults
5763
+ var cd = cfg.cardDefaults || {};
5764
+ var cdEditEl = document.getElementById("cfg-card-edit");
5765
+ var cdInlineEl = document.getElementById("cfg-card-inline");
5766
+ var cdTerminalEl = document.getElementById("cfg-card-terminal");
5767
+ var cdThinkingEl = document.getElementById("cfg-card-thinking");
5768
+ var cdToolgroupEl = document.getElementById("cfg-card-toolgroup");
5769
+ if (cdEditEl) cdEditEl.checked = cd.editCards === true;
5770
+ if (cdInlineEl) cdInlineEl.checked = cd.inlineTools === true;
5771
+ if (cdTerminalEl) cdTerminalEl.checked = cd.terminal === true;
5772
+ if (cdThinkingEl) cdThinkingEl.checked = cd.thinking === true;
5773
+ if (cdToolgroupEl) cdToolgroupEl.checked = cd.toolGroup === true;
5277
5774
  })
5278
5775
  .catch(function() {});
5279
5776
  }
@@ -5320,6 +5817,52 @@
5320
5817
  });
5321
5818
  }
5322
5819
 
5820
+ function saveDisplaySettings() {
5821
+ var msgEl = document.getElementById("display-message");
5822
+ if (msgEl) { msgEl.classList.add("hidden"); msgEl.textContent = ""; }
5823
+
5824
+ var body = {
5825
+ cardDefaults: {
5826
+ editCards: !!(document.getElementById("cfg-card-edit") || {}).checked,
5827
+ inlineTools: !!(document.getElementById("cfg-card-inline") || {}).checked,
5828
+ terminal: !!(document.getElementById("cfg-card-terminal") || {}).checked,
5829
+ thinking: !!(document.getElementById("cfg-card-thinking") || {}).checked,
5830
+ toolGroup: !!(document.getElementById("cfg-card-toolgroup") || {}).checked,
5831
+ }
5832
+ };
5833
+
5834
+ fetch("/api/settings/config", {
5835
+ method: "POST",
5836
+ headers: { "Content-Type": "application/json" },
5837
+ credentials: "same-origin",
5838
+ body: JSON.stringify(body)
5839
+ })
5840
+ .then(function(res) { return res.json(); })
5841
+ .then(function(data) {
5842
+ if (msgEl) {
5843
+ if (data.error) {
5844
+ msgEl.textContent = data.error;
5845
+ msgEl.style.color = "var(--error)";
5846
+ } else {
5847
+ msgEl.textContent = "显示设置已保存";
5848
+ msgEl.style.color = "var(--success)";
5849
+ }
5850
+ msgEl.classList.remove("hidden");
5851
+ }
5852
+ // Update local config so card defaults take effect immediately
5853
+ if (!data.error && state.config) {
5854
+ state.config.cardDefaults = body.cardDefaults;
5855
+ }
5856
+ })
5857
+ .catch(function() {
5858
+ if (msgEl) {
5859
+ msgEl.textContent = "保存失败。";
5860
+ msgEl.style.color = "var(--error)";
5861
+ msgEl.classList.remove("hidden");
5862
+ }
5863
+ });
5864
+ }
5865
+
5323
5866
  function uploadCertificates() {
5324
5867
  var keyFile = document.getElementById("cert-key-file");
5325
5868
  var certFile = document.getElementById("cert-cert-file");
@@ -5459,6 +6002,16 @@
5459
6002
 
5460
6003
  // ── Notification Settings Helpers ──
5461
6004
 
6005
+ function _updateAppIconSelection(activeIcon) {
6006
+ var opts = document.querySelectorAll(".app-icon-option");
6007
+ for (var i = 0; i < opts.length; i++) {
6008
+ var preview = opts[i].querySelector(".app-icon-preview");
6009
+ if (preview) {
6010
+ preview.style.borderColor = opts[i].getAttribute("data-icon") === activeIcon ? "var(--accent)" : "transparent";
6011
+ }
6012
+ }
6013
+ }
6014
+
5462
6015
  function updateNotificationStatus() {
5463
6016
  var statusEl = document.getElementById("notification-permission-status");
5464
6017
  var requestBtn = document.getElementById("notification-request-btn");
@@ -5466,15 +6019,20 @@
5466
6019
  var testMsgEl = document.getElementById("notification-test-message");
5467
6020
  if (!statusEl) return;
5468
6021
 
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;
6022
+ // Determine permission state: native bridge or browser API
6023
+ var perm = _getNativePermission();
6024
+ if (perm === null) {
6025
+ // No native bridge — fall back to browser Notification API
6026
+ if (typeof Notification === "undefined") {
6027
+ statusEl.textContent = "\u4e0d\u652f\u6301";
6028
+ statusEl.style.color = "var(--fg-muted)";
6029
+ if (requestBtn) requestBtn.classList.add("hidden");
6030
+ if (resetBtn) resetBtn.classList.add("hidden");
6031
+ return;
6032
+ }
6033
+ perm = Notification.permission;
5475
6034
  }
5476
6035
 
5477
- var perm = Notification.permission;
5478
6036
  if (perm === "granted") {
5479
6037
  statusEl.textContent = "\u5df2\u6388\u6743 \u2713";
5480
6038
  statusEl.style.color = "var(--success)";
@@ -5495,6 +6053,28 @@
5495
6053
 
5496
6054
  function resetNotificationPermission() {
5497
6055
  var testMsgEl = document.getElementById("notification-test-message");
6056
+
6057
+ // Native bridge path — trigger Android system permission dialog
6058
+ if (_hasNativeBridge) {
6059
+ // Listen for permission result callback from native
6060
+ window._onNativePermissionResult = function(result) {
6061
+ updateNotificationStatus();
6062
+ if (testMsgEl) {
6063
+ if (result === "granted") {
6064
+ testMsgEl.textContent = "\u2713 \u5df2\u6388\u6743";
6065
+ testMsgEl.style.color = "var(--success)";
6066
+ } else {
6067
+ testMsgEl.textContent = "\u2717 \u672a\u6388\u6743\uff0c\u8bf7\u5728\u7cfb\u7edf\u8bbe\u7f6e\u4e2d\u5f00\u542f Wand \u7684\u901a\u77e5\u6743\u9650";
6068
+ testMsgEl.style.color = "var(--danger)";
6069
+ }
6070
+ testMsgEl.classList.remove("hidden");
6071
+ }
6072
+ delete window._onNativePermissionResult;
6073
+ };
6074
+ try { WandNative.requestPermission(); } catch (_e) {}
6075
+ return;
6076
+ }
6077
+
5498
6078
  if (typeof Notification === "undefined") return;
5499
6079
 
5500
6080
  // Always call requestPermission — this triggers the browser's native
@@ -5550,9 +6130,44 @@
5550
6130
  });
5551
6131
  results.push(bubbleEnabled ? "\u2713 \u5e94\u7528\u5185\u6c14\u6ce1" : "\u2013 \u5e94\u7528\u5185\u6c14\u6ce1\uff08\u5df2\u5173\u95ed\uff09");
5552
6132
 
5553
- // 3. Test browser notification
6133
+ // 3. Test system notification (native bridge or browser API)
6134
+ if (_hasNativeBridge) {
6135
+ var nativePerm = _getNativePermission();
6136
+ if (nativePerm === "granted") {
6137
+ try {
6138
+ WandNative.sendNotification("Wand \u6d4b\u8bd5\u901a\u77e5", "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002", "wand-test");
6139
+ results.push("\u2713 \u7cfb\u7edf\u901a\u77e5");
6140
+ } catch (_e) {
6141
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff09");
6142
+ }
6143
+ } else if (nativePerm === "denied") {
6144
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff0c\u8bf7\u5728\u7cfb\u7edf\u8bbe\u7f6e\u4e2d\u5f00\u542f\uff09");
6145
+ } else {
6146
+ // "default" — request permission, then report
6147
+ window._onNativePermissionResult = function(result) {
6148
+ updateNotificationStatus();
6149
+ if (result === "granted") {
6150
+ try {
6151
+ WandNative.sendNotification("Wand \u6d4b\u8bd5\u901a\u77e5", "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002", "wand-test");
6152
+ results.push("\u2713 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
6153
+ } catch (_e2) {
6154
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff09");
6155
+ }
6156
+ } else {
6157
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
6158
+ }
6159
+ showTestResults(testMsgEl, results);
6160
+ delete window._onNativePermissionResult;
6161
+ };
6162
+ try { WandNative.requestPermission(); } catch (_e) {}
6163
+ return; // async — results shown in callback
6164
+ }
6165
+ showTestResults(testMsgEl, results);
6166
+ return;
6167
+ }
6168
+
5554
6169
  if (typeof Notification === "undefined") {
5555
- results.push("\u2013 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u4e0d\u652f\u6301\uff09");
6170
+ results.push("\u2013 \u7cfb\u7edf\u901a\u77e5\uff08\u4e0d\u652f\u6301\uff09");
5556
6171
  showTestResults(testMsgEl, results);
5557
6172
  return;
5558
6173
  }
@@ -5561,27 +6176,27 @@
5561
6176
  if (perm === "granted") {
5562
6177
  try {
5563
6178
  var n = new Notification("Wand \u6d4b\u8bd5\u901a\u77e5", {
5564
- body: "\u6d4f\u89c8\u5668\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
6179
+ body: "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
5565
6180
  icon: "/favicon.ico",
5566
6181
  tag: "wand-test",
5567
6182
  });
5568
6183
  setTimeout(function() { n.close(); }, 5000);
5569
- results.push("\u2713 \u6d4f\u89c8\u5668\u901a\u77e5");
6184
+ results.push("\u2713 \u7cfb\u7edf\u901a\u77e5");
5570
6185
  } catch (_e) {
5571
- results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS\uff09");
6186
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS\uff09");
5572
6187
  }
5573
6188
  showTestResults(testMsgEl, results);
5574
6189
  } else if (perm === "denied") {
5575
- results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff09");
6190
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff09");
5576
6191
  showTestResults(testMsgEl, results);
5577
6192
  } else {
5578
6193
  // "default" — try requesting
5579
6194
  Notification.requestPermission().then(function(result) {
5580
6195
  updateNotificationStatus();
5581
6196
  if (result === "granted") {
5582
- results.push("\u2713 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
6197
+ results.push("\u2713 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
5583
6198
  } else {
5584
- results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
6199
+ results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
5585
6200
  }
5586
6201
  showTestResults(testMsgEl, results);
5587
6202
  });
@@ -7837,6 +8452,13 @@
7837
8452
  resetInputPanelViewportSpacing();
7838
8453
  setTimeout(function() {
7839
8454
  window.scrollTo(0, 0);
8455
+ // On mobile, force terminal refit + scroll after keyboard dismissal.
8456
+ // The container height restores but xterm needs an explicit refit to
8457
+ // fill the expanded space, and the scroll position needs resetting.
8458
+ if (isTouchDevice()) {
8459
+ ensureTerminalFit();
8460
+ maybeScrollTerminalToBottom("force");
8461
+ }
7840
8462
  }, 100);
7841
8463
  }
7842
8464
 
@@ -8562,6 +9184,15 @@
8562
9184
  syncInputBoxScroll(inputBox);
8563
9185
  }
8564
9186
 
9187
+ // Keyboard just closed — force terminal refit and scroll to bottom
9188
+ // after a delay so the keyboard dismiss animation and layout settle.
9189
+ if (keyboardOpen && !isKeyboardOpen) {
9190
+ setTimeout(function() {
9191
+ ensureTerminalFit();
9192
+ maybeScrollTerminalToBottom("force");
9193
+ }, 200);
9194
+ }
9195
+
8565
9196
  keyboardOpen = isKeyboardOpen;
8566
9197
  lastHeight = vv.height;
8567
9198
  }
@@ -8666,6 +9297,11 @@
8666
9297
  }
8667
9298
  state.resizeHandler = function() { scheduleTerminalResize(true); };
8668
9299
  window.addEventListener("resize", state.resizeHandler);
9300
+ // Also listen to visualViewport resize for pinch-zoom / browser zoom
9301
+ if (window.visualViewport) {
9302
+ state.visualViewportHandler = function() { scheduleTerminalResize(true); };
9303
+ window.visualViewport.addEventListener("resize", state.visualViewportHandler);
9304
+ }
8669
9305
  requestAnimationFrame(function() { scheduleTerminalResize(true); });
8670
9306
  }
8671
9307
 
@@ -8682,6 +9318,10 @@
8682
9318
  window.removeEventListener("resize", state.resizeHandler);
8683
9319
  state.resizeHandler = null;
8684
9320
  }
9321
+ if (state.visualViewportHandler && window.visualViewport) {
9322
+ window.visualViewport.removeEventListener("resize", state.visualViewportHandler);
9323
+ state.visualViewportHandler = null;
9324
+ }
8685
9325
  [["mousemove", "resizeMouseMove"], ["mouseup", "resizeMouseUp"],
8686
9326
  ["touchmove", "resizeTouchMove"], ["touchend", "resizeTouchEnd"]
8687
9327
  ].forEach(function(pair) {
@@ -8814,6 +9454,12 @@
8814
9454
  state.pollTimer = setInterval(refreshAll, 1600);
8815
9455
  }
8816
9456
 
9457
+ // Periodically refresh session time displays (30s)
9458
+ setInterval(function() {
9459
+ var timeEls = document.querySelectorAll(".session-time");
9460
+ if (timeEls.length > 0) scheduleSessionListUpdate();
9461
+ }, 30000);
9462
+
8817
9463
  function initWebSocket() {
8818
9464
  if (!window.WebSocket) return false;
8819
9465
 
@@ -8952,14 +9598,26 @@
8952
9598
  // Trigger status bar completion animation
8953
9599
  scheduleChatRender(true);
8954
9600
  }
8955
- // Notify user when a session completes (browser + in-app if backgrounded or not viewing)
9601
+ // Notify user when a session completes show what was accomplished
8956
9602
  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
9603
  var endedExitCode = msg.data && msg.data.exitCode;
8959
9604
  var endedIsError = endedExitCode !== null && endedExitCode !== undefined && endedExitCode !== 0;
9605
+ // Build meaningful notification body
9606
+ var endedTaskSummary = endedSession ? (endedSession.summary || "") : "";
9607
+ var endedLastReply = endedSession ? getLastAssistantSummary(endedSession) : "";
9608
+ var endedNotifTitle = endedIsError ? "任务异常结束" : "任务已完成";
9609
+ var endedNotifBody = "";
9610
+ if (endedTaskSummary) {
9611
+ endedNotifBody = endedTaskSummary;
9612
+ if (endedLastReply && !endedIsError) {
9613
+ endedNotifBody += "\n" + endedLastReply;
9614
+ }
9615
+ } else {
9616
+ endedNotifBody = endedSession ? (endedSession.command || msg.sessionId) : msg.sessionId;
9617
+ }
8960
9618
  sendBrowserNotification(
8961
- endedIsError ? "\u4f1a\u8bdd\u5f02\u5e38\u7ed3\u675f" : "\u4f1a\u8bdd\u5df2\u5b8c\u6210",
8962
- endedName,
9619
+ endedNotifTitle,
9620
+ endedNotifBody,
8963
9621
  {
8964
9622
  tag: "wand-ended-" + msg.sessionId,
8965
9623
  onClick: function() {
@@ -8969,8 +9627,8 @@
8969
9627
  );
8970
9628
  if (msg.sessionId !== state.selectedId || document.hidden) {
8971
9629
  showNotificationBubble({
8972
- title: endedIsError ? "\u4f1a\u8bdd\u5f02\u5e38\u7ed3\u675f" : "\u4f1a\u8bdd\u5df2\u5b8c\u6210",
8973
- body: endedName,
9630
+ title: endedNotifTitle,
9631
+ body: endedNotifBody,
8974
9632
  type: endedIsError ? "warning" : "success",
8975
9633
  icon: endedIsError ? "!" : "\u2713",
8976
9634
  duration: 6000,
@@ -9052,6 +9710,8 @@
9052
9710
  state.currentTask = msg.data || null;
9053
9711
  updateTaskDisplay();
9054
9712
  }
9713
+ // Update session list to reflect current activity (debounced)
9714
+ scheduleSessionListUpdate();
9055
9715
  break;
9056
9716
  case 'status':
9057
9717
  if (msg.sessionId && msg.data) {
@@ -9087,10 +9747,18 @@
9087
9747
  };
9088
9748
  // Browser notification for permission waiting (background tab)
9089
9749
  var permSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
9090
- var permSessionName = permSession ? (permSession.label || permSession.command || msg.sessionId) : msg.sessionId;
9750
+ var permTaskName = permSession ? (permSession.summary || permSession.command || msg.sessionId) : msg.sessionId;
9751
+ var permDetail = msg.data.permissionRequest.prompt || "需要权限审批";
9752
+ var permTarget = msg.data.permissionRequest.target;
9753
+ var permBody = permTaskName;
9754
+ if (permTarget) {
9755
+ permBody += "\n" + permDetail + " · " + permTarget;
9756
+ } else {
9757
+ permBody += "\n" + permDetail;
9758
+ }
9091
9759
  sendBrowserNotification(
9092
- "\u4f1a\u8bdd\u7b49\u5f85\u6388\u6743",
9093
- permSessionName + " \u2014 " + (msg.data.permissionRequest.prompt || "\u9700\u8981\u6743\u9650\u5ba1\u6279"),
9760
+ "需要你的授权",
9761
+ permBody,
9094
9762
  {
9095
9763
  tag: "wand-perm-" + msg.sessionId,
9096
9764
  onClick: function() {
@@ -9103,12 +9771,12 @@
9103
9771
  // In-app bubble if not currently viewing this session
9104
9772
  if (msg.sessionId !== state.selectedId) {
9105
9773
  showNotificationBubble({
9106
- title: "\u4f1a\u8bdd\u7b49\u5f85\u6388\u6743",
9107
- body: permSessionName + " \u2014 " + (msg.data.permissionRequest.prompt || "\u9700\u8981\u6743\u9650\u5ba1\u6279"),
9774
+ title: "需要你的授权",
9775
+ body: permBody,
9108
9776
  type: "warning",
9109
9777
  icon: "!",
9110
9778
  duration: 0,
9111
- actionLabel: "\u67e5\u770b",
9779
+ actionLabel: "去处理",
9112
9780
  action: function() {
9113
9781
  selectSession(msg.sessionId);
9114
9782
  }
@@ -9542,9 +10210,9 @@
9542
10210
  return;
9543
10211
  }
9544
10212
 
9545
- var messages = state.currentMessages;
10213
+ var allMessages = state.currentMessages;
9546
10214
 
9547
- if (messages.length === 0) {
10215
+ if (allMessages.length === 0) {
9548
10216
  if (state.lastRenderedEmpty !== "empty") {
9549
10217
  renderChatEmptyState(chatOutput, '<div class="empty-state"><strong>对话已开始</strong><br>在下方输入框发送消息,Claude 会自动回复。</div>');
9550
10218
  state.lastRenderedEmpty = "empty";
@@ -9553,6 +10221,16 @@
9553
10221
  return;
9554
10222
  }
9555
10223
 
10224
+ // Lazy loading: only render the most recent chatRenderedCount messages.
10225
+ // Auto-expand when new messages arrive during active streaming to avoid hiding them.
10226
+ var totalMsgCount = allMessages.length;
10227
+ if (totalMsgCount > state.chatRenderedCount && state.chatAutoFollow) {
10228
+ state.chatRenderedCount = totalMsgCount;
10229
+ }
10230
+ var visibleOffset = Math.max(0, totalMsgCount - state.chatRenderedCount);
10231
+ var messages = visibleOffset > 0 ? allMessages.slice(visibleOffset) : allMessages;
10232
+ var hasOlderMessages = visibleOffset > 0;
10233
+
9556
10234
  // Check if messages actually changed
9557
10235
  var msgCount = messages.length;
9558
10236
  var outputHash = selectedSession.output ? selectedSession.output.length : 0;
@@ -9613,16 +10291,17 @@
9613
10291
  // Build HTML with system info cards interleaved
9614
10292
  var html = '';
9615
10293
  var reversedMessages = messages.slice().reverse();
9616
- var msgCount = messages.length;
10294
+ var visibleCount = messages.length;
9617
10295
 
9618
10296
  for (var i = 0; i < reversedMessages.length; i++) {
9619
10297
  var msg = reversedMessages[i];
9620
- var originalIndex = msgCount - 1 - i; // Original index in messages array
10298
+ var localIndex = visibleCount - 1 - i; // Index within visible slice
10299
+ var originalIndex = localIndex + visibleOffset; // Index in full messages array
9621
10300
 
9622
10301
  // Find system info for this message position
9623
10302
  var sysInfo = null;
9624
10303
  for (var j = 0; j < systemInfo.length; j++) {
9625
- if (systemInfo[j].beforeMessage === originalIndex) {
10304
+ if (systemInfo[j].beforeMessage === localIndex) {
9626
10305
  sysInfo = systemInfo[j];
9627
10306
  break;
9628
10307
  }
@@ -9642,6 +10321,13 @@
9642
10321
  html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null, originalIndex);
9643
10322
  }
9644
10323
 
10324
+ // Add sentinel for loading older messages (DOM end = visual top in column-reverse)
10325
+ if (hasOlderMessages) {
10326
+ html += '<div class="chat-load-more" id="chat-load-more-sentinel">' +
10327
+ '<button class="chat-load-more-btn" type="button">加载更早的 ' + Math.min(state.chatPageSize, visibleOffset) + ' 条消息</button>' +
10328
+ '</div>';
10329
+ }
10330
+
9645
10331
  chatMessages.innerHTML = html;
9646
10332
  attachAllCopyHandlers(chatMessages);
9647
10333
  bindChatScrollListener();
@@ -9667,6 +10353,7 @@
9667
10353
  // Scroll to bottom (newest message) - column-reverse: scrollTop=0 is visual bottom
9668
10354
  requestAnimationFrame(function() {
9669
10355
  smartScrollToBottom(chatMessages);
10356
+ observeLoadMoreSentinel();
9670
10357
  });
9671
10358
  }
9672
10359
 
@@ -9687,15 +10374,15 @@
9687
10374
  });
9688
10375
  }
9689
10376
 
9690
- // Pre-compute per-round cumulative usage.
10377
+ // Pre-compute per-round cumulative usage using original (full array) indices.
9691
10378
  // A "round" starts at a user message and includes all subsequent assistant turns
9692
10379
  // until the next user message. Only the last assistant in each round shows the total.
9693
10380
  var roundUsageByIndex = {};
9694
10381
  (function() {
9695
10382
  var acc = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, totalCostUsd: 0 };
9696
10383
  var lastAssistantIdx = -1;
9697
- for (var mi = 0; mi < messages.length; mi++) {
9698
- var m = messages[mi];
10384
+ for (var mi = 0; mi < allMessages.length; mi++) {
10385
+ var m = allMessages[mi];
9699
10386
  if (m.role === "user") {
9700
10387
  if (lastAssistantIdx >= 0 && (acc.inputTokens > 0 || acc.outputTokens > 0 || acc.totalCostUsd > 0)) {
9701
10388
  roundUsageByIndex[lastAssistantIdx] = {
@@ -9739,7 +10426,7 @@
9739
10426
  var insertedEls = [];
9740
10427
  for (var i = 0; i < newMessages.length; i++) {
9741
10428
  var div = document.createElement("div");
9742
- var nmOrigIdx = existingCount + (newMessages.length - 1 - i);
10429
+ var nmOrigIdx = visibleOffset + existingCount + (newMessages.length - 1 - i);
9743
10430
  div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null, nmOrigIdx);
9744
10431
  var el = div.firstElementChild;
9745
10432
  if (el) {
@@ -9771,7 +10458,7 @@
9771
10458
  for (var mi = 0; mi < MAX_STREAMING_SCAN; mi++) {
9772
10459
  var currentEl = existingEls[mi];
9773
10460
  var tmpWrap = document.createElement("div");
9774
- var srOrigIdx = reversedMessages.length - 1 - mi;
10461
+ var srOrigIdx = visibleOffset + reversedMessages.length - 1 - mi;
9775
10462
  tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null, srOrigIdx);
9776
10463
  var replacementEl = tmpWrap.firstElementChild;
9777
10464
  if (!replacementEl) continue;
@@ -9817,7 +10504,7 @@
9817
10504
  renderStructuredStatusBar(chatMessages, selectedSession);
9818
10505
 
9819
10506
  // Update todo progress bar from latest messages
9820
- updateTodoProgress(messages);
10507
+ updateTodoProgress(allMessages);
9821
10508
  }
9822
10509
 
9823
10510
  // Smart scroll: only auto-scroll if user is near bottom
@@ -10890,7 +11577,8 @@
10890
11577
  // Thinking card (deep thought) — from PTY parsing
10891
11578
  if (msg.role === "thinking") {
10892
11579
  var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
10893
- var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
11580
+ var thinkingPersisted = getPersistedExpandState(thinkingKey);
11581
+ var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
10894
11582
  return '<div class="chat-message thinking">' +
10895
11583
  '<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
10896
11584
  '<span class="thinking-inline-icon">⦿</span>' +
@@ -11028,7 +11716,7 @@
11028
11716
  var summaryText = parts.join(" · ");
11029
11717
  var groupKey = buildExpandKey("tool-group", [messageKey, items[0] && items[0].index, items.length]);
11030
11718
  var persistedExpanded = getPersistedExpandState(groupKey);
11031
- var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11719
+ var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.toolGroup) : persistedExpanded;
11032
11720
 
11033
11721
  // Render each item's inline-tool card
11034
11722
  var innerHtml = "";
@@ -11139,7 +11827,8 @@
11139
11827
  '</div>';
11140
11828
  }
11141
11829
  var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
11142
- var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
11830
+ var thinkingPersisted = getPersistedExpandState(thinkingKey);
11831
+ var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
11143
11832
  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
11833
  '<span class="thinking-inline-icon">⦿</span>' +
11145
11834
  '<span class="thinking-inline-preview">' + escapeHtml(thinkingExpanded ? thinkingText : preview) + '</span>' +
@@ -11236,7 +11925,7 @@
11236
11925
  var fullResult = resultContent;
11237
11926
 
11238
11927
  var expandedHtml = "";
11239
- var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11928
+ var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.inlineTools) : persistedExpanded;
11240
11929
  if (hasResult) {
11241
11930
  expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
11242
11931
  '<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
@@ -11248,16 +11937,23 @@
11248
11937
  expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';"><div class="inline-tool-loading">等待响应…</div></div>';
11249
11938
  }
11250
11939
 
11940
+ var isTruncated = toolResult && toolResult._truncated === true;
11941
+
11251
11942
  var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
11252
11943
  var extraClass = isError ? 'inline-tool-error-inline' : '';
11253
11944
  if (shouldExpand) extraClass += ' inline-tool-open';
11254
11945
 
11946
+ var truncatedAttrs = isTruncated
11947
+ ? 'data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '" '
11948
+ : '';
11949
+
11255
11950
  return '<div class="inline-tool ' + extraClass + '" ' +
11256
11951
  'data-expand-kind="inline-tool" ' +
11257
11952
  'data-expand-key="' + escapeHtml(expandKey) + '" ' +
11258
11953
  'data-result="' + escapeHtml(fullResult) + '" ' +
11259
11954
  'data-preview="' + previewDataAttr + '" ' +
11260
11955
  'data-status="' + (isError ? 'error' : (hasResult ? 'done' : 'pending')) + '" ' +
11956
+ truncatedAttrs +
11261
11957
  'onclick="__inlineToolToggle(this)">' +
11262
11958
  '<div class="inline-tool-row">' +
11263
11959
  '<span class="inline-tool-status">' + statusIcon + '</span>' +
@@ -11314,9 +12010,14 @@
11314
12010
 
11315
12011
  // Show command preview in header (truncate long commands)
11316
12012
  var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
11317
- var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
12013
+ var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.terminal) : persistedExpanded;
11318
12014
 
11319
- return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '">' +
12015
+ var termTruncated = toolResult && toolResult._truncated === true;
12016
+ var termTruncAttrs = termTruncated
12017
+ ? ' data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '"'
12018
+ : '';
12019
+
12020
+ return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '"' + termTruncAttrs + '>' +
11320
12021
  '<div class="term-header" onclick="__terminalExpand(this)">' +
11321
12022
  statusDot +
11322
12023
  '<span class="term-cmd-preview"><span class="term-prompt">$</span> ' + escapeHtml(cmdPreview) + '</span>' +
@@ -11596,10 +12297,12 @@
11596
12297
 
11597
12298
  var expandKey = buildExpandKey("tool-card", [messageKey, toolId]);
11598
12299
  var persistedExpanded = getPersistedExpandState(expandKey);
11599
- var shouldExpand = persistedExpanded === null ? statusClass === "loading" : persistedExpanded;
12300
+ var cardDefaultExpand = !!(state.config && state.config.cardDefaults && state.config.cardDefaults.editCards);
12301
+ var shouldExpand = persistedExpanded === null ? (statusClass === "loading" || cardDefaultExpand) : persistedExpanded;
12302
+ var tcTruncated = toolResult && toolResult._truncated === true;
11600
12303
  var collapsedClass = shouldExpand ? "" : " collapsed";
11601
12304
  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) + '">' +
12305
+ 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
12306
  '<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
11604
12307
  '<span class="tool-use-icon">' + headerIcon + '</span>' +
11605
12308
  '<span class="tool-use-name">' + escapeHtml(titleText) + '</span>' +
@@ -12059,7 +12762,7 @@
12059
12762
  '</div>';
12060
12763
 
12061
12764
  var bodyHtml = opts.body
12062
- ? '<div class="notification-bubble-body">' + escapeHtml(opts.body) + '</div>'
12765
+ ? '<div class="notification-bubble-body">' + escapeHtml(opts.body).replace(/\n/g, '<br>') + '</div>'
12063
12766
  : '';
12064
12767
 
12065
12768
  var actionsHtml = opts.actionLabel
@@ -12125,13 +12828,43 @@
12125
12828
 
12126
12829
  // ── Browser Notification API ──
12127
12830
 
12831
+ // Detect Android APK native bridge
12832
+ var _hasNativeBridge = typeof WandNative !== "undefined" && typeof WandNative.sendNotification === "function";
12833
+ // Detect if running inside APK and extract installed version from User-Agent
12834
+ var _apkVersionMatch = navigator.userAgent.match(/WandApp\/([^\s]+)/);
12835
+ var _apkVersion = _apkVersionMatch ? _apkVersionMatch[1] : null;
12836
+
12837
+ function _getNativePermission() {
12838
+ if (_hasNativeBridge && typeof WandNative.getPermission === "function") {
12839
+ try { return WandNative.getPermission(); } catch (_e) {}
12840
+ }
12841
+ return null;
12842
+ }
12843
+
12128
12844
  function requestNotificationPermission() {
12845
+ if (_hasNativeBridge) {
12846
+ var perm = _getNativePermission();
12847
+ if (perm === "default" || perm === "denied") {
12848
+ try { WandNative.requestPermission(); } catch (_e) {}
12849
+ }
12850
+ return;
12851
+ }
12129
12852
  if (typeof Notification !== "undefined" && Notification.permission === "default") {
12130
12853
  Notification.requestPermission();
12131
12854
  }
12132
12855
  }
12133
12856
 
12134
12857
  function sendBrowserNotification(title, body, opts) {
12858
+ // Native Android bridge path
12859
+ if (_hasNativeBridge) {
12860
+ var perm = _getNativePermission();
12861
+ if (perm !== "granted") return;
12862
+ try {
12863
+ WandNative.sendNotification(title || "Wand", body || "", (opts && opts.tag) || "");
12864
+ } catch (_e) {}
12865
+ return;
12866
+ }
12867
+ // Browser Notification API path
12135
12868
  if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
12136
12869
  if (!document.hidden) return; // Only notify when tab is in background
12137
12870
  try {