@co0ontty/wand 1.20.4 → 1.21.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-pty-bridge.d.ts +5 -2
- package/dist/claude-pty-bridge.js +26 -13
- package/dist/config.js +2 -0
- 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 +82 -19
- package/dist/pty-text-utils.d.ts +31 -1
- package/dist/pty-text-utils.js +164 -2
- package/dist/server-session-routes.d.ts +1 -1
- package/dist/server-session-routes.js +31 -11
- package/dist/server.d.ts +3 -0
- package/dist/server.js +54 -13
- package/dist/session-logger.d.ts +18 -5
- package/dist/session-logger.js +81 -20
- package/dist/structured-session-manager.d.ts +45 -2
- package/dist/structured-session-manager.js +1010 -35
- package/dist/types.d.ts +15 -2
- package/dist/web-ui/content/scripts.js +785 -238
- 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.d.ts +6 -0
- package/dist/ws-broadcast.js +69 -20
- package/package.json +2 -1
|
@@ -177,8 +177,11 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
177
177
|
private finalizeResponse;
|
|
178
178
|
/**
|
|
179
179
|
* Find the end index of the echoed user input in the PTY buffer.
|
|
180
|
-
*
|
|
181
|
-
*
|
|
180
|
+
* Returns 0 if the echo cannot be fully matched.
|
|
181
|
+
*
|
|
182
|
+
* Why: ANSI escapes and whitespace can interleave the echoed characters
|
|
183
|
+
* (line wrapping, padding, color codes), so matching skips them while
|
|
184
|
+
* comparing every printable codepoint of `userInput` in order.
|
|
182
185
|
*/
|
|
183
186
|
private findEchoEndIndex;
|
|
184
187
|
private cleanForChat;
|
|
@@ -7,9 +7,9 @@
|
|
|
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, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
|
|
11
11
|
// ── Constants ──
|
|
12
|
-
const OUTPUT_MAX_SIZE =
|
|
12
|
+
const OUTPUT_MAX_SIZE = PTY_OUTPUT_MAX_SIZE;
|
|
13
13
|
const SESSION_ID_WINDOW_SIZE = 16384;
|
|
14
14
|
const PERMISSION_WINDOW_SIZE = 2000;
|
|
15
15
|
const AUTO_APPROVE_DELAY_MS = 350;
|
|
@@ -830,29 +830,42 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
830
830
|
// ── Text Processing Utilities ──
|
|
831
831
|
/**
|
|
832
832
|
* Find the end index of the echoed user input in the PTY buffer.
|
|
833
|
-
*
|
|
834
|
-
*
|
|
833
|
+
* Returns 0 if the echo cannot be fully matched.
|
|
834
|
+
*
|
|
835
|
+
* Why: ANSI escapes and whitespace can interleave the echoed characters
|
|
836
|
+
* (line wrapping, padding, color codes), so matching skips them while
|
|
837
|
+
* comparing every printable codepoint of `userInput` in order.
|
|
835
838
|
*/
|
|
836
839
|
findEchoEndIndex(buffer, userInput) {
|
|
837
|
-
|
|
838
|
-
const inputChars = userInput.replace(/[^a-zA-Z0-9+=?!\-]/g, "");
|
|
840
|
+
const inputChars = stripForEchoMatch(userInput);
|
|
839
841
|
if (inputChars.length === 0)
|
|
840
842
|
return 0;
|
|
841
843
|
let matchedChars = 0;
|
|
842
844
|
let endIndex = 0;
|
|
843
|
-
|
|
845
|
+
let i = 0;
|
|
846
|
+
while (i < buffer.length && matchedChars < inputChars.length) {
|
|
844
847
|
const ch = buffer[i];
|
|
845
|
-
|
|
846
|
-
|
|
848
|
+
const code = ch.charCodeAt(0);
|
|
849
|
+
// Skip a complete ANSI escape sequence (CSI/OSC/etc.).
|
|
850
|
+
if (code === 0x1b) {
|
|
851
|
+
i = skipAnsiSequence(buffer, i);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
// Skip control chars and whitespace — they do not appear in `inputChars`.
|
|
855
|
+
if (code < 0x20 || code === 0x7f || code === 0x20) {
|
|
856
|
+
i++;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (ch.toLowerCase() === inputChars[matchedChars].toLowerCase()) {
|
|
847
860
|
matchedChars++;
|
|
848
861
|
endIndex = i + 1;
|
|
849
862
|
}
|
|
850
|
-
|
|
863
|
+
i++;
|
|
851
864
|
}
|
|
852
865
|
// Look for a newline or prompt marker after the echo
|
|
853
|
-
for (let
|
|
854
|
-
if (buffer[
|
|
855
|
-
endIndex =
|
|
866
|
+
for (let j = endIndex; j < buffer.length && j < endIndex + 50; j++) {
|
|
867
|
+
if (buffer[j] === "\n" || buffer[j] === "\r") {
|
|
868
|
+
endIndex = j + 1;
|
|
856
869
|
break;
|
|
857
870
|
}
|
|
858
871
|
}
|
package/dist/config.js
CHANGED
|
@@ -20,6 +20,7 @@ export const defaultConfig = () => ({
|
|
|
20
20
|
android: defaultAndroidApkConfig(),
|
|
21
21
|
cardDefaults: defaultCardExpandDefaults(),
|
|
22
22
|
defaultModel: "",
|
|
23
|
+
structuredRunner: "cli",
|
|
23
24
|
commandPresets: [
|
|
24
25
|
{
|
|
25
26
|
label: "Claude",
|
|
@@ -185,6 +186,7 @@ function mergeWithDefaults(input) {
|
|
|
185
186
|
android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
|
|
186
187
|
cardDefaults: normalizeCardDefaults(input.cardDefaults),
|
|
187
188
|
defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
|
|
189
|
+
structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
|
|
188
190
|
};
|
|
189
191
|
}
|
|
190
192
|
export function isExecutionMode(value) {
|
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
|
@@ -8,7 +8,7 @@ import pty from "node-pty";
|
|
|
8
8
|
import { SessionLogger } from "./session-logger.js";
|
|
9
9
|
import { ClaudePtyBridge } from "./claude-pty-bridge.js";
|
|
10
10
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
11
|
-
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText } from "./pty-text-utils.js";
|
|
11
|
+
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
|
|
12
12
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
13
13
|
import { getResumeCommandSessionId } from "./resume-policy.js";
|
|
14
14
|
function resolveProviderFromCommand(command) {
|
|
@@ -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) {
|
|
@@ -705,7 +744,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
705
744
|
rec.output = rec.ptyBridge.getRawOutput();
|
|
706
745
|
}
|
|
707
746
|
else {
|
|
708
|
-
rec.output = appendWindow(rec.output, chunk,
|
|
747
|
+
rec.output = appendWindow(rec.output, chunk, PTY_OUTPUT_MAX_SIZE);
|
|
709
748
|
}
|
|
710
749
|
this.logger.appendPtyOutput(id, chunk);
|
|
711
750
|
if (!rec.ptyBridge) {
|
|
@@ -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
|
@@ -1,12 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared PTY text processing utilities for consistent ANSI stripping and noise filtering.
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Hard cap on the in-memory PTY replay buffer. Shared between ProcessManager
|
|
6
|
+
* and ClaudePtyBridge so a session keeps the same amount of history regardless
|
|
7
|
+
* of which capture path is active.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PTY_OUTPUT_MAX_SIZE = 200000;
|
|
4
10
|
/** Strip ANSI escape sequences and control characters from raw PTY output. */
|
|
5
11
|
export declare function stripAnsi(text: string): string;
|
|
6
12
|
/** Lines considered as UI noise that should be excluded from chat view. */
|
|
7
13
|
export declare function isNoiseLine(line: string): boolean;
|
|
8
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Append text to a windowed buffer, trimming from start if over max size.
|
|
16
|
+
*
|
|
17
|
+
* The cut point is chosen so it never lands inside:
|
|
18
|
+
* - a UTF-16 surrogate pair (would corrupt the leading codepoint)
|
|
19
|
+
* - an unterminated ANSI escape sequence (would feed orphan "[31m..."
|
|
20
|
+
* text to a downstream terminal renderer)
|
|
21
|
+
*
|
|
22
|
+
* The returned buffer may be slightly shorter than maxSize.
|
|
23
|
+
*/
|
|
9
24
|
export declare function appendWindow(buffer: string, chunk: string, maxSize: number): string;
|
|
25
|
+
/** Slice keeping the last ~maxSize chars on a safe boundary. Exported for tests. */
|
|
26
|
+
export declare function safeSliceTail(text: string, maxSize: number): string;
|
|
27
|
+
/**
|
|
28
|
+
* Strip a string down to the printable codepoints used for echo matching.
|
|
29
|
+
* Removes control characters, whitespace and ANSI escapes; keeps all other
|
|
30
|
+
* visible characters (including `/`, `()`, `:`, CJK, emoji, etc.) so that
|
|
31
|
+
* echo alignment works for any user input.
|
|
32
|
+
*/
|
|
33
|
+
export declare function stripForEchoMatch(input: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Given an index pointing at ESC (0x1b), return the index of the first
|
|
36
|
+
* character AFTER the escape sequence. Handles CSI, OSC and simple ESC-
|
|
37
|
+
* letter forms. Returns idx+1 if nothing matches (best-effort skip).
|
|
38
|
+
*/
|
|
39
|
+
export declare function skipAnsiSequence(text: string, idx: number): number;
|
|
10
40
|
export declare function hasExplicitConfirmSyntax(normalized: string): boolean;
|
|
11
41
|
export declare function hasPermissionActionContext(normalized: string): boolean;
|
|
12
42
|
/**
|