@co0ontty/wand 1.18.12 → 1.21.4
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/claude-pty-bridge.d.ts +8 -0
- package/dist/claude-pty-bridge.js +34 -11
- package/dist/cli.js +72 -5
- package/dist/ensure-node-pty-helper.d.ts +1 -0
- package/dist/ensure-node-pty-helper.js +51 -0
- package/dist/git-quick-commit.d.ts +18 -0
- package/dist/git-quick-commit.js +381 -0
- package/dist/models.d.ts +3 -1
- package/dist/models.js +45 -7
- package/dist/process-manager.d.ts +6 -8
- package/dist/process-manager.js +90 -176
- package/dist/prompt-optimizer.d.ts +5 -0
- package/dist/prompt-optimizer.js +72 -0
- package/dist/pty-text-utils.d.ts +25 -1
- package/dist/pty-text-utils.js +158 -2
- package/dist/server-session-routes.d.ts +2 -2
- package/dist/server-session-routes.js +94 -8
- package/dist/server.d.ts +22 -1
- package/dist/server.js +138 -16
- package/dist/session-logger.d.ts +15 -4
- package/dist/session-logger.js +52 -4
- package/dist/structured-session-manager.d.ts +12 -2
- package/dist/structured-session-manager.js +465 -22
- package/dist/tui/index.d.ts +24 -0
- package/dist/tui/index.js +138 -0
- package/dist/tui/layout.d.ts +25 -0
- package/dist/tui/layout.js +198 -0
- package/dist/tui/log-bus.d.ts +23 -0
- package/dist/tui/log-bus.js +111 -0
- package/dist/tui/relative-time.d.ts +4 -0
- package/dist/tui/relative-time.js +27 -0
- package/dist/tui/session-formatter.d.ts +17 -0
- package/dist/tui/session-formatter.js +111 -0
- package/dist/types.d.ts +55 -2
- package/dist/web-ui/content/scripts.js +1371 -261
- package/dist/web-ui/content/styles.css +436 -9
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -1
- package/dist/ws-broadcast.js +74 -12
- package/package.json +3 -1
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
try { return localStorage.getItem("wand-chat-model") || ""; } catch (e) { return ""; }
|
|
120
120
|
})(),
|
|
121
121
|
availableModels: [],
|
|
122
|
+
availableCodexModels: [],
|
|
122
123
|
modelsRefreshing: false,
|
|
123
124
|
sessionCreateKind: "structured",
|
|
124
125
|
sessionCreateWorktree: false,
|
|
@@ -149,6 +150,10 @@
|
|
|
149
150
|
try { var v = localStorage.getItem("wand-notif-bubble"); return v === null ? true : v === "true"; } catch (e) { return true; }
|
|
150
151
|
})(),
|
|
151
152
|
toolContentCache: {},
|
|
153
|
+
// Per-session WS output sequence tracker. Reset on connect/reconnect.
|
|
154
|
+
// Used to detect gaps caused by server-side backpressure drops and
|
|
155
|
+
// request a fresh snapshot.
|
|
156
|
+
lastSeqBySession: {},
|
|
152
157
|
currentView: "terminal",
|
|
153
158
|
terminalScale: (function() {
|
|
154
159
|
try {
|
|
@@ -168,6 +173,16 @@
|
|
|
168
173
|
}
|
|
169
174
|
})(),
|
|
170
175
|
topbarMoreOpen: false,
|
|
176
|
+
gitStatus: null,
|
|
177
|
+
gitStatusSessionId: null,
|
|
178
|
+
gitStatusLoading: false,
|
|
179
|
+
gitStatusInflight: null,
|
|
180
|
+
gitStatusLastFetchAt: 0,
|
|
181
|
+
quickCommitOpen: false,
|
|
182
|
+
quickCommitSubmitting: false,
|
|
183
|
+
quickCommitGenerating: false,
|
|
184
|
+
quickCommitError: "",
|
|
185
|
+
quickCommitForm: { autoMessage: false, customMessage: "", makeTag: false, tag: "", push: false },
|
|
171
186
|
chatAutoFollow: (function() {
|
|
172
187
|
try {
|
|
173
188
|
var saved = localStorage.getItem(CHAT_AUTO_FOLLOW_STORAGE_KEY);
|
|
@@ -225,10 +240,90 @@
|
|
|
225
240
|
// ── Structured session status bar (in-flight timer) ──
|
|
226
241
|
var _statusBarTimerId = null;
|
|
227
242
|
var _statusBarStartTime = 0;
|
|
243
|
+
var _runningIndicatorsTimerId = null;
|
|
244
|
+
var _runningIndicatorsStartTime = 0;
|
|
245
|
+
|
|
246
|
+
// 计算会话整体的"在跑"信号,统一驱动顶部进度条/徽章计时/气泡呼吸条。
|
|
247
|
+
function computeRunningSignal(session) {
|
|
248
|
+
if (!session) return { active: false };
|
|
249
|
+
if (session.archived) return { active: false };
|
|
250
|
+
var permBlocked = !!session.permissionBlocked;
|
|
251
|
+
var inFlight = !!(isStructuredSession(session)
|
|
252
|
+
&& session.structuredState && session.structuredState.inFlight);
|
|
253
|
+
var ptyRunning = !isStructuredSession(session) && session.status === "running";
|
|
254
|
+
return {
|
|
255
|
+
active: inFlight || ptyRunning || permBlocked,
|
|
256
|
+
inFlight: inFlight,
|
|
257
|
+
ptyRunning: ptyRunning,
|
|
258
|
+
permissionBlocked: permBlocked,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatElapsedShort(ms) {
|
|
263
|
+
var s = Math.max(0, Math.floor(ms / 1000));
|
|
264
|
+
if (s < 60) return s + "s";
|
|
265
|
+
var m = Math.floor(s / 60);
|
|
266
|
+
var rs = s % 60;
|
|
267
|
+
if (m < 60) return m + "m" + (rs ? " " + rs + "s" : "");
|
|
268
|
+
var h = Math.floor(m / 60);
|
|
269
|
+
var rm = m % 60;
|
|
270
|
+
return h + "h" + (rm ? " " + rm + "m" : "");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 集中刷新:顶部进度条 + 顶部徽章计时 + 助手气泡左侧呼吸条。
|
|
274
|
+
function updateRunningIndicators(session) {
|
|
275
|
+
var sig = computeRunningSignal(session);
|
|
276
|
+
var headerRow = document.querySelector(".main-header-row");
|
|
277
|
+
var pill = headerRow ? headerRow.querySelector(".session-status-pill") : null;
|
|
278
|
+
var chatMessages = document.querySelector(".chat-messages");
|
|
279
|
+
|
|
280
|
+
// A. 顶部进度条
|
|
281
|
+
if (headerRow) {
|
|
282
|
+
headerRow.classList.toggle("is-running", sig.active);
|
|
283
|
+
headerRow.classList.toggle("is-permission-blocked", sig.permissionBlocked);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// B. 顶部徽章计时(仅 inFlight 显示,PTY running 不强制显示)
|
|
287
|
+
if (pill) {
|
|
288
|
+
var elapsedEl = pill.querySelector(".session-status-elapsed");
|
|
289
|
+
if (sig.inFlight) {
|
|
290
|
+
if (!_runningIndicatorsStartTime) {
|
|
291
|
+
// 优先复用 renderStructuredStatusBar 已记录的真实起点
|
|
292
|
+
_runningIndicatorsStartTime = _statusBarStartTime > 0 ? _statusBarStartTime : Date.now();
|
|
293
|
+
}
|
|
294
|
+
var label = formatElapsedShort(Date.now() - _runningIndicatorsStartTime);
|
|
295
|
+
if (!elapsedEl) {
|
|
296
|
+
elapsedEl = document.createElement("span");
|
|
297
|
+
elapsedEl.className = "session-status-elapsed";
|
|
298
|
+
pill.appendChild(elapsedEl);
|
|
299
|
+
}
|
|
300
|
+
elapsedEl.textContent = label;
|
|
301
|
+
} else {
|
|
302
|
+
_runningIndicatorsStartTime = 0;
|
|
303
|
+
if (elapsedEl) elapsedEl.remove();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 维持每秒一次的刷新心跳,让 elapsed 数字持续滚动
|
|
308
|
+
if (sig.active) {
|
|
309
|
+
if (!_runningIndicatorsTimerId) {
|
|
310
|
+
_runningIndicatorsTimerId = setInterval(function() {
|
|
311
|
+
var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
312
|
+
updateRunningIndicators(sel);
|
|
313
|
+
}, 1000);
|
|
314
|
+
}
|
|
315
|
+
} else if (_runningIndicatorsTimerId) {
|
|
316
|
+
clearInterval(_runningIndicatorsTimerId);
|
|
317
|
+
_runningIndicatorsTimerId = null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
228
320
|
|
|
229
321
|
function renderStructuredStatusBar(chatMessages, session) {
|
|
230
|
-
//
|
|
231
|
-
|
|
322
|
+
// 先驱动跨视图的运行指示器(顶部进度条/徽章计时/气泡呼吸条)
|
|
323
|
+
updateRunningIndicators(session);
|
|
324
|
+
|
|
325
|
+
// Status bar now lives in .composer-top-row alongside the todo-progress collapse bar
|
|
326
|
+
var topRow = document.querySelector(".composer-top-row");
|
|
232
327
|
var existing = document.querySelector(".structured-status-bar");
|
|
233
328
|
var composer = document.querySelector(".input-composer");
|
|
234
329
|
if (!session || !isStructuredSession(session)) {
|
|
@@ -250,15 +345,15 @@
|
|
|
250
345
|
// Add glow to input composer
|
|
251
346
|
if (composer) composer.classList.add("in-flight");
|
|
252
347
|
|
|
253
|
-
if (!existing &&
|
|
348
|
+
if (!existing && topRow) {
|
|
254
349
|
var bar = document.createElement("div");
|
|
255
350
|
bar.className = "structured-status-bar";
|
|
256
351
|
bar.innerHTML =
|
|
257
352
|
'<span class="status-bar-dot"></span>' +
|
|
258
353
|
'<span class="status-bar-label">回复中</span>' +
|
|
259
354
|
'<span class="status-bar-timer">0.0s</span>';
|
|
260
|
-
//
|
|
261
|
-
|
|
355
|
+
// Append as last child of the top row so it sits to the right of the todo bar
|
|
356
|
+
topRow.appendChild(bar);
|
|
262
357
|
existing = bar;
|
|
263
358
|
} else if (existing && existing.classList.contains("completed")) {
|
|
264
359
|
// Was completed, now in-flight again — reset
|
|
@@ -1023,6 +1118,17 @@
|
|
|
1023
1118
|
syncSessionModalUI();
|
|
1024
1119
|
}
|
|
1025
1120
|
}
|
|
1121
|
+
|
|
1122
|
+
// 初始加载或会话切换后惰性触发 git 状态拉取(loadGitStatus 自带节流)。
|
|
1123
|
+
if (isLoggedIn && state.selectedId && state.gitStatusSessionId !== state.selectedId) {
|
|
1124
|
+
loadGitStatus(state.selectedId);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// DOM 整体重渲后,重新挂上"运行中"指示器(顶部进度条/徽章计时/气泡呼吸条)
|
|
1128
|
+
if (isLoggedIn) {
|
|
1129
|
+
var __sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
1130
|
+
updateRunningIndicators(__sel);
|
|
1131
|
+
}
|
|
1026
1132
|
}
|
|
1027
1133
|
|
|
1028
1134
|
function renderShortcutKeys() {
|
|
@@ -1225,6 +1331,7 @@
|
|
|
1225
1331
|
'</div>' +
|
|
1226
1332
|
'<div class="topbar-right">' +
|
|
1227
1333
|
(selectedSession && selectedSession.cwd ? '<button id="topbar-file-button" class="topbar-btn square' + (state.filePanelOpen ? ' active' : '') + '" type="button" aria-label="文件" title="文件"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></button>' : '') +
|
|
1334
|
+
'<span id="topbar-git-slot" class="topbar-git-slot">' + renderTopbarGitBadgeHtml() + '</span>' +
|
|
1228
1335
|
'<div class="topbar-more-wrap">' +
|
|
1229
1336
|
'<button id="topbar-more-button" class="topbar-btn square' + (state.topbarMoreOpen ? ' active' : '') + '" type="button" aria-label="更多" aria-haspopup="menu" aria-expanded="' + (state.topbarMoreOpen ? 'true' : 'false') + '" title="更多"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>' +
|
|
1230
1337
|
'<div id="topbar-more-menu" class="topbar-more-menu' + (state.topbarMoreOpen ? '' : ' hidden') + '" role="menu">' +
|
|
@@ -1299,20 +1406,31 @@
|
|
|
1299
1406
|
'</div>' +
|
|
1300
1407
|
'</div>' +
|
|
1301
1408
|
'<div class="input-panel' + (state.selectedId ? "" : " hidden") + '">' +
|
|
1302
|
-
'<div
|
|
1303
|
-
'<div
|
|
1304
|
-
'<div class="todo-progress-
|
|
1305
|
-
'<
|
|
1306
|
-
|
|
1307
|
-
|
|
1409
|
+
'<div class="composer-top-row">' +
|
|
1410
|
+
'<div id="todo-progress" class="todo-progress hidden">' +
|
|
1411
|
+
'<div class="todo-progress-header" id="todo-progress-toggle">' +
|
|
1412
|
+
'<div class="todo-progress-left">' +
|
|
1413
|
+
'<span class="todo-progress-spinner"></span>' +
|
|
1414
|
+
'<span class="todo-progress-counter" id="todo-progress-counter">0/0</span>' +
|
|
1415
|
+
'<span class="todo-progress-task" id="todo-progress-task"></span>' +
|
|
1416
|
+
'</div>' +
|
|
1417
|
+
'<svg class="todo-progress-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>' +
|
|
1418
|
+
'</div>' +
|
|
1419
|
+
'<div class="todo-progress-body hidden" id="todo-progress-body">' +
|
|
1420
|
+
'<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
|
|
1308
1421
|
'</div>' +
|
|
1309
|
-
'<svg class="todo-progress-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>' +
|
|
1310
|
-
'</div>' +
|
|
1311
|
-
'<div class="todo-progress-body hidden" id="todo-progress-body">' +
|
|
1312
|
-
'<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
|
|
1313
1422
|
'</div>' +
|
|
1314
1423
|
'</div>' +
|
|
1315
1424
|
'<div class="input-composer">' +
|
|
1425
|
+
'<button id="prompt-optimize-btn" class="prompt-optimize-btn" type="button" title="提示词优化(AI)" aria-label="提示词优化">' +
|
|
1426
|
+
'<svg class="prompt-optimize-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
1427
|
+
'<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6z" fill="currentColor" opacity="0.25"/>' +
|
|
1428
|
+
'<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6z"/>' +
|
|
1429
|
+
'<path d="M19 14l.7 1.9L21.6 17l-1.9.7L19 19.6l-.7-1.9L16.4 17l1.9-.7z" fill="currentColor" opacity="0.35"/>' +
|
|
1430
|
+
'<path d="M5 4l.5 1.4L7 6l-1.5.6L5 8l-.5-1.4L3 6l1.5-.6z" fill="currentColor" opacity="0.35"/>' +
|
|
1431
|
+
'</svg>' +
|
|
1432
|
+
'<span class="prompt-optimize-spinner" aria-hidden="true"></span>' +
|
|
1433
|
+
'</button>' +
|
|
1316
1434
|
'<textarea id="input-box" class="input-textarea" placeholder="' + getComposerPlaceholder(selectedSession, state.terminalInteractive) + '" rows="1">' + escapeHtml(currentDraft) + '</textarea>' +
|
|
1317
1435
|
'<div id="attachment-preview" class="attachment-preview hidden"></div>' +
|
|
1318
1436
|
'<div class="input-composer-bar">' +
|
|
@@ -1325,7 +1443,7 @@
|
|
|
1325
1443
|
renderModeOptions(preferredTool, composerMode) +
|
|
1326
1444
|
'</select>' +
|
|
1327
1445
|
'<select id="chat-model-select" class="chat-mode-select chat-model-select" title="切换模型(对运行中会话发送 /model,对新会话作为 --model 启动)">' +
|
|
1328
|
-
renderChatModelOptions(getEffectiveModel(selectedSession)) +
|
|
1446
|
+
renderChatModelOptions(getEffectiveModel(selectedSession), selectedSession) +
|
|
1329
1447
|
'</select>' +
|
|
1330
1448
|
'<button id="terminal-interactive-toggle-top" class="composer-interactive-toggle' + (state.terminalInteractive ? " active" : "") + '" type="button" title="切换终端交互模式">⌨</button>' +
|
|
1331
1449
|
'<span class="permission-actions hidden" id="permission-actions">' +
|
|
@@ -1389,7 +1507,322 @@
|
|
|
1389
1507
|
'</section>' +
|
|
1390
1508
|
'</main>' +
|
|
1391
1509
|
'</div>' +
|
|
1392
|
-
'</div>' + renderSessionModal() + renderWorktreeMergeModal() + renderSettingsModal();
|
|
1510
|
+
'</div>' + renderSessionModal() + renderWorktreeMergeModal() + renderSettingsModal() + renderQuickCommitModal();
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function renderTopbarGitBadgeHtml() {
|
|
1514
|
+
if (!state.selectedId || !state.gitStatus || !state.gitStatus.isGit) return "";
|
|
1515
|
+
if (state.gitStatusSessionId !== state.selectedId) return "";
|
|
1516
|
+
var branch = state.gitStatus.branch || "?";
|
|
1517
|
+
var count = state.gitStatus.modifiedCount || 0;
|
|
1518
|
+
var titleText = branch + (count ? " · " + count + " 个文件待提交" : " · 工作区干净");
|
|
1519
|
+
return '<button id="topbar-git-badge" class="topbar-git-badge" type="button" title="' + escapeHtml(titleText) + '" aria-label="快捷提交">'
|
|
1520
|
+
+ '<svg class="topbar-git-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6" cy="6" r="2"/><circle cx="6" cy="18" r="2"/><circle cx="18" cy="9" r="2"/><path d="M6 8v8"/><path d="M18 11v1a3 3 0 0 1-3 3H9"/></svg>'
|
|
1521
|
+
+ '<span class="topbar-git-branch">' + escapeHtml(branch) + '</span>'
|
|
1522
|
+
+ (count > 0
|
|
1523
|
+
? '<span class="topbar-git-count">·' + count + '</span>'
|
|
1524
|
+
: '<span class="topbar-git-clean" aria-hidden="true">✓</span>')
|
|
1525
|
+
+ '</button>';
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function updateTopbarGitBadge() {
|
|
1529
|
+
var slot = document.getElementById("topbar-git-slot");
|
|
1530
|
+
if (!slot) return;
|
|
1531
|
+
slot.innerHTML = renderTopbarGitBadgeHtml();
|
|
1532
|
+
var btn = document.getElementById("topbar-git-badge");
|
|
1533
|
+
if (btn) {
|
|
1534
|
+
btn.addEventListener("click", function(e) {
|
|
1535
|
+
e.preventDefault();
|
|
1536
|
+
openQuickCommitModal();
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function loadGitStatus(sessionId, options) {
|
|
1542
|
+
if (!sessionId) return Promise.resolve(null);
|
|
1543
|
+
var force = options && options.force;
|
|
1544
|
+
// Same session, fetched within 1s, and no force → skip.
|
|
1545
|
+
var now = Date.now();
|
|
1546
|
+
if (!force && state.gitStatusSessionId === sessionId && state.gitStatus && (now - state.gitStatusLastFetchAt) < 1000) {
|
|
1547
|
+
return Promise.resolve(state.gitStatus);
|
|
1548
|
+
}
|
|
1549
|
+
if (state.gitStatusInflight && state.gitStatusInflight.sessionId === sessionId) {
|
|
1550
|
+
return state.gitStatusInflight.promise;
|
|
1551
|
+
}
|
|
1552
|
+
state.gitStatusLoading = true;
|
|
1553
|
+
var promise = fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/git-status", {
|
|
1554
|
+
credentials: "same-origin"
|
|
1555
|
+
})
|
|
1556
|
+
.then(function(res) { return res.ok ? res.json() : { isGit: false }; })
|
|
1557
|
+
.then(function(data) {
|
|
1558
|
+
state.gitStatus = data || { isGit: false };
|
|
1559
|
+
state.gitStatusSessionId = sessionId;
|
|
1560
|
+
state.gitStatusLastFetchAt = Date.now();
|
|
1561
|
+
updateTopbarGitBadge();
|
|
1562
|
+
return data;
|
|
1563
|
+
})
|
|
1564
|
+
.catch(function() {
|
|
1565
|
+
state.gitStatus = { isGit: false };
|
|
1566
|
+
state.gitStatusSessionId = sessionId;
|
|
1567
|
+
state.gitStatusLastFetchAt = Date.now();
|
|
1568
|
+
updateTopbarGitBadge();
|
|
1569
|
+
return null;
|
|
1570
|
+
})
|
|
1571
|
+
.finally(function() {
|
|
1572
|
+
state.gitStatusLoading = false;
|
|
1573
|
+
if (state.gitStatusInflight && state.gitStatusInflight.sessionId === sessionId) {
|
|
1574
|
+
state.gitStatusInflight = null;
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
state.gitStatusInflight = { sessionId: sessionId, promise: promise };
|
|
1578
|
+
return promise;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
var quickCommitEscHandler = null;
|
|
1582
|
+
|
|
1583
|
+
function openQuickCommitModal() {
|
|
1584
|
+
if (!state.selectedId) return;
|
|
1585
|
+
state.quickCommitOpen = true;
|
|
1586
|
+
state.quickCommitSubmitting = false;
|
|
1587
|
+
state.quickCommitError = "";
|
|
1588
|
+
state.quickCommitForm = { autoMessage: false, customMessage: "", makeTag: false, tag: "", push: false };
|
|
1589
|
+
closeWorktreeMergeModal();
|
|
1590
|
+
closeSessionModal();
|
|
1591
|
+
closeSettingsModal();
|
|
1592
|
+
rerenderQuickCommitModal();
|
|
1593
|
+
var modal = document.getElementById("quick-commit-modal");
|
|
1594
|
+
if (modal) {
|
|
1595
|
+
modal.classList.remove("hidden");
|
|
1596
|
+
lastFocusedElement = document.activeElement;
|
|
1597
|
+
setupFocusTrap(modal);
|
|
1598
|
+
}
|
|
1599
|
+
if (quickCommitEscHandler) document.removeEventListener("keydown", quickCommitEscHandler);
|
|
1600
|
+
quickCommitEscHandler = function(e) {
|
|
1601
|
+
if (e.key === "Escape" && state.quickCommitOpen && !state.quickCommitSubmitting) {
|
|
1602
|
+
closeQuickCommitModal();
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
document.addEventListener("keydown", quickCommitEscHandler);
|
|
1606
|
+
loadGitStatus(state.selectedId, { force: true }).then(function() {
|
|
1607
|
+
if (!state.quickCommitOpen) return;
|
|
1608
|
+
rerenderQuickCommitModal();
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function closeQuickCommitModal() {
|
|
1613
|
+
state.quickCommitOpen = false;
|
|
1614
|
+
state.quickCommitSubmitting = false;
|
|
1615
|
+
state.quickCommitError = "";
|
|
1616
|
+
var modal = document.getElementById("quick-commit-modal");
|
|
1617
|
+
if (modal) modal.classList.add("hidden");
|
|
1618
|
+
if (focusTrapHandler) {
|
|
1619
|
+
document.removeEventListener("keydown", focusTrapHandler);
|
|
1620
|
+
focusTrapHandler = null;
|
|
1621
|
+
}
|
|
1622
|
+
if (quickCommitEscHandler) {
|
|
1623
|
+
document.removeEventListener("keydown", quickCommitEscHandler);
|
|
1624
|
+
quickCommitEscHandler = null;
|
|
1625
|
+
}
|
|
1626
|
+
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
|
|
1627
|
+
lastFocusedElement.focus();
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function rerenderQuickCommitModal() {
|
|
1632
|
+
var modal = document.getElementById("quick-commit-modal");
|
|
1633
|
+
if (!modal) return;
|
|
1634
|
+
var html = renderQuickCommitModal();
|
|
1635
|
+
var temp = document.createElement("div");
|
|
1636
|
+
temp.innerHTML = html;
|
|
1637
|
+
var fresh = temp.querySelector("#quick-commit-modal");
|
|
1638
|
+
if (!fresh) return;
|
|
1639
|
+
modal.innerHTML = fresh.innerHTML;
|
|
1640
|
+
attachQuickCommitModalListeners();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function attachQuickCommitModalListeners() {
|
|
1644
|
+
var closeBtn = document.getElementById("quick-commit-close-btn");
|
|
1645
|
+
if (closeBtn) closeBtn.addEventListener("click", closeQuickCommitModal);
|
|
1646
|
+
var cancelBtn = document.getElementById("quick-commit-cancel-btn");
|
|
1647
|
+
if (cancelBtn) cancelBtn.addEventListener("click", closeQuickCommitModal);
|
|
1648
|
+
var submitBtn = document.getElementById("quick-commit-submit-btn");
|
|
1649
|
+
if (submitBtn) submitBtn.addEventListener("click", submitQuickCommit);
|
|
1650
|
+
var aiBtn = document.getElementById("quick-commit-ai-btn");
|
|
1651
|
+
if (aiBtn) aiBtn.addEventListener("click", generateCommitMessageAI);
|
|
1652
|
+
var msgEl = document.getElementById("quick-commit-message");
|
|
1653
|
+
if (msgEl) msgEl.addEventListener("input", function() {
|
|
1654
|
+
state.quickCommitForm.customMessage = msgEl.value;
|
|
1655
|
+
});
|
|
1656
|
+
var tagCb = document.getElementById("quick-commit-make-tag");
|
|
1657
|
+
if (tagCb) tagCb.addEventListener("change", function() {
|
|
1658
|
+
state.quickCommitForm.makeTag = tagCb.checked;
|
|
1659
|
+
var row = document.getElementById("quick-commit-tag-row");
|
|
1660
|
+
if (row) row.classList.toggle("hidden", !tagCb.checked);
|
|
1661
|
+
});
|
|
1662
|
+
var tagInput = document.getElementById("quick-commit-tag");
|
|
1663
|
+
if (tagInput) tagInput.addEventListener("input", function() {
|
|
1664
|
+
state.quickCommitForm.tag = tagInput.value;
|
|
1665
|
+
});
|
|
1666
|
+
var pushCb = document.getElementById("quick-commit-push");
|
|
1667
|
+
if (pushCb) pushCb.addEventListener("change", function() {
|
|
1668
|
+
state.quickCommitForm.push = pushCb.checked;
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function generateCommitMessageAI() {
|
|
1673
|
+
if (!state.selectedId || state.quickCommitGenerating) return;
|
|
1674
|
+
var msgEl = document.getElementById("quick-commit-message");
|
|
1675
|
+
if (msgEl) state.quickCommitForm.customMessage = msgEl.value;
|
|
1676
|
+
state.quickCommitGenerating = true;
|
|
1677
|
+
state.quickCommitError = "";
|
|
1678
|
+
rerenderQuickCommitModal();
|
|
1679
|
+
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/generate-commit-message", {
|
|
1680
|
+
method: "POST",
|
|
1681
|
+
credentials: "same-origin",
|
|
1682
|
+
headers: { "Content-Type": "application/json" },
|
|
1683
|
+
body: JSON.stringify({})
|
|
1684
|
+
})
|
|
1685
|
+
.then(function(res) {
|
|
1686
|
+
return res.json().then(function(data) { return { ok: res.ok, data: data }; });
|
|
1687
|
+
})
|
|
1688
|
+
.then(function(result) {
|
|
1689
|
+
if (!result.ok) throw new Error((result.data && result.data.error) || "AI 生成失败。");
|
|
1690
|
+
state.quickCommitForm.customMessage = (result.data && result.data.message) || "";
|
|
1691
|
+
var currentMsgEl = document.getElementById("quick-commit-message");
|
|
1692
|
+
if (currentMsgEl) currentMsgEl.value = state.quickCommitForm.customMessage;
|
|
1693
|
+
})
|
|
1694
|
+
.catch(function(error) {
|
|
1695
|
+
state.quickCommitError = (error && error.message) || "AI 生成失败。";
|
|
1696
|
+
})
|
|
1697
|
+
.finally(function() {
|
|
1698
|
+
state.quickCommitGenerating = false;
|
|
1699
|
+
if (state.quickCommitOpen) rerenderQuickCommitModal();
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function submitQuickCommit() {
|
|
1704
|
+
if (!state.selectedId || state.quickCommitSubmitting) return;
|
|
1705
|
+
var msgEl = document.getElementById("quick-commit-message");
|
|
1706
|
+
if (msgEl) state.quickCommitForm.customMessage = msgEl.value;
|
|
1707
|
+
var form = state.quickCommitForm || {};
|
|
1708
|
+
var userTag = form.makeTag ? (form.tag || "").trim() : "";
|
|
1709
|
+
var message = (form.customMessage || "").trim();
|
|
1710
|
+
var payload = {
|
|
1711
|
+
autoMessage: false,
|
|
1712
|
+
customMessage: message,
|
|
1713
|
+
tag: userTag,
|
|
1714
|
+
autoTag: form.makeTag && !userTag,
|
|
1715
|
+
push: !!form.push
|
|
1716
|
+
};
|
|
1717
|
+
if (!message) {
|
|
1718
|
+
state.quickCommitError = "请填写 commit message,或点击 AI 生成。";
|
|
1719
|
+
rerenderQuickCommitModal();
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
state.quickCommitSubmitting = true;
|
|
1723
|
+
state.quickCommitError = "";
|
|
1724
|
+
rerenderQuickCommitModal();
|
|
1725
|
+
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/quick-commit", {
|
|
1726
|
+
method: "POST",
|
|
1727
|
+
credentials: "same-origin",
|
|
1728
|
+
headers: { "Content-Type": "application/json" },
|
|
1729
|
+
body: JSON.stringify(payload)
|
|
1730
|
+
})
|
|
1731
|
+
.then(function(res) {
|
|
1732
|
+
return res.json().then(function(data) { return { ok: res.ok, data: data }; });
|
|
1733
|
+
})
|
|
1734
|
+
.then(function(result) {
|
|
1735
|
+
if (!result.ok) throw new Error((result.data && result.data.error) || "快捷提交失败。");
|
|
1736
|
+
var data = result.data || {};
|
|
1737
|
+
var hash = data.commit && data.commit.hash ? data.commit.hash.substring(0, 7) : "";
|
|
1738
|
+
var tagName = data.tag && data.tag.name ? data.tag.name : "";
|
|
1739
|
+
var base = "已提交" + (hash ? " " + hash : "") + (tagName ? ",已打 tag " + tagName : "");
|
|
1740
|
+
var pushRequested = !!payload.push;
|
|
1741
|
+
if (pushRequested && data.pushError) {
|
|
1742
|
+
var msg = base + ";push 失败:" + data.pushError;
|
|
1743
|
+
if (typeof showToast === "function") showToast(msg, "error");
|
|
1744
|
+
} else {
|
|
1745
|
+
var okMsg = base + (data.pushed ? ",已 push" : "");
|
|
1746
|
+
if (typeof showToast === "function") showToast(okMsg, "success");
|
|
1747
|
+
}
|
|
1748
|
+
closeQuickCommitModal();
|
|
1749
|
+
if (state.selectedId) loadGitStatus(state.selectedId, { force: true });
|
|
1750
|
+
})
|
|
1751
|
+
.catch(function(error) {
|
|
1752
|
+
state.quickCommitError = (error && error.message) || "快捷提交失败。";
|
|
1753
|
+
})
|
|
1754
|
+
.finally(function() {
|
|
1755
|
+
state.quickCommitSubmitting = false;
|
|
1756
|
+
if (state.quickCommitOpen) rerenderQuickCommitModal();
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function renderQuickCommitModal() {
|
|
1761
|
+
var s = state.gitStatus || {};
|
|
1762
|
+
var f = state.quickCommitForm || { autoMessage: false, customMessage: "", makeTag: false, tag: "", push: false };
|
|
1763
|
+
var langValue = (state.config && (state.config.language || "")) || "";
|
|
1764
|
+
var langLabel = langValue ? langValue : "中文";
|
|
1765
|
+
var files = Array.isArray(s.files) ? s.files : [];
|
|
1766
|
+
var fileRows = files.map(function(item) {
|
|
1767
|
+
var status = (item.status || " ").substring(0, 2);
|
|
1768
|
+
var flag = status.trim() || "?";
|
|
1769
|
+
var cls = "qc-flag";
|
|
1770
|
+
if (flag === "A" || status[0] === "A") cls += " qc-flag-add";
|
|
1771
|
+
else if (flag === "D" || status[0] === "D") cls += " qc-flag-del";
|
|
1772
|
+
else if (flag === "M" || status[0] === "M") cls += " qc-flag-mod";
|
|
1773
|
+
else if (flag === "??" || status === "??") cls += " qc-flag-untracked";
|
|
1774
|
+
else if (flag === "R") cls += " qc-flag-ren";
|
|
1775
|
+
var subBadge = "";
|
|
1776
|
+
if (item.isSubmodule) {
|
|
1777
|
+
var st = item.submoduleState || {};
|
|
1778
|
+
var parts = [];
|
|
1779
|
+
if (st.commitChanged) parts.push("新指针");
|
|
1780
|
+
if (st.hasTrackedChanges) parts.push("dirty");
|
|
1781
|
+
if (st.hasUntracked) parts.push("未跟踪");
|
|
1782
|
+
var label = parts.length ? "submodule · " + parts.join(" / ") : "submodule";
|
|
1783
|
+
subBadge = '<span class="qc-submodule-badge">' + escapeHtml(label) + '</span>';
|
|
1784
|
+
}
|
|
1785
|
+
return '<div class="qc-file-row"><span class="' + cls + '">' + escapeHtml(status) + '</span><span class="qc-file-path">' + escapeHtml(item.path || "") + '</span>' + subBadge + '</div>';
|
|
1786
|
+
}).join("");
|
|
1787
|
+
if (!fileRows) fileRows = '<div class="qc-empty">工作区干净,没有可提交的改动。</div>';
|
|
1788
|
+
var hasChanges = (s.modifiedCount || 0) > 0;
|
|
1789
|
+
|
|
1790
|
+
return '<section id="quick-commit-modal" class="modal-backdrop' + (state.quickCommitOpen ? '' : ' hidden') + '">' +
|
|
1791
|
+
'<div class="modal quick-commit-modal" role="dialog" aria-labelledby="quick-commit-title">' +
|
|
1792
|
+
'<div class="modal-header">' +
|
|
1793
|
+
'<div>' +
|
|
1794
|
+
'<h2 id="quick-commit-title" class="modal-title">快捷提交</h2>' +
|
|
1795
|
+
'<p class="modal-subtitle">' + escapeHtml((s.branch || "(no branch)") + ' · ' + (s.modifiedCount || 0) + ' 个改动') + '</p>' +
|
|
1796
|
+
'</div>' +
|
|
1797
|
+
'<button id="quick-commit-close-btn" class="btn btn-ghost btn-icon" type="button" aria-label="关闭">×</button>' +
|
|
1798
|
+
'</div>' +
|
|
1799
|
+
'<div class="modal-body">' +
|
|
1800
|
+
'<div class="qc-files-wrap">' + fileRows + '</div>' +
|
|
1801
|
+
'<div class="qc-message-row" id="quick-commit-message-row">' +
|
|
1802
|
+
'<div class="qc-message-header"><label class="field-label" for="quick-commit-message">commit message</label>' +
|
|
1803
|
+
'<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm"' + (state.quickCommitGenerating ? ' disabled' : '') + '>' + (state.quickCommitGenerating ? '生成中…' : 'AI 生成') + '</button>' +
|
|
1804
|
+
'</div>' +
|
|
1805
|
+
'<textarea id="quick-commit-message" class="field-input" rows="2" placeholder="输入 commit message 或点击 AI 生成">' + escapeHtml(f.customMessage || "") + '</textarea>' +
|
|
1806
|
+
'</div>' +
|
|
1807
|
+
'<label class="qc-checkbox-row">' +
|
|
1808
|
+
'<input type="checkbox" id="quick-commit-make-tag"' + (f.makeTag ? ' checked' : '') + '>' +
|
|
1809
|
+
'<span>提交后打 tag' + (s.latestTag ? '(当前:' + escapeHtml(s.latestTag) + ')' : '') + '</span>' +
|
|
1810
|
+
'</label>' +
|
|
1811
|
+
'<div class="qc-tag-row' + (f.makeTag ? '' : ' hidden') + '" id="quick-commit-tag-row">' +
|
|
1812
|
+
'<input type="text" id="quick-commit-tag" class="field-input" placeholder="留空自动 bump patch' + (s.suggestedNextTag ? '(如 ' + escapeHtml(s.suggestedNextTag) + ')' : '') + '" value="' + escapeHtml(f.tag || "") + '">' +
|
|
1813
|
+
'</div>' +
|
|
1814
|
+
'<label class="qc-checkbox-row">' +
|
|
1815
|
+
'<input type="checkbox" id="quick-commit-push"' + (f.push ? ' checked' : '') + '>' +
|
|
1816
|
+
'<span>提交后 push 到远端</span>' +
|
|
1817
|
+
'</label>' +
|
|
1818
|
+
'<p id="quick-commit-error" class="error-message' + (state.quickCommitError ? '' : ' hidden') + '">' + escapeHtml(state.quickCommitError || "") + '</p>' +
|
|
1819
|
+
'<div class="worktree-merge-actions">' +
|
|
1820
|
+
'<button id="quick-commit-cancel-btn" class="btn btn-secondary" type="button">取消</button>' +
|
|
1821
|
+
'<button id="quick-commit-submit-btn" class="btn btn-primary" type="button"' + (hasChanges && !state.quickCommitSubmitting ? '' : ' disabled') + '>' + (state.quickCommitSubmitting ? '提交中…' : '执行') + '</button>' +
|
|
1822
|
+
'</div>' +
|
|
1823
|
+
'</div>' +
|
|
1824
|
+
'</div>' +
|
|
1825
|
+
'</section>';
|
|
1393
1826
|
}
|
|
1394
1827
|
|
|
1395
1828
|
function renderWorktreeMergeModal() {
|
|
@@ -2332,10 +2765,16 @@
|
|
|
2332
2765
|
|
|
2333
2766
|
function applyTerminalScale() {
|
|
2334
2767
|
if (!state.terminal || !state.terminal.element) return;
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2768
|
+
// 字号和行高都向上取整到整数像素:PC 端 1× DPR 下浏览器对亚像素
|
|
2769
|
+
// 字号/行高的舍入策略不一致(fontSize 16.25 → 16 或 17,行高
|
|
2770
|
+
// 19.5 → 19 或 20),相邻行/列的吸附方向不同就会让 wterm 网格
|
|
2771
|
+
// 错位。强制整数 px 让 cell 高度、字符高度都稳定一致,等价于
|
|
2772
|
+
// 之前桌面端必须按右上角缩放才能恢复的"整像素重排"路径。
|
|
2773
|
+
var rawFontSize = state.terminalBaseFontSize * state.terminalScale;
|
|
2774
|
+
var fontPx = Math.max(1, Math.round(rawFontSize));
|
|
2775
|
+
var rowPx = Math.max(1, Math.round(rawFontSize * 1.5));
|
|
2776
|
+
state.terminal.element.style.setProperty("--term-font-size", fontPx + "px");
|
|
2777
|
+
state.terminal.element.style.setProperty("--term-row-height", rowPx + "px");
|
|
2339
2778
|
if (typeof state.terminal.remeasure === "function") {
|
|
2340
2779
|
requestAnimationFrame(function() {
|
|
2341
2780
|
if (state.terminal) state.terminal.remeasure();
|
|
@@ -2795,7 +3234,9 @@
|
|
|
2795
3234
|
if (!session) return "";
|
|
2796
3235
|
if (session.archived) return "已归档";
|
|
2797
3236
|
if (session.permissionBlocked) return "等待授权";
|
|
3237
|
+
if (isStructuredSession(session) && session.structuredState && session.structuredState.inFlight) return "思考中";
|
|
2798
3238
|
var statusMap = {
|
|
3239
|
+
"idle": "空闲",
|
|
2799
3240
|
"stopped": "已停止",
|
|
2800
3241
|
"running": "运行中",
|
|
2801
3242
|
"exited": "已退出",
|
|
@@ -2808,6 +3249,7 @@
|
|
|
2808
3249
|
if (!session) return "";
|
|
2809
3250
|
if (session.archived) return "archived";
|
|
2810
3251
|
if (session.permissionBlocked) return "permission-blocked";
|
|
3252
|
+
if (isStructuredSession(session) && session.structuredState && session.structuredState.inFlight) return "running";
|
|
2811
3253
|
return session.status || "";
|
|
2812
3254
|
}
|
|
2813
3255
|
|
|
@@ -2981,7 +3423,7 @@
|
|
|
2981
3423
|
function renderProviderOptions(selectedTool) {
|
|
2982
3424
|
var tools = [
|
|
2983
3425
|
{ id: "claude", label: "Claude", desc: "完整 Claude 会话能力" },
|
|
2984
|
-
{ id: "codex", label: "Codex", desc: "PTY
|
|
3426
|
+
{ id: "codex", label: "Codex", desc: "结构化 JSONL 或 PTY 会话" }
|
|
2985
3427
|
];
|
|
2986
3428
|
return tools.map(function(tool) {
|
|
2987
3429
|
var active = tool.id === selectedTool ? " active" : "";
|
|
@@ -2999,7 +3441,7 @@
|
|
|
2999
3441
|
];
|
|
3000
3442
|
return kinds.map(function(kind) {
|
|
3001
3443
|
var active = kind.id === selectedKind ? " active" : "";
|
|
3002
|
-
var disabled =
|
|
3444
|
+
var disabled = "";
|
|
3003
3445
|
return '<button type="button" class="mode-card session-kind-card' + active + disabled + '" data-session-kind="' + kind.id + '">' +
|
|
3004
3446
|
'<span class="mode-card-label">' + kind.label + '</span>' +
|
|
3005
3447
|
'<span class="mode-card-desc">' + kind.desc + '</span>' +
|
|
@@ -3017,10 +3459,12 @@
|
|
|
3017
3459
|
function getSessionKindHint(kind) {
|
|
3018
3460
|
var tool = state.sessionTool || "claude";
|
|
3019
3461
|
if (kind === "structured") {
|
|
3020
|
-
return "
|
|
3462
|
+
return tool === "codex"
|
|
3463
|
+
? "Codex JSONL 结构化聊天界面,支持多轮对话和工具调用展示。"
|
|
3464
|
+
: "结构化聊天界面,支持多轮对话、流式输出和工具调用展示。";
|
|
3021
3465
|
}
|
|
3022
3466
|
if (tool === "codex") {
|
|
3023
|
-
return "Codex
|
|
3467
|
+
return "Codex PTY 终端会话;terminal 是原始输出,chat 是解析后的阅读视图。";
|
|
3024
3468
|
}
|
|
3025
3469
|
return "原始 PTY 终端会话,支持持续交互、终端视图和权限流。";
|
|
3026
3470
|
}
|
|
@@ -3408,10 +3852,9 @@
|
|
|
3408
3852
|
if (provider) {
|
|
3409
3853
|
state.sessionTool = provider;
|
|
3410
3854
|
state.preferredCommand = provider;
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
}
|
|
3855
|
+
// Codex 现在同时支持 PTY 与结构化 runner,不再强制把 kind 切成 pty。
|
|
3856
|
+
// mode 由 syncSessionModalUI() 调用 getSafeModeForTool() 自动 clamp,
|
|
3857
|
+
// 不在这里硬写。
|
|
3415
3858
|
syncSessionModalUI();
|
|
3416
3859
|
}
|
|
3417
3860
|
});
|
|
@@ -3752,6 +4195,11 @@
|
|
|
3752
4195
|
fileInput.value = "";
|
|
3753
4196
|
});
|
|
3754
4197
|
}
|
|
4198
|
+
|
|
4199
|
+
var promptOptimizeBtn = document.getElementById("prompt-optimize-btn");
|
|
4200
|
+
if (promptOptimizeBtn) {
|
|
4201
|
+
promptOptimizeBtn.addEventListener("click", function() { optimizePromptText(); });
|
|
4202
|
+
}
|
|
3755
4203
|
var composer = document.querySelector(".input-composer");
|
|
3756
4204
|
if (composer) {
|
|
3757
4205
|
composer.addEventListener("dragover", function(e) {
|
|
@@ -3939,7 +4387,9 @@
|
|
|
3939
4387
|
location.reload();
|
|
3940
4388
|
return;
|
|
3941
4389
|
}
|
|
3942
|
-
|
|
4390
|
+
softResyncTerminal();
|
|
4391
|
+
resetChatRenderCache();
|
|
4392
|
+
scheduleChatRender(true);
|
|
3943
4393
|
});
|
|
3944
4394
|
var jumpBottomBtn = document.getElementById("terminal-jump-bottom");
|
|
3945
4395
|
if (jumpBottomBtn) jumpBottomBtn.addEventListener("click", function() {
|
|
@@ -4335,6 +4785,23 @@
|
|
|
4335
4785
|
});
|
|
4336
4786
|
}
|
|
4337
4787
|
|
|
4788
|
+
var topbarGitBadge = document.getElementById("topbar-git-badge");
|
|
4789
|
+
if (topbarGitBadge) {
|
|
4790
|
+
topbarGitBadge.addEventListener("click", function(e) {
|
|
4791
|
+
e.preventDefault();
|
|
4792
|
+
openQuickCommitModal();
|
|
4793
|
+
});
|
|
4794
|
+
}
|
|
4795
|
+
var quickCommitModal = document.getElementById("quick-commit-modal");
|
|
4796
|
+
if (quickCommitModal) {
|
|
4797
|
+
quickCommitModal.addEventListener("click", function(e) {
|
|
4798
|
+
if (e.target.id === "quick-commit-modal" && !state.quickCommitSubmitting) {
|
|
4799
|
+
closeQuickCommitModal();
|
|
4800
|
+
}
|
|
4801
|
+
});
|
|
4802
|
+
}
|
|
4803
|
+
attachQuickCommitModalListeners();
|
|
4804
|
+
|
|
4338
4805
|
initTerminal();
|
|
4339
4806
|
setupMobileKeyboardHandlers();
|
|
4340
4807
|
setupVisualViewportHandlers();
|
|
@@ -4565,6 +5032,17 @@
|
|
|
4565
5032
|
return distance <= state.terminalScrollThreshold;
|
|
4566
5033
|
}
|
|
4567
5034
|
|
|
5035
|
+
// 严格"真正到底"判定(仅亚像素 jitter 容忍):用于把 autoFollow 从 false
|
|
5036
|
+
// 翻回 true。不能用 isTerminalNearBottom 的 12px 阈值,否则用户在底部小幅
|
|
5037
|
+
// 向上滚时,wheel handler 把 autoFollow 设 false 后紧接着触发的 scroll
|
|
5038
|
+
// 事件会因为"还没滚出阈值"而把 autoFollow 反转回 true,丢失用户意图。
|
|
5039
|
+
function isTerminalAtBottom() {
|
|
5040
|
+
var viewport = getTerminalViewport();
|
|
5041
|
+
if (!viewport) return true;
|
|
5042
|
+
var distance = viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop;
|
|
5043
|
+
return distance <= 2;
|
|
5044
|
+
}
|
|
5045
|
+
|
|
4568
5046
|
function scrollTerminalToBottom(smooth) {
|
|
4569
5047
|
if (!state.terminal) return;
|
|
4570
5048
|
var viewport = getTerminalViewport();
|
|
@@ -4605,11 +5083,13 @@
|
|
|
4605
5083
|
updateTerminalJumpToBottomButton();
|
|
4606
5084
|
return;
|
|
4607
5085
|
}
|
|
4608
|
-
|
|
5086
|
+
// 只看 autoFollow 标志:用户主动 wheel/touch 后该标志被设为 false,
|
|
5087
|
+
// 即使当前位置仍在底部 12px 阈值内也不再强行滚回,避免把用户刚滚上去
|
|
5088
|
+
// 的几像素吞掉。autoFollow 由 scroll handler 在"真正到底"时恢复。
|
|
5089
|
+
if (!state.terminalAutoFollow) {
|
|
4609
5090
|
updateTerminalJumpToBottomButton();
|
|
4610
5091
|
return;
|
|
4611
5092
|
}
|
|
4612
|
-
state.terminalAutoFollow = true;
|
|
4613
5093
|
scrollTerminalToBottom(false);
|
|
4614
5094
|
updateTerminalJumpToBottomButton();
|
|
4615
5095
|
}
|
|
@@ -4898,6 +5378,13 @@
|
|
|
4898
5378
|
if (!terminal || data == null) return;
|
|
4899
5379
|
if (!state.wideParserState) state.wideParserState = createWideParserState();
|
|
4900
5380
|
terminal.write(widePadAnsi(data, state.wideParserState));
|
|
5381
|
+
// wterm.write 内部用 5px 阈值判定"在底部",下一帧 _doRender 据此强制
|
|
5382
|
+
// scrollTop = scrollHeight。这与 wand 的 autoFollow("真正到底"才为
|
|
5383
|
+
// true,2px 阈值)独立,会把用户主动向上滚的几像素吞掉。覆写为 wand
|
|
5384
|
+
// 的 autoFollow 状态,让 autoFollow 成为唯一真相。
|
|
5385
|
+
if ("_shouldScrollToBottom" in terminal) {
|
|
5386
|
+
terminal._shouldScrollToBottom = state.terminalAutoFollow !== false;
|
|
5387
|
+
}
|
|
4901
5388
|
}
|
|
4902
5389
|
|
|
4903
5390
|
function resetWideParserState() {
|
|
@@ -4926,49 +5413,151 @@
|
|
|
4926
5413
|
}
|
|
4927
5414
|
stripWideFillerForCopy();
|
|
4928
5415
|
|
|
5416
|
+
// ── PTY 链路时序参数索引 ──────────────────────────────────────────
|
|
5417
|
+
// 本文件中所有影响 PTY 输入/输出节流的常量集中说明(值仍在使用处定义,
|
|
5418
|
+
// 方便阅读上下文,但请保持一致命名以便此索引可 grep 跳转):
|
|
5419
|
+
//
|
|
5420
|
+
// 服务端(src/ws-broadcast.ts、src/process-manager.ts):
|
|
5421
|
+
// OUTPUT_DEBOUNCE_MS = 16 PTY data → ws 推送 debounce
|
|
5422
|
+
// OUTPUT_MAX_SIZE = 200_000 record.output ring buffer 字节上限
|
|
5423
|
+
//
|
|
5424
|
+
// 客户端(本文件):
|
|
5425
|
+
// CLIENT_OUTPUT_MAX / CLIENT_OUTPUT_TRIM_AT state.terminalOutput 窗口
|
|
5426
|
+
// RESYNC_THROTTLE_MS = 400 chunk → softResync 节流最小间隔
|
|
5427
|
+
// RESYNC_TAIL_MS = 350 节流尾巴 timer 等待
|
|
5428
|
+
// RESYNC_BUDGET_* 5s 内 resync 频次告警阈值
|
|
5429
|
+
// CHAT_RENDER_LIVE_MS = 150 活跃流时 renderChat debounce
|
|
5430
|
+
// CHAT_RENDER_IDLE_MS = 30 空闲时 renderChat debounce
|
|
5431
|
+
// PENDING_INPUT_TTL_MS = 5000 ws 离线输入队列 TTL
|
|
5432
|
+
// PENDING_INPUT_MAX = 100 离线队列长度上限
|
|
5433
|
+
//
|
|
5434
|
+
// 调参时的关键不变式:
|
|
5435
|
+
// OUTPUT_DEBOUNCE_MS < CHAT_RENDER_IDLE_MS ≤ CHAT_RENDER_LIVE_MS
|
|
5436
|
+
// RESYNC_TAIL_MS ≤ RESYNC_THROTTLE_MS
|
|
5437
|
+
// 否则会出现"上游推得比下游消化得快但下游 timer 还没到期"的堵塞。
|
|
5438
|
+
var CHAT_RENDER_LIVE_MS = 150;
|
|
5439
|
+
var CHAT_RENDER_IDLE_MS = 30;
|
|
5440
|
+
|
|
5441
|
+
// 客户端的 state.terminalOutput 仅用于 softResyncTerminal 时重放给
|
|
5442
|
+
// wterm 作状态恢复,并不是用户能看到的 scrollback —— wterm 自己有
|
|
5443
|
+
// 独立 scrollback。但如果不限长,长跑会话累加几 MB 后每次 resync
|
|
5444
|
+
// 都会把整段重新喂给 wterm,CPU/内存随时间线性变差。
|
|
5445
|
+
//
|
|
5446
|
+
// 服务端 record.output 用 appendWindow(..., 200_000) 限了 200KB,这里
|
|
5447
|
+
// 客户端给一个稍宽的上限做兜底;超过就按行边界裁掉头部,行边界处
|
|
5448
|
+
// ANSI 状态机一定是 idle 状态,重放结果与未裁等价。找不到行边界时
|
|
5449
|
+
// 退化到字节切,并避开 UTF-16 半截、ANSI 半截。
|
|
5450
|
+
var CLIENT_OUTPUT_MAX = 256 * 1024;
|
|
5451
|
+
var CLIENT_OUTPUT_TRIM_AT = 320 * 1024;
|
|
5452
|
+
function clampClientTerminalOutput(buf) {
|
|
5453
|
+
if (!buf || buf.length <= CLIENT_OUTPUT_TRIM_AT) return buf;
|
|
5454
|
+
var start = buf.length - CLIENT_OUTPUT_MAX;
|
|
5455
|
+
// UTF-16 low surrogate
|
|
5456
|
+
if (start > 0 && start < buf.length) {
|
|
5457
|
+
var c0 = buf.charCodeAt(start);
|
|
5458
|
+
if (c0 >= 0xdc00 && c0 <= 0xdfff) start++;
|
|
5459
|
+
}
|
|
5460
|
+
// 优先在 lookahead 内找下一个换行符切割
|
|
5461
|
+
var LOOKAHEAD = 4096;
|
|
5462
|
+
var upper = Math.min(start + LOOKAHEAD, buf.length);
|
|
5463
|
+
for (var i = start; i < upper; i++) {
|
|
5464
|
+
if (buf.charCodeAt(i) === 0x0a) return buf.slice(i + 1);
|
|
5465
|
+
}
|
|
5466
|
+
// 没换行 → 检查 start 是否落在未结束的 ESC 序列里
|
|
5467
|
+
var lookback = Math.max(0, start - 256);
|
|
5468
|
+
var escAt = -1;
|
|
5469
|
+
for (var j = start - 1; j >= lookback; j--) {
|
|
5470
|
+
var c = buf.charCodeAt(j);
|
|
5471
|
+
if (c === 0x1b) { escAt = j; break; }
|
|
5472
|
+
if (c === 0x07) break;
|
|
5473
|
+
if (c >= 0x40 && c <= 0x7e) break;
|
|
5474
|
+
}
|
|
5475
|
+
if (escAt !== -1) {
|
|
5476
|
+
var terminated = false;
|
|
5477
|
+
for (var k = escAt + 1; k < start; k++) {
|
|
5478
|
+
var ck = buf.charCodeAt(k);
|
|
5479
|
+
if (ck === 0x07) { terminated = true; break; }
|
|
5480
|
+
if (ck >= 0x40 && ck <= 0x7e) { terminated = true; break; }
|
|
5481
|
+
}
|
|
5482
|
+
if (!terminated) {
|
|
5483
|
+
var ahead = Math.min(start + 256, buf.length);
|
|
5484
|
+
for (var m = start; m < ahead; m++) {
|
|
5485
|
+
var cm = buf.charCodeAt(m);
|
|
5486
|
+
if (cm === 0x07 || (cm >= 0x40 && cm <= 0x7e)) {
|
|
5487
|
+
return buf.slice(m + 1);
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
return buf.slice(start);
|
|
5493
|
+
}
|
|
5494
|
+
|
|
4929
5495
|
function resetTerminal() {
|
|
4930
|
-
if (!state.terminal
|
|
4931
|
-
//
|
|
4932
|
-
//
|
|
4933
|
-
//
|
|
4934
|
-
//
|
|
4935
|
-
//
|
|
4936
|
-
|
|
4937
|
-
|
|
5496
|
+
if (!state.terminal) return;
|
|
5497
|
+
// 优先走 wterm-entry.js 自定义 WTerm 子类暴露的 reset():它会调用
|
|
5498
|
+
// bridge.init(cols, rows) 让 WASM 重新初始化整个状态机——包含
|
|
5499
|
+
// grid、光标、属性 *和* scrollback。这是跨会话切换时清空旧
|
|
5500
|
+
// scrollback 的唯一可靠方式,避免新会话向上滚还能看到旧会话内容。
|
|
5501
|
+
// 单纯写 ANSI RIS (\x1bc) 在 WASM 实现里只清当前 grid,不动 scrollback。
|
|
5502
|
+
if (typeof state.terminal.reset === "function") {
|
|
5503
|
+
state.terminal.reset();
|
|
5504
|
+
resetWideParserState();
|
|
5505
|
+
return;
|
|
5506
|
+
}
|
|
5507
|
+
if (typeof state.terminal.write === "function") {
|
|
5508
|
+
state.terminal.write("\x1bc");
|
|
5509
|
+
}
|
|
4938
5510
|
resetWideParserState();
|
|
4939
5511
|
}
|
|
4940
5512
|
|
|
4941
5513
|
// Soft resync terminal: reset WASM grid and replay full output buffer.
|
|
4942
5514
|
// Clears any stale DOM rows left over from CSI cursor-jump sequences
|
|
4943
5515
|
// (e.g. Claude permission menus redrawing in place while user holds arrow keys).
|
|
4944
|
-
|
|
5516
|
+
// Pass { skipFit: true } when the caller knows the grid was just sized
|
|
5517
|
+
// correctly (e.g. wterm.onResize fired this resync — bouncing back into
|
|
5518
|
+
// ensureTerminalFit would just trigger another remeasure → resize → onResize
|
|
5519
|
+
// → softResyncTerminal recursion).
|
|
5520
|
+
//
|
|
5521
|
+
// 重放整 buffer 而非截短:alt screen 切换 / 滚动区 / 字符集等模式开关
|
|
5522
|
+
// 依赖从 buffer 开头开始消费。从中间切会丢失这些状态机指令,治标变
|
|
5523
|
+
// 造反。H1 已经把 buffer 限到 256KB,对 wterm WASM 来说这是 ms 级开销。
|
|
5524
|
+
var _resyncStatsWindowStart = 0;
|
|
5525
|
+
var _resyncStatsCount = 0;
|
|
5526
|
+
var _resyncLastWarnAt = 0;
|
|
5527
|
+
var RESYNC_BUDGET_WINDOW_MS = 5000;
|
|
5528
|
+
var RESYNC_BUDGET_MAX = 12;
|
|
5529
|
+
var RESYNC_WARN_COOLDOWN_MS = 30000;
|
|
5530
|
+
function softResyncTerminal(options) {
|
|
4945
5531
|
if (!state.terminal || !state.terminalOutput) return false;
|
|
5532
|
+
var opts = options || {};
|
|
5533
|
+
var bufLen = state.terminalOutput.length;
|
|
5534
|
+
var startedAt = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
|
4946
5535
|
resetTerminal();
|
|
4947
5536
|
wandTerminalWrite(state.terminal, state.terminalOutput);
|
|
4948
5537
|
state.lastTerminalResyncAt = Date.now();
|
|
4949
5538
|
maybeScrollTerminalToBottom("output");
|
|
4950
|
-
|
|
4951
|
-
//
|
|
4952
|
-
//
|
|
4953
|
-
//
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
5539
|
+
if (!opts.skipFit) ensureTerminalFit("refresh");
|
|
5540
|
+
// 统计 5s 窗口内的 resync 次数,过密时打 warn 帮助诊断
|
|
5541
|
+
// ——比如 wterm 状态机被反复弄脏、上游持续推原地重绘的菜单。
|
|
5542
|
+
// 单次 warn 后冷却 30s,避免刷屏。
|
|
5543
|
+
var now = Date.now();
|
|
5544
|
+
if (now - _resyncStatsWindowStart > RESYNC_BUDGET_WINDOW_MS) {
|
|
5545
|
+
_resyncStatsWindowStart = now;
|
|
5546
|
+
_resyncStatsCount = 1;
|
|
5547
|
+
} else {
|
|
5548
|
+
_resyncStatsCount++;
|
|
5549
|
+
if (_resyncStatsCount > RESYNC_BUDGET_MAX && now - _resyncLastWarnAt > RESYNC_WARN_COOLDOWN_MS) {
|
|
5550
|
+
_resyncLastWarnAt = now;
|
|
5551
|
+
var endedAt = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
|
5552
|
+
console.warn("[wand] softResyncTerminal high frequency",
|
|
5553
|
+
"count=" + _resyncStatsCount + "/" + Math.round((now - _resyncStatsWindowStart) / 100) / 10 + "s",
|
|
5554
|
+
"bufLen=" + bufLen,
|
|
5555
|
+
"lastReplayMs=" + Math.round(endedAt - startedAt));
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
4957
5558
|
return true;
|
|
4958
5559
|
}
|
|
4959
5560
|
|
|
4960
|
-
// Soft refresh the whole current view without losing page state:
|
|
4961
|
-
// - Replays terminal buffer to clear residue
|
|
4962
|
-
// - Clears chat render cache and forces a full rebuild
|
|
4963
|
-
// Used by the refresh button and by automatic triggers
|
|
4964
|
-
// (e.g. permission escalation appearing/disappearing).
|
|
4965
|
-
function softRefreshCurrentView() {
|
|
4966
|
-
softResyncTerminal();
|
|
4967
|
-
if (typeof resetChatRenderCache === "function") resetChatRenderCache();
|
|
4968
|
-
if (typeof scheduleChatRender === "function") scheduleChatRender(true);
|
|
4969
|
-
else if (typeof render === "function") render();
|
|
4970
|
-
}
|
|
4971
|
-
|
|
4972
5561
|
function scheduleSoftResyncTerminal(delayMs) {
|
|
4973
5562
|
if (state.softResyncTimer) clearTimeout(state.softResyncTimer);
|
|
4974
5563
|
state.softResyncTimer = setTimeout(function() {
|
|
@@ -4977,6 +5566,51 @@
|
|
|
4977
5566
|
}, typeof delayMs === "number" ? delayMs : 150);
|
|
4978
5567
|
}
|
|
4979
5568
|
|
|
5569
|
+
// Claude CLI 的 permission 菜单 / 选择列表,在用户按方向键时会
|
|
5570
|
+
// 发送光标定位 (CSI H/f)、光标移动 (CSI A-D)、擦除显示/行 (CSI
|
|
5571
|
+
// J/K) 等序列在原地重绘整块区域。wterm 在这种高频原地重绘下,
|
|
5572
|
+
// DOM 行经常残留或错位,导致新写入的内容被堆到 grid 顶部 ——
|
|
5573
|
+
// 用户体感就是"明明在改菜单,结果跑到最上面去了"。
|
|
5574
|
+
//
|
|
5575
|
+
// 兜底策略是"重置 wterm 状态机 + 重放整 buffer"(softResyncTerminal)。
|
|
5576
|
+
// 这里的关键是触发时机:旧实现用 350ms debounce,但用户实际持续
|
|
5577
|
+
// 按方向键时,每次按键都会让 PTY 回流一次原地重绘 chunk,timer
|
|
5578
|
+
// 被反复 reset,永远等不到静默期,softResync 实际从不触发——
|
|
5579
|
+
// 这是这个保护机制的根本逻辑错误。
|
|
5580
|
+
//
|
|
5581
|
+
// 改成 leading + tail 的节流:第一次进入立即 resync(leading),
|
|
5582
|
+
// 节流窗口内的连续 chunk 只挂一个尾巴 timer 兜底,不重置。这样
|
|
5583
|
+
// 持续按键期间每 RESYNC_THROTTLE_MS 强制 resync 一次,用户停手
|
|
5584
|
+
// 时由尾巴 timer 收尾。不依赖按键停顿这种永远不发生的条件。
|
|
5585
|
+
var IN_PLACE_REDRAW_RE = /\x1b\[\d*(?:;\d*)?[ABCDfHJK]/;
|
|
5586
|
+
var RESYNC_THROTTLE_MS = 400;
|
|
5587
|
+
var RESYNC_TAIL_MS = 350;
|
|
5588
|
+
var _resyncChunkLastAt = 0;
|
|
5589
|
+
var _resyncChunkTailTimer = null;
|
|
5590
|
+
function maybeScheduleResyncForChunk(chunk) {
|
|
5591
|
+
if (!chunk || typeof chunk !== "string") return;
|
|
5592
|
+
if (chunk.indexOf("\x1b[") === -1) return;
|
|
5593
|
+
if (!IN_PLACE_REDRAW_RE.test(chunk)) return;
|
|
5594
|
+
var now = Date.now();
|
|
5595
|
+
var sinceLast = now - _resyncChunkLastAt;
|
|
5596
|
+
if (sinceLast >= RESYNC_THROTTLE_MS) {
|
|
5597
|
+
if (_resyncChunkTailTimer) {
|
|
5598
|
+
clearTimeout(_resyncChunkTailTimer);
|
|
5599
|
+
_resyncChunkTailTimer = null;
|
|
5600
|
+
}
|
|
5601
|
+
_resyncChunkLastAt = now;
|
|
5602
|
+
softResyncTerminal();
|
|
5603
|
+
return;
|
|
5604
|
+
}
|
|
5605
|
+
if (_resyncChunkTailTimer) return;
|
|
5606
|
+
var wait = Math.max(RESYNC_TAIL_MS, RESYNC_THROTTLE_MS - sinceLast);
|
|
5607
|
+
_resyncChunkTailTimer = setTimeout(function() {
|
|
5608
|
+
_resyncChunkTailTimer = null;
|
|
5609
|
+
_resyncChunkLastAt = Date.now();
|
|
5610
|
+
softResyncTerminal();
|
|
5611
|
+
}, wait);
|
|
5612
|
+
}
|
|
5613
|
+
|
|
4980
5614
|
function syncTerminalBuffer(sessionId, output, options) {
|
|
4981
5615
|
if (!state.terminal) return false;
|
|
4982
5616
|
var normalizedOutput = normalizeTerminalOutput(output || "");
|
|
@@ -5020,6 +5654,7 @@
|
|
|
5020
5654
|
var delta = normalizedOutput.slice(currentOutput.length);
|
|
5021
5655
|
if (delta) {
|
|
5022
5656
|
wandTerminalWrite(state.terminal, delta);
|
|
5657
|
+
maybeScheduleResyncForChunk(delta);
|
|
5023
5658
|
wrote = true;
|
|
5024
5659
|
}
|
|
5025
5660
|
} else if (currentOutput && currentOutput.startsWith(normalizedOutput)) {
|
|
@@ -5059,10 +5694,37 @@
|
|
|
5059
5694
|
state.terminalInitRetries = 0;
|
|
5060
5695
|
state.terminalInitializing = true;
|
|
5061
5696
|
|
|
5697
|
+
// wterm 构造与 init() 内部都通过 getBoundingClientRect 测字符宽高,
|
|
5698
|
+
// 要求容器及祖先链都不是 display:none。.terminal-container 默认
|
|
5699
|
+
// display:none,必须 .active 才变 flex。switchToSessionView 里
|
|
5700
|
+
// initTerminal() 在 applyCurrentView() 之前同步执行——那时容器还是
|
|
5701
|
+
// display:none,_measureCharSize 返回 null → ResizeObserver 不挂
|
|
5702
|
+
// 载、首屏 cols 永远停在硬编码的 120,必须用户刷新/弹键盘/调窗口
|
|
5703
|
+
// 才能恢复。这里在创建 wterm 之前先把 active 类挂上,让容器进入
|
|
5704
|
+
// flex 布局,确保 _measureCharSize 拿到真实字符尺寸。
|
|
5705
|
+
if (state.selectedId) {
|
|
5706
|
+
container.classList.remove("hidden");
|
|
5707
|
+
container.classList.add("active");
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
// 防御式清理:teardownTerminal 已经会移除残留 termWrap,但若有
|
|
5711
|
+
// 调用路径绕过 teardown(比如 outputContainer 被外部 render 重建),
|
|
5712
|
+
// 这里再扫一次确保新会话不会和旧 termWrap 叠在同一位置。
|
|
5713
|
+
var staleWraps = container.querySelectorAll(".terminal-scroll-wrap");
|
|
5714
|
+
for (var i = 0; i < staleWraps.length; i++) {
|
|
5715
|
+
var stale = staleWraps[i];
|
|
5716
|
+
if (stale.parentNode === container) container.removeChild(stale);
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5062
5719
|
var termWrap = document.createElement("div");
|
|
5063
5720
|
termWrap.className = "terminal-scroll-wrap";
|
|
5064
5721
|
container.appendChild(termWrap);
|
|
5065
5722
|
|
|
5723
|
+
// cols/rows 给一个保守默认即可:wterm-entry.js 重写的 init()
|
|
5724
|
+
// 会在 super.init() 之前按 termWrap 真实尺寸做一次预校准,
|
|
5725
|
+
// 保证 super.init() 里 bridge.init / renderer.setup 一上来
|
|
5726
|
+
// 就按真实 cols 初始化,从源头消除"先按 120 写一遍 → 异步
|
|
5727
|
+
// remeasure 纠正"的时序窗口。
|
|
5066
5728
|
var term = new WTermLib.WTerm(termWrap, {
|
|
5067
5729
|
cols: 120,
|
|
5068
5730
|
rows: 36,
|
|
@@ -5074,18 +5736,15 @@
|
|
|
5074
5736
|
},
|
|
5075
5737
|
onResize: function(cols, rows) {
|
|
5076
5738
|
sendTerminalResize(cols, rows);
|
|
5077
|
-
// wterm
|
|
5078
|
-
// bridge.resize()
|
|
5079
|
-
//
|
|
5080
|
-
//
|
|
5081
|
-
//
|
|
5082
|
-
//
|
|
5083
|
-
//
|
|
5084
|
-
// 的下一帧 render 之后,结果用户先看到一帧空 grid 才看到
|
|
5085
|
-
// replay 完的内容,体感上就是"刷新都没用、动一下窗口才好"。
|
|
5739
|
+
// wterm.resize() just ran renderer.setup() (DOM rows wiped) and
|
|
5740
|
+
// bridge.resize() (WASM grid reflowed). terminalOutput is the
|
|
5741
|
+
// canonical raw byte stream — replay it now so historical lines
|
|
5742
|
+
// and any in-flight CSI sequences re-render at the new width.
|
|
5743
|
+
// skipFit: wterm already did the sizing work; calling
|
|
5744
|
+
// ensureTerminalFit again here would just cycle back through
|
|
5745
|
+
// remeasure → resize → onResize → softResyncTerminal.
|
|
5086
5746
|
if (state.terminal && state.terminalOutput) {
|
|
5087
|
-
|
|
5088
|
-
softResyncTerminal();
|
|
5747
|
+
softResyncTerminal({ skipFit: true });
|
|
5089
5748
|
}
|
|
5090
5749
|
}
|
|
5091
5750
|
});
|
|
@@ -5103,13 +5762,32 @@
|
|
|
5103
5762
|
state.terminal = term;
|
|
5104
5763
|
state.terminalInitializing = false;
|
|
5105
5764
|
applyTerminalScale();
|
|
5765
|
+
|
|
5766
|
+
// wterm 构造时 cols/rows 是硬编码的 120/36,super.init() 内部
|
|
5767
|
+
// 的 ResizeObserver 要等下一个 layout 阶段异步 fire 才纠正。
|
|
5768
|
+
// 如果不在写入历史前就把 bridge reflow 到容器真实尺寸,
|
|
5769
|
+
// syncTerminalBuffer 会按 120 cols 把整段历史写进 WASM grid,
|
|
5770
|
+
// 用户首屏看到的就是错列宽折行——必须等 ResizeObserver 触发
|
|
5771
|
+
// softResync 才恢复,中间会有几帧明显错位("刚开终端布局错乱
|
|
5772
|
+
// 一下、resize 一下才正常")。这里先强制一次 layout flush,
|
|
5773
|
+
// 再同步 remeasure 把 bridge 校准到真实 cols/rows,把"写入"
|
|
5774
|
+
// 卡在正确尺寸之后,避免错位帧。
|
|
5775
|
+
if (termWrap.isConnected) {
|
|
5776
|
+
void termWrap.offsetHeight;
|
|
5777
|
+
if (typeof term.remeasure === "function") {
|
|
5778
|
+
try { term.remeasure(); } catch (e) { /* ignore: 非致命 */ }
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
|
|
5106
5782
|
state.terminalAutoFollow = true;
|
|
5107
5783
|
clearTerminalScrollIdleTimer();
|
|
5108
5784
|
|
|
5109
5785
|
var viewport = getTerminalViewport();
|
|
5110
5786
|
if (viewport) {
|
|
5111
5787
|
state.terminalViewportScrollHandler = function() {
|
|
5112
|
-
|
|
5788
|
+
// 严格"真正到底"才恢复 autoFollow:避免 wheel 设 false 后被
|
|
5789
|
+
// 紧接着的 scroll 事件因"接近底部 12px"而反转回 true。
|
|
5790
|
+
if (isTerminalAtBottom()) {
|
|
5113
5791
|
state.terminalAutoFollow = true;
|
|
5114
5792
|
clearTerminalScrollIdleTimer();
|
|
5115
5793
|
updateTerminalJumpToBottomButton();
|
|
@@ -5152,7 +5830,7 @@
|
|
|
5152
5830
|
// Container may have been hidden / zero-width at construction
|
|
5153
5831
|
// time (hard-coded 120x36). Remeasure against the real container
|
|
5154
5832
|
// so wterm reflows the just-written history to the correct cols.
|
|
5155
|
-
ensureTerminalFit("mount");
|
|
5833
|
+
ensureTerminalFit("mount", { forceReplay: true });
|
|
5156
5834
|
}).catch(function(err) {
|
|
5157
5835
|
state.terminalInitializing = false;
|
|
5158
5836
|
console.error("[wand] wterm init failed:", err);
|
|
@@ -5266,6 +5944,11 @@
|
|
|
5266
5944
|
if (terminalInteractive) {
|
|
5267
5945
|
return "终端交互模式开启中,请直接在终端中输入";
|
|
5268
5946
|
}
|
|
5947
|
+
if (session && isStructuredSession(session)) {
|
|
5948
|
+
return session.provider === "codex"
|
|
5949
|
+
? "向 Codex 发送消息;chat 为结构化对话视图"
|
|
5950
|
+
: "向 Claude 发送消息;chat 为结构化对话视图";
|
|
5951
|
+
}
|
|
5269
5952
|
if (session && session.provider === "codex") {
|
|
5270
5953
|
if (session.status !== "running") {
|
|
5271
5954
|
return "Codex 会话已结束,无法继续发送";
|
|
@@ -5287,7 +5970,7 @@
|
|
|
5287
5970
|
|
|
5288
5971
|
function getToolModeHint(tool, mode) {
|
|
5289
5972
|
if (tool === "codex") {
|
|
5290
|
-
return "Codex
|
|
5973
|
+
return "Codex 支持 PTY 终端与结构化(JSONL)两种会话,结构化模式按 full-access 启动。";
|
|
5291
5974
|
}
|
|
5292
5975
|
if (mode === "full-access") {
|
|
5293
5976
|
return "自动确认权限请求与高权限操作,适合你确认环境安全后的连续修改。";
|
|
@@ -5391,15 +6074,20 @@
|
|
|
5391
6074
|
return "";
|
|
5392
6075
|
}
|
|
5393
6076
|
|
|
5394
|
-
function
|
|
5395
|
-
var
|
|
6077
|
+
function getModelsForCurrentProvider(session) {
|
|
6078
|
+
var provider = (session && session.provider) || state.sessionTool || "claude";
|
|
6079
|
+
if (provider === "codex") return state.availableCodexModels || [];
|
|
6080
|
+
return state.availableModels || [];
|
|
6081
|
+
}
|
|
6082
|
+
|
|
6083
|
+
function renderChatModelOptions(selected, session) {
|
|
6084
|
+
var models = getModelsForCurrentProvider(session);
|
|
5396
6085
|
var html = '<option value="">默认(跟随设置)</option>';
|
|
5397
6086
|
for (var i = 0; i < models.length; i++) {
|
|
5398
6087
|
var m = models[i];
|
|
5399
6088
|
var label = m.label || m.id;
|
|
5400
6089
|
html += '<option value="' + escapeHtml(m.id) + '"' + (m.id === selected ? " selected" : "") + '>' + escapeHtml(label) + '</option>';
|
|
5401
6090
|
}
|
|
5402
|
-
// If selected is unknown (custom value), prepend it as a sticky option
|
|
5403
6091
|
if (selected && !models.some(function(m) { return m.id === selected; })) {
|
|
5404
6092
|
html += '<option value="' + escapeHtml(selected) + '" selected>' + escapeHtml(selected) + '(自定义)</option>';
|
|
5405
6093
|
}
|
|
@@ -5410,7 +6098,7 @@
|
|
|
5410
6098
|
var select = document.getElementById("chat-model-select");
|
|
5411
6099
|
if (!select) return;
|
|
5412
6100
|
var effective = getEffectiveModel(session);
|
|
5413
|
-
select.innerHTML = renderChatModelOptions(effective);
|
|
6101
|
+
select.innerHTML = renderChatModelOptions(effective, session);
|
|
5414
6102
|
select.value = effective;
|
|
5415
6103
|
}
|
|
5416
6104
|
|
|
@@ -5420,6 +6108,7 @@
|
|
|
5420
6108
|
.then(function(data) {
|
|
5421
6109
|
if (data && Array.isArray(data.models)) {
|
|
5422
6110
|
state.availableModels = data.models;
|
|
6111
|
+
state.availableCodexModels = Array.isArray(data.codexModels) ? data.codexModels : [];
|
|
5423
6112
|
syncComposerModelSelect(getSelectedSession());
|
|
5424
6113
|
updateSettingsDefaultModelSelect(data);
|
|
5425
6114
|
}
|
|
@@ -5438,6 +6127,7 @@
|
|
|
5438
6127
|
.then(function(data) {
|
|
5439
6128
|
if (data && Array.isArray(data.models)) {
|
|
5440
6129
|
state.availableModels = data.models;
|
|
6130
|
+
state.availableCodexModels = Array.isArray(data.codexModels) ? data.codexModels : [];
|
|
5441
6131
|
syncComposerModelSelect(getSelectedSession());
|
|
5442
6132
|
updateSettingsDefaultModelSelect(data);
|
|
5443
6133
|
if (typeof showToast === "function") {
|
|
@@ -5461,7 +6151,7 @@
|
|
|
5461
6151
|
if (!select) return;
|
|
5462
6152
|
var previous = select.value;
|
|
5463
6153
|
var current = previous || state.configDefaultModel || (state.config && state.config.defaultModel) || "";
|
|
5464
|
-
select.innerHTML = renderChatModelOptions(current);
|
|
6154
|
+
select.innerHTML = renderChatModelOptions(current, { provider: "claude" });
|
|
5465
6155
|
select.value = current;
|
|
5466
6156
|
var versionEl = document.getElementById("cfg-default-model-version");
|
|
5467
6157
|
if (versionEl && data) {
|
|
@@ -5483,7 +6173,6 @@
|
|
|
5483
6173
|
try { localStorage.setItem("wand-chat-model", normalized); } catch (e) {}
|
|
5484
6174
|
var session = getSelectedSession();
|
|
5485
6175
|
if (!session) return;
|
|
5486
|
-
if (session.provider && session.provider !== "claude") return;
|
|
5487
6176
|
fetch("/api/sessions/" + encodeURIComponent(session.id) + "/model", {
|
|
5488
6177
|
method: "POST",
|
|
5489
6178
|
headers: { "Content-Type": "application/json" },
|
|
@@ -5500,7 +6189,8 @@
|
|
|
5500
6189
|
updateSessionSnapshot(data);
|
|
5501
6190
|
if (typeof showToast === "function") {
|
|
5502
6191
|
var display = normalized || "默认";
|
|
5503
|
-
|
|
6192
|
+
var hint = session.provider === "codex" ? "(下次对话生效)" : "";
|
|
6193
|
+
showToast("已切换模型 → " + display + hint, "success");
|
|
5504
6194
|
}
|
|
5505
6195
|
}
|
|
5506
6196
|
})
|
|
@@ -5508,11 +6198,13 @@
|
|
|
5508
6198
|
}
|
|
5509
6199
|
|
|
5510
6200
|
function createStructuredSession(prompt, cwdOverride, modeOverride, worktreeEnabled) {
|
|
6201
|
+
var provider = state.sessionTool === "codex" ? "codex" : "claude";
|
|
5511
6202
|
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
5512
6203
|
var payload = {
|
|
5513
6204
|
cwd: cwdOverride || getEffectiveCwd(),
|
|
5514
6205
|
mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
5515
|
-
|
|
6206
|
+
provider: provider,
|
|
6207
|
+
runner: provider === "codex" ? "codex-cli-exec" : (state.structuredRunner || "claude-cli-print"),
|
|
5516
6208
|
prompt: prompt || undefined,
|
|
5517
6209
|
worktreeEnabled: worktreeEnabled === true,
|
|
5518
6210
|
model: modelPref || undefined
|
|
@@ -5582,11 +6274,6 @@
|
|
|
5582
6274
|
var tool = state.sessionTool || "claude";
|
|
5583
6275
|
var sessionKind = state.sessionCreateKind || "structured";
|
|
5584
6276
|
|
|
5585
|
-
if (tool === "codex" && sessionKind === "structured") {
|
|
5586
|
-
sessionKind = "pty";
|
|
5587
|
-
state.sessionCreateKind = "pty";
|
|
5588
|
-
}
|
|
5589
|
-
|
|
5590
6277
|
state.sessionTool = tool;
|
|
5591
6278
|
state.modeValue = getSafeModeForTool(tool, state.modeValue || state.chatMode || "default");
|
|
5592
6279
|
|
|
@@ -5603,7 +6290,7 @@
|
|
|
5603
6290
|
if (kindCards.length) {
|
|
5604
6291
|
kindCards.forEach(function(card) {
|
|
5605
6292
|
var kind = card.getAttribute("data-session-kind");
|
|
5606
|
-
var disabled =
|
|
6293
|
+
var disabled = false;
|
|
5607
6294
|
card.classList.toggle("active", kind === sessionKind);
|
|
5608
6295
|
card.classList.toggle("disabled", disabled);
|
|
5609
6296
|
});
|
|
@@ -5812,6 +6499,9 @@
|
|
|
5812
6499
|
}
|
|
5813
6500
|
}
|
|
5814
6501
|
updateShellChrome();
|
|
6502
|
+
if (state.selectedId && state.gitStatusSessionId !== state.selectedId) {
|
|
6503
|
+
loadGitStatus(state.selectedId);
|
|
6504
|
+
}
|
|
5815
6505
|
|
|
5816
6506
|
var reloadPromise = Promise.resolve();
|
|
5817
6507
|
if (!opts.skipSelectedOutputReload && state.selectedId) {
|
|
@@ -5973,10 +6663,35 @@
|
|
|
5973
6663
|
updateShellChrome();
|
|
5974
6664
|
|
|
5975
6665
|
if (state.terminal && id === state.selectedId && data.output !== undefined) {
|
|
5976
|
-
|
|
5977
|
-
//
|
|
5978
|
-
//
|
|
5979
|
-
|
|
6666
|
+
// ws 在线时不要在这里写终端:HTTP 这边返回的是 PTY transcript
|
|
6667
|
+
// 完整磁盘文件(可达数十 MB),ws 订阅 init 拿到的是内存 ring
|
|
6668
|
+
// buffer 末尾窗口(约 200KB),二者长度+起点都不同。两路都
|
|
6669
|
+
// syncTerminalBuffer 时,append 模式的前缀检查必然失败,
|
|
6670
|
+
// 落到 else 分支的 reset+全量重写,与 ws init 的 reset+
|
|
6671
|
+
// 写入交叠,造成首屏「两份内容错位重叠」。
|
|
6672
|
+
// 设计原则:terminal 写入只走 ws init 与 chunk hot-path 两条
|
|
6673
|
+
// 权威路径——参见 case "init" 的 replace 写入与 onmessage
|
|
6674
|
+
// chunk 处理。这里只在 ws 离线兜底时才 append 写入。
|
|
6675
|
+
//
|
|
6676
|
+
// wsLikelyTakingOver: 即使 wsConnected=false(onopen 还没 fire),
|
|
6677
|
+
// 只要 ws.readyState 是 CONNECTING 或 OPEN,就视为 ws 即将
|
|
6678
|
+
// 接管。否则 selectSession → loadOutput resolve 比 ws onopen
|
|
6679
|
+
// 早时(常见于刷新页面后的首次连接)会误走 fallback,写入
|
|
6680
|
+
// terminal 后 ws init 又写一次,造成双路重叠。
|
|
6681
|
+
var wsLikelyTakingOver = !!state.ws && (
|
|
6682
|
+
state.ws.readyState === WebSocket.OPEN ||
|
|
6683
|
+
state.ws.readyState === WebSocket.CONNECTING
|
|
6684
|
+
);
|
|
6685
|
+
if (!wsLikelyTakingOver) {
|
|
6686
|
+
syncTerminalBuffer(id, data.output, { mode: "append" });
|
|
6687
|
+
// 离线兜底路径自己负责 fit + replay,否则尺寸不对。
|
|
6688
|
+
ensureTerminalFit("session-switch", { forceReplay: true });
|
|
6689
|
+
} else {
|
|
6690
|
+
// ws 在线/连接中:仅校准列宽,不重 replay(init 的
|
|
6691
|
+
// ensureTerminalFitWithRetry("init") 会负责按真实
|
|
6692
|
+
// 宽度的全量基线写入)。
|
|
6693
|
+
ensureTerminalFit("session-switch");
|
|
6694
|
+
}
|
|
5980
6695
|
}
|
|
5981
6696
|
|
|
5982
6697
|
var selectedSession = state.sessions.find(function(s) { return s.id === id; });
|
|
@@ -5991,6 +6706,9 @@
|
|
|
5991
6706
|
if (!foundSession) {
|
|
5992
6707
|
return;
|
|
5993
6708
|
}
|
|
6709
|
+
if (state.selectedId !== id) {
|
|
6710
|
+
teardownTerminal();
|
|
6711
|
+
}
|
|
5994
6712
|
state.selectedId = id;
|
|
5995
6713
|
persistSelectedId();
|
|
5996
6714
|
state.toolContentCache = {};
|
|
@@ -6021,6 +6739,11 @@
|
|
|
6021
6739
|
}
|
|
6022
6740
|
loadOutput(id).then(function() { focusInputBox(true); });
|
|
6023
6741
|
subscribeToSession(id);
|
|
6742
|
+
// 切换会话时清掉旧 git 状态,再异步刷新
|
|
6743
|
+
state.gitStatus = null;
|
|
6744
|
+
state.gitStatusSessionId = null;
|
|
6745
|
+
updateTopbarGitBadge();
|
|
6746
|
+
loadGitStatus(id, { force: true });
|
|
6024
6747
|
}
|
|
6025
6748
|
|
|
6026
6749
|
function updatePinState() {
|
|
@@ -6120,7 +6843,7 @@
|
|
|
6120
6843
|
lastFocusedElement = document.activeElement;
|
|
6121
6844
|
state.sessionTool = getPreferredTool();
|
|
6122
6845
|
state.preferredCommand = state.sessionTool;
|
|
6123
|
-
state.sessionCreateKind =
|
|
6846
|
+
state.sessionCreateKind = "structured";
|
|
6124
6847
|
state.sessionCreateWorktree = false;
|
|
6125
6848
|
state.modeValue = getSafeModeForTool(state.sessionTool, state.modeValue || state.chatMode);
|
|
6126
6849
|
syncSessionModalUI();
|
|
@@ -7327,6 +8050,29 @@
|
|
|
7327
8050
|
el.classList.remove("hidden");
|
|
7328
8051
|
}
|
|
7329
8052
|
|
|
8053
|
+
// 创建 PTY 会话时把当前终端的真实 cols/rows 注入 body,让后端 pty.spawn
|
|
8054
|
+
// 直接落在正确尺寸下。否则 PTY 先按 cols=120 启动,Claude/Codex 会基于
|
|
8055
|
+
// 120 列输出 \x1b[120G 这类绝对列定位序列;等前端 remeasure 触发 resize
|
|
8056
|
+
// 时这些早期内容已经被以 80 等真实列数渲染,整条历史就错位。
|
|
8057
|
+
function withTerminalDimensions(body) {
|
|
8058
|
+
if (!body || typeof body !== "object") return body;
|
|
8059
|
+
if (!state.terminal) return body;
|
|
8060
|
+
try {
|
|
8061
|
+
if (typeof state.terminal.remeasure === "function") {
|
|
8062
|
+
state.terminal.remeasure();
|
|
8063
|
+
}
|
|
8064
|
+
} catch (e) {}
|
|
8065
|
+
var cols = state.terminal.cols;
|
|
8066
|
+
var rows = state.terminal.rows;
|
|
8067
|
+
if (typeof cols === "number" && typeof rows === "number"
|
|
8068
|
+
&& Number.isFinite(cols) && Number.isFinite(rows)
|
|
8069
|
+
&& cols > 0 && rows > 0) {
|
|
8070
|
+
body.cols = cols;
|
|
8071
|
+
body.rows = rows;
|
|
8072
|
+
}
|
|
8073
|
+
return body;
|
|
8074
|
+
}
|
|
8075
|
+
|
|
7330
8076
|
function quickStartSession() {
|
|
7331
8077
|
var command = getPreferredTool();
|
|
7332
8078
|
var defaultCwd = getEffectiveCwd();
|
|
@@ -7337,7 +8083,7 @@
|
|
|
7337
8083
|
method: "POST",
|
|
7338
8084
|
headers: { "Content-Type": "application/json" },
|
|
7339
8085
|
credentials: "same-origin",
|
|
7340
|
-
body: JSON.stringify({ command: command, provider: command, cwd: defaultCwd, mode: defaultMode })
|
|
8086
|
+
body: JSON.stringify(withTerminalDimensions({ command: command, provider: command, cwd: defaultCwd, mode: defaultMode }))
|
|
7341
8087
|
})
|
|
7342
8088
|
.then(function(res) { return res.json(); })
|
|
7343
8089
|
.then(function(data) {
|
|
@@ -7382,12 +8128,13 @@
|
|
|
7382
8128
|
}
|
|
7383
8129
|
|
|
7384
8130
|
function startStructuredSessionFromModal(cwd, mode, worktreeEnabled, errorEl) {
|
|
7385
|
-
|
|
8131
|
+
var provider = state.sessionTool === "codex" ? "codex" : "claude";
|
|
8132
|
+
console.log("[WAND] startStructuredSessionFromModal provider:", provider, "cwd:", cwd, "mode:", mode, "worktreeEnabled:", worktreeEnabled);
|
|
7386
8133
|
_sessionCreating = true;
|
|
7387
8134
|
state.modeValue = mode;
|
|
7388
8135
|
state.chatMode = mode;
|
|
7389
|
-
state.sessionTool =
|
|
7390
|
-
state.preferredCommand =
|
|
8136
|
+
state.sessionTool = provider;
|
|
8137
|
+
state.preferredCommand = provider;
|
|
7391
8138
|
syncComposerModeSelect();
|
|
7392
8139
|
syncComposerModelSelect(getSelectedSession());
|
|
7393
8140
|
return createStructuredSession(undefined, cwd, mode, worktreeEnabled)
|
|
@@ -7417,13 +8164,13 @@
|
|
|
7417
8164
|
method: "POST",
|
|
7418
8165
|
headers: { "Content-Type": "application/json" },
|
|
7419
8166
|
credentials: "same-origin",
|
|
7420
|
-
body: JSON.stringify({
|
|
8167
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
7421
8168
|
command: command,
|
|
7422
8169
|
provider: command,
|
|
7423
8170
|
cwd: cwd,
|
|
7424
8171
|
mode: mode,
|
|
7425
8172
|
worktreeEnabled: worktreeEnabled
|
|
7426
|
-
})
|
|
8173
|
+
}))
|
|
7427
8174
|
})
|
|
7428
8175
|
.then(function(res) { return res.json(); })
|
|
7429
8176
|
.then(function(data) {
|
|
@@ -7914,6 +8661,101 @@
|
|
|
7914
8661
|
}
|
|
7915
8662
|
}
|
|
7916
8663
|
|
|
8664
|
+
var promptOptimizeInFlight = false;
|
|
8665
|
+
function optimizePromptText() {
|
|
8666
|
+
if (promptOptimizeInFlight) return;
|
|
8667
|
+
var inputBox = document.getElementById("input-box");
|
|
8668
|
+
var btn = document.getElementById("prompt-optimize-btn");
|
|
8669
|
+
var composer = document.querySelector(".input-composer");
|
|
8670
|
+
if (!inputBox) return;
|
|
8671
|
+
var raw = (inputBox.value || "").trim();
|
|
8672
|
+
if (!raw) {
|
|
8673
|
+
if (typeof showToast === "function") showToast("请先输入要优化的内容。", "info");
|
|
8674
|
+
inputBox.focus();
|
|
8675
|
+
return;
|
|
8676
|
+
}
|
|
8677
|
+
promptOptimizeInFlight = true;
|
|
8678
|
+
if (btn) {
|
|
8679
|
+
btn.classList.add("is-loading");
|
|
8680
|
+
btn.disabled = true;
|
|
8681
|
+
btn.setAttribute("title", "正在优化…");
|
|
8682
|
+
}
|
|
8683
|
+
if (composer) composer.classList.add("is-optimizing");
|
|
8684
|
+
inputBox.setAttribute("aria-busy", "true");
|
|
8685
|
+
var prevReadOnly = inputBox.readOnly;
|
|
8686
|
+
inputBox.readOnly = true;
|
|
8687
|
+
|
|
8688
|
+
var payload = { text: raw };
|
|
8689
|
+
if (state && state.selectedId) payload.sessionId = state.selectedId;
|
|
8690
|
+
|
|
8691
|
+
fetch("/api/optimize-prompt", {
|
|
8692
|
+
method: "POST",
|
|
8693
|
+
credentials: "same-origin",
|
|
8694
|
+
headers: { "Content-Type": "application/json" },
|
|
8695
|
+
body: JSON.stringify(payload)
|
|
8696
|
+
})
|
|
8697
|
+
.then(function(res) {
|
|
8698
|
+
return res.json().then(function(data) { return { ok: res.ok, data: data }; });
|
|
8699
|
+
})
|
|
8700
|
+
.then(function(result) {
|
|
8701
|
+
if (!result.ok) throw new Error((result.data && result.data.error) || "提示词优化失败。");
|
|
8702
|
+
var optimized = (result.data && result.data.optimized) || "";
|
|
8703
|
+
if (!optimized) throw new Error("Claude 返回为空。");
|
|
8704
|
+
animateOptimizedReplace(inputBox, optimized);
|
|
8705
|
+
})
|
|
8706
|
+
.catch(function(error) {
|
|
8707
|
+
if (typeof showToast === "function") showToast((error && error.message) || "提示词优化失败。", "error");
|
|
8708
|
+
if (btn) {
|
|
8709
|
+
btn.classList.remove("is-loading");
|
|
8710
|
+
btn.classList.add("is-shake");
|
|
8711
|
+
setTimeout(function() { if (btn) btn.classList.remove("is-shake"); }, 400);
|
|
8712
|
+
}
|
|
8713
|
+
})
|
|
8714
|
+
.finally(function() {
|
|
8715
|
+
promptOptimizeInFlight = false;
|
|
8716
|
+
if (btn) {
|
|
8717
|
+
btn.classList.remove("is-loading");
|
|
8718
|
+
btn.disabled = false;
|
|
8719
|
+
btn.setAttribute("title", "提示词优化(AI)");
|
|
8720
|
+
}
|
|
8721
|
+
if (composer) composer.classList.remove("is-optimizing");
|
|
8722
|
+
inputBox.removeAttribute("aria-busy");
|
|
8723
|
+
inputBox.readOnly = prevReadOnly;
|
|
8724
|
+
});
|
|
8725
|
+
}
|
|
8726
|
+
|
|
8727
|
+
function animateOptimizedReplace(inputBox, finalText) {
|
|
8728
|
+
if (!inputBox) return;
|
|
8729
|
+
// Typewriter-style fill so user sees the replacement happen
|
|
8730
|
+
var chars = Array.from(finalText);
|
|
8731
|
+
var total = chars.length;
|
|
8732
|
+
if (total === 0) {
|
|
8733
|
+
inputBox.value = "";
|
|
8734
|
+
setDraftValue("", true);
|
|
8735
|
+
autoResizeInput(inputBox);
|
|
8736
|
+
return;
|
|
8737
|
+
}
|
|
8738
|
+
var totalDuration = Math.min(700, Math.max(220, total * 8));
|
|
8739
|
+
var stepCount = Math.min(total, 60);
|
|
8740
|
+
var charsPerStep = Math.ceil(total / stepCount);
|
|
8741
|
+
var stepDelay = totalDuration / stepCount;
|
|
8742
|
+
var i = 0;
|
|
8743
|
+
inputBox.value = "";
|
|
8744
|
+
autoResizeInput(inputBox);
|
|
8745
|
+
function tick() {
|
|
8746
|
+
i = Math.min(total, i + charsPerStep);
|
|
8747
|
+
inputBox.value = chars.slice(0, i).join("");
|
|
8748
|
+
autoResizeInput(inputBox);
|
|
8749
|
+
if (i < total) {
|
|
8750
|
+
setTimeout(tick, stepDelay);
|
|
8751
|
+
} else {
|
|
8752
|
+
setDraftValue(finalText, true);
|
|
8753
|
+
try { inputBox.setSelectionRange(finalText.length, finalText.length); } catch (e) { /* ignore */ }
|
|
8754
|
+
}
|
|
8755
|
+
}
|
|
8756
|
+
tick();
|
|
8757
|
+
}
|
|
8758
|
+
|
|
7917
8759
|
function autoResizeInput(el) {
|
|
7918
8760
|
if (!el) return;
|
|
7919
8761
|
var minHeight = 36;
|
|
@@ -7948,6 +8790,9 @@
|
|
|
7948
8790
|
function isSelectedSessionRunning() {
|
|
7949
8791
|
if (!state.selectedId) return false;
|
|
7950
8792
|
var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; });
|
|
8793
|
+
if (isStructuredSession(selectedSession)) {
|
|
8794
|
+
return !!(selectedSession.structuredState && selectedSession.structuredState.inFlight);
|
|
8795
|
+
}
|
|
7951
8796
|
return !!selectedSession && selectedSession.status === "running";
|
|
7952
8797
|
}
|
|
7953
8798
|
|
|
@@ -7957,6 +8802,9 @@
|
|
|
7957
8802
|
|
|
7958
8803
|
function hasAnyBusySession() {
|
|
7959
8804
|
return state.sessions.some(function(s) {
|
|
8805
|
+
if (isStructuredSession(s)) {
|
|
8806
|
+
return !!(s.structuredState && s.structuredState.inFlight) && !s.archived;
|
|
8807
|
+
}
|
|
7960
8808
|
return s.status === "running" && !s.archived;
|
|
7961
8809
|
});
|
|
7962
8810
|
}
|
|
@@ -7986,12 +8834,12 @@
|
|
|
7986
8834
|
method: "POST",
|
|
7987
8835
|
headers: { "Content-Type": "application/json" },
|
|
7988
8836
|
credentials: "same-origin",
|
|
7989
|
-
body: JSON.stringify({
|
|
8837
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
7990
8838
|
command: item.tool,
|
|
7991
8839
|
cwd: item.cwd,
|
|
7992
8840
|
mode: item.mode,
|
|
7993
8841
|
initialInput: item.text
|
|
7994
|
-
})
|
|
8842
|
+
}))
|
|
7995
8843
|
})
|
|
7996
8844
|
.then(function(res) { return res.json(); })
|
|
7997
8845
|
.then(function(data) {
|
|
@@ -8026,12 +8874,12 @@
|
|
|
8026
8874
|
method: "POST",
|
|
8027
8875
|
headers: { "Content-Type": "application/json" },
|
|
8028
8876
|
credentials: "same-origin",
|
|
8029
|
-
body: JSON.stringify({
|
|
8877
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
8030
8878
|
command: item.tool,
|
|
8031
8879
|
cwd: item.cwd,
|
|
8032
8880
|
mode: item.mode,
|
|
8033
8881
|
initialInput: item.text
|
|
8034
|
-
})
|
|
8882
|
+
}))
|
|
8035
8883
|
})
|
|
8036
8884
|
.then(function(res) { return res.json(); })
|
|
8037
8885
|
.then(function(data) {
|
|
@@ -8207,13 +9055,13 @@
|
|
|
8207
9055
|
method: "POST",
|
|
8208
9056
|
headers: { "Content-Type": "application/json" },
|
|
8209
9057
|
credentials: "same-origin",
|
|
8210
|
-
body: JSON.stringify({
|
|
9058
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
8211
9059
|
command: preferredTool,
|
|
8212
9060
|
provider: preferredTool,
|
|
8213
9061
|
cwd: defaultCwd,
|
|
8214
9062
|
mode: mode,
|
|
8215
9063
|
initialInput: value
|
|
8216
|
-
})
|
|
9064
|
+
}))
|
|
8217
9065
|
})
|
|
8218
9066
|
.then(function(res) { return res.json(); })
|
|
8219
9067
|
.then(function(data) {
|
|
@@ -8277,13 +9125,13 @@
|
|
|
8277
9125
|
method: "POST",
|
|
8278
9126
|
headers: { "Content-Type": "application/json" },
|
|
8279
9127
|
credentials: "same-origin",
|
|
8280
|
-
body: JSON.stringify({
|
|
9128
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
8281
9129
|
command: preferredTool,
|
|
8282
9130
|
provider: preferredTool,
|
|
8283
9131
|
cwd: defaultCwd,
|
|
8284
9132
|
mode: mode,
|
|
8285
9133
|
initialInput: value || undefined
|
|
8286
|
-
})
|
|
9134
|
+
}))
|
|
8287
9135
|
})
|
|
8288
9136
|
.then(function(res) { return res.json(); })
|
|
8289
9137
|
.then(function(data) {
|
|
@@ -8352,7 +9200,7 @@
|
|
|
8352
9200
|
// Container just flipped from hidden -> visible (or geometry changed
|
|
8353
9201
|
// because chat/terminal panels swapped). Refit now so the terminal
|
|
8354
9202
|
// picks up the real cols/rows instead of keeping the stale ones.
|
|
8355
|
-
if (!structured) ensureTerminalFit("view-switch");
|
|
9203
|
+
if (!structured) ensureTerminalFit("view-switch", { forceReplay: true });
|
|
8356
9204
|
}
|
|
8357
9205
|
|
|
8358
9206
|
|
|
@@ -8413,7 +9261,8 @@
|
|
|
8413
9261
|
|
|
8414
9262
|
return ensureSessionReadyForInput(selectedSession).then(function(readySession) {
|
|
8415
9263
|
if (!readySession) {
|
|
8416
|
-
|
|
9264
|
+
// ensureSessionReadyForInput / resumeClaudeSessionById 已经在失败路径里
|
|
9265
|
+
// 自行 toast,这里不再重复提示,避免叠两条消息。
|
|
8417
9266
|
return null;
|
|
8418
9267
|
}
|
|
8419
9268
|
var submitView = state.currentView;
|
|
@@ -8478,7 +9327,9 @@
|
|
|
8478
9327
|
// the HTTP response arrives.
|
|
8479
9328
|
var epochBeforePost = state.queueEpoch;
|
|
8480
9329
|
|
|
8481
|
-
|
|
9330
|
+
// 用 session.id(参数绑定,in-flight 期间不变)而不是 state.selectedId
|
|
9331
|
+
// 拼 URL,避免用户切到别的会话后 fetch 落到错误 sessionId。
|
|
9332
|
+
return fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
8482
9333
|
method: "POST",
|
|
8483
9334
|
headers: { "Content-Type": "application/json" },
|
|
8484
9335
|
credentials: "same-origin",
|
|
@@ -8501,11 +9352,14 @@
|
|
|
8501
9352
|
delete snapshot.queuedMessages;
|
|
8502
9353
|
}
|
|
8503
9354
|
updateSessionSnapshot(snapshot);
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
9355
|
+
// 仅当 snapshot 仍属当前选中会话时才覆盖视图状态,否则只更新底层数据。
|
|
9356
|
+
if (snapshot.id === state.selectedId) {
|
|
9357
|
+
var refreshedSession = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
9358
|
+
state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
|
|
9359
|
+
renderChat(true);
|
|
9360
|
+
if (isInterrupting) {
|
|
9361
|
+
showToast("已中断上一条回复,正在处理新消息…", "info");
|
|
9362
|
+
}
|
|
8509
9363
|
}
|
|
8510
9364
|
}
|
|
8511
9365
|
})
|
|
@@ -8646,7 +9500,10 @@
|
|
|
8646
9500
|
}
|
|
8647
9501
|
|
|
8648
9502
|
function canAutoResumeSession(session) {
|
|
8649
|
-
|
|
9503
|
+
// 只要是 Claude provider + 非运行中 + 有 claudeSessionId,
|
|
9504
|
+
// 就允许在用户发送时静默触发恢复。不再要求 messages 里同时
|
|
9505
|
+
// 有 user + assistant 文本(slim 列表/截断历史会让该判断失真)。
|
|
9506
|
+
return !!(session && session.provider === "claude" && session.status !== "running" && session.claudeSessionId);
|
|
8650
9507
|
}
|
|
8651
9508
|
|
|
8652
9509
|
function ensureSessionReadyForInput(session, errorEl) {
|
|
@@ -8663,7 +9520,7 @@
|
|
|
8663
9520
|
return Promise.resolve(null);
|
|
8664
9521
|
}
|
|
8665
9522
|
|
|
8666
|
-
|
|
9523
|
+
// 静默恢复:不再弹 "正在恢复历史会话…" 提示,让用户发送动作看起来无缝。
|
|
8667
9524
|
return resumeClaudeSessionById(session.claudeSessionId, errorEl).then(function(data) {
|
|
8668
9525
|
if (!data) return null;
|
|
8669
9526
|
updateSessionSnapshot(data);
|
|
@@ -8702,13 +9559,26 @@
|
|
|
8702
9559
|
}, Promise.resolve());
|
|
8703
9560
|
}
|
|
8704
9561
|
|
|
9562
|
+
// pendingMessages 用于 ws 离线时缓存输入,重连后批量回放。
|
|
9563
|
+
// 旧实现:按字符串入队,仅靠长度上限 100 控制;超出 shift 最早一条。
|
|
9564
|
+
// 问题:用户连续按方向键时几秒就把队列填满;shift 把最早按下的丢
|
|
9565
|
+
// 掉,剩下的反而是后期按的——重连后回放的"输入序列"和用户实际
|
|
9566
|
+
// 按下的顺序矛盾。给每条消息打时间戳,flush 时直接丢弃过期项,
|
|
9567
|
+
// 让"离线超过 N 秒后重连"恢复成一个干净状态而不是错位重放。
|
|
9568
|
+
var PENDING_INPUT_TTL_MS = 5000;
|
|
9569
|
+
var PENDING_INPUT_MAX = 100;
|
|
9570
|
+
function enqueuePendingInput(input) {
|
|
9571
|
+
if (!input) return;
|
|
9572
|
+
if (state.pendingMessages.length >= PENDING_INPUT_MAX) {
|
|
9573
|
+
state.pendingMessages.shift();
|
|
9574
|
+
}
|
|
9575
|
+
state.pendingMessages.push({ input: input, at: Date.now() });
|
|
9576
|
+
}
|
|
9577
|
+
|
|
8705
9578
|
function queueOfflineTerminalChunks(chunks) {
|
|
8706
9579
|
var sequence = Array.isArray(chunks) ? chunks.filter(function(chunk) { return !!chunk; }) : [];
|
|
8707
9580
|
sequence.forEach(function(chunk) {
|
|
8708
|
-
|
|
8709
|
-
state.pendingMessages.shift();
|
|
8710
|
-
}
|
|
8711
|
-
state.pendingMessages.push(chunk);
|
|
9581
|
+
enqueuePendingInput(chunk);
|
|
8712
9582
|
});
|
|
8713
9583
|
}
|
|
8714
9584
|
|
|
@@ -8726,16 +9596,21 @@
|
|
|
8726
9596
|
|
|
8727
9597
|
function postInput(input, shortcutKey, viewOverride) {
|
|
8728
9598
|
if (!state.selectedId) return Promise.resolve();
|
|
9599
|
+
// 锁定本次请求归属的 sessionId。fetch 发起后用户可能切到别的会话,
|
|
9600
|
+
// 后续 then 回调里直接用 state.selectedId 会误把 A 的响应应用到 B:
|
|
9601
|
+
// - URL 上拼错会话(虽然 fetch 已经求值过 URL,但 markSessionStopped
|
|
9602
|
+
// 等 in-flight 引用会读最新值 → 把 B 标为 stopped 但实际是 A 失败)
|
|
9603
|
+
// - response.snapshot 属于 A,被 setCurrentMessages 误覆盖到 B 视图
|
|
9604
|
+
// 用 requestSessionId 锁住请求方,渲染相关动作再单独判断 snapshot.id
|
|
9605
|
+
// === 当前 state.selectedId 才执行。
|
|
9606
|
+
var requestSessionId = state.selectedId;
|
|
8729
9607
|
var effectiveView = viewOverride || state.currentView;
|
|
8730
9608
|
|
|
8731
9609
|
// Pre-check: don't send if session is not running
|
|
8732
9610
|
if (!isSelectedSessionRunning()) {
|
|
8733
9611
|
// If WebSocket is disconnected, queue for flush on reconnect
|
|
8734
9612
|
if (!state.wsConnected) {
|
|
8735
|
-
|
|
8736
|
-
state.pendingMessages.shift();
|
|
8737
|
-
}
|
|
8738
|
-
state.pendingMessages.push(input);
|
|
9613
|
+
enqueuePendingInput(input);
|
|
8739
9614
|
console.log("[wand] postInput: session not running, queued for reconnect", {
|
|
8740
9615
|
sessionId: state.selectedId,
|
|
8741
9616
|
inputLength: input.length
|
|
@@ -8751,10 +9626,7 @@
|
|
|
8751
9626
|
|
|
8752
9627
|
// If WebSocket is disconnected, queue the message (no HTTP fetch while offline)
|
|
8753
9628
|
if (!state.wsConnected) {
|
|
8754
|
-
|
|
8755
|
-
state.pendingMessages.shift();
|
|
8756
|
-
}
|
|
8757
|
-
state.pendingMessages.push(input);
|
|
9629
|
+
enqueuePendingInput(input);
|
|
8758
9630
|
console.log("[wand] postInput: WebSocket disconnected, queued message", {
|
|
8759
9631
|
sessionId: state.selectedId,
|
|
8760
9632
|
inputLength: input.length
|
|
@@ -8769,7 +9641,7 @@
|
|
|
8769
9641
|
wsConnected: state.wsConnected
|
|
8770
9642
|
});
|
|
8771
9643
|
|
|
8772
|
-
return fetch("/api/sessions/" +
|
|
9644
|
+
return fetch("/api/sessions/" + requestSessionId + "/input", {
|
|
8773
9645
|
method: "POST",
|
|
8774
9646
|
headers: { "Content-Type": "application/json" },
|
|
8775
9647
|
credentials: "same-origin",
|
|
@@ -8784,11 +9656,11 @@
|
|
|
8784
9656
|
status: res.status,
|
|
8785
9657
|
errorCode: error.errorCode,
|
|
8786
9658
|
message: error.message,
|
|
8787
|
-
sessionId:
|
|
9659
|
+
sessionId: requestSessionId
|
|
8788
9660
|
});
|
|
8789
9661
|
// Mark session as stopped for unavailable errors
|
|
8790
9662
|
if (isSessionUnavailableError(error)) {
|
|
8791
|
-
markSessionStopped(
|
|
9663
|
+
markSessionStopped(requestSessionId, error.sessionStatus || "exited");
|
|
8792
9664
|
}
|
|
8793
9665
|
throw error;
|
|
8794
9666
|
});
|
|
@@ -8797,11 +9669,18 @@
|
|
|
8797
9669
|
})
|
|
8798
9670
|
.then(function(snapshot) {
|
|
8799
9671
|
if (snapshot && snapshot.id) {
|
|
9672
|
+
// 底层 sessions 数据按 id 索引,无论是否仍是当前选中都可以
|
|
9673
|
+
// 安全更新(不会污染其他会话)。
|
|
8800
9674
|
updateSessionSnapshot(snapshot);
|
|
8801
|
-
|
|
8802
|
-
|
|
9675
|
+
// 但 currentMessages / renderChat 是当前视图状态,必须仅当
|
|
9676
|
+
// snapshot 仍属当前选中会话时才执行;否则会把 A 的消息列表
|
|
9677
|
+
// 渲染到 B 的 chat 视图。
|
|
9678
|
+
if (snapshot.id === state.selectedId) {
|
|
9679
|
+
if (snapshot.messages && snapshot.messages.length > 0) {
|
|
9680
|
+
state.currentMessages = snapshot.messages;
|
|
9681
|
+
}
|
|
9682
|
+
renderChat(true);
|
|
8803
9683
|
}
|
|
8804
|
-
renderChat(true);
|
|
8805
9684
|
}
|
|
8806
9685
|
return snapshot;
|
|
8807
9686
|
});
|
|
@@ -8823,6 +9702,18 @@
|
|
|
8823
9702
|
return !!state.selectedId && state.currentView === "terminal";
|
|
8824
9703
|
}
|
|
8825
9704
|
|
|
9705
|
+
// 判断一条带 sessionId 的 ws 消息是否应该被当前 wterm 实例消费。
|
|
9706
|
+
// 收敛多处散落的"selectedId 一致 + terminalSessionId 兼容"判断,避免
|
|
9707
|
+
// 后续重构时漏改某一处导致旧会话的输出污染当前终端。
|
|
9708
|
+
// terminalSessionId 为空(尚未首次 init/切换刚发生)视为可接受任何
|
|
9709
|
+
// sessionId —— 这是首条 chunk 触发自我初始化的场景。
|
|
9710
|
+
function isCurrentTerminalSession(sessionId) {
|
|
9711
|
+
if (!state.terminal || !sessionId) return false;
|
|
9712
|
+
if (sessionId !== state.selectedId) return false;
|
|
9713
|
+
if (state.terminalSessionId && state.terminalSessionId !== sessionId) return false;
|
|
9714
|
+
return true;
|
|
9715
|
+
}
|
|
9716
|
+
|
|
8826
9717
|
function shouldCaptureTerminalEvent(event) {
|
|
8827
9718
|
if (!state.terminalInteractive || !isTerminalInteractionAvailable()) return false;
|
|
8828
9719
|
if (event.defaultPrevented || event.isComposing) return false;
|
|
@@ -8990,7 +9881,9 @@
|
|
|
8990
9881
|
var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; });
|
|
8991
9882
|
var structured = isStructuredSession(selectedSession);
|
|
8992
9883
|
var isCodex = selectedSession && selectedSession.provider === "codex";
|
|
8993
|
-
var isRunning =
|
|
9884
|
+
var isRunning = structured
|
|
9885
|
+
? !!(selectedSession && selectedSession.structuredState && selectedSession.structuredState.inFlight)
|
|
9886
|
+
: !!selectedSession && selectedSession.status === "running";
|
|
8994
9887
|
var composer = document.getElementById("input-box");
|
|
8995
9888
|
// Update both toggle buttons (topbar and terminal-header)
|
|
8996
9889
|
var toggles = ["terminal-interactive-toggle-top"];
|
|
@@ -9015,18 +9908,28 @@
|
|
|
9015
9908
|
: "Enter 发送 · Shift+Enter 换行";
|
|
9016
9909
|
}
|
|
9017
9910
|
}
|
|
9018
|
-
var disableStructuredInput =
|
|
9911
|
+
var disableStructuredInput = false;
|
|
9912
|
+
// 历史会话只要可自动恢复(Claude provider + 有 claudeSessionId),
|
|
9913
|
+
// 输入框/发送按钮就保持可用——发送时由 ensureSessionReadyForInput 透明完成恢复。
|
|
9914
|
+
var canResumeOnSend = !structured && !isRunning && canAutoResumeSession(selectedSession);
|
|
9019
9915
|
if (composer) {
|
|
9020
9916
|
composer.placeholder = getComposerPlaceholder(selectedSession, state.terminalInteractive);
|
|
9021
|
-
composer.disabled = structured ? disableStructuredInput : (!!selectedSession && !isRunning);
|
|
9917
|
+
composer.disabled = structured ? disableStructuredInput : (!!selectedSession && !isRunning && !canResumeOnSend);
|
|
9022
9918
|
composer.setAttribute("aria-disabled", composer.disabled ? "true" : "false");
|
|
9919
|
+
// 终端交互模式下,按键由 document capture phase 直接透传到 PTY;
|
|
9920
|
+
// 把 textarea 设为 readonly 避免浏览器同时把字符落进输入框
|
|
9921
|
+
// (IME 组合输入、preventDefault 不彻底等边界场景下会出现"键
|
|
9922
|
+
// 发到了 PTY、输入框里也留下了字"的双状态)。disabled 会让
|
|
9923
|
+
// textarea 失去焦点能力影响一些场景,readOnly 更轻、保留焦点。
|
|
9924
|
+
composer.readOnly = !!state.terminalInteractive;
|
|
9925
|
+
composer.classList.toggle("is-terminal-passthrough", !!state.terminalInteractive);
|
|
9023
9926
|
}
|
|
9024
9927
|
var sendBtn = document.getElementById("send-input-button");
|
|
9025
9928
|
if (sendBtn) {
|
|
9026
|
-
sendBtn.disabled = structured ? disableStructuredInput : (!!selectedSession && !isRunning);
|
|
9027
|
-
sendBtn.setAttribute("title",
|
|
9028
|
-
?
|
|
9029
|
-
: (
|
|
9929
|
+
sendBtn.disabled = structured ? disableStructuredInput : (!!selectedSession && !isRunning && !canResumeOnSend);
|
|
9930
|
+
sendBtn.setAttribute("title", structured
|
|
9931
|
+
? "发送"
|
|
9932
|
+
: (isCodex ? (isRunning ? "发送给 Codex" : "Codex 会话已结束") : (!selectedSession || isRunning || canResumeOnSend ? "发送" : "会话已结束")));
|
|
9030
9933
|
}
|
|
9031
9934
|
var container = document.getElementById("output");
|
|
9032
9935
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
@@ -9057,6 +9960,7 @@
|
|
|
9057
9960
|
var sequence = buildPtySequence(key, { ctrl: state.modifiers.ctrl, alt: state.modifiers.alt, shift: state.modifiers.shift });
|
|
9058
9961
|
if (sequence) sendTerminalSequence(sequence, key);
|
|
9059
9962
|
clearModifiers();
|
|
9963
|
+
scheduleShortcutResync();
|
|
9060
9964
|
}
|
|
9061
9965
|
|
|
9062
9966
|
function handleInlineKeyboardClick(event) {
|
|
@@ -9073,12 +9977,28 @@
|
|
|
9073
9977
|
if (key === "ctrl_enter") {
|
|
9074
9978
|
var sequence = buildPtySequence("enter", { ctrl: true, alt: false, shift: false });
|
|
9075
9979
|
if (sequence) sendTerminalSequence(sequence, "ctrl_enter");
|
|
9980
|
+
scheduleShortcutResync();
|
|
9076
9981
|
return;
|
|
9077
9982
|
}
|
|
9078
9983
|
var sequence = buildPtySequence(key, { ctrl: state.modifiers.ctrl, alt: state.modifiers.alt, shift: false });
|
|
9079
9984
|
if (sequence) sendTerminalSequence(sequence, key);
|
|
9080
9985
|
clearModifiers();
|
|
9081
9986
|
updateKeyboardPopupUI();
|
|
9987
|
+
scheduleShortcutResync();
|
|
9988
|
+
}
|
|
9989
|
+
|
|
9990
|
+
// 用户点击下方快捷键栏(↑↓←→/Enter/Esc 等)后,PTY 通常会回流大量
|
|
9991
|
+
// 原地重绘序列(CSI A-D / J / K / H / f)。maybeScheduleResyncForChunk
|
|
9992
|
+
// 已经在收到这类 chunk 时做节流 resync,但 wterm 状态机偶尔会漏抓
|
|
9993
|
+
// (比如 Codex 的菜单切换),导致 DOM 行残留 / 错位 —— 表现就是用户
|
|
9994
|
+
// 反馈"按方向键之后画面错位,必须按一下右上角缩放才恢复"。
|
|
9995
|
+
// 这里在每次快捷键点击之后安排一次延迟的 softResyncTerminal 兜底,
|
|
9996
|
+
// 等价于自动按一次缩放按钮:reset 状态机 + 重放 buffer,把残留的
|
|
9997
|
+
// 错位洗掉。延迟 ~500ms 是为了让服务端先把这次按键的回执完整推过来,
|
|
9998
|
+
// 避免 resync 时只回放到 chunk 一半。
|
|
9999
|
+
function scheduleShortcutResync() {
|
|
10000
|
+
if (!state.terminal) return;
|
|
10001
|
+
scheduleSoftResyncTerminal(500);
|
|
9082
10002
|
}
|
|
9083
10003
|
|
|
9084
10004
|
function updateKeyboardPopupUI() {
|
|
@@ -9197,8 +10117,20 @@
|
|
|
9197
10117
|
|
|
9198
10118
|
// Send queued messages in order, bypassing the session-running check
|
|
9199
10119
|
// since our local state may be stale right after reconnect
|
|
9200
|
-
var
|
|
10120
|
+
var now = Date.now();
|
|
10121
|
+
var queue = [];
|
|
10122
|
+
var dropped = 0;
|
|
10123
|
+
state.pendingMessages.forEach(function(item) {
|
|
10124
|
+
// Backward-compatible: 老逻辑里 entries 可能是裸字符串。
|
|
10125
|
+
if (typeof item === "string") { queue.push(item); return; }
|
|
10126
|
+
if (!item || typeof item.input !== "string") return;
|
|
10127
|
+
if (now - (item.at || 0) > PENDING_INPUT_TTL_MS) { dropped++; return; }
|
|
10128
|
+
queue.push(item.input);
|
|
10129
|
+
});
|
|
9201
10130
|
state.pendingMessages = [];
|
|
10131
|
+
if (dropped > 0) {
|
|
10132
|
+
console.log("[wand] flushPendingMessages: dropped " + dropped + " stale input(s)");
|
|
10133
|
+
}
|
|
9202
10134
|
|
|
9203
10135
|
var sendPromise = Promise.resolve();
|
|
9204
10136
|
queue.forEach(function(input) {
|
|
@@ -9212,7 +10144,10 @@
|
|
|
9212
10144
|
|
|
9213
10145
|
function sendInputDirect(input) {
|
|
9214
10146
|
if (!input || !state.selectedId) return Promise.resolve();
|
|
9215
|
-
|
|
10147
|
+
// 同 postInput:flushPendingMessages 重连后批量回放离线消息时,
|
|
10148
|
+
// 用户可能已在切到别的会话,必须用本次请求的 sessionId 快照。
|
|
10149
|
+
var requestSessionId = state.selectedId;
|
|
10150
|
+
return fetch("/api/sessions/" + requestSessionId + "/input", {
|
|
9216
10151
|
method: "POST",
|
|
9217
10152
|
headers: { "Content-Type": "application/json" },
|
|
9218
10153
|
credentials: "same-origin",
|
|
@@ -9227,7 +10162,7 @@
|
|
|
9227
10162
|
// on the user's next message, and stale queue items would cause duplicates
|
|
9228
10163
|
if (isSessionUnavailableError(error)) {
|
|
9229
10164
|
console.log("[wand] sendInputDirect: session unavailable, dropping", {
|
|
9230
|
-
sessionId:
|
|
10165
|
+
sessionId: requestSessionId,
|
|
9231
10166
|
errorCode: error.errorCode
|
|
9232
10167
|
});
|
|
9233
10168
|
return null;
|
|
@@ -9240,10 +10175,13 @@
|
|
|
9240
10175
|
.then(function(snapshot) {
|
|
9241
10176
|
if (snapshot && snapshot.id) {
|
|
9242
10177
|
updateSessionSnapshot(snapshot);
|
|
9243
|
-
|
|
9244
|
-
|
|
10178
|
+
// 仅当 snapshot 仍属当前选中会话时才覆盖视图,否则只更新底层数据。
|
|
10179
|
+
if (snapshot.id === state.selectedId) {
|
|
10180
|
+
if (snapshot.messages && snapshot.messages.length > 0) {
|
|
10181
|
+
state.currentMessages = snapshot.messages;
|
|
10182
|
+
}
|
|
10183
|
+
renderChat(true);
|
|
9245
10184
|
}
|
|
9246
|
-
renderChat(true);
|
|
9247
10185
|
}
|
|
9248
10186
|
return snapshot;
|
|
9249
10187
|
});
|
|
@@ -9372,12 +10310,12 @@
|
|
|
9372
10310
|
method: "POST",
|
|
9373
10311
|
headers: { "Content-Type": "application/json" },
|
|
9374
10312
|
credentials: "same-origin",
|
|
9375
|
-
body: JSON.stringify({
|
|
10313
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
9376
10314
|
command: command,
|
|
9377
10315
|
cwd: cwd || "",
|
|
9378
10316
|
mode: state.chatMode || state.config.defaultMode || "default",
|
|
9379
10317
|
model: modelPref || undefined
|
|
9380
|
-
})
|
|
10318
|
+
}))
|
|
9381
10319
|
})
|
|
9382
10320
|
.then(function(res) { return res.json(); })
|
|
9383
10321
|
.then(function(data) {
|
|
@@ -9402,9 +10340,9 @@
|
|
|
9402
10340
|
method: "POST",
|
|
9403
10341
|
headers: { "Content-Type": "application/json" },
|
|
9404
10342
|
credentials: "same-origin",
|
|
9405
|
-
body: JSON.stringify({
|
|
10343
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
9406
10344
|
mode: state.chatMode || state.config.defaultMode || "default"
|
|
9407
|
-
})
|
|
10345
|
+
}))
|
|
9408
10346
|
})
|
|
9409
10347
|
.then(function(res) { return res.json(); })
|
|
9410
10348
|
.then(function(data) {
|
|
@@ -9433,9 +10371,9 @@
|
|
|
9433
10371
|
method: "POST",
|
|
9434
10372
|
headers: { "Content-Type": "application/json" },
|
|
9435
10373
|
credentials: "same-origin",
|
|
9436
|
-
body: JSON.stringify({
|
|
10374
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
9437
10375
|
mode: state.chatMode || state.config.defaultMode || "default"
|
|
9438
|
-
})
|
|
10376
|
+
}))
|
|
9439
10377
|
})
|
|
9440
10378
|
.then(function(res) { return res.json(); })
|
|
9441
10379
|
.then(function(data) {
|
|
@@ -9462,6 +10400,10 @@
|
|
|
9462
10400
|
|
|
9463
10401
|
function activateSession(data) {
|
|
9464
10402
|
if (!data || !data.id) return Promise.resolve();
|
|
10403
|
+
state.selectedId = data.id;
|
|
10404
|
+
persistSelectedId();
|
|
10405
|
+
state.currentMessages = [];
|
|
10406
|
+
teardownTerminal();
|
|
9465
10407
|
resetChatRenderCache();
|
|
9466
10408
|
switchToSessionView(data.id);
|
|
9467
10409
|
updateSessionSnapshot(data);
|
|
@@ -9509,13 +10451,13 @@
|
|
|
9509
10451
|
method: "POST",
|
|
9510
10452
|
headers: { "Content-Type": "application/json" },
|
|
9511
10453
|
credentials: "same-origin",
|
|
9512
|
-
body: JSON.stringify({
|
|
10454
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
9513
10455
|
command: preferredTool,
|
|
9514
10456
|
cwd: defaultCwd,
|
|
9515
10457
|
mode: mode,
|
|
9516
10458
|
initialInput: value,
|
|
9517
10459
|
model: modelPref || undefined
|
|
9518
|
-
})
|
|
10460
|
+
}))
|
|
9519
10461
|
})
|
|
9520
10462
|
.then(function(res) { return res.json(); })
|
|
9521
10463
|
.then(function(data) {
|
|
@@ -9547,13 +10489,13 @@
|
|
|
9547
10489
|
method: "POST",
|
|
9548
10490
|
headers: { "Content-Type": "application/json" },
|
|
9549
10491
|
credentials: "same-origin",
|
|
9550
|
-
body: JSON.stringify({
|
|
10492
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
9551
10493
|
command: preferredTool,
|
|
9552
10494
|
cwd: defaultCwd,
|
|
9553
10495
|
mode: mode,
|
|
9554
10496
|
initialInput: value || undefined,
|
|
9555
10497
|
model: modelPref || undefined
|
|
9556
|
-
})
|
|
10498
|
+
}))
|
|
9557
10499
|
})
|
|
9558
10500
|
.then(function(res) { return res.json(); })
|
|
9559
10501
|
.then(function(data) {
|
|
@@ -9609,10 +10551,10 @@
|
|
|
9609
10551
|
method: "POST",
|
|
9610
10552
|
headers: { "Content-Type": "application/json" },
|
|
9611
10553
|
credentials: "same-origin",
|
|
9612
|
-
body: JSON.stringify({
|
|
10554
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
9613
10555
|
mode: state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
9614
10556
|
cwd: cwd
|
|
9615
|
-
})
|
|
10557
|
+
}))
|
|
9616
10558
|
})
|
|
9617
10559
|
.then(function(res) { return res.json(); })
|
|
9618
10560
|
.then(function(data) {
|
|
@@ -9652,15 +10594,15 @@
|
|
|
9652
10594
|
}
|
|
9653
10595
|
|
|
9654
10596
|
function updateInputPanelViewportSpacing() {
|
|
10597
|
+
// 旧实现给 input-panel 加底部 padding = 键盘高度,意图是腾出键盘
|
|
10598
|
+
// 空间。但 input-panel 本身位置由 flex 决定,padding 增大只是把
|
|
10599
|
+
// panel 自身撑高、内部底部多出空白,textarea(panel 顶部)反而
|
|
10600
|
+
// 被往上推、离键盘更远。新方案改为让 body 高度跟随 visualViewport
|
|
10601
|
+
// 收缩(见 syncAppViewportHeight),input-panel 自然贴键盘上沿。
|
|
10602
|
+
// 这里清掉旧 keyboard-offset,避免新旧双重补偿。
|
|
9655
10603
|
var inputPanel = document.querySelector('.input-panel');
|
|
9656
10604
|
if (!inputPanel) return;
|
|
9657
|
-
|
|
9658
|
-
inputPanel.style.removeProperty('--keyboard-offset');
|
|
9659
|
-
return;
|
|
9660
|
-
}
|
|
9661
|
-
var vv = window.visualViewport;
|
|
9662
|
-
var offsetBottom = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
9663
|
-
inputPanel.style.setProperty('--keyboard-offset', offsetBottom + 'px');
|
|
10605
|
+
inputPanel.style.removeProperty('--keyboard-offset');
|
|
9664
10606
|
}
|
|
9665
10607
|
|
|
9666
10608
|
function resetInputPanelViewportSpacing() {
|
|
@@ -9713,7 +10655,7 @@
|
|
|
9713
10655
|
// The container height restores but terminal needs time to
|
|
9714
10656
|
// fill the expanded space, and the scroll position needs resetting.
|
|
9715
10657
|
if (isTouchDevice()) {
|
|
9716
|
-
ensureTerminalFit();
|
|
10658
|
+
ensureTerminalFit("keyboard-blur", { forceReplay: true });
|
|
9717
10659
|
maybeScrollTerminalToBottom("force");
|
|
9718
10660
|
}
|
|
9719
10661
|
}, 100);
|
|
@@ -10391,14 +11333,14 @@
|
|
|
10391
11333
|
var terminalContainer = document.querySelector('.terminal-container');
|
|
10392
11334
|
|
|
10393
11335
|
// Virtual Keyboard API (Chrome/Edge)
|
|
11336
|
+
// 不再给 input-panel 直接 setPaddingBottom——新方案通过
|
|
11337
|
+
// syncAppViewportHeight 让 body 跟随可见视口收缩,input-panel
|
|
11338
|
+
// 自然上移。这里只把事件留作未来钩子,避免和新方案双重补偿。
|
|
10394
11339
|
if ('virtualKeyboard' in navigator) {
|
|
10395
11340
|
var vk = navigator.virtualKeyboard;
|
|
10396
|
-
|
|
10397
11341
|
vk.addEventListener('geometrychange', function() {
|
|
10398
11342
|
if (!inputPanel) return;
|
|
10399
|
-
|
|
10400
|
-
var kbHeight = rect ? rect.height : 0;
|
|
10401
|
-
inputPanel.style.paddingBottom = kbHeight > 0 ? kbHeight + 'px' : '';
|
|
11343
|
+
inputPanel.style.removeProperty('padding-bottom');
|
|
10402
11344
|
});
|
|
10403
11345
|
}
|
|
10404
11346
|
|
|
@@ -10421,6 +11363,26 @@
|
|
|
10421
11363
|
}
|
|
10422
11364
|
}
|
|
10423
11365
|
|
|
11366
|
+
// 把 body / .app-container 的高度从 100dvh 切换为可见视口高度,
|
|
11367
|
+
// 这样键盘弹起时整个 flex column 自动收缩,input-panel 跟着上移到
|
|
11368
|
+
// 键盘上沿。Android targetSdk 36 在 edge-to-edge 默认开启时,
|
|
11369
|
+
// adjustResize 不再自动 resize WebView 内容;同时仅给 input-panel
|
|
11370
|
+
// 加 padding-bottom 只是把 panel 内部底部撑空,并不会让 panel 自身
|
|
11371
|
+
// 上移。这里通过 CSS 变量驱动整层高度,是跨 WebView/Chrome/PWA 的
|
|
11372
|
+
// 统一兜底。仅在视口比窗口明显变小时(典型 = 软键盘弹起)覆盖,
|
|
11373
|
+
// 桌面与无键盘场景维持 100dvh 不抖。
|
|
11374
|
+
function syncAppViewportHeight() {
|
|
11375
|
+
var vv = window.visualViewport;
|
|
11376
|
+
if (!vv) return;
|
|
11377
|
+
var diff = window.innerHeight - vv.height - vv.offsetTop;
|
|
11378
|
+
var root = document.documentElement;
|
|
11379
|
+
if (diff > 50) {
|
|
11380
|
+
root.style.setProperty('--app-viewport-height', vv.height + 'px');
|
|
11381
|
+
} else {
|
|
11382
|
+
root.style.removeProperty('--app-viewport-height');
|
|
11383
|
+
}
|
|
11384
|
+
}
|
|
11385
|
+
|
|
10424
11386
|
// Visual viewport handling for better mobile keyboard support
|
|
10425
11387
|
function setupVisualViewportHandlers() {
|
|
10426
11388
|
if (!('visualViewport' in window)) return;
|
|
@@ -10436,6 +11398,10 @@
|
|
|
10436
11398
|
var isKeyboardOpen = offsetBottom > 50;
|
|
10437
11399
|
var heightChanged = Math.abs(vv.height - lastHeight) > 8;
|
|
10438
11400
|
|
|
11401
|
+
// 键盘开/关与视口尺寸变化时同步 --app-viewport-height,
|
|
11402
|
+
// 让 body 高度跟随可见区域,input-panel 自然贴键盘上沿。
|
|
11403
|
+
syncAppViewportHeight();
|
|
11404
|
+
|
|
10439
11405
|
if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
|
|
10440
11406
|
syncInputBoxScroll(inputBox);
|
|
10441
11407
|
}
|
|
@@ -10445,14 +11411,14 @@
|
|
|
10445
11411
|
// Without an immediate refit, any chunk arriving while the keyboard
|
|
10446
11412
|
// animates in renders against the old grid and tears the screen.
|
|
10447
11413
|
if (!keyboardOpen && isKeyboardOpen) {
|
|
10448
|
-
ensureTerminalFit("keyboard-open");
|
|
11414
|
+
ensureTerminalFit("keyboard-open", { forceReplay: true });
|
|
10449
11415
|
}
|
|
10450
11416
|
|
|
10451
11417
|
// Keyboard just closed — force terminal refit and scroll to bottom
|
|
10452
11418
|
// after a delay so the keyboard dismiss animation and layout settle.
|
|
10453
11419
|
if (keyboardOpen && !isKeyboardOpen) {
|
|
10454
11420
|
setTimeout(function() {
|
|
10455
|
-
ensureTerminalFit("keyboard-close");
|
|
11421
|
+
ensureTerminalFit("keyboard-close", { forceReplay: true });
|
|
10456
11422
|
maybeScrollTerminalToBottom("force");
|
|
10457
11423
|
}, 200);
|
|
10458
11424
|
}
|
|
@@ -10586,11 +11552,11 @@
|
|
|
10586
11552
|
// Page returning from background: container dimensions may have
|
|
10587
11553
|
// drifted (PWA standalone, tab switch, iOS address-bar toggle).
|
|
10588
11554
|
state.visibilityHandler = function() {
|
|
10589
|
-
if (!document.hidden) ensureTerminalFit("visibility");
|
|
11555
|
+
if (!document.hidden) ensureTerminalFit("visibility", { forceReplay: true });
|
|
10590
11556
|
};
|
|
10591
11557
|
document.addEventListener("visibilitychange", state.visibilityHandler);
|
|
10592
11558
|
// Mobile device rotation — large geometry change.
|
|
10593
|
-
state.orientationHandler = function() { ensureTerminalFit("orientation"); };
|
|
11559
|
+
state.orientationHandler = function() { ensureTerminalFit("orientation", { forceReplay: true }); };
|
|
10594
11560
|
window.addEventListener("orientationchange", state.orientationHandler);
|
|
10595
11561
|
requestAnimationFrame(function() { scheduleTerminalResize(true); });
|
|
10596
11562
|
}
|
|
@@ -10686,11 +11652,43 @@
|
|
|
10686
11652
|
state.terminal.destroy();
|
|
10687
11653
|
state.terminal = null;
|
|
10688
11654
|
}
|
|
11655
|
+
// wterm.destroy() 只把 termWrap.innerHTML 置空,节点本身还挂在
|
|
11656
|
+
// #output 上。多次会话切换会让 N 个 .terminal-scroll-wrap 叠在
|
|
11657
|
+
// 同一 inset:0 位置;新 init 又 appendChild 一个新 termWrap,
|
|
11658
|
+
// 旧节点的 DOM 行虽被清空,但 scroll/层叠状态可能造成跨会话视觉
|
|
11659
|
+
// 污染。这里把残留节点彻底移除。
|
|
11660
|
+
if (output) {
|
|
11661
|
+
var staleWraps = output.querySelectorAll(".terminal-scroll-wrap");
|
|
11662
|
+
for (var i = 0; i < staleWraps.length; i++) {
|
|
11663
|
+
var wrap = staleWraps[i];
|
|
11664
|
+
if (wrap.parentNode === output) output.removeChild(wrap);
|
|
11665
|
+
}
|
|
11666
|
+
}
|
|
11667
|
+
// widePadAnsi 是模块级状态机,跨终端实例时若卡在 esc/csi/string 等
|
|
11668
|
+
// 中间态,下一个 wterm 实例的首批字节会被错误归类(首字符被吃成
|
|
11669
|
+
// ANSI 序列尾巴)。重建终端前显式复位,避免状态泄漏到新实例。
|
|
11670
|
+
resetWideParserState();
|
|
10689
11671
|
state.terminalSessionId = null;
|
|
10690
11672
|
state.terminalOutput = "";
|
|
10691
11673
|
state.terminalAutoFollow = true;
|
|
10692
11674
|
state.showTerminalJumpToBottom = false;
|
|
10693
11675
|
updateTerminalJumpToBottomButton();
|
|
11676
|
+
// 清理本轮新增的、依赖当前 wterm 实例的模块级 timer 和频次统计。
|
|
11677
|
+
// 不清掉的话,旧会话上挂起的 tail timer 在新 wterm 实例上触发会
|
|
11678
|
+
// 用 state.terminalOutput 做一次无意义的 resync;resyncStats 计数
|
|
11679
|
+
// 跨会话累加也会让告警阈值在新会话立即触发误报。
|
|
11680
|
+
if (state.softResyncTimer) {
|
|
11681
|
+
clearTimeout(state.softResyncTimer);
|
|
11682
|
+
state.softResyncTimer = null;
|
|
11683
|
+
}
|
|
11684
|
+
if (_resyncChunkTailTimer) {
|
|
11685
|
+
clearTimeout(_resyncChunkTailTimer);
|
|
11686
|
+
_resyncChunkTailTimer = null;
|
|
11687
|
+
}
|
|
11688
|
+
_resyncChunkLastAt = 0;
|
|
11689
|
+
_resyncStatsWindowStart = 0;
|
|
11690
|
+
_resyncStatsCount = 0;
|
|
11691
|
+
_resyncLastWarnAt = 0;
|
|
10694
11692
|
}
|
|
10695
11693
|
|
|
10696
11694
|
function sendTerminalResize(cols, rows) {
|
|
@@ -10709,24 +11707,71 @@
|
|
|
10709
11707
|
}
|
|
10710
11708
|
}
|
|
10711
11709
|
|
|
10712
|
-
// Unified entry point for re-fitting the
|
|
10713
|
-
//
|
|
10714
|
-
//
|
|
10715
|
-
//
|
|
10716
|
-
//
|
|
10717
|
-
//
|
|
10718
|
-
//
|
|
10719
|
-
//
|
|
10720
|
-
//
|
|
10721
|
-
//
|
|
10722
|
-
//
|
|
10723
|
-
//
|
|
10724
|
-
//
|
|
10725
|
-
//
|
|
10726
|
-
//
|
|
10727
|
-
|
|
10728
|
-
|
|
11710
|
+
// Unified entry point for re-fitting the wterm grid to its container.
|
|
11711
|
+
//
|
|
11712
|
+
// wterm's internal ResizeObserver only fires when newCols/newRows
|
|
11713
|
+
// actually differ from the current values. So a "soft refresh" path
|
|
11714
|
+
// (refresh button, ws-reconnect, view-switch — container size unchanged)
|
|
11715
|
+
// never reaches wterm.resize() on its own; we have to drive replay
|
|
11716
|
+
// explicitly via { forceReplay: true }.
|
|
11717
|
+
//
|
|
11718
|
+
// When cols *do* change in the rAF body, our remeasure() calls
|
|
11719
|
+
// wterm.resize() which synchronously fires the onResize callback —
|
|
11720
|
+
// and that callback already runs softResyncTerminal({ skipFit: true }).
|
|
11721
|
+
// So the rAF body must NOT replay again in that case (would flicker /
|
|
11722
|
+
// double-scroll). The two outcomes are mutually exclusive: either
|
|
11723
|
+
// remeasure resized and onResize replayed, or cols stayed put and we
|
|
11724
|
+
// honor forceReplay.
|
|
11725
|
+
function ensureTerminalFit(reason, options) {
|
|
11726
|
+
if (!state.terminal) return false;
|
|
11727
|
+
var opts = options || {};
|
|
11728
|
+
var forceReplay = opts.forceReplay === true;
|
|
11729
|
+
var el = document.getElementById("output");
|
|
11730
|
+
if (!el || el.offsetWidth === 0 || el.offsetHeight === 0) {
|
|
11731
|
+
// Container has no visible size yet (hidden, mid-transition,
|
|
11732
|
+
// pre-keyboard layout frame, Android WebView resume). Defer to
|
|
11733
|
+
// the retry loop; without it, a missed fit means PTY chunks keep
|
|
11734
|
+
// wrapping at the wrong width until the next external trigger
|
|
11735
|
+
// (rotation, keyboard toggle), and content piles at the top.
|
|
11736
|
+
ensureTerminalFitWithRetry(reason || "fit-retry", { forceReplay: forceReplay });
|
|
11737
|
+
return false;
|
|
11738
|
+
}
|
|
11739
|
+
var prevCols = state.terminal.cols;
|
|
11740
|
+
var prevRows = state.terminal.rows;
|
|
11741
|
+
requestAnimationFrame(function() {
|
|
11742
|
+
requestAnimationFrame(function() {
|
|
11743
|
+
if (!state.terminal) return;
|
|
11744
|
+
if (typeof state.terminal.remeasure === "function") {
|
|
11745
|
+
// remeasure → wterm.resize (if cols changed) → onResize →
|
|
11746
|
+
// softResyncTerminal({ skipFit: true }). Replay happens there.
|
|
11747
|
+
state.terminal.remeasure();
|
|
11748
|
+
}
|
|
11749
|
+
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
11750
|
+
var didResize = state.terminal.cols !== prevCols
|
|
11751
|
+
|| state.terminal.rows !== prevRows;
|
|
11752
|
+
// Mutex: didResize already replayed via onResize; otherwise the
|
|
11753
|
+
// caller may still demand a replay (e.g. ws-reconnect, refresh
|
|
11754
|
+
// button — DOM may be stale even at the same cols).
|
|
11755
|
+
if (!didResize && forceReplay && state.terminalOutput) {
|
|
11756
|
+
softResyncTerminal({ skipFit: true });
|
|
11757
|
+
}
|
|
11758
|
+
if (state.terminalAutoFollow || isTerminalNearBottom()) {
|
|
11759
|
+
maybeScrollTerminalToBottom("resize");
|
|
11760
|
+
}
|
|
11761
|
+
});
|
|
11762
|
+
});
|
|
11763
|
+
return true;
|
|
11764
|
+
}
|
|
11765
|
+
|
|
11766
|
+
// Same as ensureTerminalFit but spins through requestAnimationFrame /
|
|
11767
|
+
// setTimeout up to ~8 frames waiting for a non-zero container size
|
|
11768
|
+
// (Android WebView.onResume, keyboard transitions, hidden→visible
|
|
11769
|
+
// panel flips). Forwards forceReplay so the caller's intent is
|
|
11770
|
+
// preserved when the container finally settles.
|
|
11771
|
+
function ensureTerminalFitWithRetry(reason, options) {
|
|
10729
11772
|
if (!state.terminal) return;
|
|
11773
|
+
var opts = options || {};
|
|
11774
|
+
var forceReplay = opts.forceReplay !== false; // default true: retry path implies "may be stale"
|
|
10730
11775
|
var attempts = 0;
|
|
10731
11776
|
var maxAttempts = 8;
|
|
10732
11777
|
function tryFit() {
|
|
@@ -10738,16 +11783,7 @@
|
|
|
10738
11783
|
void el.offsetHeight;
|
|
10739
11784
|
}
|
|
10740
11785
|
if (el && el.offsetWidth > 0 && el.offsetHeight > 0) {
|
|
10741
|
-
ensureTerminalFit(reason);
|
|
10742
|
-
// After fit, force a buffer replay: even when cols didn't
|
|
10743
|
-
// change, the WASM grid state may be inconsistent after a
|
|
10744
|
-
// long suspend (DOM rows clipped, scroll position lost).
|
|
10745
|
-
// softResyncTerminal is cheap because terminalOutput is
|
|
10746
|
-
// already in memory.
|
|
10747
|
-
if (state.terminalOutput) {
|
|
10748
|
-
state.suppressFitReplay = true;
|
|
10749
|
-
softResyncTerminal();
|
|
10750
|
-
}
|
|
11786
|
+
ensureTerminalFit(reason, { forceReplay: forceReplay });
|
|
10751
11787
|
return;
|
|
10752
11788
|
}
|
|
10753
11789
|
if (++attempts >= maxAttempts) return;
|
|
@@ -10763,51 +11799,6 @@
|
|
|
10763
11799
|
tryFit();
|
|
10764
11800
|
}
|
|
10765
11801
|
|
|
10766
|
-
function ensureTerminalFit(reason) {
|
|
10767
|
-
if (!state.terminal) return false;
|
|
10768
|
-
var el = document.getElementById("output");
|
|
10769
|
-
if (!el || el.offsetWidth === 0 || el.offsetHeight === 0) {
|
|
10770
|
-
// 容器暂时没有可见尺寸(hidden、动画过渡、键盘弹起前的 layout
|
|
10771
|
-
// 中间帧、Android WebView resume 头几帧),不要静默放弃——
|
|
10772
|
-
// 改成丢给 ensureTerminalFitWithRetry 兜底,等容器有了真实
|
|
10773
|
-
// 尺寸再 fit + replay。否则一旦错过这一次,只能等下一次外部
|
|
10774
|
-
// 触发(旋转屏幕、开关键盘等),中间的输出就会一直按错误
|
|
10775
|
-
// 宽度堆在视图上方,看起来像"中间一大段都没有显示"。
|
|
10776
|
-
ensureTerminalFitWithRetry(reason || "fit-retry");
|
|
10777
|
-
return false;
|
|
10778
|
-
}
|
|
10779
|
-
var prevCols = state.terminal.cols;
|
|
10780
|
-
var prevRows = state.terminal.rows;
|
|
10781
|
-
requestAnimationFrame(function() {
|
|
10782
|
-
requestAnimationFrame(function() {
|
|
10783
|
-
if (!state.terminal) return;
|
|
10784
|
-
if (typeof state.terminal.remeasure === "function") {
|
|
10785
|
-
state.terminal.remeasure();
|
|
10786
|
-
}
|
|
10787
|
-
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
10788
|
-
// Cache the container width that produced this cols/rows so the
|
|
10789
|
-
// hot-path chunk writer can detect drift cheaply (avoids running
|
|
10790
|
-
// a full remeasure on every WebSocket message).
|
|
10791
|
-
state.lastFitContainerWidth = el.offsetWidth;
|
|
10792
|
-
state.lastFitContainerHeight = el.offsetHeight;
|
|
10793
|
-
// If cols actually changed, the previously written buffer was
|
|
10794
|
-
// wrapped to the old width. Replay the full buffer so historical
|
|
10795
|
-
// lines and any in-flight CSI cursor sequences re-render against
|
|
10796
|
-
// the new grid — this is what fixes the "torn" screens users see
|
|
10797
|
-
// after rotating, opening the keyboard, or resizing the panel.
|
|
10798
|
-
var skipReplay = state.suppressFitReplay === true;
|
|
10799
|
-
state.suppressFitReplay = false;
|
|
10800
|
-
if (!skipReplay && (state.terminal.cols !== prevCols || state.terminal.rows !== prevRows)) {
|
|
10801
|
-
if (state.terminalOutput) softResyncTerminal();
|
|
10802
|
-
}
|
|
10803
|
-
if (state.terminalAutoFollow || isTerminalNearBottom()) {
|
|
10804
|
-
maybeScrollTerminalToBottom("resize");
|
|
10805
|
-
}
|
|
10806
|
-
});
|
|
10807
|
-
});
|
|
10808
|
-
return true;
|
|
10809
|
-
}
|
|
10810
|
-
|
|
10811
11802
|
function scheduleTerminalResize(immediate) {
|
|
10812
11803
|
if (state.resizeTimer) {
|
|
10813
11804
|
clearTimeout(state.resizeTimer);
|
|
@@ -10916,6 +11907,9 @@
|
|
|
10916
11907
|
// starts the ladder from 500ms again.
|
|
10917
11908
|
state.wsReconnectAttempts = 0;
|
|
10918
11909
|
cancelWsReconnect();
|
|
11910
|
+
// Server's per-client output sequence counter restarts on every
|
|
11911
|
+
// new socket; clear ours so the first init isn't treated as a gap.
|
|
11912
|
+
state.lastSeqBySession = {};
|
|
10919
11913
|
// Subscribe to current session if any
|
|
10920
11914
|
subscribeToSession(state.selectedId);
|
|
10921
11915
|
// Flush pending messages after reconnection
|
|
@@ -10932,6 +11926,43 @@
|
|
|
10932
11926
|
ws.onmessage = function(event) {
|
|
10933
11927
|
try {
|
|
10934
11928
|
var msg = JSON.parse(event.data);
|
|
11929
|
+
if (msg && msg.type === "resync_required" && msg.sessionId) {
|
|
11930
|
+
// Server dropped some output events under backpressure and
|
|
11931
|
+
// is asking us for a fresh snapshot. Send a resync so the
|
|
11932
|
+
// server replies with a new init carrying the full output.
|
|
11933
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
11934
|
+
try {
|
|
11935
|
+
state.ws.send(JSON.stringify({ type: "resync", sessionId: msg.sessionId }));
|
|
11936
|
+
} catch (sendErr) { /* ignore */ }
|
|
11937
|
+
}
|
|
11938
|
+
if (!state.lastSeqBySession) state.lastSeqBySession = {};
|
|
11939
|
+
state.lastSeqBySession[msg.sessionId] = 0;
|
|
11940
|
+
return;
|
|
11941
|
+
}
|
|
11942
|
+
if (msg && (msg.type === "init" || msg.type === "output") && msg.sessionId && typeof msg.seq === "number") {
|
|
11943
|
+
if (!state.lastSeqBySession) state.lastSeqBySession = {};
|
|
11944
|
+
var prevSeq = state.lastSeqBySession[msg.sessionId] || 0;
|
|
11945
|
+
if (msg.type === "init") {
|
|
11946
|
+
state.lastSeqBySession[msg.sessionId] = msg.seq;
|
|
11947
|
+
} else if (msg.seq === prevSeq + 1) {
|
|
11948
|
+
state.lastSeqBySession[msg.sessionId] = msg.seq;
|
|
11949
|
+
} else if (msg.seq > prevSeq + 1 && prevSeq > 0) {
|
|
11950
|
+
// We missed at least one event — request a resync and
|
|
11951
|
+
// skip this stale event so we don't apply a partial gap.
|
|
11952
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
11953
|
+
try {
|
|
11954
|
+
state.ws.send(JSON.stringify({ type: "resync", sessionId: msg.sessionId }));
|
|
11955
|
+
} catch (sendErr) { /* ignore */ }
|
|
11956
|
+
}
|
|
11957
|
+
state.lastSeqBySession[msg.sessionId] = 0;
|
|
11958
|
+
return;
|
|
11959
|
+
} else {
|
|
11960
|
+
// seq <= prevSeq: duplicate or out-of-order from a stale
|
|
11961
|
+
// queue; drop quietly.
|
|
11962
|
+
if (msg.seq < prevSeq) return;
|
|
11963
|
+
state.lastSeqBySession[msg.sessionId] = msg.seq;
|
|
11964
|
+
}
|
|
11965
|
+
}
|
|
10935
11966
|
handleWebSocketMessage(msg);
|
|
10936
11967
|
} catch (e) {
|
|
10937
11968
|
// Ignore parse errors
|
|
@@ -10962,6 +11993,30 @@
|
|
|
10962
11993
|
case 'output':
|
|
10963
11994
|
// Update session output (for terminal display and local message parsing)
|
|
10964
11995
|
// NOTE: For structured sessions, output may be "" during streaming — check messages too
|
|
11996
|
+
// thinking → idle 边界自愈:桥接层在 output.chat 事件里把 isResponding
|
|
11997
|
+
// 透传过来。当某会话由 true 变 false(assistant 完成一轮响应)时,
|
|
11998
|
+
// 主动做一次 softResyncTerminal —— 等价于自动按一次右上角缩放按钮,
|
|
11999
|
+
// 把 Claude/Codex 流式渲染中残留的错位光标定位序列洗掉。
|
|
12000
|
+
// 用 120ms 微延迟 + 单 timer 防抖,避免连续 false→true→false 触发多次重放。
|
|
12001
|
+
if (msg.data && msg.sessionId
|
|
12002
|
+
&& Object.prototype.hasOwnProperty.call(msg.data, 'isResponding')) {
|
|
12003
|
+
if (!state._lastIsResponding) state._lastIsResponding = {};
|
|
12004
|
+
var _prevResp = !!state._lastIsResponding[msg.sessionId];
|
|
12005
|
+
var _nextResp = !!msg.data.isResponding;
|
|
12006
|
+
state._lastIsResponding[msg.sessionId] = _nextResp;
|
|
12007
|
+
if (_prevResp && !_nextResp
|
|
12008
|
+
&& msg.sessionId === state.selectedId
|
|
12009
|
+
&& state.terminal
|
|
12010
|
+
&& state.terminalOutput) {
|
|
12011
|
+
if (state._idleResyncTimer) clearTimeout(state._idleResyncTimer);
|
|
12012
|
+
var _idleResyncSid = msg.sessionId;
|
|
12013
|
+
state._idleResyncTimer = setTimeout(function() {
|
|
12014
|
+
state._idleResyncTimer = null;
|
|
12015
|
+
if (state.selectedId !== _idleResyncSid) return;
|
|
12016
|
+
try { softResyncTerminal({ skipFit: true }); } catch (e) {}
|
|
12017
|
+
}, 120);
|
|
12018
|
+
}
|
|
12019
|
+
}
|
|
10965
12020
|
if (msg.data && msg.sessionId) {
|
|
10966
12021
|
var isIncremental = !!msg.data.incremental;
|
|
10967
12022
|
var snapshot = { id: msg.sessionId };
|
|
@@ -11035,7 +12090,7 @@
|
|
|
11035
12090
|
}
|
|
11036
12091
|
// Real-time terminal output
|
|
11037
12092
|
if (msg.sessionId === state.selectedId && state.terminal && msg.data) {
|
|
11038
|
-
if (msg.data.chunk && (
|
|
12093
|
+
if (msg.data.chunk && isCurrentTerminalSession(msg.sessionId)) {
|
|
11039
12094
|
// Fast path: write chunk directly to avoid full-output comparison.
|
|
11040
12095
|
state.lastChunkAt = Date.now();
|
|
11041
12096
|
state.terminalLiveStreamSessions[msg.sessionId] = true;
|
|
@@ -11046,11 +12101,12 @@
|
|
|
11046
12101
|
// 变化的视觉错位无法被自愈,直到用户手动改窗口才修。现在让
|
|
11047
12102
|
// wterm 内部 ResizeObserver 独占 cols 跟踪职责。
|
|
11048
12103
|
wandTerminalWrite(state.terminal, msg.data.chunk);
|
|
12104
|
+
maybeScheduleResyncForChunk(msg.data.chunk);
|
|
11049
12105
|
state.terminalSessionId = msg.sessionId;
|
|
11050
12106
|
if (msg.data.output) {
|
|
11051
|
-
state.terminalOutput = normalizeTerminalOutput(msg.data.output);
|
|
12107
|
+
state.terminalOutput = clampClientTerminalOutput(normalizeTerminalOutput(msg.data.output));
|
|
11052
12108
|
} else {
|
|
11053
|
-
state.terminalOutput = (state.terminalOutput || "") + normalizeTerminalOutput(msg.data.chunk);
|
|
12109
|
+
state.terminalOutput = clampClientTerminalOutput((state.terminalOutput || "") + normalizeTerminalOutput(msg.data.chunk));
|
|
11054
12110
|
}
|
|
11055
12111
|
maybeScrollTerminalToBottom("output");
|
|
11056
12112
|
updateTerminalJumpToBottomButton();
|
|
@@ -11575,7 +12631,9 @@
|
|
|
11575
12631
|
var selectedForDelay = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
11576
12632
|
var isActiveStream = selectedForDelay && selectedForDelay.status === "running"
|
|
11577
12633
|
&& selectedForDelay.sessionKind !== "structured";
|
|
11578
|
-
|
|
12634
|
+
// 活跃流时拉到 CHAT_RENDER_LIVE_MS 减少高频重渲;空闲时用 IDLE 快速响应。
|
|
12635
|
+
// 旧实现里这两档在 setTimeout 调用时被覆盖成固定 30ms,分档逻辑形同虚设。
|
|
12636
|
+
var delay = isActiveStream ? CHAT_RENDER_LIVE_MS : CHAT_RENDER_IDLE_MS;
|
|
11579
12637
|
chatRenderTimer = setTimeout(function() {
|
|
11580
12638
|
chatRenderTimer = null;
|
|
11581
12639
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
@@ -11583,7 +12641,7 @@
|
|
|
11583
12641
|
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, selectedSession.output, true));
|
|
11584
12642
|
}
|
|
11585
12643
|
renderChat();
|
|
11586
|
-
},
|
|
12644
|
+
}, delay);
|
|
11587
12645
|
}
|
|
11588
12646
|
|
|
11589
12647
|
// Extract system info from PTY output that's not in structured messages
|
|
@@ -14421,6 +15479,21 @@
|
|
|
14421
15479
|
var _progressSyncTimers = {};
|
|
14422
15480
|
var _PROGRESS_SYNC_DEBOUNCE_MS = 300;
|
|
14423
15481
|
|
|
15482
|
+
// Strip markdown formatting and clamp to a single short line so the
|
|
15483
|
+
// native Live Activity / lock-screen card stays readable. 100 chars
|
|
15484
|
+
// matches getLastAssistantSummary; OPPO truncates harder anyway.
|
|
15485
|
+
function _compactNotificationText(text) {
|
|
15486
|
+
if (!text) return "";
|
|
15487
|
+
var t = String(text)
|
|
15488
|
+
.replace(/^#+\s+/gm, "")
|
|
15489
|
+
.replace(/\*\*/g, "")
|
|
15490
|
+
.replace(/`/g, "")
|
|
15491
|
+
.trim();
|
|
15492
|
+
var firstLine = t.split("\n")[0].trim();
|
|
15493
|
+
if (firstLine.length > 100) firstLine = firstLine.slice(0, 100) + "…";
|
|
15494
|
+
return firstLine;
|
|
15495
|
+
}
|
|
15496
|
+
|
|
14424
15497
|
function syncSessionProgressToNative(sessionId) {
|
|
14425
15498
|
if (!_hasNativeBridge || typeof WandNative.updateSessionProgress !== "function") return;
|
|
14426
15499
|
if (!sessionId) return;
|
|
@@ -14446,21 +15519,56 @@
|
|
|
14446
15519
|
return;
|
|
14447
15520
|
}
|
|
14448
15521
|
|
|
14449
|
-
// Get latest todos from session messages
|
|
15522
|
+
// Get latest todos from session messages, plus the most recent user
|
|
15523
|
+
// prompt and assistant text in the same scan. sessionLabel is frozen
|
|
15524
|
+
// to the first prompt (session.summary), so without these fields the
|
|
15525
|
+
// OPPO Live Activity / lock-screen card stays stuck on round-1 text
|
|
15526
|
+
// forever. We carry the latest round across so native can refresh.
|
|
14450
15527
|
var todos = null;
|
|
15528
|
+
var latestUserText = "";
|
|
15529
|
+
var latestAssistantText = "";
|
|
14451
15530
|
var messages = session.messages || [];
|
|
14452
15531
|
for (var i = messages.length - 1; i >= 0; i--) {
|
|
14453
15532
|
var msg = messages[i];
|
|
14454
15533
|
if (!msg.content || !Array.isArray(msg.content)) continue;
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
14458
|
-
|
|
14459
|
-
|
|
14460
|
-
|
|
15534
|
+
|
|
15535
|
+
if (!latestAssistantText && msg.role === "assistant") {
|
|
15536
|
+
for (var ai = msg.content.length - 1; ai >= 0; ai--) {
|
|
15537
|
+
var ablock = msg.content[ai];
|
|
15538
|
+
if (ablock && ablock.type === "text" && ablock.text && ablock.text.trim()) {
|
|
15539
|
+
latestAssistantText = _compactNotificationText(ablock.text);
|
|
15540
|
+
break;
|
|
15541
|
+
}
|
|
14461
15542
|
}
|
|
14462
15543
|
}
|
|
14463
|
-
|
|
15544
|
+
|
|
15545
|
+
if (!latestUserText && msg.role === "user") {
|
|
15546
|
+
// Skip queued / synthetic placeholder turns — they don't represent
|
|
15547
|
+
// user-visible "I just asked this" prompts.
|
|
15548
|
+
var isPlaceholder = msg.content.some(function(b) { return b && b.__queued; });
|
|
15549
|
+
if (!isPlaceholder) {
|
|
15550
|
+
for (var ui = 0; ui < msg.content.length; ui++) {
|
|
15551
|
+
var ublock = msg.content[ui];
|
|
15552
|
+
if (ublock && ublock.type === "text" && ublock.text && ublock.text.trim()) {
|
|
15553
|
+
latestUserText = _compactNotificationText(ublock.text);
|
|
15554
|
+
break;
|
|
15555
|
+
}
|
|
15556
|
+
}
|
|
15557
|
+
}
|
|
15558
|
+
}
|
|
15559
|
+
|
|
15560
|
+
if (!todos) {
|
|
15561
|
+
for (var j = msg.content.length - 1; j >= 0; j--) {
|
|
15562
|
+
var block = msg.content[j];
|
|
15563
|
+
if (block && block.type === "tool_use" && block.name === "TodoWrite"
|
|
15564
|
+
&& block.input && block.input.todos) {
|
|
15565
|
+
todos = block.input.todos;
|
|
15566
|
+
break;
|
|
15567
|
+
}
|
|
15568
|
+
}
|
|
15569
|
+
}
|
|
15570
|
+
|
|
15571
|
+
if (todos && latestUserText && latestAssistantText) break;
|
|
14464
15572
|
}
|
|
14465
15573
|
|
|
14466
15574
|
// Get current task
|
|
@@ -14473,6 +15581,8 @@
|
|
|
14473
15581
|
sessionLabel: sessionLabel,
|
|
14474
15582
|
status: sessionStatus,
|
|
14475
15583
|
currentTask: currentTask,
|
|
15584
|
+
latestUserText: latestUserText,
|
|
15585
|
+
latestAssistantText: latestAssistantText,
|
|
14476
15586
|
todos: todos || []
|
|
14477
15587
|
};
|
|
14478
15588
|
|