@co0ontty/wand 1.15.1 → 1.17.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 +3 -0
- package/dist/claude-pty-bridge.js +35 -1
- package/dist/config.js +2 -0
- package/dist/models.d.ts +13 -0
- package/dist/models.js +54 -0
- package/dist/process-manager.d.ts +7 -0
- package/dist/process-manager.js +47 -15
- package/dist/pty-text-utils.d.ts +7 -0
- package/dist/pty-text-utils.js +14 -0
- package/dist/pwa.js +2 -4
- package/dist/server-session-routes.js +25 -1
- package/dist/server.js +113 -15
- package/dist/structured-session-manager.d.ts +4 -0
- package/dist/structured-session-manager.js +30 -1
- package/dist/types.d.ts +16 -0
- package/dist/upload-routes.d.ts +3 -0
- package/dist/upload-routes.js +53 -0
- package/dist/web-ui/content/scripts.js +950 -517
- package/dist/web-ui/content/styles.css +312 -118
- package/dist/web-ui/content/vendor/wterm/terminal.css +162 -0
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -0
- package/dist/web-ui/index.js +2 -4
- package/dist/ws-broadcast.js +12 -7
- package/package.json +6 -5
|
@@ -85,6 +85,8 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
85
85
|
private _exited;
|
|
86
86
|
private rememberedScopes;
|
|
87
87
|
private rememberedTargets;
|
|
88
|
+
private lastChatEmitAt;
|
|
89
|
+
private chatEmitTimer;
|
|
88
90
|
private lastOutputAt;
|
|
89
91
|
private lastUserInputAt;
|
|
90
92
|
private idleProbeTimer;
|
|
@@ -170,6 +172,7 @@ export declare class ClaudePtyBridge extends EventEmitter {
|
|
|
170
172
|
private inferScope;
|
|
171
173
|
private parseChatResponse;
|
|
172
174
|
private detectCompletion;
|
|
175
|
+
private static readonly CHAT_THROTTLE_MS;
|
|
173
176
|
private updateAssistantContent;
|
|
174
177
|
private finalizeResponse;
|
|
175
178
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
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 } from "./pty-text-utils.js";
|
|
10
|
+
import { stripAnsi, isNoiseLine, appendWindow, normalizePromptText, hasExplicitConfirmSyntax, hasPermissionActionContext, scorePermissionLikelihood, FALLBACK_SCORE_THRESHOLD, isSlashCommandMenu } from "./pty-text-utils.js";
|
|
11
11
|
// ── Constants ──
|
|
12
12
|
const OUTPUT_MAX_SIZE = 120000;
|
|
13
13
|
const SESSION_ID_WINDOW_SIZE = 16384;
|
|
@@ -69,6 +69,9 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
69
69
|
// Permission memory for "approve_turn" policy
|
|
70
70
|
rememberedScopes = new Set();
|
|
71
71
|
rememberedTargets = new Set();
|
|
72
|
+
// Chat event throttle
|
|
73
|
+
lastChatEmitAt = 0;
|
|
74
|
+
chatEmitTimer = null;
|
|
72
75
|
// Idle probe state (last-resort robustness)
|
|
73
76
|
lastOutputAt;
|
|
74
77
|
lastUserInputAt;
|
|
@@ -202,6 +205,11 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
202
205
|
clearTimeout(this.taskDebounceTimer);
|
|
203
206
|
this.taskDebounceTimer = null;
|
|
204
207
|
}
|
|
208
|
+
// Flush pending chat emit
|
|
209
|
+
if (this.chatEmitTimer) {
|
|
210
|
+
clearTimeout(this.chatEmitTimer);
|
|
211
|
+
this.chatEmitTimer = null;
|
|
212
|
+
}
|
|
205
213
|
// Clear permission state — prevents stale blocked state after exit
|
|
206
214
|
this.cancelPendingAutoApprove();
|
|
207
215
|
this.clearIdleProbeTimer();
|
|
@@ -655,6 +663,10 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
655
663
|
this.ptyWrite("\r");
|
|
656
664
|
}
|
|
657
665
|
isPermissionPromptDetected(normalized) {
|
|
666
|
+
// Slash-command selection menus (/model, /effort, /output-style …) share
|
|
667
|
+
// "Enter to confirm" with permission prompts but must be left alone.
|
|
668
|
+
if (isSlashCommandMenu(normalized))
|
|
669
|
+
return false;
|
|
658
670
|
const hasIntent = /\bdo you want to\b/i.test(normalized)
|
|
659
671
|
|| /\bwould you like to\b/i.test(normalized)
|
|
660
672
|
|| /\benter to confirm\b/i.test(normalized)
|
|
@@ -737,6 +749,7 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
737
749
|
}
|
|
738
750
|
return false;
|
|
739
751
|
}
|
|
752
|
+
static CHAT_THROTTLE_MS = 80;
|
|
740
753
|
updateAssistantContent() {
|
|
741
754
|
const idx = this.chatState.assistantIndex;
|
|
742
755
|
if (idx === null)
|
|
@@ -745,6 +758,27 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
745
758
|
if (text) {
|
|
746
759
|
this.messages[idx].content = [{ type: "text", text }];
|
|
747
760
|
}
|
|
761
|
+
const now = Date.now();
|
|
762
|
+
if (now - this.lastChatEmitAt < ClaudePtyBridge.CHAT_THROTTLE_MS) {
|
|
763
|
+
if (!this.chatEmitTimer) {
|
|
764
|
+
this.chatEmitTimer = setTimeout(() => {
|
|
765
|
+
this.chatEmitTimer = null;
|
|
766
|
+
this.lastChatEmitAt = Date.now();
|
|
767
|
+
this.emitEvent({
|
|
768
|
+
type: "output.chat",
|
|
769
|
+
sessionId: this.sessionId,
|
|
770
|
+
timestamp: Date.now(),
|
|
771
|
+
data: {
|
|
772
|
+
messages: this.messages,
|
|
773
|
+
streamingIndex: this.chatState.assistantIndex,
|
|
774
|
+
isResponding: true,
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
}, ClaudePtyBridge.CHAT_THROTTLE_MS);
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
this.lastChatEmitAt = now;
|
|
748
782
|
this.emitEvent({
|
|
749
783
|
type: "output.chat",
|
|
750
784
|
sessionId: this.sessionId,
|
package/dist/config.js
CHANGED
|
@@ -19,6 +19,7 @@ export const defaultConfig = () => ({
|
|
|
19
19
|
language: "",
|
|
20
20
|
android: defaultAndroidApkConfig(),
|
|
21
21
|
cardDefaults: defaultCardExpandDefaults(),
|
|
22
|
+
defaultModel: "",
|
|
22
23
|
commandPresets: [
|
|
23
24
|
{
|
|
24
25
|
label: "Claude",
|
|
@@ -183,6 +184,7 @@ function mergeWithDefaults(input) {
|
|
|
183
184
|
: crypto.randomBytes(32).toString("hex"),
|
|
184
185
|
android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
|
|
185
186
|
cardDefaults: normalizeCardDefaults(input.cardDefaults),
|
|
187
|
+
defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
|
|
186
188
|
};
|
|
187
189
|
}
|
|
188
190
|
export function isExecutionMode(value) {
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ClaudeModelInfo } from "./types.js";
|
|
2
|
+
interface ModelCache {
|
|
3
|
+
models: ClaudeModelInfo[];
|
|
4
|
+
claudeVersion: string | null;
|
|
5
|
+
refreshedAt: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getCachedModels(): ModelCache;
|
|
8
|
+
export declare function refreshModels(): Promise<ModelCache>;
|
|
9
|
+
/** 返回可用于 claude CLI 的全部已知 model id(含别名) */
|
|
10
|
+
export declare function knownModelIds(): string[];
|
|
11
|
+
/** 判断传入值是否是已知模型;允许自由文本,因此总是返回 true。保留接口以便将来严格校验。 */
|
|
12
|
+
export declare function isKnownModel(_value: string): boolean;
|
|
13
|
+
export {};
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
const BUILT_IN_MODELS = [
|
|
5
|
+
{ id: "default", label: "default(跟随 Claude Code 默认)", alias: true },
|
|
6
|
+
{ id: "opus", label: "opus(最新 Opus)", alias: true },
|
|
7
|
+
{ id: "sonnet", label: "sonnet(最新 Sonnet)", alias: true },
|
|
8
|
+
{ id: "haiku", label: "haiku(最新 Haiku)", alias: true },
|
|
9
|
+
{ id: "claude-opus-4-7", label: "Opus 4.7 · claude-opus-4-7" },
|
|
10
|
+
{ id: "claude-opus-4-6", label: "Opus 4.6 · claude-opus-4-6" },
|
|
11
|
+
{ id: "claude-sonnet-4-6", label: "Sonnet 4.6 · claude-sonnet-4-6" },
|
|
12
|
+
{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5 · claude-haiku-4-5-20251001" },
|
|
13
|
+
];
|
|
14
|
+
let cache = null;
|
|
15
|
+
function cloneDefaults() {
|
|
16
|
+
return BUILT_IN_MODELS.map((m) => ({ ...m }));
|
|
17
|
+
}
|
|
18
|
+
async function probeClaudeVersion() {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execAsync("claude --version", { timeout: 5000 });
|
|
21
|
+
const match = stdout.match(/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?/);
|
|
22
|
+
return match ? match[0] : stdout.trim().slice(0, 64) || null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function getCachedModels() {
|
|
29
|
+
if (!cache) {
|
|
30
|
+
cache = {
|
|
31
|
+
models: cloneDefaults(),
|
|
32
|
+
claudeVersion: null,
|
|
33
|
+
refreshedAt: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return cache;
|
|
37
|
+
}
|
|
38
|
+
export async function refreshModels() {
|
|
39
|
+
const version = await probeClaudeVersion();
|
|
40
|
+
cache = {
|
|
41
|
+
models: cloneDefaults(),
|
|
42
|
+
claudeVersion: version,
|
|
43
|
+
refreshedAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
return cache;
|
|
46
|
+
}
|
|
47
|
+
/** 返回可用于 claude CLI 的全部已知 model id(含别名) */
|
|
48
|
+
export function knownModelIds() {
|
|
49
|
+
return BUILT_IN_MODELS.map((m) => m.id);
|
|
50
|
+
}
|
|
51
|
+
/** 判断传入值是否是已知模型;允许自由文本,因此总是返回 true。保留接口以便将来严格校验。 */
|
|
52
|
+
export function isKnownModel(_value) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
@@ -43,6 +43,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
43
43
|
autoRecovered?: boolean;
|
|
44
44
|
worktreeEnabled?: boolean;
|
|
45
45
|
provider?: SessionProvider;
|
|
46
|
+
model?: string;
|
|
46
47
|
}): SessionSnapshot;
|
|
47
48
|
list(): SessionSnapshot[];
|
|
48
49
|
/** Return lightweight snapshots for the session list (no output/messages). */
|
|
@@ -57,6 +58,12 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
57
58
|
}[]): number;
|
|
58
59
|
get(id: string): SessionSnapshot | null;
|
|
59
60
|
getPtyTranscript(id: string): string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Set the Claude model for an existing PTY session. Persists the selection
|
|
63
|
+
* and, when the session is live, pipes a `/model <id>` slash command into
|
|
64
|
+
* the PTY so Claude Code switches on the fly.
|
|
65
|
+
*/
|
|
66
|
+
setSessionModel(id: string, model: string | null): SessionSnapshot;
|
|
60
67
|
sendInput(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string): SessionSnapshot;
|
|
61
68
|
/** Emit a task event for a session, debounced to avoid flooding */
|
|
62
69
|
private emitTask;
|
package/dist/process-manager.js
CHANGED
|
@@ -314,12 +314,14 @@ function readClaudeSessionSummary(filePath, id, cwd) {
|
|
|
314
314
|
return null;
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
|
+
const WORKTREE_DIR_PATTERN = /--?\.?(?:wand-worktrees|claude-worktrees)-/;
|
|
317
318
|
/** Scan all ~/.claude/projects/ directories for session JSONL files. */
|
|
318
319
|
function listAllClaudeHistorySessions() {
|
|
319
320
|
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
320
321
|
try {
|
|
321
322
|
const projectDirs = readdirSync(projectsDir, { withFileTypes: true })
|
|
322
|
-
.filter((entry) => entry.isDirectory())
|
|
323
|
+
.filter((entry) => entry.isDirectory())
|
|
324
|
+
.filter((entry) => !WORKTREE_DIR_PATTERN.test(entry.name));
|
|
323
325
|
const results = [];
|
|
324
326
|
for (const dir of projectDirs) {
|
|
325
327
|
const dirPath = path.join(projectsDir, dir.name);
|
|
@@ -620,7 +622,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
620
622
|
const provider = opts?.provider ?? resolveProviderFromCommand(command);
|
|
621
623
|
const effectiveMode = provider === "codex" ? "full-access" : mode;
|
|
622
624
|
const isClaudeProvider = provider === "claude";
|
|
623
|
-
const
|
|
625
|
+
const selectedModel = opts?.model?.trim() || undefined;
|
|
626
|
+
const processedCommand = this.processCommandForMode(command, effectiveMode, provider, selectedModel);
|
|
624
627
|
const resumeCommandSessionId = isClaudeProvider
|
|
625
628
|
? getResumeCommandSessionId(processedCommand) ?? getResumeCommandSessionId(command)
|
|
626
629
|
: null;
|
|
@@ -671,7 +674,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
671
674
|
knownClaudeTaskIds: knownClaudeTaskIds ?? undefined,
|
|
672
675
|
claudeTaskDiscoveryTimer: null,
|
|
673
676
|
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
|
|
674
|
-
approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
|
|
677
|
+
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
678
|
+
selectedModel: selectedModel ?? null,
|
|
675
679
|
};
|
|
676
680
|
if (isClaudeProvider) {
|
|
677
681
|
record.ptyBridge = new ClaudePtyBridge({
|
|
@@ -945,6 +949,26 @@ export class ProcessManager extends EventEmitter {
|
|
|
945
949
|
getPtyTranscript(id) {
|
|
946
950
|
return this.logger.readPtyOutput(id);
|
|
947
951
|
}
|
|
952
|
+
/**
|
|
953
|
+
* Set the Claude model for an existing PTY session. Persists the selection
|
|
954
|
+
* and, when the session is live, pipes a `/model <id>` slash command into
|
|
955
|
+
* the PTY so Claude Code switches on the fly.
|
|
956
|
+
*/
|
|
957
|
+
setSessionModel(id, model) {
|
|
958
|
+
const record = this.mustGet(id);
|
|
959
|
+
if (record.provider !== "claude") {
|
|
960
|
+
throw new Error("仅 Claude 会话支持切换模型。");
|
|
961
|
+
}
|
|
962
|
+
const normalized = model?.trim() || null;
|
|
963
|
+
record.selectedModel = normalized;
|
|
964
|
+
if (record.status === "running" && record.ptyProcess) {
|
|
965
|
+
const value = normalized && normalized !== "default" ? normalized : "default";
|
|
966
|
+
record.ptyProcess.write(`/model ${value}\r`);
|
|
967
|
+
}
|
|
968
|
+
this.persist(record);
|
|
969
|
+
this.emitEvent({ type: "status", sessionId: id, data: { selectedModel: normalized } });
|
|
970
|
+
return this.snapshot(record);
|
|
971
|
+
}
|
|
948
972
|
sendInput(id, input, view, shortcutKey) {
|
|
949
973
|
const record = this.mustGet(id);
|
|
950
974
|
if (record.status !== "running") {
|
|
@@ -974,11 +998,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
974
998
|
if (record.ptyBridge) {
|
|
975
999
|
record.ptyBridge.onUserInput(input);
|
|
976
1000
|
}
|
|
977
|
-
// Ensure input advances to a new line so subsequent PTY output doesn't overwrite it
|
|
978
1001
|
record.ptyProcess.write(input);
|
|
979
|
-
if (view !== "terminal" && !input.endsWith("\n")) {
|
|
980
|
-
record.ptyProcess.write("\n");
|
|
981
|
-
}
|
|
982
1002
|
this.persist(record);
|
|
983
1003
|
return this.snapshot(record);
|
|
984
1004
|
}
|
|
@@ -1180,6 +1200,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1180
1200
|
approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined,
|
|
1181
1201
|
summary: deriveSessionSummary(messages, record.currentTask?.title ?? null),
|
|
1182
1202
|
currentTaskTitle: record.status === "running" ? record.currentTask?.title ?? undefined : undefined,
|
|
1203
|
+
selectedModel: record.selectedModel ?? null,
|
|
1183
1204
|
};
|
|
1184
1205
|
}
|
|
1185
1206
|
/** Lightweight snapshot for list views — omits output and messages. */
|
|
@@ -1453,9 +1474,20 @@ export class ProcessManager extends EventEmitter {
|
|
|
1453
1474
|
*/
|
|
1454
1475
|
handleBridgeEvent(record, event) {
|
|
1455
1476
|
switch (event.type) {
|
|
1456
|
-
case "output.raw":
|
|
1477
|
+
case "output.raw": {
|
|
1478
|
+
record.output = record.ptyBridge?.getRawOutput() ?? record.output;
|
|
1479
|
+
this.emitEvent({
|
|
1480
|
+
type: "output",
|
|
1481
|
+
sessionId: event.sessionId,
|
|
1482
|
+
data: {
|
|
1483
|
+
incremental: true,
|
|
1484
|
+
chunk: event.data.chunk,
|
|
1485
|
+
permissionBlocked: this.isPermissionBlocked(record),
|
|
1486
|
+
},
|
|
1487
|
+
});
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1457
1490
|
case "output.chat": {
|
|
1458
|
-
// Sync record.output from bridge before emitting so the event carries fresh data
|
|
1459
1491
|
record.output = record.ptyBridge?.getRawOutput() ?? record.output;
|
|
1460
1492
|
const rawMessages = record.ptyBridge?.getMessages() ?? [];
|
|
1461
1493
|
const isStreaming = record.status === "running";
|
|
@@ -1463,7 +1495,6 @@ export class ProcessManager extends EventEmitter {
|
|
|
1463
1495
|
permissionBlocked: this.isPermissionBlocked(record),
|
|
1464
1496
|
};
|
|
1465
1497
|
if (isStreaming && rawMessages.length > 0) {
|
|
1466
|
-
// Incremental mode: send only chunk + last (streaming) turn
|
|
1467
1498
|
data.incremental = true;
|
|
1468
1499
|
const lastTurn = rawMessages[rawMessages.length - 1];
|
|
1469
1500
|
const truncatedLast = truncateMessagesForTransport([lastTurn], this.config.cardDefaults ?? {}, 0);
|
|
@@ -1471,13 +1502,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
1471
1502
|
data.messageCount = rawMessages.length;
|
|
1472
1503
|
}
|
|
1473
1504
|
else {
|
|
1474
|
-
// Full mode: non-streaming or empty messages
|
|
1475
1505
|
data.output = record.output;
|
|
1476
1506
|
data.messages = truncateMessagesForTransport(rawMessages, this.config.cardDefaults ?? {}, rawMessages.length - 1);
|
|
1477
1507
|
}
|
|
1478
|
-
if (event.type === "output.raw") {
|
|
1479
|
-
data.chunk = event.data.chunk;
|
|
1480
|
-
}
|
|
1481
1508
|
this.emitEvent({
|
|
1482
1509
|
type: "output",
|
|
1483
1510
|
sessionId: event.sessionId,
|
|
@@ -1618,7 +1645,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1618
1645
|
}
|
|
1619
1646
|
return false;
|
|
1620
1647
|
}
|
|
1621
|
-
processCommandForMode(command, mode, provider) {
|
|
1648
|
+
processCommandForMode(command, mode, provider, model) {
|
|
1622
1649
|
if (provider === "codex") {
|
|
1623
1650
|
if (mode !== "full-access") {
|
|
1624
1651
|
return command;
|
|
@@ -1632,6 +1659,11 @@ export class ProcessManager extends EventEmitter {
|
|
|
1632
1659
|
if (!isClaudeCmd)
|
|
1633
1660
|
return command;
|
|
1634
1661
|
let result = command;
|
|
1662
|
+
const trimmedModel = model?.trim();
|
|
1663
|
+
if (trimmedModel && trimmedModel !== "default" && !/--model\s/.test(command)) {
|
|
1664
|
+
const escapedModel = trimmedModel.replace(/'/g, "'\\''");
|
|
1665
|
+
result += ` --model '${escapedModel}'`;
|
|
1666
|
+
}
|
|
1635
1667
|
const hasPermFlag = /--permission-mode\s/.test(command);
|
|
1636
1668
|
if (!hasPermFlag) {
|
|
1637
1669
|
if (isRunningAsRoot()) {
|
package/dist/pty-text-utils.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export declare function isNoiseLine(line: string): boolean;
|
|
|
11
11
|
export declare function appendWindow(buffer: string, chunk: string, maxSize: number): string;
|
|
12
12
|
export declare function hasExplicitConfirmSyntax(normalized: string): boolean;
|
|
13
13
|
export declare function hasPermissionActionContext(normalized: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Detect Claude CLI slash-command selection menus (/model, /effort, /output-style, etc.).
|
|
16
|
+
* These share "Enter to confirm" with permission prompts but are user-driven choices
|
|
17
|
+
* that must never be auto-approved. Distinguishing footer: "Esc to exit" (vs permission
|
|
18
|
+
* prompts' "Esc to cancel" / "Tab to amend").
|
|
19
|
+
*/
|
|
20
|
+
export declare function isSlashCommandMenu(normalized: string): boolean;
|
|
14
21
|
interface PermissionScore {
|
|
15
22
|
score: number;
|
|
16
23
|
matched: string[];
|
package/dist/pty-text-utils.js
CHANGED
|
@@ -144,6 +144,15 @@ export function hasExplicitConfirmSyntax(normalized) {
|
|
|
144
144
|
export function hasPermissionActionContext(normalized) {
|
|
145
145
|
return PERMISSION_ACTION_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Detect Claude CLI slash-command selection menus (/model, /effort, /output-style, etc.).
|
|
149
|
+
* These share "Enter to confirm" with permission prompts but are user-driven choices
|
|
150
|
+
* that must never be auto-approved. Distinguishing footer: "Esc to exit" (vs permission
|
|
151
|
+
* prompts' "Esc to cancel" / "Tab to amend").
|
|
152
|
+
*/
|
|
153
|
+
export function isSlashCommandMenu(normalized) {
|
|
154
|
+
return /\besc\s+to\s+exit\b/i.test(normalized);
|
|
155
|
+
}
|
|
147
156
|
const PERMISSION_KEYWORD_WEIGHTS = [
|
|
148
157
|
{ pattern: /\bdo you want to proceed\b/i, weight: 5, label: "do you want to proceed" },
|
|
149
158
|
{ pattern: /\bwould you like to proceed\b/i, weight: 5, label: "would you like to proceed" },
|
|
@@ -168,6 +177,11 @@ export function scorePermissionLikelihood(normalized) {
|
|
|
168
177
|
// Take the last ~5 lines
|
|
169
178
|
const lines = normalized.split("\n");
|
|
170
179
|
const tail = lines.slice(-8).join("\n");
|
|
180
|
+
// Slash-command menus are never permission prompts — zero the score so
|
|
181
|
+
// fallback auto-approve and idle-probe both skip them.
|
|
182
|
+
if (isSlashCommandMenu(tail)) {
|
|
183
|
+
return { score: 0, matched: [] };
|
|
184
|
+
}
|
|
171
185
|
let score = 0;
|
|
172
186
|
const matched = [];
|
|
173
187
|
for (const { pattern, weight, label } of PERMISSION_KEYWORD_WEIGHTS) {
|
package/dist/pwa.js
CHANGED
|
@@ -53,10 +53,8 @@ const STATIC_ASSETS = [
|
|
|
53
53
|
'/icon.svg',
|
|
54
54
|
'/icon-192.png',
|
|
55
55
|
'/icon-512.png',
|
|
56
|
-
'/vendor/
|
|
57
|
-
'/vendor/
|
|
58
|
-
'/vendor/xterm-addon-fit/lib/addon-fit.js',
|
|
59
|
-
'/vendor/xterm-addon-serialize/lib/xterm-addon-serialize.js'
|
|
56
|
+
'/vendor/wterm/terminal.css',
|
|
57
|
+
'/vendor/wterm/wterm.bundle.js'
|
|
60
58
|
];
|
|
61
59
|
|
|
62
60
|
self.addEventListener('install', (event) => {
|
|
@@ -144,7 +144,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
144
144
|
});
|
|
145
145
|
app.post("/api/structured-sessions", express.json(), async (req, res) => {
|
|
146
146
|
const body = req.body;
|
|
147
|
-
console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, provider: body.provider, worktreeEnabled: body.worktreeEnabled === true, hasPrompt: !!body.prompt }));
|
|
147
|
+
console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, provider: body.provider, worktreeEnabled: body.worktreeEnabled === true, hasPrompt: !!body.prompt, model: body.model }));
|
|
148
148
|
try {
|
|
149
149
|
if (body.provider && body.provider !== "claude") {
|
|
150
150
|
res.status(400).json({ error: "结构化会话当前仅支持 Claude provider。" });
|
|
@@ -156,6 +156,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
156
156
|
prompt: body.prompt,
|
|
157
157
|
runner: body.runner ?? "claude-cli-print",
|
|
158
158
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
159
|
+
model: typeof body.model === "string" ? body.model.trim() : undefined,
|
|
159
160
|
});
|
|
160
161
|
console.log("[WAND] structured session created:", JSON.stringify({ id: snapshot.id, sessionKind: snapshot.sessionKind, runner: snapshot.runner, status: snapshot.status }));
|
|
161
162
|
res.status(201).json(snapshot);
|
|
@@ -164,6 +165,29 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
164
165
|
res.status(400).json({ error: getErrorMessage(error, "无法启动结构化会话。") });
|
|
165
166
|
}
|
|
166
167
|
});
|
|
168
|
+
app.post("/api/sessions/:id/model", express.json(), (req, res) => {
|
|
169
|
+
const body = req.body;
|
|
170
|
+
const rawModel = typeof body?.model === "string" ? body.model.trim() : null;
|
|
171
|
+
const id = req.params.id;
|
|
172
|
+
try {
|
|
173
|
+
const structuredSnapshot = structured.get(id);
|
|
174
|
+
if (structuredSnapshot) {
|
|
175
|
+
const updated = structured.setSessionModel(id, rawModel);
|
|
176
|
+
res.json(updated);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const ptySnapshot = processes.get(id);
|
|
180
|
+
if (!ptySnapshot) {
|
|
181
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const updated = processes.setSessionModel(id, rawModel);
|
|
185
|
+
res.json(updated);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
res.status(400).json({ error: getErrorMessage(error, "切换模型失败。") });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
167
191
|
app.get("/api/structured-sessions/:id/messages", (req, res) => {
|
|
168
192
|
const snapshot = structured.get(req.params.id);
|
|
169
193
|
if (!snapshot) {
|