@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.
@@ -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> Use a custom config file 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 */
@@ -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 in managed/full-access modes for auto-confirm analysis
1183
- if (shortcutKey && record.autoApprovePermissions) {
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
- this.logger.appendShortcutLog(id, shortcutKey, tailLines);
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 hidden = getHiddenClaudeSessionIds(storage);
512
- if (!hidden.has(claudeSessionId)) {
513
- hidden.add(claudeSessionId);
514
- saveHiddenClaudeSessionIds(storage, hidden);
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 hidden = getHiddenClaudeSessionIds(storage);
527
- let added = 0;
544
+ const toDelete = [];
528
545
  for (const session of sessions) {
529
- if (!session.claudeSessionId || session.cwd !== cwd) {
530
- continue;
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
- if (added > 0) {
539
- saveHiddenClaudeSessionIds(storage, hidden);
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 hidden = getHiddenClaudeSessionIds(storage);
557
- let added = 0;
558
- for (const claudeSessionId of claudeSessionIds) {
559
- if (hidden.has(claudeSessionId)) {
560
- continue;
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
- if (added > 0) {
566
- saveHiddenClaudeSessionIds(storage, hidden);
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: added });
598
+ res.json({ ok: true, deleted: deleted + toHide.length });
569
599
  }
570
600
  catch (error) {
571
601
  res.status(500).json({ error: getErrorMessage(error, "无法批量删除历史会话。") });
@@ -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
- constructor(configDir: string);
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
  }
@@ -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
- constructor(configDir) {
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
- appendFileSync(path.join(dir, "shortcut-interactions.jsonl"), entry);
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
- '<button class="blank-chat-tool-btn" id="welcome-tool-folder" type="button" title="选择工作目录">' +
498
- '<span class="tool-icon">📎</span>目录' +
499
- '</button>' +
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">会话将在此目录启动,支持路径自动补全。</p>' +
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
- var welcomeFolderBtn = document.getElementById("welcome-tool-folder");
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
- selectSession(item.dataset.sessionId);
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
- selectSession(item.dataset.sessionId);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {