@co0ontty/wand 1.15.0 → 1.17.2
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 +67 -19
- package/dist/pty-text-utils.d.ts +7 -0
- package/dist/pty-text-utils.js +14 -0
- package/dist/pwa.js +11 -6
- package/dist/server-session-routes.js +25 -1
- package/dist/server.js +125 -16
- package/dist/structured-session-manager.d.ts +4 -0
- package/dist/structured-session-manager.js +51 -5
- 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 +828 -516
- package/dist/web-ui/content/styles.css +293 -119
- 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 +8 -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({
|
|
@@ -787,8 +791,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
787
791
|
type: "output",
|
|
788
792
|
sessionId: id,
|
|
789
793
|
data: {
|
|
794
|
+
incremental: true,
|
|
790
795
|
chunk,
|
|
791
|
-
output: rec.output,
|
|
792
796
|
permissionBlocked: this.isPermissionBlocked(rec),
|
|
793
797
|
},
|
|
794
798
|
});
|
|
@@ -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,19 +1474,36 @@ 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
|
-
const
|
|
1493
|
+
const isStreaming = record.status === "running";
|
|
1462
1494
|
const data = {
|
|
1463
|
-
output: record.output,
|
|
1464
|
-
messages,
|
|
1465
1495
|
permissionBlocked: this.isPermissionBlocked(record),
|
|
1466
1496
|
};
|
|
1467
|
-
if (
|
|
1468
|
-
data.
|
|
1497
|
+
if (isStreaming && rawMessages.length > 0) {
|
|
1498
|
+
data.incremental = true;
|
|
1499
|
+
const lastTurn = rawMessages[rawMessages.length - 1];
|
|
1500
|
+
const truncatedLast = truncateMessagesForTransport([lastTurn], this.config.cardDefaults ?? {}, 0);
|
|
1501
|
+
data.lastMessage = truncatedLast[0];
|
|
1502
|
+
data.messageCount = rawMessages.length;
|
|
1503
|
+
}
|
|
1504
|
+
else {
|
|
1505
|
+
data.output = record.output;
|
|
1506
|
+
data.messages = truncateMessagesForTransport(rawMessages, this.config.cardDefaults ?? {}, rawMessages.length - 1);
|
|
1469
1507
|
}
|
|
1470
1508
|
this.emitEvent({
|
|
1471
1509
|
type: "output",
|
|
@@ -1607,7 +1645,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1607
1645
|
}
|
|
1608
1646
|
return false;
|
|
1609
1647
|
}
|
|
1610
|
-
processCommandForMode(command, mode, provider) {
|
|
1648
|
+
processCommandForMode(command, mode, provider, model) {
|
|
1611
1649
|
if (provider === "codex") {
|
|
1612
1650
|
if (mode !== "full-access") {
|
|
1613
1651
|
return command;
|
|
@@ -1621,6 +1659,11 @@ export class ProcessManager extends EventEmitter {
|
|
|
1621
1659
|
if (!isClaudeCmd)
|
|
1622
1660
|
return command;
|
|
1623
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
|
+
}
|
|
1624
1667
|
const hasPermFlag = /--permission-mode\s/.test(command);
|
|
1625
1668
|
if (!hasPermFlag) {
|
|
1626
1669
|
if (isRunningAsRoot()) {
|
|
@@ -1638,14 +1681,19 @@ export class ProcessManager extends EventEmitter {
|
|
|
1638
1681
|
}
|
|
1639
1682
|
}
|
|
1640
1683
|
}
|
|
1684
|
+
const language = this.config.language?.trim();
|
|
1685
|
+
const isChinese = language === "中文";
|
|
1641
1686
|
if (mode === "managed") {
|
|
1642
|
-
const autonomousPrompt =
|
|
1687
|
+
const autonomousPrompt = isChinese
|
|
1688
|
+
? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
|
|
1689
|
+
: "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.";
|
|
1643
1690
|
const escaped = autonomousPrompt.replace(/'/g, "'\\''");
|
|
1644
1691
|
result += ` --append-system-prompt '${escaped}'`;
|
|
1645
1692
|
}
|
|
1646
|
-
const language = this.config.language?.trim();
|
|
1647
1693
|
if (language) {
|
|
1648
|
-
const langPrompt =
|
|
1694
|
+
const langPrompt = isChinese
|
|
1695
|
+
? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
|
|
1696
|
+
: `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`;
|
|
1649
1697
|
const escaped = langPrompt.replace(/'/g, "'\\''");
|
|
1650
1698
|
result += ` --append-system-prompt '${escaped}'`;
|
|
1651
1699
|
}
|
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
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PWA manifest and Service Worker generation.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
10
|
+
const pkgVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0";
|
|
11
|
+
/** Cache version derived from package version — only busts on actual releases */
|
|
12
|
+
const CACHE_VERSION = createHash("md5").update(pkgVersion).digest("hex").slice(0, 8);
|
|
6
13
|
export function generatePwaManifest() {
|
|
7
14
|
return JSON.stringify({
|
|
8
15
|
id: "/wand",
|
|
@@ -46,10 +53,8 @@ const STATIC_ASSETS = [
|
|
|
46
53
|
'/icon.svg',
|
|
47
54
|
'/icon-192.png',
|
|
48
55
|
'/icon-512.png',
|
|
49
|
-
'/vendor/
|
|
50
|
-
'/vendor/
|
|
51
|
-
'/vendor/xterm-addon-fit/lib/addon-fit.js',
|
|
52
|
-
'/vendor/xterm-addon-serialize/lib/xterm-addon-serialize.js'
|
|
56
|
+
'/vendor/wterm/terminal.css',
|
|
57
|
+
'/vendor/wterm/wterm.bundle.js'
|
|
53
58
|
];
|
|
54
59
|
|
|
55
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) {
|