@co0ontty/wand 1.17.2 → 1.17.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +21 -3
- package/dist/web-ui/content/scripts.js +228 -37
- package/dist/web-ui/content/styles.css +27 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -60,13 +60,31 @@ async function checkNpmLatestVersion(forceRefresh = false) {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
function compareSemver(a, b) {
|
|
63
|
-
const
|
|
64
|
-
|
|
63
|
+
const parse = (v) => {
|
|
64
|
+
const [main, ...rest] = v.split("-");
|
|
65
|
+
const pre = rest.join("-");
|
|
66
|
+
const mainParts = main.split(".").map((n) => Number(n) || 0);
|
|
67
|
+
return { mainParts, pre };
|
|
68
|
+
};
|
|
69
|
+
const pa = parse(a);
|
|
70
|
+
const pb = parse(b);
|
|
65
71
|
for (let i = 0; i < 3; i++) {
|
|
66
|
-
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
72
|
+
const diff = (pa.mainParts[i] || 0) - (pb.mainParts[i] || 0);
|
|
67
73
|
if (diff !== 0)
|
|
68
74
|
return diff;
|
|
69
75
|
}
|
|
76
|
+
// Main version equal — apply semver prerelease rule: no prerelease > with prerelease.
|
|
77
|
+
if (!pa.pre && pb.pre)
|
|
78
|
+
return 1;
|
|
79
|
+
if (pa.pre && !pb.pre)
|
|
80
|
+
return -1;
|
|
81
|
+
if (!pa.pre && !pb.pre)
|
|
82
|
+
return 0;
|
|
83
|
+
// Both have prerelease: lexical compare handles debug.MMDDHHMM ordering.
|
|
84
|
+
if (pa.pre < pb.pre)
|
|
85
|
+
return -1;
|
|
86
|
+
if (pa.pre > pb.pre)
|
|
87
|
+
return 1;
|
|
70
88
|
return 0;
|
|
71
89
|
}
|
|
72
90
|
let cachedGitHubApk = null;
|
|
@@ -201,6 +201,7 @@
|
|
|
201
201
|
claudeHistoryLoaded: false,
|
|
202
202
|
claudeHistoryExpanded: true,
|
|
203
203
|
claudeHistoryExpandedDirs: {},
|
|
204
|
+
archivedExpanded: false,
|
|
204
205
|
sessionsManageMode: false,
|
|
205
206
|
selectedSessionIds: {},
|
|
206
207
|
selectedClaudeHistoryIds: {},
|
|
@@ -1256,7 +1257,7 @@
|
|
|
1256
1257
|
'<button id="attach-btn" class="btn-circle btn-circle-attach" type="button" title="附加文件">' +
|
|
1257
1258
|
'<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="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>' +
|
|
1258
1259
|
'</button>' +
|
|
1259
|
-
'<input type="file" id="file-upload-input" multiple style="
|
|
1260
|
+
'<input type="file" id="file-upload-input" multiple style="position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;clip:rect(0,0,0,0);pointer-events:none">' +
|
|
1260
1261
|
'<select id="chat-mode-select" class="chat-mode-select" title="仅对新建会话生效">' +
|
|
1261
1262
|
renderModeOptions(preferredTool, composerMode) +
|
|
1262
1263
|
'</select>' +
|
|
@@ -1733,7 +1734,7 @@
|
|
|
1733
1734
|
groups.push(renderRecentGroup(activeSessions, recentHistorySessions));
|
|
1734
1735
|
}
|
|
1735
1736
|
if (archivedSessions.length > 0) {
|
|
1736
|
-
groups.push(
|
|
1737
|
+
groups.push(renderArchivedGroup(archivedSessions));
|
|
1737
1738
|
}
|
|
1738
1739
|
groups.push(renderClaudeHistorySection());
|
|
1739
1740
|
if (activeSessions.length === 0 && archivedSessions.length === 0 && recentHistorySessions.length === 0) {
|
|
@@ -1754,12 +1755,17 @@
|
|
|
1754
1755
|
var historyCount = getSelectedClaudeHistoryIds().length;
|
|
1755
1756
|
var totalCount = sessionCount + historyCount;
|
|
1756
1757
|
var hasAny = totalCount > 0;
|
|
1758
|
+
var selectable = countSelectableItems();
|
|
1759
|
+
var allSelected = selectable > 0 && totalCount >= selectable;
|
|
1760
|
+
var selectAllLabel = allSelected ? "取消全选" : "全选";
|
|
1761
|
+
var selectAllAction = allSelected ? "clear-selection" : "select-all-visible";
|
|
1762
|
+
var selectAllDisabled = selectable === 0 ? ' disabled' : '';
|
|
1757
1763
|
|
|
1758
1764
|
return '<div class="session-manage-bar active">' +
|
|
1759
1765
|
'<div class="session-manage-summary">已选择 ' + totalCount + ' 项</div>' +
|
|
1760
1766
|
'<div class="session-manage-actions">' +
|
|
1761
|
-
'<button class="session-manage-btn" data-action="
|
|
1762
|
-
'<button class="session-manage-btn" data-action="clear-selection" type="button">清空</button>' +
|
|
1767
|
+
'<button class="session-manage-btn" data-action="' + selectAllAction + '" type="button"' + selectAllDisabled + '>' + selectAllLabel + '</button>' +
|
|
1768
|
+
'<button class="session-manage-btn" data-action="clear-selection" type="button"' + (hasAny ? '' : ' disabled') + '>清空</button>' +
|
|
1763
1769
|
'<button class="session-manage-btn danger" data-action="delete-selected" type="button"' + (hasAny ? '' : ' disabled') + '>删除所选</button>' +
|
|
1764
1770
|
'<button class="session-manage-btn" data-action="toggle-manage-mode" type="button">完成</button>' +
|
|
1765
1771
|
'</div>' +
|
|
@@ -1773,6 +1779,20 @@
|
|
|
1773
1779
|
'</section>';
|
|
1774
1780
|
}
|
|
1775
1781
|
|
|
1782
|
+
function renderArchivedGroup(archivedSessions) {
|
|
1783
|
+
var expanded = !!state.archivedExpanded;
|
|
1784
|
+
var chevron = expanded ? "▾" : "▸";
|
|
1785
|
+
var header = '<div class="session-group-title claude-history-toggle" data-action="toggle-archived-group">' +
|
|
1786
|
+
'<span class="chevron">' + chevron + '</span> 已归档 ' +
|
|
1787
|
+
'<span class="history-count">' + archivedSessions.length + '</span>' +
|
|
1788
|
+
'</div>';
|
|
1789
|
+
if (!expanded) {
|
|
1790
|
+
return '<section class="session-group">' + header + '</section>';
|
|
1791
|
+
}
|
|
1792
|
+
var items = archivedSessions.map(function(session) { return renderSessionItem(session, "sessions"); }).join("");
|
|
1793
|
+
return '<section class="session-group">' + header + items + '</section>';
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1776
1796
|
function renderRecentGroup(activeSessions, recentHistorySessions) {
|
|
1777
1797
|
var html = '<section class="session-group">' +
|
|
1778
1798
|
'<div class="session-group-title">最近</div>';
|
|
@@ -1864,11 +1884,19 @@
|
|
|
1864
1884
|
updateSessionsList();
|
|
1865
1885
|
}
|
|
1866
1886
|
|
|
1887
|
+
function getSelectableSessions() {
|
|
1888
|
+
return state.sessions.filter(function(session) {
|
|
1889
|
+
return session.archived || !session.resumedToSessionId;
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
function countSelectableItems() {
|
|
1894
|
+
return getSelectableSessions().length + getVisibleClaudeHistorySessions().length;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1867
1897
|
function selectAllVisibleItems() {
|
|
1868
1898
|
var nextSessionIds = {};
|
|
1869
|
-
|
|
1870
|
-
return !session.resumedToSessionId;
|
|
1871
|
-
}).forEach(function(session) {
|
|
1899
|
+
getSelectableSessions().forEach(function(session) {
|
|
1872
1900
|
nextSessionIds[session.id] = true;
|
|
1873
1901
|
});
|
|
1874
1902
|
var nextHistoryIds = {};
|
|
@@ -2507,15 +2535,8 @@
|
|
|
2507
2535
|
// Ordered lists
|
|
2508
2536
|
escaped = escaped.replace(/^\d+\.\s+(.*)$/gm, '<li>$1</li>');
|
|
2509
2537
|
|
|
2510
|
-
// Tables
|
|
2511
|
-
escaped = escaped
|
|
2512
|
-
var cells = match.split("|").slice(1, -1);
|
|
2513
|
-
if (cells.every(function(c) { return /^[\-:]+$/.test(c.trim()); })) {
|
|
2514
|
-
return "";
|
|
2515
|
-
}
|
|
2516
|
-
return '<tr>' + cells.map(function(c) { return '<td>' + c.trim() + '</td>'; }).join("") + '</tr>';
|
|
2517
|
-
});
|
|
2518
|
-
escaped = escaped.replace(/(<tr>.*<\/tr>\n?)+/g, '<table>$&</table>');
|
|
2538
|
+
// Tables (GFM)
|
|
2539
|
+
escaped = parseMarkdownTables(escaped);
|
|
2519
2540
|
|
|
2520
2541
|
// Paragraphs
|
|
2521
2542
|
var paragraphs = escaped.split(/\n{2,}/);
|
|
@@ -3635,7 +3656,16 @@
|
|
|
3635
3656
|
if (scaleDownBtn) scaleDownBtn.addEventListener("click", function() { adjustTerminalScale(-0.25); });
|
|
3636
3657
|
if (scaleUpBtn) scaleUpBtn.addEventListener("click", function() { adjustTerminalScale(0.25); });
|
|
3637
3658
|
var pageRefreshBtn = document.getElementById("page-refresh-btn");
|
|
3638
|
-
if (pageRefreshBtn) pageRefreshBtn.addEventListener("click", function() {
|
|
3659
|
+
if (pageRefreshBtn) pageRefreshBtn.addEventListener("click", function(ev) {
|
|
3660
|
+
// Soft refresh: replay terminal buffer + rebuild chat view.
|
|
3661
|
+
// Fixes residual DOM from CSI cursor-jump sequences without losing page state.
|
|
3662
|
+
// Hold Shift to force a full page reload as an escape hatch.
|
|
3663
|
+
if (ev && ev.shiftKey) {
|
|
3664
|
+
location.reload();
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
softRefreshCurrentView();
|
|
3668
|
+
});
|
|
3639
3669
|
var jumpBottomBtn = document.getElementById("terminal-jump-bottom");
|
|
3640
3670
|
if (jumpBottomBtn) jumpBottomBtn.addEventListener("click", function() {
|
|
3641
3671
|
maybeScrollTerminalToBottom("force");
|
|
@@ -4125,6 +4155,9 @@
|
|
|
4125
4155
|
}
|
|
4126
4156
|
} else if (actionButton.dataset.action === "clear-all-history") {
|
|
4127
4157
|
clearAllClaudeHistory();
|
|
4158
|
+
} else if (actionButton.dataset.action === "toggle-archived-group") {
|
|
4159
|
+
state.archivedExpanded = !state.archivedExpanded;
|
|
4160
|
+
updateSessionsList();
|
|
4128
4161
|
} else if (actionButton.dataset.action === "resume" && actionButton.dataset.sessionId) {
|
|
4129
4162
|
handleResumeAction(actionButton);
|
|
4130
4163
|
} else if (actionButton.dataset.action === "resume-history" && actionButton.dataset.claudeSessionId) {
|
|
@@ -4493,6 +4526,42 @@
|
|
|
4493
4526
|
state.terminal.reset();
|
|
4494
4527
|
}
|
|
4495
4528
|
|
|
4529
|
+
// Soft resync terminal: reset WASM grid and replay full output buffer.
|
|
4530
|
+
// Clears any stale DOM rows left over from CSI cursor-jump sequences
|
|
4531
|
+
// (e.g. Claude permission menus redrawing in place while user holds arrow keys).
|
|
4532
|
+
function softResyncTerminal() {
|
|
4533
|
+
if (!state.terminal || !state.terminalOutput) return false;
|
|
4534
|
+
resetTerminal();
|
|
4535
|
+
state.terminal.write(state.terminalOutput);
|
|
4536
|
+
state.lastTerminalResyncAt = Date.now();
|
|
4537
|
+
maybeScrollTerminalToBottom("output");
|
|
4538
|
+
// Remeasure against real container: the refresh button used to only
|
|
4539
|
+
// reset+write, so a stale cols/rows (set at mount time with hidden
|
|
4540
|
+
// container) would survive the refresh and keep wrapping output wrong.
|
|
4541
|
+
ensureTerminalFit("refresh");
|
|
4542
|
+
return true;
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
// Soft refresh the whole current view without losing page state:
|
|
4546
|
+
// - Replays terminal buffer to clear residue
|
|
4547
|
+
// - Clears chat render cache and forces a full rebuild
|
|
4548
|
+
// Used by the refresh button and by automatic triggers
|
|
4549
|
+
// (e.g. permission escalation appearing/disappearing).
|
|
4550
|
+
function softRefreshCurrentView() {
|
|
4551
|
+
softResyncTerminal();
|
|
4552
|
+
if (typeof resetChatRenderCache === "function") resetChatRenderCache();
|
|
4553
|
+
if (typeof scheduleChatRender === "function") scheduleChatRender(true);
|
|
4554
|
+
else if (typeof render === "function") render();
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
function scheduleSoftResyncTerminal(delayMs) {
|
|
4558
|
+
if (state.softResyncTimer) clearTimeout(state.softResyncTimer);
|
|
4559
|
+
state.softResyncTimer = setTimeout(function() {
|
|
4560
|
+
state.softResyncTimer = null;
|
|
4561
|
+
softResyncTerminal();
|
|
4562
|
+
}, typeof delayMs === "number" ? delayMs : 150);
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4496
4565
|
function syncTerminalBuffer(sessionId, output, options) {
|
|
4497
4566
|
if (!state.terminal) return false;
|
|
4498
4567
|
var normalizedOutput = normalizeTerminalOutput(output || "");
|
|
@@ -4643,6 +4712,10 @@
|
|
|
4643
4712
|
initTerminalResizeHandle();
|
|
4644
4713
|
observeTerminalResize();
|
|
4645
4714
|
startTerminalHealthCheck();
|
|
4715
|
+
// Container may have been hidden / zero-width at construction
|
|
4716
|
+
// time (hard-coded 120x36). Remeasure against the real container
|
|
4717
|
+
// so wterm reflows the just-written history to the correct cols.
|
|
4718
|
+
ensureTerminalFit("mount");
|
|
4646
4719
|
}).catch(function(err) {
|
|
4647
4720
|
state.terminalInitializing = false;
|
|
4648
4721
|
console.error("[wand] wterm init failed:", err);
|
|
@@ -5131,6 +5204,17 @@
|
|
|
5131
5204
|
if (normalizedSnapshot.id === state.selectedId) {
|
|
5132
5205
|
reconcileInteractiveState();
|
|
5133
5206
|
updateTaskDisplay();
|
|
5207
|
+
// Escalation/permission toggles are the common trigger for CSI cursor-jump
|
|
5208
|
+
// redraw sequences from Claude CLI. When they appear or dismiss, schedule a
|
|
5209
|
+
// debounced terminal resync so residual DOM rows get cleaned up automatically
|
|
5210
|
+
// — same fix the user used to have to reach for via the refresh button.
|
|
5211
|
+
var prevEsc = prevSession && prevSession.pendingEscalation ? 1 : 0;
|
|
5212
|
+
var nextEsc = updatedSession && updatedSession.pendingEscalation ? 1 : 0;
|
|
5213
|
+
var prevBlocked = prevSession && prevSession.permissionBlocked ? 1 : 0;
|
|
5214
|
+
var nextBlocked = updatedSession && updatedSession.permissionBlocked ? 1 : 0;
|
|
5215
|
+
if (prevEsc !== nextEsc || prevBlocked !== nextBlocked) {
|
|
5216
|
+
scheduleSoftResyncTerminal(200);
|
|
5217
|
+
}
|
|
5134
5218
|
}
|
|
5135
5219
|
// When a session transitions to a non-running state, try flushing cross-session queue
|
|
5136
5220
|
if (normalizedSnapshot.status && normalizedSnapshot.status !== "running" && state.crossSessionQueue.length > 0) {
|
|
@@ -5444,11 +5528,9 @@
|
|
|
5444
5528
|
|
|
5445
5529
|
if (state.terminal && id === state.selectedId && data.output !== undefined) {
|
|
5446
5530
|
syncTerminalBuffer(id, data.output, { mode: "append" });
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
});
|
|
5451
|
-
}
|
|
5531
|
+
// Session-switch / history replay: force a real fit so wterm
|
|
5532
|
+
// reflows the just-written output against the real container.
|
|
5533
|
+
ensureTerminalFit("session-switch");
|
|
5452
5534
|
}
|
|
5453
5535
|
|
|
5454
5536
|
var selectedSession = state.sessions.find(function(s) { return s.id === id; });
|
|
@@ -6274,6 +6356,9 @@
|
|
|
6274
6356
|
defaultModel: (document.getElementById("cfg-default-model") || {}).value || "",
|
|
6275
6357
|
};
|
|
6276
6358
|
|
|
6359
|
+
var previousDefaultModel = (state.config && state.config.defaultModel) || "";
|
|
6360
|
+
var nextDefaultModel = body.defaultModel || "";
|
|
6361
|
+
|
|
6277
6362
|
fetch("/api/settings/config", {
|
|
6278
6363
|
method: "POST",
|
|
6279
6364
|
headers: { "Content-Type": "application/json" },
|
|
@@ -6292,6 +6377,15 @@
|
|
|
6292
6377
|
}
|
|
6293
6378
|
msgEl.classList.remove("hidden");
|
|
6294
6379
|
}
|
|
6380
|
+
if (!data || !data.error) {
|
|
6381
|
+
if (state.config) state.config.defaultModel = nextDefaultModel;
|
|
6382
|
+
state.configDefaultModel = nextDefaultModel;
|
|
6383
|
+
if (nextDefaultModel !== previousDefaultModel) {
|
|
6384
|
+
state.chatModel = "";
|
|
6385
|
+
try { localStorage.removeItem("wand-chat-model"); } catch (e) {}
|
|
6386
|
+
syncComposerModelSelect(getSelectedSession());
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6295
6389
|
})
|
|
6296
6390
|
.catch(function() {
|
|
6297
6391
|
if (msgEl) {
|
|
@@ -7796,6 +7890,10 @@
|
|
|
7796
7890
|
}
|
|
7797
7891
|
applyCurrentView();
|
|
7798
7892
|
focusInputBox(true);
|
|
7893
|
+
// Container just flipped from hidden -> visible (or geometry changed
|
|
7894
|
+
// because chat/terminal panels swapped). Refit now so the terminal
|
|
7895
|
+
// picks up the real cols/rows instead of keeping the stale ones.
|
|
7896
|
+
if (!structured) ensureTerminalFit("view-switch");
|
|
7799
7897
|
}
|
|
7800
7898
|
|
|
7801
7899
|
|
|
@@ -10001,6 +10099,15 @@
|
|
|
10001
10099
|
state.visualViewportHandler = function() { scheduleTerminalResize(true); };
|
|
10002
10100
|
window.visualViewport.addEventListener("resize", state.visualViewportHandler);
|
|
10003
10101
|
}
|
|
10102
|
+
// Page returning from background: container dimensions may have
|
|
10103
|
+
// drifted (PWA standalone, tab switch, iOS address-bar toggle).
|
|
10104
|
+
state.visibilityHandler = function() {
|
|
10105
|
+
if (!document.hidden) ensureTerminalFit("visibility");
|
|
10106
|
+
};
|
|
10107
|
+
document.addEventListener("visibilitychange", state.visibilityHandler);
|
|
10108
|
+
// Mobile device rotation — large geometry change.
|
|
10109
|
+
state.orientationHandler = function() { ensureTerminalFit("orientation"); };
|
|
10110
|
+
window.addEventListener("orientationchange", state.orientationHandler);
|
|
10004
10111
|
requestAnimationFrame(function() { scheduleTerminalResize(true); });
|
|
10005
10112
|
}
|
|
10006
10113
|
|
|
@@ -10010,17 +10117,14 @@
|
|
|
10010
10117
|
if (!state.terminal || state.currentView !== "terminal" || document.hidden) return;
|
|
10011
10118
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
10012
10119
|
if (!selectedSession || selectedSession.sessionKind === "structured") return;
|
|
10013
|
-
// Lightweight
|
|
10014
|
-
|
|
10015
|
-
// Full re-sync every 30s during output pauses
|
|
10120
|
+
// Lightweight fit every 5s: gated + double-rAF + remeasure.
|
|
10121
|
+
ensureTerminalFit("health");
|
|
10122
|
+
// Full re-sync every 30s during output pauses — also refits.
|
|
10016
10123
|
var now = Date.now();
|
|
10017
10124
|
var chunkPause = state.lastChunkAt > 0 && (now - state.lastChunkAt > 300);
|
|
10018
10125
|
var resyncDue = (now - state.lastTerminalResyncAt) > 30000;
|
|
10019
10126
|
if (resyncDue && (chunkPause || selectedSession.status !== "running") && state.terminalOutput) {
|
|
10020
|
-
|
|
10021
|
-
state.terminal.reset();
|
|
10022
|
-
state.terminal.write(state.terminalOutput);
|
|
10023
|
-
maybeScrollTerminalToBottom("output");
|
|
10127
|
+
softResyncTerminal();
|
|
10024
10128
|
}
|
|
10025
10129
|
}, 5000);
|
|
10026
10130
|
}
|
|
@@ -10050,6 +10154,14 @@
|
|
|
10050
10154
|
window.visualViewport.removeEventListener("resize", state.visualViewportHandler);
|
|
10051
10155
|
state.visualViewportHandler = null;
|
|
10052
10156
|
}
|
|
10157
|
+
if (state.visibilityHandler) {
|
|
10158
|
+
document.removeEventListener("visibilitychange", state.visibilityHandler);
|
|
10159
|
+
state.visibilityHandler = null;
|
|
10160
|
+
}
|
|
10161
|
+
if (state.orientationHandler) {
|
|
10162
|
+
window.removeEventListener("orientationchange", state.orientationHandler);
|
|
10163
|
+
state.orientationHandler = null;
|
|
10164
|
+
}
|
|
10053
10165
|
[["mousemove", "resizeMouseMove"], ["mouseup", "resizeMouseUp"],
|
|
10054
10166
|
["touchmove", "resizeTouchMove"], ["touchend", "resizeTouchEnd"]
|
|
10055
10167
|
].forEach(function(pair) {
|
|
@@ -10113,10 +10225,30 @@
|
|
|
10113
10225
|
}
|
|
10114
10226
|
}
|
|
10115
10227
|
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10228
|
+
// Unified entry point for re-fitting the xterm grid to its container.
|
|
10229
|
+
// Why: wterm's `autoResize` ResizeObserver only fires on subsequent
|
|
10230
|
+
// container size changes. If the terminal is mounted or written to
|
|
10231
|
+
// while the container is hidden/zero-width, cols/rows stay wrong and
|
|
10232
|
+
// new output renders with broken wrapping (content visually piles at
|
|
10233
|
+
// the top). Call this after any layout change that might have altered
|
|
10234
|
+
// container geometry (mount, session switch, view switch, refresh).
|
|
10235
|
+
function ensureTerminalFit(reason) {
|
|
10236
|
+
if (!state.terminal) return false;
|
|
10237
|
+
var el = document.getElementById("output");
|
|
10238
|
+
if (!el || el.offsetWidth === 0 || el.offsetHeight === 0) return false;
|
|
10239
|
+
requestAnimationFrame(function() {
|
|
10240
|
+
requestAnimationFrame(function() {
|
|
10241
|
+
if (!state.terminal) return;
|
|
10242
|
+
if (typeof state.terminal.remeasure === "function") {
|
|
10243
|
+
state.terminal.remeasure();
|
|
10244
|
+
}
|
|
10245
|
+
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
10246
|
+
if (state.terminalAutoFollow || isTerminalNearBottom()) {
|
|
10247
|
+
maybeScrollTerminalToBottom("resize");
|
|
10248
|
+
}
|
|
10249
|
+
});
|
|
10250
|
+
});
|
|
10251
|
+
return true;
|
|
10120
10252
|
}
|
|
10121
10253
|
|
|
10122
10254
|
function scheduleTerminalResize(immediate) {
|
|
@@ -10181,10 +10313,9 @@
|
|
|
10181
10313
|
// Flush pending messages after reconnection
|
|
10182
10314
|
flushPendingMessages();
|
|
10183
10315
|
// Re-fit terminal on reconnect — the viewport may have changed
|
|
10184
|
-
// while disconnected,
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
}
|
|
10316
|
+
// while disconnected, so remeasure against real container size
|
|
10317
|
+
// rather than sending stale cols/rows from before the disconnect.
|
|
10318
|
+
ensureTerminalFit("ws-reconnect");
|
|
10188
10319
|
};
|
|
10189
10320
|
|
|
10190
10321
|
ws.onmessage = function(event) {
|
|
@@ -13113,6 +13244,65 @@
|
|
|
13113
13244
|
return deduped.join(newline);
|
|
13114
13245
|
}
|
|
13115
13246
|
|
|
13247
|
+
function parseMarkdownTables(source) {
|
|
13248
|
+
var NL = "\n";
|
|
13249
|
+
var lines = source.split(NL);
|
|
13250
|
+
var out = [];
|
|
13251
|
+
var i = 0;
|
|
13252
|
+
|
|
13253
|
+
function splitRow(line) {
|
|
13254
|
+
var s = line.trim();
|
|
13255
|
+
if (s.charAt(0) === "|") s = s.slice(1);
|
|
13256
|
+
if (s.charAt(s.length - 1) === "|") s = s.slice(0, -1);
|
|
13257
|
+
return s.split("|");
|
|
13258
|
+
}
|
|
13259
|
+
function styleAttr(a) { return a ? ' style="text-align:' + a + '"' : ""; }
|
|
13260
|
+
function buildTable(headers, aligns, rows) {
|
|
13261
|
+
var thead = "<thead><tr>" + headers.map(function(c, idx) {
|
|
13262
|
+
return "<th" + styleAttr(aligns[idx]) + ">" + c.trim() + "</th>";
|
|
13263
|
+
}).join("") + "</tr></thead>";
|
|
13264
|
+
var tbody = rows.length ? ("<tbody>" + rows.map(function(r) {
|
|
13265
|
+
return "<tr>" + r.map(function(c, idx) {
|
|
13266
|
+
return "<td" + styleAttr(aligns[idx]) + ">" + c.trim() + "</td>";
|
|
13267
|
+
}).join("") + "</tr>";
|
|
13268
|
+
}).join("") + "</tbody>") : "";
|
|
13269
|
+
return '<div class="md-table-wrap"><table class="md-table">' + thead + tbody + "</table></div>";
|
|
13270
|
+
}
|
|
13271
|
+
|
|
13272
|
+
while (i < lines.length) {
|
|
13273
|
+
var header = lines[i];
|
|
13274
|
+
if (header.indexOf("|") !== -1 && i + 1 < lines.length) {
|
|
13275
|
+
var sep = lines[i + 1].trim();
|
|
13276
|
+
if (/^\|?\s*:?-+:?(\s*\|\s*:?-+:?)+\s*\|?$/.test(sep)) {
|
|
13277
|
+
var headers = splitRow(header);
|
|
13278
|
+
var aligns = splitRow(sep).map(function(c) {
|
|
13279
|
+
var t = c.trim();
|
|
13280
|
+
var L = t.charAt(0) === ":";
|
|
13281
|
+
var R = t.charAt(t.length - 1) === ":";
|
|
13282
|
+
if (L && R) return "center";
|
|
13283
|
+
if (R) return "right";
|
|
13284
|
+
if (L) return "left";
|
|
13285
|
+
return "";
|
|
13286
|
+
});
|
|
13287
|
+
var rows = [];
|
|
13288
|
+
var j = i + 2;
|
|
13289
|
+
while (j < lines.length) {
|
|
13290
|
+
var trimmed = lines[j].trim();
|
|
13291
|
+
if (!trimmed || trimmed.indexOf("|") === -1) break;
|
|
13292
|
+
rows.push(splitRow(lines[j]));
|
|
13293
|
+
j += 1;
|
|
13294
|
+
}
|
|
13295
|
+
out.push("", buildTable(headers, aligns, rows), "");
|
|
13296
|
+
i = j;
|
|
13297
|
+
continue;
|
|
13298
|
+
}
|
|
13299
|
+
}
|
|
13300
|
+
out.push(header);
|
|
13301
|
+
i += 1;
|
|
13302
|
+
}
|
|
13303
|
+
return out.join(NL);
|
|
13304
|
+
}
|
|
13305
|
+
|
|
13116
13306
|
function renderMarkdown(text) {
|
|
13117
13307
|
if (!text) return "";
|
|
13118
13308
|
|
|
@@ -13237,6 +13427,7 @@
|
|
|
13237
13427
|
result = replaceLinePrefix(result, "- ", '<li>', '</li>');
|
|
13238
13428
|
result = replaceLinePrefix(result, "* ", '<li>', '</li>');
|
|
13239
13429
|
result = replaceOrderedList(result);
|
|
13430
|
+
result = parseMarkdownTables(result);
|
|
13240
13431
|
|
|
13241
13432
|
var lines = result.split(newline);
|
|
13242
13433
|
var grouped = [];
|
|
@@ -4463,6 +4463,33 @@
|
|
|
4463
4463
|
padding: 2px 5px;
|
|
4464
4464
|
border-radius: 4px;
|
|
4465
4465
|
}
|
|
4466
|
+
.markdown-content .md-table-wrap {
|
|
4467
|
+
margin: 12px 0;
|
|
4468
|
+
overflow-x: auto;
|
|
4469
|
+
border: 1px solid var(--border);
|
|
4470
|
+
border-radius: var(--radius-sm);
|
|
4471
|
+
-webkit-overflow-scrolling: touch;
|
|
4472
|
+
}
|
|
4473
|
+
.markdown-content .md-table {
|
|
4474
|
+
border-collapse: collapse;
|
|
4475
|
+
width: 100%;
|
|
4476
|
+
font-size: 0.8125rem;
|
|
4477
|
+
white-space: normal;
|
|
4478
|
+
}
|
|
4479
|
+
.markdown-content .md-table th,
|
|
4480
|
+
.markdown-content .md-table td {
|
|
4481
|
+
border: 1px solid var(--border);
|
|
4482
|
+
padding: 6px 10px;
|
|
4483
|
+
text-align: left;
|
|
4484
|
+
vertical-align: top;
|
|
4485
|
+
}
|
|
4486
|
+
.markdown-content .md-table th {
|
|
4487
|
+
background: rgba(150, 118, 85, 0.1);
|
|
4488
|
+
font-weight: 600;
|
|
4489
|
+
}
|
|
4490
|
+
.markdown-content .md-table tbody tr:nth-child(even) {
|
|
4491
|
+
background: rgba(150, 118, 85, 0.04);
|
|
4492
|
+
}
|
|
4466
4493
|
.markdown-content .code-block {
|
|
4467
4494
|
margin: 12px 0;
|
|
4468
4495
|
border-radius: var(--radius-md);
|