@co0ontty/wand 1.20.4 → 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/git-quick-commit.js +12 -4
- package/dist/models.d.ts +3 -1
- package/dist/models.js +45 -7
- package/dist/process-manager.d.ts +2 -0
- package/dist/process-manager.js +80 -17
- package/dist/pty-text-utils.d.ts +25 -1
- package/dist/pty-text-utils.js +158 -2
- package/dist/server-session-routes.d.ts +1 -1
- package/dist/server-session-routes.js +22 -8
- package/dist/server.d.ts +3 -0
- package/dist/server.js +49 -12
- 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/types.d.ts +13 -2
- package/dist/web-ui/content/scripts.js +680 -178
- package/dist/web-ui/content/styles.css +137 -41
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -1
- package/dist/ws-broadcast.js +74 -12
- package/package.json +1 -1
|
@@ -179,6 +179,14 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
179
179
|
* Find the end index of the echoed user input in the PTY buffer.
|
|
180
180
|
* The echo may contain ANSI codes between characters.
|
|
181
181
|
* Returns the index after the last character of the echo.
|
|
182
|
+
*
|
|
183
|
+
* Matching strategy:
|
|
184
|
+
* - Keep every printable codepoint of `userInput` (anything that is not a
|
|
185
|
+
* control char or whitespace) for comparison. The previous version dropped
|
|
186
|
+
* common symbols like `/`, `(`, `:`, space — which made commands such as
|
|
187
|
+
* `ls /tmp` mismatch and start parsing the chat response from a wrong offset.
|
|
188
|
+
* - In the buffer, skip ANSI escape sequences entirely, and skip whitespace
|
|
189
|
+
* so wrapped echoes (line continuation, padded columns) still align.
|
|
182
190
|
*/
|
|
183
191
|
private findEchoEndIndex;
|
|
184
192
|
private cleanForChat;
|
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
* 2. Structured messages for chat view (parsed)
|
|
8
8
|
*/
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
|
-
import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitConfirmSyntax, hasPermissionActionContext, scorePermissionLikelihood, FALLBACK_SCORE_THRESHOLD, isSlashCommandMenu } from "./pty-text-utils.js";
|
|
10
|
+
import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitConfirmSyntax, hasPermissionActionContext, scorePermissionLikelihood, FALLBACK_SCORE_THRESHOLD, isSlashCommandMenu, stripForEchoMatch, skipAnsiSequence } from "./pty-text-utils.js";
|
|
11
11
|
// ── Constants ──
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Hard cap on the in-memory PTY replay buffer. Aligned with the non-bridge
|
|
14
|
+
* branch of `ProcessManager.start()` so a session keeps the same amount of
|
|
15
|
+
* history regardless of which capture path is active.
|
|
16
|
+
*/
|
|
17
|
+
const OUTPUT_MAX_SIZE = 200000;
|
|
13
18
|
const SESSION_ID_WINDOW_SIZE = 16384;
|
|
14
19
|
const PERMISSION_WINDOW_SIZE = 2000;
|
|
15
20
|
const AUTO_APPROVE_DELAY_MS = 350;
|
|
@@ -832,27 +837,45 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
832
837
|
* Find the end index of the echoed user input in the PTY buffer.
|
|
833
838
|
* The echo may contain ANSI codes between characters.
|
|
834
839
|
* Returns the index after the last character of the echo.
|
|
840
|
+
*
|
|
841
|
+
* Matching strategy:
|
|
842
|
+
* - Keep every printable codepoint of `userInput` (anything that is not a
|
|
843
|
+
* control char or whitespace) for comparison. The previous version dropped
|
|
844
|
+
* common symbols like `/`, `(`, `:`, space — which made commands such as
|
|
845
|
+
* `ls /tmp` mismatch and start parsing the chat response from a wrong offset.
|
|
846
|
+
* - In the buffer, skip ANSI escape sequences entirely, and skip whitespace
|
|
847
|
+
* so wrapped echoes (line continuation, padded columns) still align.
|
|
835
848
|
*/
|
|
836
849
|
findEchoEndIndex(buffer, userInput) {
|
|
837
|
-
|
|
838
|
-
const inputChars = userInput.replace(/[^a-zA-Z0-9+=?!\-]/g, "");
|
|
850
|
+
const inputChars = stripForEchoMatch(userInput);
|
|
839
851
|
if (inputChars.length === 0)
|
|
840
852
|
return 0;
|
|
841
853
|
let matchedChars = 0;
|
|
842
854
|
let endIndex = 0;
|
|
843
|
-
|
|
855
|
+
let i = 0;
|
|
856
|
+
while (i < buffer.length && matchedChars < inputChars.length) {
|
|
844
857
|
const ch = buffer[i];
|
|
845
|
-
|
|
846
|
-
|
|
858
|
+
const code = ch.charCodeAt(0);
|
|
859
|
+
// Skip a complete ANSI escape sequence (CSI/OSC/etc.).
|
|
860
|
+
if (code === 0x1b) {
|
|
861
|
+
i = skipAnsiSequence(buffer, i);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
// Skip control chars and whitespace — they do not appear in `inputChars`.
|
|
865
|
+
if (code < 0x20 || code === 0x7f || code === 0x20) {
|
|
866
|
+
i++;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (ch.toLowerCase() === inputChars[matchedChars].toLowerCase()) {
|
|
847
870
|
matchedChars++;
|
|
848
871
|
endIndex = i + 1;
|
|
849
872
|
}
|
|
850
|
-
|
|
873
|
+
i++;
|
|
851
874
|
}
|
|
852
875
|
// Look for a newline or prompt marker after the echo
|
|
853
|
-
for (let
|
|
854
|
-
if (buffer[
|
|
855
|
-
endIndex =
|
|
876
|
+
for (let j = endIndex; j < buffer.length && j < endIndex + 50; j++) {
|
|
877
|
+
if (buffer[j] === "\n" || buffer[j] === "\r") {
|
|
878
|
+
endIndex = j + 1;
|
|
856
879
|
break;
|
|
857
880
|
}
|
|
858
881
|
}
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -341,9 +341,19 @@ export async function runQuickCommit(opts) {
|
|
|
341
341
|
if (push) {
|
|
342
342
|
try {
|
|
343
343
|
let hasUpstream = false;
|
|
344
|
+
let pushRemote = "origin";
|
|
344
345
|
try {
|
|
345
346
|
runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
|
|
346
347
|
hasUpstream = true;
|
|
348
|
+
try {
|
|
349
|
+
const currentBranch = runGit(["branch", "--show-current"], cwd);
|
|
350
|
+
if (currentBranch) {
|
|
351
|
+
pushRemote = runGit(["config", "--get", `branch.${currentBranch}.remote`], cwd) || "origin";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
pushRemote = "origin";
|
|
356
|
+
}
|
|
347
357
|
}
|
|
348
358
|
catch {
|
|
349
359
|
hasUpstream = false;
|
|
@@ -352,11 +362,9 @@ export async function runQuickCommit(opts) {
|
|
|
352
362
|
runGit(["push", "--recurse-submodules=on-demand"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
353
363
|
}
|
|
354
364
|
else {
|
|
355
|
-
runGit(["push", "-u", "--recurse-submodules=on-demand",
|
|
356
|
-
}
|
|
357
|
-
if (tagName) {
|
|
358
|
-
runGit(["push", "origin", `refs/tags/${tagName}`], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
365
|
+
runGit(["push", "-u", "--recurse-submodules=on-demand", pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
359
366
|
}
|
|
367
|
+
runGit(["push", pushRemote, "--tags"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
360
368
|
pushed = true;
|
|
361
369
|
}
|
|
362
370
|
catch (error) {
|
package/dist/models.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { ClaudeModelInfo } from "./types.js";
|
|
1
|
+
import { ClaudeModelInfo, SessionProvider } from "./types.js";
|
|
2
2
|
interface ModelCache {
|
|
3
3
|
models: ClaudeModelInfo[];
|
|
4
|
+
codexModels: ClaudeModelInfo[];
|
|
4
5
|
claudeVersion: string | null;
|
|
5
6
|
refreshedAt: string;
|
|
6
7
|
}
|
|
7
8
|
export declare function getCachedModels(): ModelCache;
|
|
8
9
|
export declare function refreshModels(): Promise<ModelCache>;
|
|
10
|
+
export declare function getModelsForProvider(provider: SessionProvider): ClaudeModelInfo[];
|
|
9
11
|
/** 返回可用于 claude CLI 的全部已知 model id(含别名) */
|
|
10
12
|
export declare function knownModelIds(): string[];
|
|
11
13
|
/** 判断传入值是否是已知模型;允许自由文本,因此总是返回 true。保留接口以便将来严格校验。 */
|
package/dist/models.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
const execAsync = promisify(exec);
|
|
4
|
-
const
|
|
4
|
+
const CLAUDE_MODELS = [
|
|
5
5
|
{ id: "default", label: "default(跟随 Claude Code 默认)", alias: true },
|
|
6
6
|
{ id: "opus", label: "opus(最新 Opus)", alias: true },
|
|
7
7
|
{ id: "sonnet", label: "sonnet(最新 Sonnet)", alias: true },
|
|
@@ -11,9 +11,12 @@ const BUILT_IN_MODELS = [
|
|
|
11
11
|
{ id: "claude-sonnet-4-6", label: "Sonnet 4.6 · claude-sonnet-4-6" },
|
|
12
12
|
{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5 · claude-haiku-4-5-20251001" },
|
|
13
13
|
];
|
|
14
|
+
const CODEX_FALLBACK_MODELS = [
|
|
15
|
+
{ id: "default", label: "default(跟随 Codex 默认)", alias: true },
|
|
16
|
+
];
|
|
14
17
|
let cache = null;
|
|
15
|
-
function
|
|
16
|
-
return
|
|
18
|
+
function cloneClaudeModels() {
|
|
19
|
+
return CLAUDE_MODELS.map((m) => ({ ...m }));
|
|
17
20
|
}
|
|
18
21
|
async function probeClaudeVersion() {
|
|
19
22
|
try {
|
|
@@ -25,10 +28,37 @@ async function probeClaudeVersion() {
|
|
|
25
28
|
return null;
|
|
26
29
|
}
|
|
27
30
|
}
|
|
31
|
+
async function probeCodexModels() {
|
|
32
|
+
try {
|
|
33
|
+
const { stdout } = await execAsync("codex debug models", { timeout: 8000 });
|
|
34
|
+
const data = JSON.parse(stdout);
|
|
35
|
+
const visible = data.models
|
|
36
|
+
.filter((m) => m.visibility === "list")
|
|
37
|
+
.sort((a, b) => (a.priority ?? 99) - (b.priority ?? 99));
|
|
38
|
+
if (!visible.length)
|
|
39
|
+
return CODEX_FALLBACK_MODELS.map((m) => ({ ...m }));
|
|
40
|
+
const result = [
|
|
41
|
+
{ id: "default", label: "default(跟随 Codex 默认)", alias: true },
|
|
42
|
+
];
|
|
43
|
+
for (const m of visible) {
|
|
44
|
+
result.push({
|
|
45
|
+
id: m.slug,
|
|
46
|
+
label: m.display_name && m.display_name !== m.slug
|
|
47
|
+
? `${m.display_name} · ${m.slug}`
|
|
48
|
+
: m.slug,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return CODEX_FALLBACK_MODELS.map((m) => ({ ...m }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
28
57
|
export function getCachedModels() {
|
|
29
58
|
if (!cache) {
|
|
30
59
|
cache = {
|
|
31
|
-
models:
|
|
60
|
+
models: cloneClaudeModels(),
|
|
61
|
+
codexModels: CODEX_FALLBACK_MODELS.map((m) => ({ ...m })),
|
|
32
62
|
claudeVersion: null,
|
|
33
63
|
refreshedAt: new Date().toISOString(),
|
|
34
64
|
};
|
|
@@ -36,17 +66,25 @@ export function getCachedModels() {
|
|
|
36
66
|
return cache;
|
|
37
67
|
}
|
|
38
68
|
export async function refreshModels() {
|
|
39
|
-
const version = await
|
|
69
|
+
const [version, codexModels] = await Promise.all([
|
|
70
|
+
probeClaudeVersion(),
|
|
71
|
+
probeCodexModels(),
|
|
72
|
+
]);
|
|
40
73
|
cache = {
|
|
41
|
-
models:
|
|
74
|
+
models: cloneClaudeModels(),
|
|
75
|
+
codexModels,
|
|
42
76
|
claudeVersion: version,
|
|
43
77
|
refreshedAt: new Date().toISOString(),
|
|
44
78
|
};
|
|
45
79
|
return cache;
|
|
46
80
|
}
|
|
81
|
+
export function getModelsForProvider(provider) {
|
|
82
|
+
const cached = getCachedModels();
|
|
83
|
+
return provider === "codex" ? cached.codexModels : cached.models;
|
|
84
|
+
}
|
|
47
85
|
/** 返回可用于 claude CLI 的全部已知 model id(含别名) */
|
|
48
86
|
export function knownModelIds() {
|
|
49
|
-
return
|
|
87
|
+
return CLAUDE_MODELS.map((m) => m.id);
|
|
50
88
|
}
|
|
51
89
|
/** 判断传入值是否是已知模型;允许自由文本,因此总是返回 true。保留接口以便将来严格校验。 */
|
|
52
90
|
export function isKnownModel(_value) {
|
|
@@ -50,6 +50,8 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
50
50
|
provider?: SessionProvider;
|
|
51
51
|
model?: string;
|
|
52
52
|
reuseId?: string;
|
|
53
|
+
cols?: number;
|
|
54
|
+
rows?: number;
|
|
53
55
|
}): SessionSnapshot;
|
|
54
56
|
list(): SessionSnapshot[];
|
|
55
57
|
/** Return lightweight snapshots for the session list (no output/messages). */
|
package/dist/process-manager.js
CHANGED
|
@@ -108,6 +108,10 @@ function hasRecentProjectActivity(candidate, startedAt) {
|
|
|
108
108
|
}
|
|
109
109
|
function selectClaudeProjectSessionForRecord(record) {
|
|
110
110
|
const knownMtimes = record.knownClaudeProjectMtimes ?? new Map();
|
|
111
|
+
// Only consider files created/touched AFTER this wand session started — those
|
|
112
|
+
// are the ones a fresh `claude` invocation could have produced. Files that
|
|
113
|
+
// existed before (knownMtimes entry present) are tolerated only if they
|
|
114
|
+
// grew since we observed them, but we de-prioritize them below.
|
|
111
115
|
const candidates = listClaudeProjectSessionCandidates(record.cwd)
|
|
112
116
|
.filter((candidate) => {
|
|
113
117
|
const previousMtime = knownMtimes.get(candidate.id);
|
|
@@ -125,7 +129,19 @@ function selectClaudeProjectSessionForRecord(record) {
|
|
|
125
129
|
if (!hasUserTurn) {
|
|
126
130
|
return null;
|
|
127
131
|
}
|
|
128
|
-
|
|
132
|
+
// Prefer brand-new files (id not in knownMtimes at session start). When more
|
|
133
|
+
// than one fresh candidate exists in parallel sessions, refuse to bind —
|
|
134
|
+
// mis-binding another session's history is worse than waiting for the bridge
|
|
135
|
+
// to capture the canonical session id from PTY output.
|
|
136
|
+
const fresh = candidates.filter((candidate) => !knownMtimes.has(candidate.id));
|
|
137
|
+
if (fresh.length === 1) {
|
|
138
|
+
return fresh[0];
|
|
139
|
+
}
|
|
140
|
+
if (fresh.length > 1) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
// Fallback: existing file that grew. Only bind if a single grown candidate.
|
|
144
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
129
145
|
}
|
|
130
146
|
function getLatestClaudeProjectSessionId(record) {
|
|
131
147
|
return selectClaudeProjectSessionForRecord(record)?.id ?? null;
|
|
@@ -407,7 +423,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
407
423
|
confirmWindow: "",
|
|
408
424
|
ptyPermissionBlocked: false,
|
|
409
425
|
lastAutoConfirmAt: 0,
|
|
410
|
-
|
|
426
|
+
// Preserve a user-toggled auto-approve setting across server restarts
|
|
427
|
+
// instead of recomputing it from the command/mode pair.
|
|
428
|
+
autoApprovePermissions: snapshot.autoApprovePermissions
|
|
429
|
+
?? this.shouldAutoApprovePermissions(snapshot.command, snapshot.mode, provider),
|
|
411
430
|
pendingEscalation: snapshot.pendingEscalation ?? null,
|
|
412
431
|
lastEscalationResult: snapshot.lastEscalationResult ?? null,
|
|
413
432
|
autonomyPolicy: snapshot.autonomyPolicy ?? this.defaultAutonomyPolicy(snapshot.mode),
|
|
@@ -426,7 +445,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
426
445
|
claudeTaskDiscoveryTimer: null,
|
|
427
446
|
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(updated.cwd) : undefined,
|
|
428
447
|
claudeSessionId: resumeCommandSessionId ?? updated.claudeSessionId,
|
|
429
|
-
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
448
|
+
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
449
|
+
ptyCols: snapshot.ptyCols ?? 120,
|
|
450
|
+
ptyRows: snapshot.ptyRows ?? 36,
|
|
430
451
|
});
|
|
431
452
|
this.orphanRecoveredCount += 1;
|
|
432
453
|
}
|
|
@@ -440,7 +461,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
440
461
|
confirmWindow: "",
|
|
441
462
|
ptyPermissionBlocked: false,
|
|
442
463
|
lastAutoConfirmAt: 0,
|
|
443
|
-
autoApprovePermissions:
|
|
464
|
+
autoApprovePermissions: snapshot.autoApprovePermissions
|
|
465
|
+
?? this.shouldAutoApprovePermissions(snapshot.command, snapshot.mode, provider),
|
|
444
466
|
pendingEscalation: snapshot.pendingEscalation ?? null,
|
|
445
467
|
lastEscalationResult: snapshot.lastEscalationResult ?? null,
|
|
446
468
|
autonomyPolicy: snapshot.autonomyPolicy ?? this.defaultAutonomyPolicy(snapshot.mode),
|
|
@@ -459,7 +481,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
459
481
|
claudeTaskDiscoveryTimer: null,
|
|
460
482
|
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(snapshot.cwd) : undefined,
|
|
461
483
|
claudeSessionId: resumeCommandSessionId ?? snapshot.claudeSessionId,
|
|
462
|
-
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
484
|
+
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
485
|
+
ptyCols: snapshot.ptyCols ?? 120,
|
|
486
|
+
ptyRows: snapshot.ptyRows ?? 36,
|
|
463
487
|
});
|
|
464
488
|
}
|
|
465
489
|
}
|
|
@@ -531,12 +555,22 @@ export class ProcessManager extends EventEmitter {
|
|
|
531
555
|
? path.resolve(process.cwd(), cwd)
|
|
532
556
|
: path.resolve(process.cwd(), this.config.defaultCwd);
|
|
533
557
|
const id = opts?.reuseId || randomUUID();
|
|
558
|
+
// When a session is being resumed under the same id, capture its prior
|
|
559
|
+
// structured messages so the new bridge can present them as the chat
|
|
560
|
+
// history. We deliberately do NOT carry over rawOutput — `claude --resume`
|
|
561
|
+
// re-prints its own banner and replayed history into the new PTY, and
|
|
562
|
+
// mixing the two would surface every line twice in the terminal view.
|
|
563
|
+
let priorMessages = [];
|
|
534
564
|
if (opts?.reuseId) {
|
|
535
565
|
const oldRecord = this.sessions.get(id);
|
|
536
566
|
if (oldRecord) {
|
|
567
|
+
priorMessages = oldRecord.ptyBridge?.getMessages() ?? oldRecord.messages ?? [];
|
|
537
568
|
this.cleanupRecord(oldRecord);
|
|
538
569
|
this.sessions.delete(id);
|
|
539
570
|
}
|
|
571
|
+
else {
|
|
572
|
+
priorMessages = this.storage.getSession(id)?.messages ?? [];
|
|
573
|
+
}
|
|
540
574
|
}
|
|
541
575
|
const worktreeSetup = opts?.worktreeEnabled
|
|
542
576
|
? prepareSessionWorktree({ cwd: baseCwd, sessionId: id })
|
|
@@ -588,7 +622,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
588
622
|
rememberedEscalationScopes: new Set(),
|
|
589
623
|
rememberedEscalationTargets: new Set(),
|
|
590
624
|
storedOutput: "",
|
|
591
|
-
messages:
|
|
625
|
+
messages: priorMessages,
|
|
592
626
|
childProcess: null,
|
|
593
627
|
ptyBridge: null,
|
|
594
628
|
currentTask: null,
|
|
@@ -599,6 +633,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
599
633
|
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
|
|
600
634
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
601
635
|
selectedModel: selectedModel ?? null,
|
|
636
|
+
ptyCols: opts?.cols !== undefined ? clampDimension(opts.cols, 20, 400) : 120,
|
|
637
|
+
ptyRows: opts?.rows !== undefined ? clampDimension(opts.rows, 10, 160) : 36,
|
|
602
638
|
};
|
|
603
639
|
if (isClaudeProvider) {
|
|
604
640
|
record.ptyBridge = new ClaudePtyBridge({
|
|
@@ -606,6 +642,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
606
642
|
isClaudeCommand: true,
|
|
607
643
|
autoApprove: record.autoApprovePermissions,
|
|
608
644
|
approvalPolicy: record.approvalPolicy,
|
|
645
|
+
initialMessages: priorMessages,
|
|
609
646
|
});
|
|
610
647
|
record.ptyBridge.on("event", (event) => {
|
|
611
648
|
this.handleBridgeEvent(record, event);
|
|
@@ -629,8 +666,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
629
666
|
WAND_AUTO_EDIT: effectiveMode === "auto-edit" ? "1" : "0"
|
|
630
667
|
},
|
|
631
668
|
name: "xterm-color",
|
|
632
|
-
|
|
633
|
-
|
|
669
|
+
// 使用 record 上由前端协商好的真实尺寸,避免"先 120 列、几百毫秒后再 resize"
|
|
670
|
+
// 期间 Claude/Codex 用错列宽渲染出 \x1b[120G 这类绝对列定位序列。
|
|
671
|
+
cols: record.ptyCols,
|
|
672
|
+
rows: record.ptyRows
|
|
634
673
|
});
|
|
635
674
|
}
|
|
636
675
|
catch (err) {
|
|
@@ -874,12 +913,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
874
913
|
*/
|
|
875
914
|
setSessionModel(id, model) {
|
|
876
915
|
const record = this.mustGet(id);
|
|
877
|
-
if (record.provider !== "claude") {
|
|
878
|
-
throw new Error("仅 Claude 会话支持切换模型。");
|
|
879
|
-
}
|
|
880
916
|
const normalized = model?.trim() || null;
|
|
881
917
|
record.selectedModel = normalized;
|
|
882
|
-
if (record.status === "running" && record.ptyProcess) {
|
|
918
|
+
if (record.provider === "claude" && record.status === "running" && record.ptyProcess) {
|
|
883
919
|
const value = normalized && normalized !== "default" ? normalized : "default";
|
|
884
920
|
record.ptyProcess.write(`/model ${value}\r`);
|
|
885
921
|
}
|
|
@@ -952,7 +988,20 @@ export class ProcessManager extends EventEmitter {
|
|
|
952
988
|
}
|
|
953
989
|
const safeCols = clampDimension(cols, 20, 400);
|
|
954
990
|
const safeRows = clampDimension(rows, 10, 160);
|
|
991
|
+
const changed = safeCols !== record.ptyCols || safeRows !== record.ptyRows;
|
|
955
992
|
record.ptyProcess.resize(safeCols, safeRows);
|
|
993
|
+
record.ptyCols = safeCols;
|
|
994
|
+
record.ptyRows = safeRows;
|
|
995
|
+
if (changed) {
|
|
996
|
+
// Notify every subscribed client of the new authoritative dimensions so
|
|
997
|
+
// any other tab/device can re-fit its terminal instead of rendering
|
|
998
|
+
// wrap-broken output sized for someone else's viewport.
|
|
999
|
+
this.emitEvent({
|
|
1000
|
+
type: "status",
|
|
1001
|
+
sessionId: id,
|
|
1002
|
+
data: { ptyCols: safeCols, ptyRows: safeRows },
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
956
1005
|
return this.snapshot(record);
|
|
957
1006
|
}
|
|
958
1007
|
stop(id) {
|
|
@@ -1151,6 +1200,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1151
1200
|
summary: deriveSessionSummary(messages, record.currentTask?.title ?? null),
|
|
1152
1201
|
currentTaskTitle: record.status === "running" ? record.currentTask?.title ?? undefined : undefined,
|
|
1153
1202
|
selectedModel: record.selectedModel ?? null,
|
|
1203
|
+
ptyCols: record.ptyCols,
|
|
1204
|
+
ptyRows: record.ptyRows,
|
|
1154
1205
|
};
|
|
1155
1206
|
}
|
|
1156
1207
|
/** Lightweight snapshot for list views — omits output and messages. */
|
|
@@ -1369,9 +1420,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
1369
1420
|
record.output = record.ptyBridge?.getRawOutput() ?? record.output;
|
|
1370
1421
|
const rawMessages = record.ptyBridge?.getMessages() ?? [];
|
|
1371
1422
|
const isStreaming = record.status === "running";
|
|
1423
|
+
const bridgeData = event.data;
|
|
1372
1424
|
const data = {
|
|
1373
1425
|
permissionBlocked: this.isPermissionBlocked(record),
|
|
1374
1426
|
};
|
|
1427
|
+
// 透传 bridge 给出的 isResponding(true=流式中, false=本轮已完成)。
|
|
1428
|
+
// 前端用它检测 thinking→idle 边界并主动做一次终端 resync,把 Claude/Codex
|
|
1429
|
+
// 在流式渲染过程中残留的错位光标定位序列洗掉(等价于按一次右上角缩放)。
|
|
1430
|
+
if (bridgeData && typeof bridgeData.isResponding === "boolean") {
|
|
1431
|
+
data.isResponding = bridgeData.isResponding;
|
|
1432
|
+
}
|
|
1375
1433
|
if (isStreaming && rawMessages.length > 0) {
|
|
1376
1434
|
data.incremental = true;
|
|
1377
1435
|
const lastTurn = rawMessages[rawMessages.length - 1];
|
|
@@ -1523,13 +1581,18 @@ export class ProcessManager extends EventEmitter {
|
|
|
1523
1581
|
}
|
|
1524
1582
|
processCommandForMode(command, mode, provider, model) {
|
|
1525
1583
|
if (provider === "codex") {
|
|
1526
|
-
|
|
1527
|
-
|
|
1584
|
+
let result = command;
|
|
1585
|
+
const trimmedModel = model?.trim();
|
|
1586
|
+
if (trimmedModel && trimmedModel !== "default" && !/--model\s/.test(command) && !/-m\s/.test(command)) {
|
|
1587
|
+
const escapedModel = trimmedModel.replace(/'/g, "'\\''");
|
|
1588
|
+
result += ` --model '${escapedModel}'`;
|
|
1528
1589
|
}
|
|
1529
|
-
if (
|
|
1530
|
-
|
|
1590
|
+
if (mode === "full-access") {
|
|
1591
|
+
if (!/--dangerously-bypass-approvals-and-sandbox(?:\s|$)/.test(result)) {
|
|
1592
|
+
result += " --dangerously-bypass-approvals-and-sandbox";
|
|
1593
|
+
}
|
|
1531
1594
|
}
|
|
1532
|
-
return
|
|
1595
|
+
return result;
|
|
1533
1596
|
}
|
|
1534
1597
|
const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
|
|
1535
1598
|
if (!isClaudeCmd)
|
package/dist/pty-text-utils.d.ts
CHANGED
|
@@ -5,8 +5,32 @@
|
|
|
5
5
|
export declare function stripAnsi(text: string): string;
|
|
6
6
|
/** Lines considered as UI noise that should be excluded from chat view. */
|
|
7
7
|
export declare function isNoiseLine(line: string): boolean;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Append text to a windowed buffer, trimming from start if over max size.
|
|
10
|
+
*
|
|
11
|
+
* The cut point is chosen so it never lands inside:
|
|
12
|
+
* - a UTF-16 surrogate pair (would corrupt the leading codepoint)
|
|
13
|
+
* - an unterminated ANSI escape sequence (would feed orphan "[31m..."
|
|
14
|
+
* text to a downstream terminal renderer)
|
|
15
|
+
*
|
|
16
|
+
* The returned buffer may be slightly shorter than maxSize.
|
|
17
|
+
*/
|
|
9
18
|
export declare function appendWindow(buffer: string, chunk: string, maxSize: number): string;
|
|
19
|
+
/** Slice keeping the last ~maxSize chars on a safe boundary. Exported for tests. */
|
|
20
|
+
export declare function safeSliceTail(text: string, maxSize: number): string;
|
|
21
|
+
/**
|
|
22
|
+
* Strip a string down to the printable codepoints used for echo matching.
|
|
23
|
+
* Removes control characters, whitespace and ANSI escapes; keeps all other
|
|
24
|
+
* visible characters (including `/`, `()`, `:`, CJK, emoji, etc.) so that
|
|
25
|
+
* echo alignment works for any user input.
|
|
26
|
+
*/
|
|
27
|
+
export declare function stripForEchoMatch(input: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Given an index pointing at ESC (0x1b), return the index of the first
|
|
30
|
+
* character AFTER the escape sequence. Handles CSI, OSC and simple ESC-
|
|
31
|
+
* letter forms. Returns idx+1 if nothing matches (best-effort skip).
|
|
32
|
+
*/
|
|
33
|
+
export declare function skipAnsiSequence(text: string, idx: number): number;
|
|
10
34
|
export declare function hasExplicitConfirmSyntax(normalized: string): boolean;
|
|
11
35
|
export declare function hasPermissionActionContext(normalized: string): boolean;
|
|
12
36
|
/**
|
package/dist/pty-text-utils.js
CHANGED
|
@@ -115,10 +115,166 @@ export function isNoiseLine(line) {
|
|
|
115
115
|
return true;
|
|
116
116
|
return false;
|
|
117
117
|
}
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Append text to a windowed buffer, trimming from start if over max size.
|
|
120
|
+
*
|
|
121
|
+
* The cut point is chosen so it never lands inside:
|
|
122
|
+
* - a UTF-16 surrogate pair (would corrupt the leading codepoint)
|
|
123
|
+
* - an unterminated ANSI escape sequence (would feed orphan "[31m..."
|
|
124
|
+
* text to a downstream terminal renderer)
|
|
125
|
+
*
|
|
126
|
+
* The returned buffer may be slightly shorter than maxSize.
|
|
127
|
+
*/
|
|
119
128
|
export function appendWindow(buffer, chunk, maxSize) {
|
|
120
129
|
const next = buffer + chunk;
|
|
121
|
-
|
|
130
|
+
if (next.length <= maxSize)
|
|
131
|
+
return next;
|
|
132
|
+
return safeSliceTail(next, maxSize);
|
|
133
|
+
}
|
|
134
|
+
/** Slice keeping the last ~maxSize chars on a safe boundary. Exported for tests. */
|
|
135
|
+
export function safeSliceTail(text, maxSize) {
|
|
136
|
+
if (text.length <= maxSize)
|
|
137
|
+
return text;
|
|
138
|
+
let start = text.length - maxSize;
|
|
139
|
+
// 1. Skip UTF-16 low surrogate half so we don't strand a high surrogate.
|
|
140
|
+
if (start > 0 && start < text.length) {
|
|
141
|
+
const code = text.charCodeAt(start);
|
|
142
|
+
if (code >= 0xdc00 && code <= 0xdfff)
|
|
143
|
+
start++;
|
|
144
|
+
}
|
|
145
|
+
// 2. Prefer cutting at the next newline within a small lookahead window.
|
|
146
|
+
// Newlines are always safe boundaries (no ANSI sequence spans a newline
|
|
147
|
+
// in well-formed terminal output) and keep lines aligned for replay.
|
|
148
|
+
const LOOKAHEAD = 4096;
|
|
149
|
+
const upper = Math.min(start + LOOKAHEAD, text.length);
|
|
150
|
+
for (let i = start; i < upper; i++) {
|
|
151
|
+
if (text.charCodeAt(i) === 0x0a)
|
|
152
|
+
return text.slice(i + 1);
|
|
153
|
+
}
|
|
154
|
+
// 3. No nearby newline. Detect whether `start` lands inside an open ANSI
|
|
155
|
+
// escape sequence by scanning backward for an ESC (0x1b). If we find one
|
|
156
|
+
// that is not yet terminated, advance past the sequence's final byte.
|
|
157
|
+
const lookback = Math.max(0, start - 256);
|
|
158
|
+
let escAt = -1;
|
|
159
|
+
for (let i = start - 1; i >= lookback; i--) {
|
|
160
|
+
const code = text.charCodeAt(i);
|
|
161
|
+
if (code === 0x1b) {
|
|
162
|
+
escAt = i;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
// If we hit a terminator before an ESC, the previous sequence is closed.
|
|
166
|
+
if (code === 0x07)
|
|
167
|
+
break;
|
|
168
|
+
if (code >= 0x40 && code <= 0x7e && i > 0 && isLikelyAnsiBody(text, i - 1))
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (escAt !== -1) {
|
|
172
|
+
// OSC 序列以 `ESC ]` (0x1b 0x5d) 开头,必须用 BEL (0x07) 或
|
|
173
|
+
// ST (`ESC \\` = 0x1b 0x5c) 终止。其它范围内字节(包括裸 `\`)
|
|
174
|
+
// 都属于 payload,不能当终止符。CSI 等序列才用 0x40-0x7e final byte。
|
|
175
|
+
const isOsc = escAt + 1 < text.length && text.charCodeAt(escAt + 1) === 0x5d;
|
|
176
|
+
let terminated = false;
|
|
177
|
+
for (let i = escAt + 1; i < start; i++) {
|
|
178
|
+
const code = text.charCodeAt(i);
|
|
179
|
+
if (code === 0x07) {
|
|
180
|
+
terminated = true;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (isOsc) {
|
|
184
|
+
if (code === 0x1b && i + 1 < start && text.charCodeAt(i + 1) === 0x5c) {
|
|
185
|
+
terminated = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
191
|
+
terminated = true;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!terminated) {
|
|
196
|
+
const ansiUpper = Math.min(start + 256, text.length);
|
|
197
|
+
for (let i = start; i < ansiUpper; i++) {
|
|
198
|
+
const code = text.charCodeAt(i);
|
|
199
|
+
if (code === 0x07)
|
|
200
|
+
return text.slice(i + 1);
|
|
201
|
+
if (isOsc) {
|
|
202
|
+
if (code === 0x1b && i + 1 < ansiUpper && text.charCodeAt(i + 1) === 0x5c) {
|
|
203
|
+
return text.slice(i + 2);
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
208
|
+
return text.slice(i + 1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return text.slice(start);
|
|
213
|
+
}
|
|
214
|
+
function isLikelyAnsiBody(text, idx) {
|
|
215
|
+
// CSI parameter/intermediate range covers most common ANSI bodies.
|
|
216
|
+
const code = text.charCodeAt(idx);
|
|
217
|
+
return code === 0x5b /* [ */ || code === 0x3f /* ? */ || (code >= 0x30 && code <= 0x3f);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Strip a string down to the printable codepoints used for echo matching.
|
|
221
|
+
* Removes control characters, whitespace and ANSI escapes; keeps all other
|
|
222
|
+
* visible characters (including `/`, `()`, `:`, CJK, emoji, etc.) so that
|
|
223
|
+
* echo alignment works for any user input.
|
|
224
|
+
*/
|
|
225
|
+
export function stripForEchoMatch(input) {
|
|
226
|
+
let out = "";
|
|
227
|
+
for (let i = 0; i < input.length; i++) {
|
|
228
|
+
const code = input.charCodeAt(i);
|
|
229
|
+
if (code === 0x1b) {
|
|
230
|
+
i = skipAnsiSequence(input, i) - 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (code < 0x20 || code === 0x7f)
|
|
234
|
+
continue;
|
|
235
|
+
if (code === 0x20)
|
|
236
|
+
continue;
|
|
237
|
+
out += input[i];
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Given an index pointing at ESC (0x1b), return the index of the first
|
|
243
|
+
* character AFTER the escape sequence. Handles CSI, OSC and simple ESC-
|
|
244
|
+
* letter forms. Returns idx+1 if nothing matches (best-effort skip).
|
|
245
|
+
*/
|
|
246
|
+
export function skipAnsiSequence(text, idx) {
|
|
247
|
+
if (text.charCodeAt(idx) !== 0x1b)
|
|
248
|
+
return idx;
|
|
249
|
+
const next = text.charCodeAt(idx + 1);
|
|
250
|
+
if (Number.isNaN(next))
|
|
251
|
+
return idx + 1;
|
|
252
|
+
// CSI: ESC [ ... final-byte (0x40-0x7E)
|
|
253
|
+
if (next === 0x5b /* [ */) {
|
|
254
|
+
let i = idx + 2;
|
|
255
|
+
while (i < text.length) {
|
|
256
|
+
const code = text.charCodeAt(i);
|
|
257
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
258
|
+
return i + 1;
|
|
259
|
+
i++;
|
|
260
|
+
}
|
|
261
|
+
return text.length;
|
|
262
|
+
}
|
|
263
|
+
// OSC: ESC ] ... terminator (BEL or ESC \)
|
|
264
|
+
if (next === 0x5d /* ] */) {
|
|
265
|
+
let i = idx + 2;
|
|
266
|
+
while (i < text.length) {
|
|
267
|
+
const code = text.charCodeAt(i);
|
|
268
|
+
if (code === 0x07)
|
|
269
|
+
return i + 1;
|
|
270
|
+
if (code === 0x1b && text.charCodeAt(i + 1) === 0x5c)
|
|
271
|
+
return i + 2;
|
|
272
|
+
i++;
|
|
273
|
+
}
|
|
274
|
+
return text.length;
|
|
275
|
+
}
|
|
276
|
+
// Two-character ESC sequences (ESC = / ESC > / ESC M / etc.)
|
|
277
|
+
return idx + 2;
|
|
122
278
|
}
|
|
123
279
|
const EXPLICIT_CONFIRM_PATTERNS = [
|
|
124
280
|
/(?:^|\b)(?:press\s+)?(?:y|yes)\s*(?:\/|\bor\b)\s*(?:n|no)(?:\b|$)/i,
|