@co0ontty/wand 1.2.3 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-pty-bridge.d.ts +8 -0
- package/dist/claude-pty-bridge.js +56 -44
- package/dist/config.js +1 -0
- package/dist/pwa.js +2 -1
- package/dist/server-session-routes.js +2 -2
- package/dist/server.js +6 -1
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +308 -29
- package/dist/web-ui/content/styles.css +110 -0
- package/dist/web-ui/index.js +2 -1
- package/package.json +3 -2
|
@@ -21,6 +21,8 @@ interface PermissionState {
|
|
|
21
21
|
lastTarget: string | null;
|
|
22
22
|
/** Timestamp of last auto-confirm to prevent rapid repeats */
|
|
23
23
|
lastAutoConfirmAt: number;
|
|
24
|
+
/** Timer for delayed auto-approve (gives CLI time to be ready) */
|
|
25
|
+
pendingAutoApproveTimer: ReturnType<typeof setTimeout> | null;
|
|
24
26
|
}
|
|
25
27
|
/** Permission resolution result */
|
|
26
28
|
export type PermissionResolution = "approve_once" | "approve_turn" | "deny";
|
|
@@ -118,6 +120,12 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
118
120
|
private isRealChatInput;
|
|
119
121
|
private captureSessionId;
|
|
120
122
|
private detectPermission;
|
|
123
|
+
/**
|
|
124
|
+
* Schedule a delayed auto-approve. The delay gives the Claude CLI's interactive
|
|
125
|
+
* selection prompt time to fully render and enter its input loop before we send \r.
|
|
126
|
+
*/
|
|
127
|
+
private scheduleAutoApprove;
|
|
128
|
+
private cancelPendingAutoApprove;
|
|
121
129
|
private isPermissionPromptDetected;
|
|
122
130
|
private extractPromptText;
|
|
123
131
|
private extractPermissionTarget;
|
|
@@ -11,7 +11,8 @@ import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitC
|
|
|
11
11
|
// ── Constants ──
|
|
12
12
|
const OUTPUT_MAX_SIZE = 120000;
|
|
13
13
|
const SESSION_ID_WINDOW_SIZE = 16384;
|
|
14
|
-
const PERMISSION_WINDOW_SIZE =
|
|
14
|
+
const PERMISSION_WINDOW_SIZE = 2000;
|
|
15
|
+
const AUTO_APPROVE_DELAY_MS = 150;
|
|
15
16
|
const UUID_PATTERN = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})";
|
|
16
17
|
const CLAUDE_SESSION_ID_PATTERNS = [
|
|
17
18
|
new RegExp(`"session_id"\\s*:\\s*"${UUID_PATTERN}"`, "i"),
|
|
@@ -85,6 +86,7 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
85
86
|
lastScope: null,
|
|
86
87
|
lastTarget: null,
|
|
87
88
|
lastAutoConfirmAt: 0,
|
|
89
|
+
pendingAutoApproveTimer: null,
|
|
88
90
|
};
|
|
89
91
|
this.sessionIdWindow = "";
|
|
90
92
|
}
|
|
@@ -178,6 +180,7 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
178
180
|
this.taskDebounceTimer = null;
|
|
179
181
|
}
|
|
180
182
|
// Clear permission state — prevents stale blocked state after exit
|
|
183
|
+
this.cancelPendingAutoApprove();
|
|
181
184
|
this.permissionState.isBlocked = false;
|
|
182
185
|
this.permissionState.lastPrompt = null;
|
|
183
186
|
this.permissionState.lastScope = null;
|
|
@@ -234,6 +237,8 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
234
237
|
this.rememberedTargets.add(this.permissionState.lastTarget);
|
|
235
238
|
}
|
|
236
239
|
}
|
|
240
|
+
// Cancel any pending auto-approve timer (user resolved manually)
|
|
241
|
+
this.cancelPendingAutoApprove();
|
|
237
242
|
// Send approval/denial to PTY
|
|
238
243
|
if (this.ptyWrite) {
|
|
239
244
|
if (resolution === "deny") {
|
|
@@ -277,6 +282,7 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
277
282
|
* Clear permission blocked state (called when permission is resolved externally).
|
|
278
283
|
*/
|
|
279
284
|
clearPermissionBlocked() {
|
|
285
|
+
this.cancelPendingAutoApprove();
|
|
280
286
|
this.permissionState.isBlocked = false;
|
|
281
287
|
this.permissionState.window = "";
|
|
282
288
|
this.permissionState.lastPrompt = null;
|
|
@@ -342,25 +348,7 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
342
348
|
this.permissionState.lastTarget = target ?? null;
|
|
343
349
|
const shouldAutoApprove = this.autoApprove || this.shouldAutoApprove(scope, target);
|
|
344
350
|
if (shouldAutoApprove) {
|
|
345
|
-
|
|
346
|
-
if (now - this.permissionState.lastAutoConfirmAt < 500)
|
|
347
|
-
return;
|
|
348
|
-
this.permissionState.lastAutoConfirmAt = now;
|
|
349
|
-
process.stderr.write(`[wand] Auto-confirming permission for ${scope}${target ? `: ${target}` : ""}\n`);
|
|
350
|
-
if (this.ptyWrite) {
|
|
351
|
-
this.ptyWrite("\r");
|
|
352
|
-
}
|
|
353
|
-
this.permissionState.isBlocked = false;
|
|
354
|
-
this.permissionState.window = "";
|
|
355
|
-
this.permissionState.lastPrompt = null;
|
|
356
|
-
this.permissionState.lastScope = null;
|
|
357
|
-
this.permissionState.lastTarget = null;
|
|
358
|
-
this.emitEvent({
|
|
359
|
-
type: "permission.resolved",
|
|
360
|
-
sessionId: this.sessionId,
|
|
361
|
-
timestamp: Date.now(),
|
|
362
|
-
data: { resolution: "approve_once", autoApproved: true },
|
|
363
|
-
});
|
|
351
|
+
this.scheduleAutoApprove(scope, target);
|
|
364
352
|
}
|
|
365
353
|
else {
|
|
366
354
|
this.emitEvent({
|
|
@@ -385,28 +373,7 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
385
373
|
// Check if we should auto-approve
|
|
386
374
|
const shouldAutoApprove = this.autoApprove || this.shouldAutoApprove(scope, target);
|
|
387
375
|
if (shouldAutoApprove) {
|
|
388
|
-
|
|
389
|
-
const now = Date.now();
|
|
390
|
-
if (now - this.permissionState.lastAutoConfirmAt < 500)
|
|
391
|
-
return;
|
|
392
|
-
this.permissionState.lastAutoConfirmAt = now;
|
|
393
|
-
process.stderr.write(`[wand] Auto-confirming permission for ${scope}${target ? `: ${target}` : ""}\n`);
|
|
394
|
-
// Send approval to PTY
|
|
395
|
-
if (this.ptyWrite) {
|
|
396
|
-
this.ptyWrite("\r");
|
|
397
|
-
}
|
|
398
|
-
// Clear blocked state immediately
|
|
399
|
-
this.permissionState.isBlocked = false;
|
|
400
|
-
this.permissionState.window = "";
|
|
401
|
-
this.permissionState.lastPrompt = null;
|
|
402
|
-
this.permissionState.lastScope = null;
|
|
403
|
-
this.permissionState.lastTarget = null;
|
|
404
|
-
this.emitEvent({
|
|
405
|
-
type: "permission.resolved",
|
|
406
|
-
sessionId: this.sessionId,
|
|
407
|
-
timestamp: Date.now(),
|
|
408
|
-
data: { resolution: "approve_once", autoApproved: true },
|
|
409
|
-
});
|
|
376
|
+
this.scheduleAutoApprove(scope, target);
|
|
410
377
|
}
|
|
411
378
|
else {
|
|
412
379
|
// Emit permission prompt event for UI to handle
|
|
@@ -430,6 +397,46 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
430
397
|
});
|
|
431
398
|
}
|
|
432
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Schedule a delayed auto-approve. The delay gives the Claude CLI's interactive
|
|
402
|
+
* selection prompt time to fully render and enter its input loop before we send \r.
|
|
403
|
+
*/
|
|
404
|
+
scheduleAutoApprove(scope, target) {
|
|
405
|
+
// Debounce: skip if another auto-approve was recently sent or is pending
|
|
406
|
+
const now = Date.now();
|
|
407
|
+
if (now - this.permissionState.lastAutoConfirmAt < 500)
|
|
408
|
+
return;
|
|
409
|
+
if (this.permissionState.pendingAutoApproveTimer)
|
|
410
|
+
return;
|
|
411
|
+
this.permissionState.lastAutoConfirmAt = now;
|
|
412
|
+
process.stderr.write(`[wand] Scheduling auto-confirm for ${scope}${target ? `: ${target}` : ""} (${AUTO_APPROVE_DELAY_MS}ms)\n`);
|
|
413
|
+
this.permissionState.pendingAutoApproveTimer = setTimeout(() => {
|
|
414
|
+
this.permissionState.pendingAutoApproveTimer = null;
|
|
415
|
+
if (this._exited)
|
|
416
|
+
return;
|
|
417
|
+
process.stderr.write(`[wand] Auto-confirming permission for ${scope}${target ? `: ${target}` : ""}\n`);
|
|
418
|
+
if (this.ptyWrite) {
|
|
419
|
+
this.ptyWrite("\r");
|
|
420
|
+
}
|
|
421
|
+
this.permissionState.isBlocked = false;
|
|
422
|
+
this.permissionState.window = "";
|
|
423
|
+
this.permissionState.lastPrompt = null;
|
|
424
|
+
this.permissionState.lastScope = null;
|
|
425
|
+
this.permissionState.lastTarget = null;
|
|
426
|
+
this.emitEvent({
|
|
427
|
+
type: "permission.resolved",
|
|
428
|
+
sessionId: this.sessionId,
|
|
429
|
+
timestamp: Date.now(),
|
|
430
|
+
data: { resolution: "approve_once", autoApproved: true },
|
|
431
|
+
});
|
|
432
|
+
}, AUTO_APPROVE_DELAY_MS);
|
|
433
|
+
}
|
|
434
|
+
cancelPendingAutoApprove() {
|
|
435
|
+
if (this.permissionState.pendingAutoApproveTimer) {
|
|
436
|
+
clearTimeout(this.permissionState.pendingAutoApproveTimer);
|
|
437
|
+
this.permissionState.pendingAutoApproveTimer = null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
433
440
|
isPermissionPromptDetected(normalized) {
|
|
434
441
|
const hasIntent = /\bdo you want to\b/i.test(normalized)
|
|
435
442
|
|| /\bwould you like to\b/i.test(normalized)
|
|
@@ -438,11 +445,16 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
438
445
|
|| /\bhaven't granted\b/i.test(normalized);
|
|
439
446
|
const hasConfirmSyntax = hasExplicitConfirmSyntax(normalized);
|
|
440
447
|
const hasActionCtx = hasPermissionActionContext(normalized);
|
|
448
|
+
// For numbered selection prompts (Claude CLI v2+), require the readiness marker
|
|
449
|
+
// "Esc to cancel" / "Tab to amend" which appears only after the full menu is rendered
|
|
450
|
+
// and the input handler is active
|
|
451
|
+
const hasReadyMarker = /\besc\b.*\bcancel\b/i.test(normalized)
|
|
452
|
+
|| /\btab\b.*\bamend\b/i.test(normalized);
|
|
441
453
|
// Intent phrase + explicit confirm syntax (e.g. "Do you want to proceed? (yes/no)")
|
|
442
454
|
if (hasIntent && hasConfirmSyntax)
|
|
443
455
|
return true;
|
|
444
|
-
// Intent phrase + action keyword
|
|
445
|
-
if (hasIntent && hasActionCtx)
|
|
456
|
+
// Intent phrase + action keyword + readiness marker (numbered selection prompts)
|
|
457
|
+
if (hasIntent && hasActionCtx && hasReadyMarker)
|
|
446
458
|
return true;
|
|
447
459
|
// Standalone confirm syntax + action keyword (e.g. "[y/n] Allow bash command")
|
|
448
460
|
if (hasConfirmSyntax && hasActionCtx)
|
package/dist/config.js
CHANGED
package/dist/pwa.js
CHANGED
|
@@ -48,7 +48,8 @@ const STATIC_ASSETS = [
|
|
|
48
48
|
'/icon-512.png',
|
|
49
49
|
'/vendor/xterm/css/xterm.css',
|
|
50
50
|
'/vendor/xterm/lib/xterm.js',
|
|
51
|
-
'/vendor/xterm-addon-fit/lib/addon-fit.js'
|
|
51
|
+
'/vendor/xterm-addon-fit/lib/addon-fit.js',
|
|
52
|
+
'/vendor/xterm-addon-serialize/lib/xterm-addon-serialize.js'
|
|
52
53
|
];
|
|
53
54
|
|
|
54
55
|
self.addEventListener('install', (event) => {
|
|
@@ -130,7 +130,7 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
130
130
|
const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
|
|
131
131
|
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
132
132
|
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { resumedFromSessionId: sessionId });
|
|
133
|
-
storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id });
|
|
133
|
+
storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id, archived: true });
|
|
134
134
|
res.status(201).json({ resumedFromSessionId: sessionId, ...newSnapshot });
|
|
135
135
|
}
|
|
136
136
|
catch (error) {
|
|
@@ -159,7 +159,7 @@ export function registerSessionRoutes(app, processes, storage, defaultMode) {
|
|
|
159
159
|
const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
|
|
160
160
|
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
161
161
|
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { resumedFromSessionId: existingSession.id });
|
|
162
|
-
storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id });
|
|
162
|
+
storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id, archived: true });
|
|
163
163
|
res.status(201).json({ resumedFromSessionId: existingSession.id, resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
164
164
|
}
|
|
165
165
|
else {
|
package/dist/server.js
CHANGED
|
@@ -279,6 +279,7 @@ export async function startServer(config, configPath) {
|
|
|
279
279
|
app.use(express.json({ limit: "1mb" }));
|
|
280
280
|
app.use("/vendor/xterm", express.static(path.join(nodeModulesDir, "xterm")));
|
|
281
281
|
app.use("/vendor/xterm-addon-fit", express.static(path.join(nodeModulesDir, "@xterm", "addon-fit")));
|
|
282
|
+
app.use("/vendor/xterm-addon-serialize", express.static(path.join(nodeModulesDir, "xterm-addon-serialize")));
|
|
282
283
|
// ── Web UI and PWA endpoints ──
|
|
283
284
|
app.get("/", (_req, res) => {
|
|
284
285
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -357,6 +358,7 @@ export async function startServer(config, configPath) {
|
|
|
357
358
|
defaultMode: config.defaultMode,
|
|
358
359
|
defaultCwd: config.defaultCwd,
|
|
359
360
|
commandPresets: config.commandPresets,
|
|
361
|
+
experimentalDomTerminal: config.experimentalDomTerminal ?? false,
|
|
360
362
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
361
363
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
362
364
|
currentVersion: PKG_VERSION,
|
|
@@ -380,7 +382,7 @@ export async function startServer(config, configPath) {
|
|
|
380
382
|
});
|
|
381
383
|
app.post("/api/settings/config", async (req, res) => {
|
|
382
384
|
const body = req.body;
|
|
383
|
-
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell"];
|
|
385
|
+
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "experimentalDomTerminal"];
|
|
384
386
|
let changed = false;
|
|
385
387
|
for (const field of allowedFields) {
|
|
386
388
|
if (field in body && body[field] !== undefined) {
|
|
@@ -411,6 +413,9 @@ export async function startServer(config, configPath) {
|
|
|
411
413
|
else if (field === "shell") {
|
|
412
414
|
config.shell = String(body.shell);
|
|
413
415
|
}
|
|
416
|
+
else if (field === "experimentalDomTerminal") {
|
|
417
|
+
config.experimentalDomTerminal = body.experimentalDomTerminal === true;
|
|
418
|
+
}
|
|
414
419
|
changed = true;
|
|
415
420
|
}
|
|
416
421
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface WandConfig {
|
|
|
42
42
|
commandPresets: CommandPreset[];
|
|
43
43
|
/** Max total size (bytes) for shortcut interaction logs per session (default: 10 MB). Set 0 to disable logging. */
|
|
44
44
|
shortcutLogMaxBytes?: number;
|
|
45
|
+
/** Experimental: use DOM-based terminal rendering on mobile for native text selection (default: false) */
|
|
46
|
+
experimentalDomTerminal?: boolean;
|
|
45
47
|
}
|
|
46
48
|
export interface CommandRequest {
|
|
47
49
|
command: string;
|
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
suggestionTimer: null,
|
|
70
70
|
terminal: null,
|
|
71
71
|
fitAddon: null,
|
|
72
|
+
serializeAddon: null,
|
|
73
|
+
terminalDomView: null,
|
|
74
|
+
terminalDomUpdateTimer: null,
|
|
75
|
+
_lastDomHtml: "",
|
|
72
76
|
terminalSessionId: null,
|
|
73
77
|
terminalOutput: "",
|
|
74
78
|
terminalViewportSize: { width: 0, height: 0 },
|
|
@@ -94,8 +98,8 @@
|
|
|
94
98
|
modalOpen: false,
|
|
95
99
|
presetValue: "",
|
|
96
100
|
cwdValue: "",
|
|
97
|
-
modeValue: "
|
|
98
|
-
chatMode: "
|
|
101
|
+
modeValue: "managed",
|
|
102
|
+
chatMode: "managed",
|
|
99
103
|
sessionTool: "claude",
|
|
100
104
|
preferredCommand: "claude",
|
|
101
105
|
lastResize: { cols: 0, rows: 0 },
|
|
@@ -704,6 +708,11 @@
|
|
|
704
708
|
'<label class="field-label" for="cfg-shell">Shell</label>' +
|
|
705
709
|
'<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
|
|
706
710
|
'</div>' +
|
|
711
|
+
'<div class="field field-inline">' +
|
|
712
|
+
'<label class="field-label" for="cfg-dom-terminal">终端 DOM 渲染 <span style="font-size:0.7em;color:var(--warning);font-weight:600;">实验性</span></label>' +
|
|
713
|
+
'<input id="cfg-dom-terminal" type="checkbox" class="field-checkbox" />' +
|
|
714
|
+
'</div>' +
|
|
715
|
+
'<p class="hint" style="margin-top:-8px;margin-bottom:8px;">移动端使用 DOM 渲染终端,支持原生文本选择与复制。保存后刷新页面生效。</p>' +
|
|
707
716
|
'<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
|
|
708
717
|
'<p id="config-message" class="hint hidden"></p>' +
|
|
709
718
|
'</div>' +
|
|
@@ -747,7 +756,7 @@
|
|
|
747
756
|
}
|
|
748
757
|
|
|
749
758
|
function renderSessions() {
|
|
750
|
-
var activeSessions = state.sessions.filter(function(session) { return !session.archived; });
|
|
759
|
+
var activeSessions = state.sessions.filter(function(session) { return !session.archived && !session.resumedToSessionId; });
|
|
751
760
|
var archivedSessions = state.sessions.filter(function(session) { return session.archived; });
|
|
752
761
|
var groups = [];
|
|
753
762
|
groups.push(renderSessionManageBar());
|
|
@@ -1176,6 +1185,10 @@
|
|
|
1176
1185
|
if (!state.terminal) return;
|
|
1177
1186
|
state.terminal.options.fontSize = state.terminalBaseFontSize * state.terminalScale;
|
|
1178
1187
|
state.terminal.refresh(0, state.terminal.rows - 1);
|
|
1188
|
+
// Apply to DOM terminal view as well
|
|
1189
|
+
if (state.terminalDomView) {
|
|
1190
|
+
state.terminalDomView.style.fontSize = (state.terminalBaseFontSize * state.terminalScale) + "px";
|
|
1191
|
+
}
|
|
1179
1192
|
}
|
|
1180
1193
|
|
|
1181
1194
|
function updateScaleLabel() {
|
|
@@ -1630,10 +1643,6 @@
|
|
|
1630
1643
|
|
|
1631
1644
|
if (session.autoRecovered) {
|
|
1632
1645
|
recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
|
|
1633
|
-
} else if (session.resumedToSessionId) {
|
|
1634
|
-
recoveryHint = '<span class="session-id" title="已恢复到新会话">已恢复</span>';
|
|
1635
|
-
} else if (session.resumedFromSessionId) {
|
|
1636
|
-
recoveryHint = '<span class="session-id" title="从旧会话恢复而来">续接</span>';
|
|
1637
1646
|
}
|
|
1638
1647
|
|
|
1639
1648
|
var deleteButton = state.sessionsManageMode ? '' : '<button class="session-action-btn delete-btn" data-action="delete-session" data-session-id="' + session.id + '" type="button" aria-label="删除会话" title="删除此会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
|
|
@@ -1644,7 +1653,7 @@
|
|
|
1644
1653
|
'<div class="session-item-row">' +
|
|
1645
1654
|
checkbox +
|
|
1646
1655
|
'<div class="session-main">' +
|
|
1647
|
-
'<div class="session-command">' + escapeHtml(session.command) + '</div>' +
|
|
1656
|
+
'<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>' +
|
|
1648
1657
|
'<div class="session-meta">' +
|
|
1649
1658
|
'<span>' + escapeHtml(modeName) + '</span>' +
|
|
1650
1659
|
'<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
|
|
@@ -1659,11 +1668,11 @@
|
|
|
1659
1668
|
}
|
|
1660
1669
|
function renderModeCards(selectedMode) {
|
|
1661
1670
|
var modes = [
|
|
1662
|
-
{ id: "
|
|
1671
|
+
{ id: "managed", label: "托管", desc: "全自动完成任务" },
|
|
1663
1672
|
{ id: "full-access", label: "全权限", desc: "自动确认权限" },
|
|
1664
1673
|
{ id: "auto-edit", label: "自动编辑", desc: "自动确认修改" },
|
|
1665
|
-
{ id: "
|
|
1666
|
-
{ id: "
|
|
1674
|
+
{ id: "default", label: "标准", desc: "逐步确认操作" },
|
|
1675
|
+
{ id: "native", label: "原生", desc: "结构化单轮输出" }
|
|
1667
1676
|
];
|
|
1668
1677
|
return modes.map(function(m) {
|
|
1669
1678
|
var active = m.id === selectedMode ? " active" : "";
|
|
@@ -2672,6 +2681,11 @@
|
|
|
2672
2681
|
}
|
|
2673
2682
|
|
|
2674
2683
|
function isTerminalNearBottom() {
|
|
2684
|
+
// On mobile, check DOM view scroll position
|
|
2685
|
+
if (state.terminalDomView) {
|
|
2686
|
+
var d = state.terminalDomView.scrollHeight - state.terminalDomView.clientHeight - state.terminalDomView.scrollTop;
|
|
2687
|
+
return d <= state.terminalScrollThreshold;
|
|
2688
|
+
}
|
|
2675
2689
|
var viewport = getTerminalViewport();
|
|
2676
2690
|
if (!viewport) return true;
|
|
2677
2691
|
var distance = viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop;
|
|
@@ -2680,6 +2694,14 @@
|
|
|
2680
2694
|
|
|
2681
2695
|
function scrollTerminalToBottom(smooth) {
|
|
2682
2696
|
if (!state.terminal) return;
|
|
2697
|
+
// Also scroll mobile DOM view
|
|
2698
|
+
if (state.terminalDomView) {
|
|
2699
|
+
if (smooth) {
|
|
2700
|
+
state.terminalDomView.scrollTo({ top: state.terminalDomView.scrollHeight, behavior: "smooth" });
|
|
2701
|
+
} else {
|
|
2702
|
+
state.terminalDomView.scrollTop = state.terminalDomView.scrollHeight;
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2683
2705
|
if (smooth) {
|
|
2684
2706
|
var viewport = getTerminalViewport();
|
|
2685
2707
|
if (viewport) {
|
|
@@ -2967,6 +2989,7 @@
|
|
|
2967
2989
|
|
|
2968
2990
|
state.terminalSessionId = nextSessionId;
|
|
2969
2991
|
state.terminalOutput = normalizedOutput;
|
|
2992
|
+
scheduleMobileDomUpdate();
|
|
2970
2993
|
if (shouldScroll && (wrote || sessionChanged || mode === "replace")) {
|
|
2971
2994
|
maybeScrollTerminalToBottom(sessionChanged || mode === "replace" ? "force" : "output");
|
|
2972
2995
|
} else {
|
|
@@ -3050,6 +3073,12 @@
|
|
|
3050
3073
|
console.error("[wand] xterm fit addon failed to load; continuing without fit support.");
|
|
3051
3074
|
}
|
|
3052
3075
|
|
|
3076
|
+
// Load serialize addon for mobile DOM rendering
|
|
3077
|
+
if (typeof SerializeAddon !== "undefined" && SerializeAddon && typeof SerializeAddon.SerializeAddon === "function") {
|
|
3078
|
+
state.serializeAddon = new SerializeAddon.SerializeAddon();
|
|
3079
|
+
state.terminal.loadAddon(state.serializeAddon);
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3053
3082
|
state.terminal.open(container);
|
|
3054
3083
|
applyTerminalScale();
|
|
3055
3084
|
state.terminalViewportSize = { width: 0, height: 0 };
|
|
@@ -3088,6 +3117,9 @@
|
|
|
3088
3117
|
// Create custom scrollbar overlay
|
|
3089
3118
|
initTerminalScrollbar(container);
|
|
3090
3119
|
|
|
3120
|
+
// Terminal copy button for mobile
|
|
3121
|
+
initMobileDomTerminal(container);
|
|
3122
|
+
|
|
3091
3123
|
if (state.selectedId) {
|
|
3092
3124
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
3093
3125
|
if (session) {
|
|
@@ -3821,6 +3853,8 @@
|
|
|
3821
3853
|
if (modeEl) modeEl.value = cfg.defaultMode || "default";
|
|
3822
3854
|
if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
|
|
3823
3855
|
if (shellEl) shellEl.value = cfg.shell || "";
|
|
3856
|
+
var domTermEl = document.getElementById("cfg-dom-terminal");
|
|
3857
|
+
if (domTermEl) domTermEl.checked = cfg.experimentalDomTerminal === true;
|
|
3824
3858
|
|
|
3825
3859
|
// Cert status
|
|
3826
3860
|
var certStatus = document.getElementById("cert-status");
|
|
@@ -3858,6 +3892,7 @@
|
|
|
3858
3892
|
defaultMode: (document.getElementById("cfg-mode") || {}).value,
|
|
3859
3893
|
defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
|
|
3860
3894
|
shell: (document.getElementById("cfg-shell") || {}).value,
|
|
3895
|
+
experimentalDomTerminal: (document.getElementById("cfg-dom-terminal") || {}).checked,
|
|
3861
3896
|
};
|
|
3862
3897
|
|
|
3863
3898
|
fetch("/api/settings/config", {
|
|
@@ -4549,12 +4584,11 @@
|
|
|
4549
4584
|
el.scrollTop = 0;
|
|
4550
4585
|
return;
|
|
4551
4586
|
}
|
|
4552
|
-
//
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
el.style.
|
|
4556
|
-
el.style.
|
|
4557
|
-
void el.offsetHeight;
|
|
4587
|
+
// Measure content height by temporarily setting height to minHeight
|
|
4588
|
+
// and reading scrollHeight. Avoid collapsing to 0 which causes layout jumps.
|
|
4589
|
+
var prevOverflow = el.style.overflowY;
|
|
4590
|
+
el.style.overflowY = "hidden";
|
|
4591
|
+
el.style.height = minHeight + "px";
|
|
4558
4592
|
var contentHeight = el.scrollHeight;
|
|
4559
4593
|
var newHeight = Math.max(minHeight, Math.min(contentHeight, maxHeight));
|
|
4560
4594
|
var shouldScrollInside = contentHeight > maxHeight;
|
|
@@ -4585,7 +4619,7 @@
|
|
|
4585
4619
|
welcomeInput.value = "";
|
|
4586
4620
|
welcomeInput.placeholder = "正在启动会话...";
|
|
4587
4621
|
welcomeInput.disabled = true;
|
|
4588
|
-
var mode = state.chatMode || "
|
|
4622
|
+
var mode = state.chatMode || "managed";
|
|
4589
4623
|
var defaultCwd = getEffectiveCwd();
|
|
4590
4624
|
var preferredTool = getPreferredTool();
|
|
4591
4625
|
fetch("/api/commands", {
|
|
@@ -4645,7 +4679,7 @@
|
|
|
4645
4679
|
}
|
|
4646
4680
|
|
|
4647
4681
|
// No selected session, create a new one
|
|
4648
|
-
var mode = state.chatMode || "
|
|
4682
|
+
var mode = state.chatMode || "managed";
|
|
4649
4683
|
var defaultCwd = getEffectiveCwd();
|
|
4650
4684
|
var preferredTool = getPreferredTool();
|
|
4651
4685
|
fetch("/api/commands", {
|
|
@@ -4867,6 +4901,7 @@
|
|
|
4867
4901
|
var idx = state.messageQueue.indexOf(input);
|
|
4868
4902
|
if (idx > -1) state.messageQueue.splice(idx, 1);
|
|
4869
4903
|
updateQueueCounter();
|
|
4904
|
+
scheduleMobileDomUpdate();
|
|
4870
4905
|
});
|
|
4871
4906
|
});
|
|
4872
4907
|
return state.inputQueue;
|
|
@@ -5592,7 +5627,7 @@
|
|
|
5592
5627
|
if (!welcomeInput) return;
|
|
5593
5628
|
welcomeInput.placeholder = "Claude 正在思考,请稍候...";
|
|
5594
5629
|
welcomeInput.disabled = true;
|
|
5595
|
-
var mode = state.chatMode || "
|
|
5630
|
+
var mode = state.chatMode || "managed";
|
|
5596
5631
|
var defaultCwd = getEffectiveCwd();
|
|
5597
5632
|
var preferredTool = getPreferredTool();
|
|
5598
5633
|
fetch("/api/commands", {
|
|
@@ -5628,7 +5663,7 @@
|
|
|
5628
5663
|
}
|
|
5629
5664
|
|
|
5630
5665
|
function createSessionFromInput(value, inputBox, welcomeInput) {
|
|
5631
|
-
var mode = state.chatMode || "
|
|
5666
|
+
var mode = state.chatMode || "managed";
|
|
5632
5667
|
var defaultCwd = getEffectiveCwd();
|
|
5633
5668
|
var preferredTool = getPreferredTool();
|
|
5634
5669
|
fetch("/api/commands", {
|
|
@@ -5728,9 +5763,10 @@
|
|
|
5728
5763
|
function scrollLatestMessageIntoView() {
|
|
5729
5764
|
var chatMessages = document.querySelector('.chat-messages');
|
|
5730
5765
|
if (!chatMessages) return;
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5766
|
+
// column-reverse: scrollTop=0 is the visual bottom.
|
|
5767
|
+
// Use direct scrollTop instead of scrollIntoView() to avoid
|
|
5768
|
+
// shifting ancestor containers and causing the input box to jump.
|
|
5769
|
+
chatMessages.scrollTop = 0;
|
|
5734
5770
|
}
|
|
5735
5771
|
|
|
5736
5772
|
function updateInputPanelViewportSpacing() {
|
|
@@ -6469,9 +6505,6 @@
|
|
|
6469
6505
|
var rect = vk.boundingRect;
|
|
6470
6506
|
var kbHeight = rect ? rect.height : 0;
|
|
6471
6507
|
inputPanel.style.paddingBottom = kbHeight > 0 ? kbHeight + 'px' : '';
|
|
6472
|
-
if (kbHeight > 0 && document.activeElement === document.getElementById('input-box')) {
|
|
6473
|
-
scrollLatestMessageIntoView();
|
|
6474
|
-
}
|
|
6475
6508
|
});
|
|
6476
6509
|
}
|
|
6477
6510
|
|
|
@@ -6521,7 +6554,6 @@
|
|
|
6521
6554
|
}
|
|
6522
6555
|
|
|
6523
6556
|
if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
|
|
6524
|
-
scrollLatestMessageIntoView();
|
|
6525
6557
|
syncInputBoxScroll(inputBox);
|
|
6526
6558
|
}
|
|
6527
6559
|
|
|
@@ -6539,7 +6571,6 @@
|
|
|
6539
6571
|
}
|
|
6540
6572
|
|
|
6541
6573
|
vv.addEventListener('resize', debouncedUpdate);
|
|
6542
|
-
vv.addEventListener('scroll', debouncedUpdate);
|
|
6543
6574
|
|
|
6544
6575
|
updateViewport();
|
|
6545
6576
|
}
|
|
@@ -6668,6 +6699,16 @@
|
|
|
6668
6699
|
state.terminal = null;
|
|
6669
6700
|
}
|
|
6670
6701
|
state.fitAddon = null;
|
|
6702
|
+
state.serializeAddon = null;
|
|
6703
|
+
if (state.terminalDomView && state.terminalDomView.parentNode) {
|
|
6704
|
+
state.terminalDomView.parentNode.removeChild(state.terminalDomView);
|
|
6705
|
+
}
|
|
6706
|
+
state.terminalDomView = null;
|
|
6707
|
+
state._lastDomHtml = "";
|
|
6708
|
+
if (state.terminalDomUpdateTimer) {
|
|
6709
|
+
clearTimeout(state.terminalDomUpdateTimer);
|
|
6710
|
+
state.terminalDomUpdateTimer = null;
|
|
6711
|
+
}
|
|
6671
6712
|
state.terminalSessionId = null;
|
|
6672
6713
|
state.terminalOutput = "";
|
|
6673
6714
|
state.terminalViewportSize = { width: 0, height: 0 };
|
|
@@ -6845,6 +6886,7 @@
|
|
|
6845
6886
|
}
|
|
6846
6887
|
maybeScrollTerminalToBottom("output");
|
|
6847
6888
|
updateTerminalJumpToBottomButton();
|
|
6889
|
+
scheduleMobileDomUpdate();
|
|
6848
6890
|
} else if (Object.prototype.hasOwnProperty.call(msg.data, "output")) {
|
|
6849
6891
|
// Fallback: no chunk available, use full-output comparison
|
|
6850
6892
|
syncTerminalBuffer(msg.sessionId, msg.data.output || "", { mode: "append" });
|
|
@@ -7675,6 +7717,243 @@
|
|
|
7675
7717
|
}
|
|
7676
7718
|
});
|
|
7677
7719
|
});
|
|
7720
|
+
// Attach message-level copy buttons for touch devices
|
|
7721
|
+
attachMessageCopyButtons(container);
|
|
7722
|
+
}
|
|
7723
|
+
|
|
7724
|
+
// ===== Mobile message copy (long-press or tap copy button) =====
|
|
7725
|
+
var _msgCopyState = { timer: null, activeBtn: null };
|
|
7726
|
+
|
|
7727
|
+
function attachMessageCopyButtons(container) {
|
|
7728
|
+
var isTouch = window.matchMedia("(pointer: coarse)").matches;
|
|
7729
|
+
if (!isTouch) return;
|
|
7730
|
+
container.querySelectorAll(".chat-message").forEach(function(msgEl) {
|
|
7731
|
+
if (msgEl.querySelector(".msg-copy-btn")) return; // already attached
|
|
7732
|
+
var bubble = msgEl.querySelector(".chat-message-bubble");
|
|
7733
|
+
if (!bubble) return;
|
|
7734
|
+
var btn = document.createElement("button");
|
|
7735
|
+
btn.className = "msg-copy-btn";
|
|
7736
|
+
btn.textContent = "复制";
|
|
7737
|
+
btn.addEventListener("click", function(e) {
|
|
7738
|
+
e.stopPropagation();
|
|
7739
|
+
var text = bubble.innerText || bubble.textContent || "";
|
|
7740
|
+
navigator.clipboard.writeText(text.trim()).then(function() {
|
|
7741
|
+
btn.textContent = "已复制";
|
|
7742
|
+
btn.classList.add("copied");
|
|
7743
|
+
setTimeout(function() {
|
|
7744
|
+
btn.textContent = "复制";
|
|
7745
|
+
btn.classList.remove("copied");
|
|
7746
|
+
btn.classList.remove("visible");
|
|
7747
|
+
}, 1500);
|
|
7748
|
+
});
|
|
7749
|
+
});
|
|
7750
|
+
msgEl.appendChild(btn);
|
|
7751
|
+
});
|
|
7752
|
+
}
|
|
7753
|
+
|
|
7754
|
+
// Long-press to show copy button on chat messages
|
|
7755
|
+
(function initMobileCopyLongPress() {
|
|
7756
|
+
var isTouch = window.matchMedia("(pointer: coarse)").matches;
|
|
7757
|
+
if (!isTouch) return;
|
|
7758
|
+
|
|
7759
|
+
var longPressTimer = null;
|
|
7760
|
+
var touchStartY = 0;
|
|
7761
|
+
|
|
7762
|
+
document.addEventListener("touchstart", function(e) {
|
|
7763
|
+
var msgEl = e.target.closest(".chat-message");
|
|
7764
|
+
if (!msgEl) return;
|
|
7765
|
+
var bubble = msgEl.querySelector(".chat-message-bubble");
|
|
7766
|
+
if (!bubble) return;
|
|
7767
|
+
touchStartY = e.touches[0].clientY;
|
|
7768
|
+
longPressTimer = setTimeout(function() {
|
|
7769
|
+
var btn = msgEl.querySelector(".msg-copy-btn");
|
|
7770
|
+
if (btn) {
|
|
7771
|
+
// Hide any other visible copy buttons
|
|
7772
|
+
document.querySelectorAll(".msg-copy-btn.visible").forEach(function(b) {
|
|
7773
|
+
b.classList.remove("visible");
|
|
7774
|
+
});
|
|
7775
|
+
btn.classList.add("visible");
|
|
7776
|
+
}
|
|
7777
|
+
}, 500);
|
|
7778
|
+
}, { passive: true });
|
|
7779
|
+
|
|
7780
|
+
document.addEventListener("touchmove", function(e) {
|
|
7781
|
+
if (longPressTimer && Math.abs(e.touches[0].clientY - touchStartY) > 10) {
|
|
7782
|
+
clearTimeout(longPressTimer);
|
|
7783
|
+
longPressTimer = null;
|
|
7784
|
+
}
|
|
7785
|
+
}, { passive: true });
|
|
7786
|
+
|
|
7787
|
+
document.addEventListener("touchend", function() {
|
|
7788
|
+
if (longPressTimer) {
|
|
7789
|
+
clearTimeout(longPressTimer);
|
|
7790
|
+
longPressTimer = null;
|
|
7791
|
+
}
|
|
7792
|
+
}, { passive: true });
|
|
7793
|
+
|
|
7794
|
+
// Dismiss copy buttons when tapping elsewhere
|
|
7795
|
+
document.addEventListener("click", function(e) {
|
|
7796
|
+
if (!e.target.closest(".msg-copy-btn")) {
|
|
7797
|
+
document.querySelectorAll(".msg-copy-btn.visible").forEach(function(b) {
|
|
7798
|
+
b.classList.remove("visible");
|
|
7799
|
+
});
|
|
7800
|
+
}
|
|
7801
|
+
});
|
|
7802
|
+
})();
|
|
7803
|
+
|
|
7804
|
+
// ===== Terminal copy button for mobile =====
|
|
7805
|
+
// ===== Mobile DOM terminal view =====
|
|
7806
|
+
function initMobileDomTerminal(container) {
|
|
7807
|
+
var isTouch = window.matchMedia("(pointer: coarse)").matches;
|
|
7808
|
+
if (!isTouch) return;
|
|
7809
|
+
// Gated by experimental config flag
|
|
7810
|
+
if (!state.config || !state.config.experimentalDomTerminal) return;
|
|
7811
|
+
|
|
7812
|
+
// Create DOM view container
|
|
7813
|
+
var domView = document.createElement("div");
|
|
7814
|
+
domView.className = "terminal-dom-view active";
|
|
7815
|
+
container.appendChild(domView);
|
|
7816
|
+
|
|
7817
|
+
// Always set font-size explicitly to match xterm
|
|
7818
|
+
domView.style.fontSize = (state.terminalBaseFontSize * state.terminalScale) + "px";
|
|
7819
|
+
|
|
7820
|
+
// Hide xterm canvas but keep it in layout for FitAddon sizing.
|
|
7821
|
+
// Use opacity:0 + pointer-events:none so the element still occupies
|
|
7822
|
+
// space in the flex container and fit() can compute cols/rows correctly.
|
|
7823
|
+
setTimeout(function() {
|
|
7824
|
+
var xtermEl = container.querySelector(".xterm");
|
|
7825
|
+
if (xtermEl) {
|
|
7826
|
+
xtermEl.style.opacity = "0";
|
|
7827
|
+
xtermEl.style.pointerEvents = "none";
|
|
7828
|
+
}
|
|
7829
|
+
}, 100);
|
|
7830
|
+
|
|
7831
|
+
// Save reference
|
|
7832
|
+
state.terminalDomView = domView;
|
|
7833
|
+
state.terminalDomUpdateTimer = null;
|
|
7834
|
+
|
|
7835
|
+
// Scroll events for auto-follow
|
|
7836
|
+
domView.addEventListener("scroll", function() {
|
|
7837
|
+
var distance = domView.scrollHeight - domView.clientHeight - domView.scrollTop;
|
|
7838
|
+
if (distance <= state.terminalScrollThreshold) {
|
|
7839
|
+
state.terminalAutoFollow = true;
|
|
7840
|
+
clearTerminalScrollIdleTimer();
|
|
7841
|
+
updateTerminalJumpToBottomButton();
|
|
7842
|
+
} else {
|
|
7843
|
+
setTerminalManualScrollActive();
|
|
7844
|
+
}
|
|
7845
|
+
}, { passive: true });
|
|
7846
|
+
|
|
7847
|
+
domView.addEventListener("touchmove", function() {
|
|
7848
|
+
setTerminalManualScrollActive();
|
|
7849
|
+
}, { passive: true });
|
|
7850
|
+
|
|
7851
|
+
// Trigger initial render
|
|
7852
|
+
scheduleMobileDomUpdate();
|
|
7853
|
+
}
|
|
7854
|
+
|
|
7855
|
+
function updateMobileDomView() {
|
|
7856
|
+
if (!state.terminalDomView || !state.serializeAddon) return;
|
|
7857
|
+
|
|
7858
|
+
try {
|
|
7859
|
+
var html = state.serializeAddon.serializeAsHTML({
|
|
7860
|
+
includeGlobalBackground: true
|
|
7861
|
+
});
|
|
7862
|
+
|
|
7863
|
+
// Extract the <pre>...</pre> portion
|
|
7864
|
+
var match = html.match(/<pre[\s\S]*<\/pre>/);
|
|
7865
|
+
var preHtml = match ? match[0] : "";
|
|
7866
|
+
|
|
7867
|
+
// Strip inline font-size/font-family from the serialized HTML
|
|
7868
|
+
// so our CSS controls sizing and font consistently
|
|
7869
|
+
preHtml = preHtml.replace(/font-size:\s*[^;"']+;?/g, "");
|
|
7870
|
+
preHtml = preHtml.replace(/font-family:\s*[^;"']+;?/g, "");
|
|
7871
|
+
|
|
7872
|
+
// Fix colors for dark background
|
|
7873
|
+
preHtml = fixDarkTerminalColors(preHtml);
|
|
7874
|
+
|
|
7875
|
+
// Skip update if content unchanged
|
|
7876
|
+
if (preHtml === state._lastDomHtml) return;
|
|
7877
|
+
state._lastDomHtml = preHtml;
|
|
7878
|
+
|
|
7879
|
+
// Preserve scroll position for non-auto-follow mode
|
|
7880
|
+
var wasAtBottom = state.terminalAutoFollow;
|
|
7881
|
+
var scrollTop = state.terminalDomView.scrollTop;
|
|
7882
|
+
|
|
7883
|
+
state.terminalDomView.innerHTML = preHtml;
|
|
7884
|
+
|
|
7885
|
+
if (wasAtBottom) {
|
|
7886
|
+
state.terminalDomView.scrollTop = state.terminalDomView.scrollHeight;
|
|
7887
|
+
} else {
|
|
7888
|
+
state.terminalDomView.scrollTop = scrollTop;
|
|
7889
|
+
}
|
|
7890
|
+
} catch (e) {
|
|
7891
|
+
// Fallback: plain text if serialize fails
|
|
7892
|
+
if (state.terminal && state.terminal.buffer) {
|
|
7893
|
+
var buf = state.terminal.buffer.active;
|
|
7894
|
+
var lines = [];
|
|
7895
|
+
for (var i = 0; i < buf.length; i++) {
|
|
7896
|
+
var line = buf.getLine(i);
|
|
7897
|
+
if (line) lines.push(line.translateToString(true));
|
|
7898
|
+
}
|
|
7899
|
+
var text = lines.join("\n").replace(/\n+$/, "");
|
|
7900
|
+
state.terminalDomView.innerHTML = '<pre><div style="padding:8px 12px">' + escapeHtml(text) + '</div></pre>';
|
|
7901
|
+
if (state.terminalAutoFollow) {
|
|
7902
|
+
state.terminalDomView.scrollTop = state.terminalDomView.scrollHeight;
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
}
|
|
7907
|
+
|
|
7908
|
+
// Fix serialize addon's color issues on dark terminal background
|
|
7909
|
+
function fixDarkTerminalColors(html) {
|
|
7910
|
+
// Theme reference: bg=#1f1b17, fg=#f5eadc, black=#1f1b17, brightBlack=#625347
|
|
7911
|
+
// 1. Hardcoded inverse: black text on gray → theme fg on theme brightBlack bg
|
|
7912
|
+
html = html.replace(
|
|
7913
|
+
/color:\s*#000000;\s*background-color:\s*#BFBFBF/g,
|
|
7914
|
+
"color: #f5eadc; background-color: #625347"
|
|
7915
|
+
);
|
|
7916
|
+
// 2. Fix foreground colors that are too dark to read on #1f1b17 background.
|
|
7917
|
+
// Process each style attribute: split into declarations, fix only "color:" (not "background-color:").
|
|
7918
|
+
html = html.replace(/style='([^']*)'/g, function(_match, styles) {
|
|
7919
|
+
var parts = styles.split(";");
|
|
7920
|
+
for (var i = 0; i < parts.length; i++) {
|
|
7921
|
+
var decl = parts[i].trim();
|
|
7922
|
+
// Skip background-color declarations
|
|
7923
|
+
if (/^background-color\s*:/.test(decl)) continue;
|
|
7924
|
+
// Match standalone color declaration
|
|
7925
|
+
if (/^color\s*:/.test(decl)) {
|
|
7926
|
+
var hexMatch = decl.match(/#([0-9a-fA-F]{6})\b/);
|
|
7927
|
+
if (hexMatch && isColorTooDark("#" + hexMatch[1])) {
|
|
7928
|
+
parts[i] = parts[i].replace(/#[0-9a-fA-F]{6}/, "#625347");
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
return "style='" + parts.join(";") + "'";
|
|
7933
|
+
});
|
|
7934
|
+
return html;
|
|
7935
|
+
}
|
|
7936
|
+
|
|
7937
|
+
function isColorTooDark(hex) {
|
|
7938
|
+
// Parse hex color and check relative luminance
|
|
7939
|
+
var r = parseInt(hex.substring(1, 3), 16);
|
|
7940
|
+
var g = parseInt(hex.substring(3, 5), 16);
|
|
7941
|
+
var b = parseInt(hex.substring(5, 7), 16);
|
|
7942
|
+
// Simple perceived brightness: if below threshold, it's too dark for #1f1b17 bg
|
|
7943
|
+
var brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
|
7944
|
+
return brightness < 45; // #1f1b17 has brightness ~22, threshold catches colors close to it
|
|
7945
|
+
}
|
|
7946
|
+
|
|
7947
|
+
function scheduleMobileDomUpdate() {
|
|
7948
|
+
if (!state.terminalDomView) return;
|
|
7949
|
+
// Trailing-edge debounce: reset timer on each call to batch rapid updates
|
|
7950
|
+
if (state.terminalDomUpdateTimer) {
|
|
7951
|
+
clearTimeout(state.terminalDomUpdateTimer);
|
|
7952
|
+
}
|
|
7953
|
+
state.terminalDomUpdateTimer = setTimeout(function() {
|
|
7954
|
+
state.terminalDomUpdateTimer = null;
|
|
7955
|
+
updateMobileDomView();
|
|
7956
|
+
}, 150);
|
|
7678
7957
|
}
|
|
7679
7958
|
|
|
7680
7959
|
function parseMessages(output, command) {
|
|
@@ -2211,6 +2211,18 @@
|
|
|
2211
2211
|
.chat-message:hover {
|
|
2212
2212
|
transform: translateY(-1px);
|
|
2213
2213
|
}
|
|
2214
|
+
@media (hover: none) {
|
|
2215
|
+
.chat-message:hover {
|
|
2216
|
+
transform: none;
|
|
2217
|
+
}
|
|
2218
|
+
.chat-message:hover .chat-message-bubble {
|
|
2219
|
+
box-shadow: var(--shadow-sm);
|
|
2220
|
+
}
|
|
2221
|
+
.chat-message.assistant:hover .chat-message-bubble {
|
|
2222
|
+
border-color: var(--border-subtle);
|
|
2223
|
+
box-shadow: var(--shadow-sm);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2214
2226
|
|
|
2215
2227
|
.chat-message.user {
|
|
2216
2228
|
align-self: flex-end;
|
|
@@ -7177,4 +7189,102 @@
|
|
|
7177
7189
|
100% { background-position: -200% 0; }
|
|
7178
7190
|
}
|
|
7179
7191
|
|
|
7192
|
+
/* ===== 移动端文本选择与复制优化 ===== */
|
|
7193
|
+
.chat-message-bubble {
|
|
7194
|
+
-webkit-user-select: text;
|
|
7195
|
+
user-select: text;
|
|
7196
|
+
}
|
|
7197
|
+
|
|
7198
|
+
/* 消息气泡复制按钮 */
|
|
7199
|
+
.msg-copy-btn {
|
|
7200
|
+
display: none;
|
|
7201
|
+
position: absolute;
|
|
7202
|
+
top: -32px;
|
|
7203
|
+
right: 4px;
|
|
7204
|
+
padding: 4px 10px;
|
|
7205
|
+
font-size: 0.75rem;
|
|
7206
|
+
background: var(--bg-elevated);
|
|
7207
|
+
color: var(--text-secondary);
|
|
7208
|
+
border: 1px solid var(--border-default);
|
|
7209
|
+
border-radius: var(--radius-sm);
|
|
7210
|
+
box-shadow: var(--shadow-md);
|
|
7211
|
+
cursor: pointer;
|
|
7212
|
+
user-select: none;
|
|
7213
|
+
-webkit-user-select: none;
|
|
7214
|
+
z-index: 10;
|
|
7215
|
+
white-space: nowrap;
|
|
7216
|
+
-webkit-tap-highlight-color: transparent;
|
|
7217
|
+
touch-action: manipulation;
|
|
7218
|
+
}
|
|
7219
|
+
.msg-copy-btn.visible {
|
|
7220
|
+
display: block;
|
|
7221
|
+
}
|
|
7222
|
+
.msg-copy-btn.copied {
|
|
7223
|
+
background: var(--success-muted);
|
|
7224
|
+
color: var(--success);
|
|
7225
|
+
border-color: var(--success);
|
|
7226
|
+
}
|
|
7227
|
+
.chat-message {
|
|
7228
|
+
position: relative;
|
|
7229
|
+
}
|
|
7230
|
+
|
|
7231
|
+
/* ===== 移动端 DOM 终端视图 ===== */
|
|
7232
|
+
.terminal-dom-view {
|
|
7233
|
+
display: none;
|
|
7234
|
+
position: absolute;
|
|
7235
|
+
top: 0;
|
|
7236
|
+
left: 0;
|
|
7237
|
+
right: 0;
|
|
7238
|
+
bottom: 0;
|
|
7239
|
+
overflow-y: auto;
|
|
7240
|
+
overflow-x: hidden;
|
|
7241
|
+
-webkit-overflow-scrolling: touch;
|
|
7242
|
+
background: var(--bg-terminal);
|
|
7243
|
+
z-index: 1;
|
|
7244
|
+
font-size: 13px;
|
|
7245
|
+
}
|
|
7246
|
+
|
|
7247
|
+
.terminal-dom-view.active {
|
|
7248
|
+
display: block;
|
|
7249
|
+
}
|
|
7250
|
+
|
|
7251
|
+
.terminal-dom-view pre {
|
|
7252
|
+
margin: 0;
|
|
7253
|
+
padding: 0;
|
|
7254
|
+
background: transparent !important;
|
|
7255
|
+
font-family: "Geist Mono", "SF Mono", monospace !important;
|
|
7256
|
+
font-size: inherit !important;
|
|
7257
|
+
line-height: 1.5 !important;
|
|
7258
|
+
white-space: pre-wrap;
|
|
7259
|
+
overflow-wrap: break-word;
|
|
7260
|
+
word-break: normal;
|
|
7261
|
+
-webkit-user-select: text;
|
|
7262
|
+
user-select: text;
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
/* serializeAsHTML 外层 div 携带主题色,直接继承 */
|
|
7266
|
+
.terminal-dom-view pre > div {
|
|
7267
|
+
font-family: "Geist Mono", "SF Mono", monospace !important;
|
|
7268
|
+
font-size: inherit !important;
|
|
7269
|
+
line-height: 1.5 !important;
|
|
7270
|
+
padding: 8px 12px;
|
|
7271
|
+
}
|
|
7272
|
+
|
|
7273
|
+
/* serializeAsHTML 每行 div */
|
|
7274
|
+
.terminal-dom-view pre > div > div {
|
|
7275
|
+
min-height: 1.5em;
|
|
7276
|
+
}
|
|
7277
|
+
|
|
7278
|
+
/* span 继承终端字体 */
|
|
7279
|
+
.terminal-dom-view span {
|
|
7280
|
+
font-family: inherit !important;
|
|
7281
|
+
font-size: inherit !important;
|
|
7282
|
+
line-height: inherit !important;
|
|
7283
|
+
}
|
|
7284
|
+
|
|
7285
|
+
/* 确保按钮在 DOM 视图之上 */
|
|
7286
|
+
.terminal-jump-bottom {
|
|
7287
|
+
z-index: 20;
|
|
7288
|
+
}
|
|
7289
|
+
|
|
7180
7290
|
/* 结束标记 */
|
package/dist/web-ui/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export function renderApp(configPath) {
|
|
|
12
12
|
<html lang="zh-CN">
|
|
13
13
|
<head>
|
|
14
14
|
<meta charset="utf-8" />
|
|
15
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
16
16
|
<title>Wand Console</title>
|
|
17
17
|
<meta name="description" content="Local CLI Console for Vibe Coding - Manage terminal sessions from your browser" />
|
|
18
18
|
<meta name="theme-color" content="#f6f1e8" media="(prefers-color-scheme: light)" />
|
|
@@ -37,6 +37,7 @@ ${cssStyles}
|
|
|
37
37
|
<div id="app"></div>
|
|
38
38
|
${scriptOpen} src="/vendor/xterm/lib/xterm.js">${scriptClose}
|
|
39
39
|
${scriptOpen} src="/vendor/xterm-addon-fit/lib/addon-fit.js">${scriptClose}
|
|
40
|
+
${scriptOpen} src="/vendor/xterm-addon-serialize/lib/xterm-addon-serialize.js">${scriptClose}
|
|
40
41
|
${scriptOpen}>
|
|
41
42
|
${scriptContent}
|
|
42
43
|
${scriptClose}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@co0ontty/wand",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "A web terminal for local CLI tools like Claude.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"express": "^4.21.2",
|
|
39
39
|
"node-pty": "^1.1.0",
|
|
40
40
|
"ws": "^8.19.0",
|
|
41
|
-
"xterm": "^5.3.0"
|
|
41
|
+
"xterm": "^5.3.0",
|
|
42
|
+
"xterm-addon-serialize": "^0.11.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/express": "^4.17.21",
|