@co0ontty/wand 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-pty-bridge.js +4 -2
- package/dist/cli.js +2 -2
- package/dist/config.js +4 -0
- package/dist/process-manager.d.ts +4 -0
- package/dist/process-manager.js +33 -5
- package/dist/server.js +57 -27
- package/dist/session-logger.d.ts +17 -3
- package/dist/session-logger.js +40 -4
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +111 -16
- package/dist/web-ui/content/styles.css +97 -0
- package/package.json +1 -1
|
@@ -434,11 +434,13 @@ export class ClaudePtyBridge extends EventEmitter {
|
|
|
434
434
|
return (/\bdo you want to\b/i.test(normalized) ||
|
|
435
435
|
/\bgrant\b.*\bpermission\b/i.test(normalized) ||
|
|
436
436
|
/\bhaven't granted\b/i.test(normalized) ||
|
|
437
|
-
/\benter to confirm\b/i.test(normalized)
|
|
437
|
+
/\benter to confirm\b/i.test(normalized) ||
|
|
438
|
+
/\bwould you like to proceed\b/i.test(normalized) ||
|
|
439
|
+
/❯/.test(normalized));
|
|
438
440
|
}
|
|
439
441
|
extractPromptText(normalized) {
|
|
440
442
|
// Return a snippet around the permission prompt
|
|
441
|
-
const match = normalized.match(/.{0,100}(?:do you want to|permission|grant|enter to confirm).{0,100}/i);
|
|
443
|
+
const match = normalized.match(/.{0,100}(?:do you want to|permission|grant|enter to confirm|would you like to proceed|❯).{0,100}/i);
|
|
442
444
|
return match?.[0] ?? normalized.slice(-100);
|
|
443
445
|
}
|
|
444
446
|
extractPermissionTarget(normalized) {
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ensureDatabaseFile, resolveDatabasePath } from "./storage.js";
|
|
|
6
6
|
async function main() {
|
|
7
7
|
const args = process.argv.slice(2);
|
|
8
8
|
const command = args[0] || "help";
|
|
9
|
-
const configPath = resolveConfigPath(readFlagValue(args, "--config"));
|
|
9
|
+
const configPath = resolveConfigPath(readFlagValue(args, "-c") || readFlagValue(args, "--config"));
|
|
10
10
|
switch (command) {
|
|
11
11
|
case "init": {
|
|
12
12
|
await ensureRequiredFiles(configPath);
|
|
@@ -63,7 +63,7 @@ Commands:
|
|
|
63
63
|
wand config:set Update a simple config value
|
|
64
64
|
|
|
65
65
|
Options:
|
|
66
|
-
--config <path>
|
|
66
|
+
-c, --config <path> Use a custom config file (default: ~/.wand/config.json)
|
|
67
67
|
`);
|
|
68
68
|
}
|
|
69
69
|
async function ensureRequiredFiles(configPath) {
|
package/dist/config.js
CHANGED
|
@@ -14,6 +14,7 @@ export const defaultConfig = () => ({
|
|
|
14
14
|
defaultCwd: process.cwd(),
|
|
15
15
|
startupCommands: [],
|
|
16
16
|
allowedCommandPrefixes: [],
|
|
17
|
+
shortcutLogMaxBytes: 10 * 1024 * 1024,
|
|
17
18
|
commandPresets: [
|
|
18
19
|
{
|
|
19
20
|
label: "Claude",
|
|
@@ -87,6 +88,9 @@ function mergeWithDefaults(input) {
|
|
|
87
88
|
defaultCwd: typeof input.defaultCwd === "string" && input.defaultCwd.trim()
|
|
88
89
|
? input.defaultCwd
|
|
89
90
|
: defaults.defaultCwd,
|
|
91
|
+
shortcutLogMaxBytes: typeof input.shortcutLogMaxBytes === "number" && input.shortcutLogMaxBytes >= 0
|
|
92
|
+
? input.shortcutLogMaxBytes
|
|
93
|
+
: defaults.shortcutLogMaxBytes,
|
|
90
94
|
startupCommands: Array.isArray(input.startupCommands) ? input.startupCommands : defaults.startupCommands,
|
|
91
95
|
allowedCommandPrefixes: Array.isArray(input.allowedCommandPrefixes)
|
|
92
96
|
? input.allowedCommandPrefixes
|
|
@@ -52,6 +52,10 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
52
52
|
private claudeHistoryCache;
|
|
53
53
|
private static readonly HISTORY_CACHE_TTL_MS;
|
|
54
54
|
listClaudeHistorySessions(): ClaudeHistorySession[];
|
|
55
|
+
deleteClaudeHistoryFiles(sessions: {
|
|
56
|
+
claudeSessionId: string;
|
|
57
|
+
cwd: string;
|
|
58
|
+
}[]): number;
|
|
55
59
|
get(id: string): SessionSnapshot | null;
|
|
56
60
|
sendInput(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string): SessionSnapshot;
|
|
57
61
|
/** Emit a task event for a session, debounced to avoid flooding */
|
package/dist/process-manager.js
CHANGED
|
@@ -40,7 +40,8 @@ const PROMPT_PATTERNS = [
|
|
|
40
40
|
/\bwould you like to\b/i,
|
|
41
41
|
/\bshall i\b/i,
|
|
42
42
|
/\bcan i\b/i,
|
|
43
|
-
/\bgrant\b.*\bpermission\b/i
|
|
43
|
+
/\bgrant\b.*\bpermission\b/i,
|
|
44
|
+
/❯/
|
|
44
45
|
];
|
|
45
46
|
const REAL_CONVERSATION_MIN_LINES = 2;
|
|
46
47
|
const REAL_CONVERSATION_MIN_MESSAGES = 2;
|
|
@@ -731,7 +732,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
731
732
|
super();
|
|
732
733
|
this.config = config;
|
|
733
734
|
this.storage = storage;
|
|
734
|
-
this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"));
|
|
735
|
+
this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"), config.shortcutLogMaxBytes);
|
|
735
736
|
// Initialize lifecycle manager
|
|
736
737
|
this.lifecycleManager = new SessionLifecycleManager({
|
|
737
738
|
onStateChange: (sessionId, oldState, newState) => {
|
|
@@ -1135,6 +1136,27 @@ export class ProcessManager extends EventEmitter {
|
|
|
1135
1136
|
this.claudeHistoryCache = { data: allSessions, expiresAt: now + ProcessManager.HISTORY_CACHE_TTL_MS };
|
|
1136
1137
|
return allSessions;
|
|
1137
1138
|
}
|
|
1139
|
+
deleteClaudeHistoryFiles(sessions) {
|
|
1140
|
+
let deleted = 0;
|
|
1141
|
+
for (const { claudeSessionId, cwd } of sessions) {
|
|
1142
|
+
if (!UUID_V4_PATTERN.test(claudeSessionId))
|
|
1143
|
+
continue;
|
|
1144
|
+
const jsonlPath = path.join(getClaudeProjectDir(cwd), `${claudeSessionId}.jsonl`);
|
|
1145
|
+
try {
|
|
1146
|
+
if (existsSync(jsonlPath)) {
|
|
1147
|
+
unlinkSync(jsonlPath);
|
|
1148
|
+
deleted++;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
// Best-effort — Claude cache cleanup is non-critical
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (deleted > 0) {
|
|
1156
|
+
this.claudeHistoryCache = null;
|
|
1157
|
+
}
|
|
1158
|
+
return deleted;
|
|
1159
|
+
}
|
|
1138
1160
|
get(id) {
|
|
1139
1161
|
this.archiveExpiredSessions();
|
|
1140
1162
|
const record = this.sessions.get(id);
|
|
@@ -1179,11 +1201,17 @@ export class ProcessManager extends EventEmitter {
|
|
|
1179
1201
|
inputLength: input.length,
|
|
1180
1202
|
view: view ?? "chat"
|
|
1181
1203
|
});
|
|
1182
|
-
// Log shortcut key interactions
|
|
1183
|
-
if (shortcutKey
|
|
1204
|
+
// Log shortcut key interactions for auto-confirm and mode analysis
|
|
1205
|
+
if (shortcutKey) {
|
|
1184
1206
|
const outputLines = record.output.split("\n");
|
|
1185
1207
|
const tailLines = outputLines.slice(-15).join("\n");
|
|
1186
|
-
|
|
1208
|
+
const ctx = {
|
|
1209
|
+
mode: record.mode,
|
|
1210
|
+
autoApprove: record.autoApprovePermissions,
|
|
1211
|
+
permissionBlocked: record.ptyPermissionBlocked || !!record.pendingEscalation,
|
|
1212
|
+
input,
|
|
1213
|
+
};
|
|
1214
|
+
this.logger.appendShortcutLog(id, shortcutKey, tailLines, ctx);
|
|
1187
1215
|
}
|
|
1188
1216
|
// Track user input via bridge for Chat mode
|
|
1189
1217
|
if (record.ptyBridge) {
|
package/dist/server.js
CHANGED
|
@@ -255,6 +255,16 @@ function getHiddenClaudeSessionIds(storage) {
|
|
|
255
255
|
function saveHiddenClaudeSessionIds(storage, hidden) {
|
|
256
256
|
storage.setConfigValue(HIDDEN_CLAUDE_SESSIONS_KEY, JSON.stringify(Array.from(hidden)));
|
|
257
257
|
}
|
|
258
|
+
function removeFromHiddenClaudeSessionIds(storage, idsToRemove) {
|
|
259
|
+
const hidden = getHiddenClaudeSessionIds(storage);
|
|
260
|
+
let changed = false;
|
|
261
|
+
for (const id of idsToRemove) {
|
|
262
|
+
if (hidden.delete(id))
|
|
263
|
+
changed = true;
|
|
264
|
+
}
|
|
265
|
+
if (changed)
|
|
266
|
+
saveHiddenClaudeSessionIds(storage, hidden);
|
|
267
|
+
}
|
|
258
268
|
const MAX_RECENT_PATHS = 10;
|
|
259
269
|
// ── File language detection ──
|
|
260
270
|
function getLanguageFromExt(ext, filePath) {
|
|
@@ -508,10 +518,18 @@ export async function startServer(config, configPath) {
|
|
|
508
518
|
res.status(400).json({ error: "会话 ID 不能为空。" });
|
|
509
519
|
return;
|
|
510
520
|
}
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
521
|
+
const session = processes.listClaudeHistorySessions()
|
|
522
|
+
.find((s) => s.claudeSessionId === claudeSessionId);
|
|
523
|
+
if (session) {
|
|
524
|
+
processes.deleteClaudeHistoryFiles([{ claudeSessionId, cwd: session.cwd }]);
|
|
525
|
+
removeFromHiddenClaudeSessionIds(storage, [claudeSessionId]);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const hidden = getHiddenClaudeSessionIds(storage);
|
|
529
|
+
if (!hidden.has(claudeSessionId)) {
|
|
530
|
+
hidden.add(claudeSessionId);
|
|
531
|
+
saveHiddenClaudeSessionIds(storage, hidden);
|
|
532
|
+
}
|
|
515
533
|
}
|
|
516
534
|
res.json({ ok: true });
|
|
517
535
|
});
|
|
@@ -523,22 +541,15 @@ export async function startServer(config, configPath) {
|
|
|
523
541
|
}
|
|
524
542
|
try {
|
|
525
543
|
const sessions = processes.listClaudeHistorySessions();
|
|
526
|
-
const
|
|
527
|
-
let added = 0;
|
|
544
|
+
const toDelete = [];
|
|
528
545
|
for (const session of sessions) {
|
|
529
|
-
if (
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
if (hidden.has(session.claudeSessionId)) {
|
|
533
|
-
continue;
|
|
546
|
+
if (session.claudeSessionId && session.cwd === cwd) {
|
|
547
|
+
toDelete.push({ claudeSessionId: session.claudeSessionId, cwd: session.cwd });
|
|
534
548
|
}
|
|
535
|
-
hidden.add(session.claudeSessionId);
|
|
536
|
-
added += 1;
|
|
537
549
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
res.json({ ok: true, deleted: added });
|
|
550
|
+
const deleted = processes.deleteClaudeHistoryFiles(toDelete);
|
|
551
|
+
removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
|
|
552
|
+
res.json({ ok: true, deleted });
|
|
542
553
|
}
|
|
543
554
|
catch (error) {
|
|
544
555
|
res.status(500).json({ error: getErrorMessage(error, "无法删除该目录下的历史会话。") });
|
|
@@ -553,19 +564,38 @@ export async function startServer(config, configPath) {
|
|
|
553
564
|
return;
|
|
554
565
|
}
|
|
555
566
|
try {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
for (const
|
|
559
|
-
if (
|
|
560
|
-
|
|
567
|
+
const allSessions = processes.listClaudeHistorySessions();
|
|
568
|
+
const sessionMap = new Map();
|
|
569
|
+
for (const s of allSessions) {
|
|
570
|
+
if (s.claudeSessionId)
|
|
571
|
+
sessionMap.set(s.claudeSessionId, s.cwd);
|
|
572
|
+
}
|
|
573
|
+
const toDelete = [];
|
|
574
|
+
const toHide = [];
|
|
575
|
+
for (const id of claudeSessionIds) {
|
|
576
|
+
const cwd = sessionMap.get(id);
|
|
577
|
+
if (cwd) {
|
|
578
|
+
toDelete.push({ claudeSessionId: id, cwd });
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
toHide.push(id);
|
|
561
582
|
}
|
|
562
|
-
hidden.add(claudeSessionId);
|
|
563
|
-
added += 1;
|
|
564
583
|
}
|
|
565
|
-
|
|
566
|
-
|
|
584
|
+
const deleted = processes.deleteClaudeHistoryFiles(toDelete);
|
|
585
|
+
removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
|
|
586
|
+
if (toHide.length > 0) {
|
|
587
|
+
const hidden = getHiddenClaudeSessionIds(storage);
|
|
588
|
+
let added = 0;
|
|
589
|
+
for (const id of toHide) {
|
|
590
|
+
if (!hidden.has(id)) {
|
|
591
|
+
hidden.add(id);
|
|
592
|
+
added++;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (added > 0)
|
|
596
|
+
saveHiddenClaudeSessionIds(storage, hidden);
|
|
567
597
|
}
|
|
568
|
-
res.json({ ok: true, deleted:
|
|
598
|
+
res.json({ ok: true, deleted: deleted + toHide.length });
|
|
569
599
|
}
|
|
570
600
|
catch (error) {
|
|
571
601
|
res.status(500).json({ error: getErrorMessage(error, "无法批量删除历史会话。") });
|
package/dist/session-logger.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import type { ConversationTurn } from "./types.js";
|
|
1
|
+
import type { ConversationTurn, ExecutionMode } from "./types.js";
|
|
2
|
+
/** Context passed alongside a shortcut key interaction for richer logging */
|
|
3
|
+
export interface ShortcutLogContext {
|
|
4
|
+
/** Execution mode the session is running in (e.g. "managed", "full-access") */
|
|
5
|
+
mode: ExecutionMode;
|
|
6
|
+
/** Whether auto-approve is active for this session */
|
|
7
|
+
autoApprove: boolean;
|
|
8
|
+
/** Whether a permission prompt was blocking at the time of the keypress */
|
|
9
|
+
permissionBlocked: boolean;
|
|
10
|
+
/** The actual input string sent to PTY */
|
|
11
|
+
input: string;
|
|
12
|
+
}
|
|
2
13
|
/**
|
|
3
14
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
|
4
15
|
*
|
|
@@ -11,7 +22,8 @@ import type { ConversationTurn } from "./types.js";
|
|
|
11
22
|
export declare class SessionLogger {
|
|
12
23
|
private readonly baseDir;
|
|
13
24
|
private readonly dirs;
|
|
14
|
-
|
|
25
|
+
private readonly shortcutLogMaxBytes;
|
|
26
|
+
constructor(configDir: string, shortcutLogMaxBytes?: number);
|
|
15
27
|
private ensureDir;
|
|
16
28
|
/**
|
|
17
29
|
* Rotate PTY log files if the current one exceeds the size limit.
|
|
@@ -31,5 +43,7 @@ export declare class SessionLogger {
|
|
|
31
43
|
/** Delete all log files for a session */
|
|
32
44
|
deleteSession(sessionId: string): void;
|
|
33
45
|
/** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
|
|
34
|
-
appendShortcutLog(sessionId: string, shortcutKey: string, tailLines: string): void;
|
|
46
|
+
appendShortcutLog(sessionId: string, shortcutKey: string, tailLines: string, ctx?: ShortcutLogContext): void;
|
|
47
|
+
/** Truncate shortcut log by keeping only the most recent half of entries */
|
|
48
|
+
private truncateShortcutLog;
|
|
35
49
|
}
|
package/dist/session-logger.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, rmSync, appendFileSync, writeFileSync, existsSync, statSync, renameSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, rmSync, appendFileSync, writeFileSync, readFileSync, existsSync, statSync, renameSync, unlinkSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
// ── Constants ──
|
|
@@ -6,6 +6,8 @@ import process from "node:process";
|
|
|
6
6
|
const PTY_LOG_MAX_SIZE = 50 * 1024 * 1024;
|
|
7
7
|
/** Maximum number of rotated log files to keep */
|
|
8
8
|
const PTY_LOG_MAX_ROTATIONS = 3;
|
|
9
|
+
/** Default max size for shortcut interaction logs per session (10 MB) */
|
|
10
|
+
const DEFAULT_SHORTCUT_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
9
11
|
/**
|
|
10
12
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
|
11
13
|
*
|
|
@@ -18,8 +20,10 @@ const PTY_LOG_MAX_ROTATIONS = 3;
|
|
|
18
20
|
export class SessionLogger {
|
|
19
21
|
baseDir;
|
|
20
22
|
dirs = new Map();
|
|
21
|
-
|
|
23
|
+
shortcutLogMaxBytes;
|
|
24
|
+
constructor(configDir, shortcutLogMaxBytes) {
|
|
22
25
|
this.baseDir = path.join(configDir, "sessions");
|
|
26
|
+
this.shortcutLogMaxBytes = shortcutLogMaxBytes ?? DEFAULT_SHORTCUT_LOG_MAX_BYTES;
|
|
23
27
|
try {
|
|
24
28
|
mkdirSync(this.baseDir, { recursive: true });
|
|
25
29
|
}
|
|
@@ -127,18 +131,50 @@ export class SessionLogger {
|
|
|
127
131
|
this.dirs.delete(sessionId);
|
|
128
132
|
}
|
|
129
133
|
/** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
|
|
130
|
-
appendShortcutLog(sessionId, shortcutKey, tailLines) {
|
|
134
|
+
appendShortcutLog(sessionId, shortcutKey, tailLines, ctx) {
|
|
135
|
+
if (this.shortcutLogMaxBytes <= 0)
|
|
136
|
+
return;
|
|
131
137
|
try {
|
|
132
138
|
const dir = this.ensureDir(sessionId);
|
|
139
|
+
const logPath = path.join(dir, "shortcut-interactions.jsonl");
|
|
133
140
|
const entry = JSON.stringify({
|
|
134
141
|
ts: new Date().toISOString(),
|
|
135
142
|
key: shortcutKey,
|
|
143
|
+
mode: ctx?.mode,
|
|
144
|
+
autoApprove: ctx?.autoApprove,
|
|
145
|
+
permissionBlocked: ctx?.permissionBlocked,
|
|
146
|
+
input: ctx?.input,
|
|
136
147
|
tail: tailLines,
|
|
137
148
|
}) + "\n";
|
|
138
|
-
|
|
149
|
+
// Check size and truncate if needed
|
|
150
|
+
if (existsSync(logPath)) {
|
|
151
|
+
const size = statSync(logPath).size;
|
|
152
|
+
if (size + entry.length > this.shortcutLogMaxBytes) {
|
|
153
|
+
this.truncateShortcutLog(logPath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
appendFileSync(logPath, entry);
|
|
139
157
|
}
|
|
140
158
|
catch {
|
|
141
159
|
// Non-critical
|
|
142
160
|
}
|
|
143
161
|
}
|
|
162
|
+
/** Truncate shortcut log by keeping only the most recent half of entries */
|
|
163
|
+
truncateShortcutLog(logPath) {
|
|
164
|
+
try {
|
|
165
|
+
const content = readFileSync(logPath, "utf8");
|
|
166
|
+
const lines = content.split("\n").filter(Boolean);
|
|
167
|
+
// Keep the latter half
|
|
168
|
+
const keepFrom = Math.floor(lines.length / 2);
|
|
169
|
+
const trimmed = lines.slice(keepFrom).join("\n") + "\n";
|
|
170
|
+
writeFileSync(logPath, trimmed);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// If truncation fails, delete the file to prevent unbounded growth
|
|
174
|
+
try {
|
|
175
|
+
unlinkSync(logPath);
|
|
176
|
+
}
|
|
177
|
+
catch { /* ignore */ }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
144
180
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface WandConfig {
|
|
|
40
40
|
startupCommands: string[];
|
|
41
41
|
allowedCommandPrefixes: string[];
|
|
42
42
|
commandPresets: CommandPreset[];
|
|
43
|
+
/** Max total size (bytes) for shortcut interaction logs per session (default: 10 MB). Set 0 to disable logging. */
|
|
44
|
+
shortcutLogMaxBytes?: number;
|
|
43
45
|
}
|
|
44
46
|
export interface CommandRequest {
|
|
45
47
|
command: string;
|
|
@@ -494,9 +494,14 @@
|
|
|
494
494
|
'<button class="blank-chat-tool-btn" id="welcome-tool-claude" type="button">' +
|
|
495
495
|
'<span class="tool-icon">🤖</span>新建终端会话' +
|
|
496
496
|
'</button>' +
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
'
|
|
497
|
+
'</div>' +
|
|
498
|
+
'<div class="blank-chat-cwd-wrap">' +
|
|
499
|
+
'<div class="blank-chat-cwd" id="blank-chat-cwd" role="button" tabindex="0" title="点击切换工作目录">' +
|
|
500
|
+
'<span class="blank-chat-cwd-icon">📁</span>' +
|
|
501
|
+
'<span class="blank-chat-cwd-path" id="blank-chat-cwd-path">' + escapeHtml(state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp")) + '</span>' +
|
|
502
|
+
'<span class="blank-chat-cwd-arrow" id="blank-chat-cwd-arrow">▼</span>' +
|
|
503
|
+
'</div>' +
|
|
504
|
+
'<div class="blank-chat-cwd-dropdown hidden" id="blank-chat-cwd-dropdown"></div>' +
|
|
500
505
|
'</div>' +
|
|
501
506
|
'</div>' +
|
|
502
507
|
'</div>' +
|
|
@@ -1648,10 +1653,10 @@
|
|
|
1648
1653
|
'<div class="field">' +
|
|
1649
1654
|
'<label class="field-label" for="cwd">工作目录</label>' +
|
|
1650
1655
|
'<div class="suggestions-wrap">' +
|
|
1651
|
-
'<input id="cwd" type="text" class="field-input" autocomplete="off" placeholder="
|
|
1656
|
+
'<input id="cwd" type="text" class="field-input" autocomplete="off" placeholder="' + escapeHtml(state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp")) + '" />' +
|
|
1652
1657
|
'<div id="cwd-suggestions" class="suggestions hidden"></div>' +
|
|
1653
1658
|
'</div>' +
|
|
1654
|
-
'<p class="field-hint"
|
|
1659
|
+
'<p class="field-hint">留空则使用上方目录,支持路径自动补全。</p>' +
|
|
1655
1660
|
'<div id="recent-paths-bubbles" class="recent-paths-bubbles"></div>' +
|
|
1656
1661
|
'</div>' +
|
|
1657
1662
|
'<button id="run-button" class="btn btn-primary btn-block">启动会话</button>' +
|
|
@@ -1810,10 +1815,7 @@
|
|
|
1810
1815
|
quickStartSession();
|
|
1811
1816
|
});
|
|
1812
1817
|
}
|
|
1813
|
-
|
|
1814
|
-
if (welcomeFolderBtn) {
|
|
1815
|
-
welcomeFolderBtn.addEventListener("click", openFolderPickerWithInitialPath);
|
|
1816
|
-
}
|
|
1818
|
+
initBlankChatCwd();
|
|
1817
1819
|
|
|
1818
1820
|
var sessionsList = document.getElementById("sessions-list");
|
|
1819
1821
|
if (sessionsList) {
|
|
@@ -2403,11 +2405,7 @@
|
|
|
2403
2405
|
renderBreadcrumb(initialPath);
|
|
2404
2406
|
}
|
|
2405
2407
|
|
|
2406
|
-
// Welcome screen folder button
|
|
2407
|
-
var welcomeFolderBtn = document.getElementById("welcome-tool-folder");
|
|
2408
|
-
if (welcomeFolderBtn) {
|
|
2409
|
-
welcomeFolderBtn.addEventListener("click", openFolderPickerWithInitialPath);
|
|
2410
|
-
}
|
|
2408
|
+
// Welcome screen folder button (legacy, now handled by initBlankChatCwd)
|
|
2411
2409
|
|
|
2412
2410
|
if (closeFolderPicker && folderPickerModal) {
|
|
2413
2411
|
closeFolderPicker.addEventListener("click", function() {
|
|
@@ -2504,7 +2502,12 @@
|
|
|
2504
2502
|
}
|
|
2505
2503
|
if (_swipeState) return;
|
|
2506
2504
|
if (item.dataset.sessionId) {
|
|
2507
|
-
|
|
2505
|
+
var clickedSession = state.sessions.find(function(s) { return s.id === item.dataset.sessionId; });
|
|
2506
|
+
if (clickedSession && clickedSession.status !== "running") {
|
|
2507
|
+
resumeSessionFromList(item.dataset.sessionId);
|
|
2508
|
+
} else {
|
|
2509
|
+
selectSession(item.dataset.sessionId);
|
|
2510
|
+
}
|
|
2508
2511
|
closeSessionsDrawer();
|
|
2509
2512
|
}
|
|
2510
2513
|
}
|
|
@@ -2524,7 +2527,12 @@
|
|
|
2524
2527
|
return;
|
|
2525
2528
|
}
|
|
2526
2529
|
if (item.dataset.sessionId) {
|
|
2527
|
-
|
|
2530
|
+
var keySession = state.sessions.find(function(s) { return s.id === item.dataset.sessionId; });
|
|
2531
|
+
if (keySession && keySession.status !== "running") {
|
|
2532
|
+
resumeSessionFromList(item.dataset.sessionId);
|
|
2533
|
+
} else {
|
|
2534
|
+
selectSession(item.dataset.sessionId);
|
|
2535
|
+
}
|
|
2528
2536
|
closeSessionsDrawer();
|
|
2529
2537
|
}
|
|
2530
2538
|
}
|
|
@@ -3951,6 +3959,93 @@
|
|
|
3951
3959
|
});
|
|
3952
3960
|
}
|
|
3953
3961
|
|
|
3962
|
+
// Blank-chat CWD inline display + dropdown
|
|
3963
|
+
function initBlankChatCwd() {
|
|
3964
|
+
var cwdEl = document.getElementById("blank-chat-cwd");
|
|
3965
|
+
if (!cwdEl) return;
|
|
3966
|
+
cwdEl.addEventListener("click", toggleBlankChatCwdDropdown);
|
|
3967
|
+
cwdEl.addEventListener("keydown", function(e) {
|
|
3968
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
3969
|
+
e.preventDefault();
|
|
3970
|
+
toggleBlankChatCwdDropdown();
|
|
3971
|
+
}
|
|
3972
|
+
});
|
|
3973
|
+
document.addEventListener("click", function(e) {
|
|
3974
|
+
var dropdown = document.getElementById("blank-chat-cwd-dropdown");
|
|
3975
|
+
if (!dropdown || dropdown.classList.contains("hidden")) return;
|
|
3976
|
+
if (!e.target.closest(".blank-chat-cwd-wrap")) {
|
|
3977
|
+
dropdown.classList.add("hidden");
|
|
3978
|
+
var arrow = document.getElementById("blank-chat-cwd-arrow");
|
|
3979
|
+
if (arrow) arrow.textContent = "▼";
|
|
3980
|
+
}
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
function toggleBlankChatCwdDropdown() {
|
|
3985
|
+
var dropdown = document.getElementById("blank-chat-cwd-dropdown");
|
|
3986
|
+
var arrow = document.getElementById("blank-chat-cwd-arrow");
|
|
3987
|
+
if (!dropdown) return;
|
|
3988
|
+
var isHidden = dropdown.classList.contains("hidden");
|
|
3989
|
+
if (isHidden) {
|
|
3990
|
+
loadBlankChatCwdDropdown(dropdown);
|
|
3991
|
+
dropdown.classList.remove("hidden");
|
|
3992
|
+
if (arrow) arrow.textContent = "▲";
|
|
3993
|
+
} else {
|
|
3994
|
+
dropdown.classList.add("hidden");
|
|
3995
|
+
if (arrow) arrow.textContent = "▼";
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
function loadBlankChatCwdDropdown(dropdown) {
|
|
4000
|
+
var defaultCwd = state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp";
|
|
4001
|
+
dropdown.innerHTML = '<div class="blank-chat-cwd-loading">加载中...</div>';
|
|
4002
|
+
fetch("/api/recent-paths", { credentials: "same-origin" })
|
|
4003
|
+
.then(function(res) { return res.json(); })
|
|
4004
|
+
.then(function(items) {
|
|
4005
|
+
var html = "";
|
|
4006
|
+
// Default directory always first
|
|
4007
|
+
var currentDir = state.workingDir || defaultCwd;
|
|
4008
|
+
html += '<div class="blank-chat-cwd-item' + (currentDir === defaultCwd ? " active" : "") + '" data-path="' + escapeHtml(defaultCwd) + '">' +
|
|
4009
|
+
'<span class="blank-chat-cwd-item-label">默认</span>' +
|
|
4010
|
+
'<span class="blank-chat-cwd-item-path">' + escapeHtml(defaultCwd) + '</span>' +
|
|
4011
|
+
'</div>';
|
|
4012
|
+
// Recent paths (exclude default to avoid duplicate)
|
|
4013
|
+
if (items && items.length) {
|
|
4014
|
+
var seen = {};
|
|
4015
|
+
seen[defaultCwd] = true;
|
|
4016
|
+
items.forEach(function(item) {
|
|
4017
|
+
if (seen[item.path]) return;
|
|
4018
|
+
seen[item.path] = true;
|
|
4019
|
+
html += '<div class="blank-chat-cwd-item' + (currentDir === item.path ? " active" : "") + '" data-path="' + escapeHtml(item.path) + '">' +
|
|
4020
|
+
'<span class="blank-chat-cwd-item-path">' + escapeHtml(item.path) + '</span>' +
|
|
4021
|
+
'</div>';
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
dropdown.innerHTML = html;
|
|
4025
|
+
dropdown.querySelectorAll(".blank-chat-cwd-item").forEach(function(el) {
|
|
4026
|
+
el.addEventListener("click", function(e) {
|
|
4027
|
+
e.stopPropagation();
|
|
4028
|
+
var path = el.dataset.path;
|
|
4029
|
+
state.workingDir = path;
|
|
4030
|
+
try { localStorage.setItem("wand-working-dir", path); } catch(e) {}
|
|
4031
|
+
var pathEl = document.getElementById("blank-chat-cwd-path");
|
|
4032
|
+
if (pathEl) pathEl.textContent = path;
|
|
4033
|
+
dropdown.classList.add("hidden");
|
|
4034
|
+
var arrow = document.getElementById("blank-chat-cwd-arrow");
|
|
4035
|
+
if (arrow) arrow.textContent = "▼";
|
|
4036
|
+
// Update folder picker input if exists
|
|
4037
|
+
var fpInput = document.getElementById("folder-picker-input");
|
|
4038
|
+
if (fpInput) fpInput.value = path;
|
|
4039
|
+
});
|
|
4040
|
+
});
|
|
4041
|
+
})
|
|
4042
|
+
.catch(function() {
|
|
4043
|
+
dropdown.innerHTML = '<div class="blank-chat-cwd-item" data-path="' + escapeHtml(defaultCwd) + '">' +
|
|
4044
|
+
'<span class="blank-chat-cwd-item-path">' + escapeHtml(defaultCwd) + '</span>' +
|
|
4045
|
+
'</div>';
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
|
|
3954
4049
|
function loadRecentPathBubbles() {
|
|
3955
4050
|
var container = document.getElementById("recent-paths-bubbles");
|
|
3956
4051
|
if (!container) return;
|
|
@@ -5670,6 +5670,103 @@
|
|
|
5670
5670
|
.blank-chat-tool-btn .tool-icon {
|
|
5671
5671
|
font-size: 1.125rem;
|
|
5672
5672
|
}
|
|
5673
|
+
/* Blank-chat CWD inline selector */
|
|
5674
|
+
.blank-chat-cwd-wrap {
|
|
5675
|
+
position: relative;
|
|
5676
|
+
display: flex;
|
|
5677
|
+
flex-direction: column;
|
|
5678
|
+
align-items: center;
|
|
5679
|
+
margin-top: 8px;
|
|
5680
|
+
}
|
|
5681
|
+
.blank-chat-cwd {
|
|
5682
|
+
display: inline-flex;
|
|
5683
|
+
align-items: center;
|
|
5684
|
+
gap: 4px;
|
|
5685
|
+
cursor: pointer;
|
|
5686
|
+
font-size: 0.75rem;
|
|
5687
|
+
color: var(--text-muted);
|
|
5688
|
+
padding: 4px 8px;
|
|
5689
|
+
border-radius: var(--radius-sm);
|
|
5690
|
+
transition: color var(--transition-fast), background var(--transition-fast);
|
|
5691
|
+
user-select: none;
|
|
5692
|
+
}
|
|
5693
|
+
.blank-chat-cwd:hover {
|
|
5694
|
+
color: var(--accent);
|
|
5695
|
+
background: var(--accent-muted);
|
|
5696
|
+
}
|
|
5697
|
+
.blank-chat-cwd-icon {
|
|
5698
|
+
font-size: 0.8125rem;
|
|
5699
|
+
}
|
|
5700
|
+
.blank-chat-cwd-path {
|
|
5701
|
+
font-family: var(--font-mono);
|
|
5702
|
+
max-width: 280px;
|
|
5703
|
+
overflow: hidden;
|
|
5704
|
+
text-overflow: ellipsis;
|
|
5705
|
+
white-space: nowrap;
|
|
5706
|
+
}
|
|
5707
|
+
.blank-chat-cwd-arrow {
|
|
5708
|
+
font-size: 0.625rem;
|
|
5709
|
+
opacity: 0.6;
|
|
5710
|
+
transition: transform var(--transition-fast);
|
|
5711
|
+
}
|
|
5712
|
+
.blank-chat-cwd-dropdown {
|
|
5713
|
+
position: absolute;
|
|
5714
|
+
top: 100%;
|
|
5715
|
+
left: 50%;
|
|
5716
|
+
transform: translateX(-50%);
|
|
5717
|
+
min-width: 260px;
|
|
5718
|
+
max-width: 400px;
|
|
5719
|
+
max-height: 240px;
|
|
5720
|
+
overflow-y: auto;
|
|
5721
|
+
background: var(--bg-primary);
|
|
5722
|
+
border: 1px solid var(--border-default);
|
|
5723
|
+
border-radius: var(--radius-md);
|
|
5724
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
5725
|
+
z-index: 50;
|
|
5726
|
+
margin-top: 4px;
|
|
5727
|
+
padding: 4px;
|
|
5728
|
+
}
|
|
5729
|
+
.blank-chat-cwd-item {
|
|
5730
|
+
display: flex;
|
|
5731
|
+
align-items: center;
|
|
5732
|
+
gap: 6px;
|
|
5733
|
+
padding: 6px 10px;
|
|
5734
|
+
border-radius: var(--radius-sm);
|
|
5735
|
+
cursor: pointer;
|
|
5736
|
+
font-size: 0.75rem;
|
|
5737
|
+
color: var(--text-secondary);
|
|
5738
|
+
transition: background var(--transition-fast);
|
|
5739
|
+
}
|
|
5740
|
+
.blank-chat-cwd-item:hover {
|
|
5741
|
+
background: var(--accent-muted);
|
|
5742
|
+
color: var(--accent);
|
|
5743
|
+
}
|
|
5744
|
+
.blank-chat-cwd-item.active {
|
|
5745
|
+
background: var(--accent-muted);
|
|
5746
|
+
color: var(--accent);
|
|
5747
|
+
font-weight: 500;
|
|
5748
|
+
}
|
|
5749
|
+
.blank-chat-cwd-item-label {
|
|
5750
|
+
font-size: 0.625rem;
|
|
5751
|
+
background: var(--accent-muted);
|
|
5752
|
+
color: var(--accent);
|
|
5753
|
+
padding: 1px 5px;
|
|
5754
|
+
border-radius: 3px;
|
|
5755
|
+
font-weight: 500;
|
|
5756
|
+
white-space: nowrap;
|
|
5757
|
+
}
|
|
5758
|
+
.blank-chat-cwd-item-path {
|
|
5759
|
+
font-family: var(--font-mono);
|
|
5760
|
+
overflow: hidden;
|
|
5761
|
+
text-overflow: ellipsis;
|
|
5762
|
+
white-space: nowrap;
|
|
5763
|
+
}
|
|
5764
|
+
.blank-chat-cwd-loading {
|
|
5765
|
+
padding: 8px 10px;
|
|
5766
|
+
font-size: 0.75rem;
|
|
5767
|
+
color: var(--text-muted);
|
|
5768
|
+
text-align: center;
|
|
5769
|
+
}
|
|
5673
5770
|
.blank-chat-hint {
|
|
5674
5771
|
font-size: 0.8125rem;
|
|
5675
5772
|
color: var(--text-muted);
|