demian-cli 1.0.9 → 1.1.1

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.
@@ -1,8 +1,8 @@
1
1
  import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
2
2
 
3
3
  // src/vscode-worker.ts
4
- import { mkdir as mkdir15, rm as rm4, writeFile as writeFile14 } from "node:fs/promises";
5
- import path35 from "node:path";
4
+ import { mkdir as mkdir16, rm as rm5, writeFile as writeFile15 } from "node:fs/promises";
5
+ import path36 from "node:path";
6
6
 
7
7
  // src/permissions/presets.ts
8
8
  var PERMISSION_PRESETS = ["deny", "ask", "auto", "full"];
@@ -67,6 +67,26 @@ function isCompactCommand(input2) {
67
67
  function isRetryCommand(input2) {
68
68
  return input2.trim().toLowerCase() === "/retry";
69
69
  }
70
+ function isSessionCommand(input2) {
71
+ return /^\/(?:session|sessions|conversation|conversations|conv)(?:\s|$)/i.test(input2.trim());
72
+ }
73
+ function parseSessionCommand(input2) {
74
+ const raw = input2.trim();
75
+ if (!isSessionCommand(raw)) return void 0;
76
+ const rest = raw.replace(/^\/(?:session|sessions|conversation|conversations|conv)\b/i, "").trim();
77
+ if (!rest) return { action: "help" };
78
+ const tokens = shellishSplit(rest);
79
+ const action = tokens.shift()?.toLowerCase() ?? "help";
80
+ if (action === "help" || action === "-h" || action === "--help") return { action: "help" };
81
+ if (action === "list" || action === "ls") return { action: "list" };
82
+ if (action === "current" || action === "status") return { action: "current" };
83
+ if (action === "new" || action === "create") return { action: "new", title: tokens.join(" ").trim() || void 0 };
84
+ if (action === "switch" || action === "select" || action === "use" || action === "open") return { action: "switch", target: tokens.join(" ").trim() || void 0 };
85
+ if (action === "delete" || action === "remove" || action === "rm") return { action: "delete", target: tokens.join(" ").trim() || void 0 };
86
+ if (action === "rename" || action === "name") return { action: "rename", title: tokens.join(" ").trim() || void 0 };
87
+ if (action === "clear" || action === "reset") return { action: "clear" };
88
+ return { action: "switch", target: [action, ...tokens].join(" ").trim() };
89
+ }
70
90
  function isCoworkCommand(input2) {
71
91
  return /^\/cowork(?:\s|$)/i.test(input2.trim());
72
92
  }
@@ -748,6 +768,8 @@ var TuiStore = class {
748
768
  blocks: [],
749
769
  activity: "starting",
750
770
  inputMode: "starting",
771
+ sessionOptions: [],
772
+ sessionCursor: 0,
751
773
  providerOptions: [],
752
774
  providerCursor: 0,
753
775
  agentOptions: [],
@@ -769,6 +791,7 @@ var TuiStore = class {
769
791
  #blockId = 0;
770
792
  #stopActiveTask;
771
793
  #promptResolve;
794
+ #sessionResolve;
772
795
  #queuedPrompt;
773
796
  #exitRequested = false;
774
797
  #promptHistory = [];
@@ -789,6 +812,7 @@ var TuiStore = class {
789
812
  ...this.#state,
790
813
  status: { ...this.#state.status },
791
814
  selection: this.#state.selection ? { ...this.#state.selection } : void 0,
815
+ sessionOptions: this.#state.sessionOptions.map((option) => ({ ...option })),
792
816
  providerOptions: this.#state.providerOptions.map((option) => ({ ...option })),
793
817
  agentOptions: this.#state.agentOptions.map((option) => ({ ...option })),
794
818
  blocks: this.#state.blocks.map((block) => ({ ...block, lines: [...block.lines], toolDetails: cloneToolRunDetails(block.toolDetails), goalWork: cloneGoalWork(block.goalWork), cowork: cloneCoworkGroup(block.cowork) })),
@@ -827,6 +851,7 @@ var TuiStore = class {
827
851
  requestExit() {
828
852
  this.#exitRequested = true;
829
853
  if (this.#promptResolve) this.#resolvePrompt("");
854
+ if (this.#sessionResolve) this.#resolveSession({ kind: "exit" });
830
855
  this.#stopActiveTask?.();
831
856
  this.#state.inputMode = "done";
832
857
  this.#state.done = true;
@@ -850,7 +875,7 @@ var TuiStore = class {
850
875
  }
851
876
  markTaskFailed(message) {
852
877
  const lines = [message];
853
- if (this.#lastRetryPrompt()) lines.push("Press ctrl+r or type /retry to run the last task again.");
878
+ if (this.#lastRetryPrompt()) lines.push("Press Esc then r or type /retry to run the last task again.");
854
879
  this.#append({ kind: "warning", title: "Task Failed", lines });
855
880
  this.#state.status.reason = "error";
856
881
  this.#state.inputMode = this.#exitRequested ? "done" : "prompt";
@@ -906,6 +931,84 @@ var TuiStore = class {
906
931
  this.#state.activity = `context compacted: ${summary.beforeTokenEstimate} -> ${summary.afterTokenEstimate}`;
907
932
  this.#notify();
908
933
  }
934
+ conversationSnapshot() {
935
+ const snapshot = this.snapshot();
936
+ return {
937
+ ...snapshot,
938
+ inputMode: "prompt",
939
+ promptInput: "",
940
+ promptError: void 0,
941
+ streamingText: "",
942
+ streamingFinalized: true,
943
+ permission: void 0,
944
+ done: false
945
+ };
946
+ }
947
+ restoreConversationSnapshot(snapshot, fallback) {
948
+ this.#resetTransientConversationState();
949
+ const currentSettings = {
950
+ selection: this.#state.selection,
951
+ providerOptions: this.#state.providerOptions,
952
+ providerCursor: this.#state.providerCursor,
953
+ agentOptions: this.#state.agentOptions,
954
+ agentCursor: this.#state.agentCursor,
955
+ selectedAgent: this.#state.selectedAgent,
956
+ permissionPreset: this.#state.permissionPreset,
957
+ permissionPresetCursor: this.#state.permissionPresetCursor,
958
+ contextEfficiency: this.#state.contextEfficiency
959
+ };
960
+ const restoredSelection = snapshot?.selection ? { ...snapshot.selection } : currentSettings.selection;
961
+ const restoredAgent = snapshot?.selectedAgent ?? snapshot?.status?.agent ?? currentSettings.selectedAgent;
962
+ const restoredPermissionPreset = normalizePermissionPreset(snapshot?.permissionPreset, currentSettings.permissionPreset);
963
+ this.#state = {
964
+ ...this.#state,
965
+ ...safeConversationSnapshot(snapshot),
966
+ ...currentSettings,
967
+ selection: restoredSelection,
968
+ providerCursor: Math.max(0, currentSettings.providerOptions.findIndex((option) => option.name === restoredSelection?.providerName)),
969
+ selectedAgent: restoredAgent,
970
+ agentCursor: Math.max(0, currentSettings.agentOptions.findIndex((option) => option.name === restoredAgent)),
971
+ permissionPreset: restoredPermissionPreset,
972
+ permissionPresetCursor: Math.max(0, PERMISSION_PRESETS.indexOf(restoredPermissionPreset)),
973
+ status: {
974
+ ...snapshot?.status ?? this.#state.status,
975
+ sessionId: fallback.sessionId,
976
+ cwd: fallback.cwd,
977
+ provider: restoredSelection?.providerName ?? snapshot?.status?.provider ?? this.#state.status.provider,
978
+ model: restoredSelection?.model ?? snapshot?.status?.model ?? this.#state.status.model,
979
+ agent: restoredAgent ?? snapshot?.status?.agent ?? this.#state.status.agent
980
+ },
981
+ inputMode: "prompt",
982
+ promptInput: "",
983
+ promptError: void 0,
984
+ streamingText: "",
985
+ streamingFinalized: true,
986
+ permission: void 0,
987
+ done: false,
988
+ activity: `conversation: ${fallback.title}`
989
+ };
990
+ this.#blockId = nextBlockId(this.#state.blocks);
991
+ this.#notify();
992
+ }
993
+ clearConversation(title = "New session") {
994
+ this.#resetTransientConversationState();
995
+ this.#state.blocks = [];
996
+ this.#state.streamingText = "";
997
+ this.#state.streamingFinalized = true;
998
+ this.#state.permission = void 0;
999
+ this.#state.warnings = [];
1000
+ this.#state.done = false;
1001
+ this.#state.finalAnswer = void 0;
1002
+ this.#state.progressNotes = [];
1003
+ this.#state.turnDiff = void 0;
1004
+ this.#state.diffExpanded = false;
1005
+ this.#state.workPlan = void 0;
1006
+ this.#state.workPlanExpanded = false;
1007
+ this.#state.goal = void 0;
1008
+ this.#state.pendingClaudeCodePlan = void 0;
1009
+ this.#state.activity = `cleared conversation: ${title}`;
1010
+ this.prepareForPrompt("waiting for next message");
1011
+ }
909
1012
  prepareForPrompt(message = "waiting for next message") {
910
1013
  if (this.#exitRequested) return;
911
1014
  this.#state.inputMode = "prompt";
@@ -963,7 +1066,45 @@ var TuiStore = class {
963
1066
  currentPermissionPreset() {
964
1067
  return this.#state.permissionPreset;
965
1068
  }
1069
+ requestSessionSelection(options2) {
1070
+ if (this.#exitRequested) return Promise.resolve({ kind: "exit" });
1071
+ const normalized = options2.length > 0 ? options2.map((option) => ({ ...option })) : [{ kind: "new", title: "New session", status: "ready", currentWorkspace: true }];
1072
+ return new Promise((resolve) => {
1073
+ this.#sessionResolve = resolve;
1074
+ this.#state.inputMode = "session";
1075
+ this.#state.sessionOptions = normalized;
1076
+ this.#state.sessionCursor = 0;
1077
+ this.#state.promptInput = "";
1078
+ this.#state.promptError = void 0;
1079
+ this.#state.streamingText = "";
1080
+ this.#state.streamingFinalized = true;
1081
+ this.#state.done = false;
1082
+ this.#state.activity = "select session";
1083
+ this.#notify();
1084
+ });
1085
+ }
1086
+ moveSessionCursor(delta) {
1087
+ if (this.#state.inputMode !== "session" || this.#state.sessionOptions.length === 0) return;
1088
+ const count = this.#state.sessionOptions.length;
1089
+ this.#state.sessionCursor = (this.#state.sessionCursor + delta + count) % count;
1090
+ this.#notify();
1091
+ }
1092
+ submitSessionSelection() {
1093
+ if (this.#state.inputMode !== "session") return;
1094
+ const option = this.#state.sessionOptions[this.#state.sessionCursor];
1095
+ if (!option) return;
1096
+ if (option.kind === "new") {
1097
+ this.#resolveSession({ kind: "new" });
1098
+ return;
1099
+ }
1100
+ if (option.id) this.#resolveSession({ kind: "session", id: option.id });
1101
+ }
1102
+ submitNewSessionSelection() {
1103
+ if (this.#state.inputMode !== "session") return;
1104
+ this.#resolveSession({ kind: "new" });
1105
+ }
966
1106
  requestPrompt(initialPrompt) {
1107
+ if (this.#exitRequested) return Promise.resolve("");
967
1108
  const prompt = (initialPrompt ?? "").trim();
968
1109
  if (prompt) {
969
1110
  this.#rememberPrompt(prompt);
@@ -1004,12 +1145,12 @@ var TuiStore = class {
1004
1145
  this.#notify();
1005
1146
  });
1006
1147
  }
1007
- appendPromptInput(input2) {
1148
+ appendPromptInput(input2, options2 = {}) {
1008
1149
  if (this.#state.inputMode !== "prompt" && this.#state.inputMode !== "running") return;
1009
1150
  if (this.#state.inputMode === "prompt") this.#detachPromptHistory();
1010
1151
  this.#state.promptInput += input2;
1011
1152
  this.#state.promptError = void 0;
1012
- this.#notify();
1153
+ if (options2.notify !== false) this.#notify();
1013
1154
  }
1014
1155
  navigatePromptHistory(delta) {
1015
1156
  if (this.#state.inputMode !== "prompt" || this.#promptHistory.length === 0) return;
@@ -1164,19 +1305,19 @@ var TuiStore = class {
1164
1305
  this.#state.activity = "waiting for first message";
1165
1306
  this.#notify();
1166
1307
  }
1167
- backspacePromptInput() {
1308
+ backspacePromptInput(options2 = {}) {
1168
1309
  if (this.#state.inputMode !== "prompt" && this.#state.inputMode !== "running") return;
1169
1310
  if (this.#state.inputMode === "prompt") this.#detachPromptHistory();
1170
1311
  this.#state.promptInput = Array.from(this.#state.promptInput).slice(0, -1).join("");
1171
1312
  this.#state.promptError = void 0;
1172
- this.#notify();
1313
+ if (options2.notify !== false) this.#notify();
1173
1314
  }
1174
- clearPromptInput() {
1315
+ clearPromptInput(options2 = {}) {
1175
1316
  if (this.#state.inputMode !== "prompt" && this.#state.inputMode !== "running") return;
1176
1317
  if (this.#state.inputMode === "prompt") this.#detachPromptHistory();
1177
1318
  this.#state.promptInput = "";
1178
1319
  this.#state.promptError = void 0;
1179
- this.#notify();
1320
+ if (options2.notify !== false) this.#notify();
1180
1321
  }
1181
1322
  usePendingClaudeCodePlan() {
1182
1323
  if (this.#state.inputMode !== "prompt") return false;
@@ -2004,9 +2145,24 @@ var TuiStore = class {
2004
2145
  resolve(prompt);
2005
2146
  this.#notify();
2006
2147
  }
2148
+ #resolveSession(selection) {
2149
+ const resolve = this.#sessionResolve;
2150
+ if (!resolve) return;
2151
+ this.#sessionResolve = void 0;
2152
+ if (selection.kind === "exit") {
2153
+ this.#state.inputMode = "done";
2154
+ this.#state.done = true;
2155
+ this.#state.activity = "exiting";
2156
+ } else {
2157
+ this.#state.inputMode = "starting";
2158
+ this.#state.activity = selection.kind === "new" ? "creating session" : "opening session";
2159
+ }
2160
+ this.#notify();
2161
+ resolve(selection);
2162
+ }
2007
2163
  #rememberPrompt(prompt) {
2008
2164
  const value = prompt.trim();
2009
- if (!value || isExitCommand(value) || isStopCommand(value) || isCompactCommand(value) || isRetryCommand(value)) return;
2165
+ if (!value || isExitCommand(value) || isStopCommand(value) || isCompactCommand(value) || isRetryCommand(value) || isSessionCommand(value)) return;
2010
2166
  if (this.#promptHistory.at(-1) === value) {
2011
2167
  this.#state.canRetryLastPrompt = true;
2012
2168
  return;
@@ -2018,6 +2174,20 @@ var TuiStore = class {
2018
2174
  #lastRetryPrompt() {
2019
2175
  return this.#promptHistory.at(-1);
2020
2176
  }
2177
+ #resetTransientConversationState() {
2178
+ this.#toolRuns.clear();
2179
+ this.#goalWorkBlocks.clear();
2180
+ this.#coworkBlocks.clear();
2181
+ this.#activeGoalWorkBlockId = void 0;
2182
+ this.#currentGoalNarrationId = void 0;
2183
+ this.#timelineProgressIds.clear();
2184
+ this.#pendingPermissionCallId = void 0;
2185
+ if (this.#pendingPermissionTimeout) clearTimeout(this.#pendingPermissionTimeout);
2186
+ this.#pendingPermissionTimeout = void 0;
2187
+ this.#activeClaudeCodePlan = void 0;
2188
+ this.#promptHistoryCursor = void 0;
2189
+ this.#promptDraft = "";
2190
+ }
2021
2191
  #detachPromptHistory() {
2022
2192
  this.#promptHistoryCursor = void 0;
2023
2193
  this.#promptDraft = "";
@@ -2309,6 +2479,36 @@ function cloneGoalToolRun(tool) {
2309
2479
  details: cloneToolRunDetails(tool.details)
2310
2480
  };
2311
2481
  }
2482
+ function safeConversationSnapshot(snapshot) {
2483
+ if (!snapshot || typeof snapshot !== "object") return {};
2484
+ return {
2485
+ status: snapshot.status ? { ...snapshot.status } : void 0,
2486
+ blocks: Array.isArray(snapshot.blocks) ? snapshot.blocks.map((block) => ({ ...block, lines: [...block.lines ?? []], toolDetails: cloneToolRunDetails(block.toolDetails), goalWork: cloneGoalWork(block.goalWork), cowork: cloneCoworkGroup(block.cowork) })) : [],
2487
+ activity: typeof snapshot.activity === "string" ? snapshot.activity : "restored conversation",
2488
+ warnings: Array.isArray(snapshot.warnings) ? [...snapshot.warnings] : [],
2489
+ finalAnswer: snapshot.finalAnswer,
2490
+ progressNotes: Array.isArray(snapshot.progressNotes) ? snapshot.progressNotes.map((note) => ({ ...note })) : [],
2491
+ turnDiff: snapshot.turnDiff ? cloneDiffSummary(snapshot.turnDiff) : void 0,
2492
+ diffExpanded: snapshot.diffExpanded === true,
2493
+ workPlan: snapshot.workPlan ? {
2494
+ ...snapshot.workPlan,
2495
+ steps: snapshot.workPlan.steps.map((step) => ({ ...step }))
2496
+ } : void 0,
2497
+ workPlanExpanded: snapshot.workPlanExpanded === true,
2498
+ goal: snapshot.goal ? { ...snapshot.goal } : void 0,
2499
+ contextEfficiency: snapshot.contextEfficiency ? cloneContextEfficiency(snapshot.contextEfficiency) : void 0,
2500
+ canRetryLastPrompt: snapshot.canRetryLastPrompt === true,
2501
+ pendingClaudeCodePlan: snapshot.pendingClaudeCodePlan ? { ...snapshot.pendingClaudeCodePlan } : void 0
2502
+ };
2503
+ }
2504
+ function nextBlockId(blocks) {
2505
+ let max = 0;
2506
+ for (const block of blocks ?? []) {
2507
+ const match = /^block_(\d+)$/.exec(block.id);
2508
+ if (match) max = Math.max(max, Number(match[1]));
2509
+ }
2510
+ return max;
2511
+ }
2312
2512
  function formatDuration2(durationMs) {
2313
2513
  if (durationMs < 1e3) return `${durationMs}ms`;
2314
2514
  return `${Math.round(durationMs / 1e3)}s`;
@@ -2327,7 +2527,7 @@ function buildClaudeCodePlanExecutionPrompt(planText, requestText) {
2327
2527
  }
2328
2528
 
2329
2529
  // src/ui/tui/controller.ts
2330
- import path32 from "node:path";
2530
+ import path33 from "node:path";
2331
2531
 
2332
2532
  // src/agents/types.ts
2333
2533
  function normalizeAgent(input2) {
@@ -25421,8 +25621,8 @@ function permissionLabel(req) {
25421
25621
  if ((req.tool === "write_file" || req.tool === "edit_file" || req.tool === "read_file") && typeof inputObject.path === "string") {
25422
25622
  return `${req.tool}: ${inputObject.path}`;
25423
25623
  }
25424
- const path36 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
25425
- if (path36) return `${tool}: ${path36}`;
25624
+ const path37 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
25625
+ if (path37) return `${tool}: ${path37}`;
25426
25626
  return tool;
25427
25627
  }
25428
25628
 
@@ -30046,8 +30246,8 @@ async function summarizeGoalTitleWithProvider(options2) {
30046
30246
  }
30047
30247
  function normalizeGoalTitle(value, objective) {
30048
30248
  const raw = typeof value === "string" ? value : "";
30049
- const firstLine = raw.split(/\r?\n/).find((line) => line.trim()) ?? "";
30050
- const cleaned = stripDecorations(firstLine);
30249
+ const firstLine2 = raw.split(/\r?\n/).find((line) => line.trim()) ?? "";
30250
+ const cleaned = stripDecorations(firstLine2);
30051
30251
  return truncateGoalTitle(cleaned || fallbackGoalTitle(objective));
30052
30252
  }
30053
30253
  function fallbackGoalTitle(objective) {
@@ -31646,17 +31846,17 @@ function findDependencyCycle(group) {
31646
31846
  const byId = new Map(group.map((member) => [member.memberId, member]));
31647
31847
  const visiting = /* @__PURE__ */ new Set();
31648
31848
  const visited = /* @__PURE__ */ new Set();
31649
- const path36 = [];
31849
+ const path37 = [];
31650
31850
  const visit = (id) => {
31651
- if (visiting.has(id)) return [...path36.slice(path36.indexOf(id)), id];
31851
+ if (visiting.has(id)) return [...path37.slice(path37.indexOf(id)), id];
31652
31852
  if (visited.has(id)) return void 0;
31653
31853
  visiting.add(id);
31654
- path36.push(id);
31854
+ path37.push(id);
31655
31855
  for (const dep of byId.get(id)?.dependsOn ?? []) {
31656
31856
  const cycle = visit(dep);
31657
31857
  if (cycle) return cycle;
31658
31858
  }
31659
- path36.pop();
31859
+ path37.pop();
31660
31860
  visiting.delete(id);
31661
31861
  visited.add(id);
31662
31862
  return void 0;
@@ -32477,9 +32677,212 @@ function errorMessage3(error) {
32477
32677
  return error instanceof Error ? error.message : String(error);
32478
32678
  }
32479
32679
 
32680
+ // src/ui/conversations.ts
32681
+ import { mkdir as mkdir14, readFile as readFile16, readdir as readdir4, rm as rm4, writeFile as writeFile13 } from "node:fs/promises";
32682
+ import path32 from "node:path";
32683
+ var CONVERSATIONS_DIR = "conversations";
32684
+ var CONVERSATION_INDEX_FILE = "conversations.json";
32685
+ var CONVERSATION_FILE = "conversation.json";
32686
+ var MAX_STORED_CONVERSATIONS = 40;
32687
+ var MAX_MODEL_HISTORY = 80;
32688
+ function createConversationRecord(input2) {
32689
+ const timestamp = input2.now ?? Date.now();
32690
+ return {
32691
+ id: sanitizeConversationId(input2.id),
32692
+ title: input2.title?.trim() || "New session",
32693
+ createdAt: timestamp,
32694
+ updatedAt: timestamp,
32695
+ cwd: path32.resolve(input2.cwd),
32696
+ modelHistory: cleanModelHistory(input2.history ?? []),
32697
+ snapshot: input2.snapshot
32698
+ };
32699
+ }
32700
+ async function loadConversationIndex(storageDir = defaultDemianStorageDir()) {
32701
+ const index = await readJson(path32.join(storageDir, CONVERSATION_INDEX_FILE));
32702
+ const conversations = Array.isArray(index?.conversations) ? index.conversations.map((item) => {
32703
+ if (!item || typeof item !== "object") return void 0;
32704
+ const object2 = item;
32705
+ const id = typeof object2.id === "string" ? sanitizeConversationId(object2.id) : "";
32706
+ if (!id) return void 0;
32707
+ return {
32708
+ id,
32709
+ title: typeof object2.title === "string" && object2.title.trim() ? object2.title.trim() : "New session",
32710
+ updatedAt: numberOrNow(object2.updatedAt),
32711
+ cwd: typeof object2.cwd === "string" ? object2.cwd : void 0
32712
+ };
32713
+ }).filter((item) => Boolean(item)) : [];
32714
+ return {
32715
+ selectedSessionId: typeof index?.selectedSessionId === "string" ? sanitizeConversationId(index.selectedSessionId) : void 0,
32716
+ conversations
32717
+ };
32718
+ }
32719
+ async function loadConversationRecords(storageDir = defaultDemianStorageDir()) {
32720
+ const index = await loadConversationIndex(storageDir);
32721
+ const ids = new Set(index.conversations.map((item) => item.id));
32722
+ for (const id of await listConversationRecordIds(storageDir)) ids.add(id);
32723
+ const records = (await Promise.all(
32724
+ [...ids].map(async (id) => {
32725
+ const item = await readJson(conversationRecordPath(storageDir, id));
32726
+ return normalizeConversationRecord(item, id);
32727
+ })
32728
+ )).filter((item) => Boolean(item));
32729
+ records.sort((a, b2) => b2.updatedAt - a.updatedAt);
32730
+ return { selectedSessionId: index.selectedSessionId, records };
32731
+ }
32732
+ async function saveConversationRecords(storageDir, records, selectedSessionId) {
32733
+ const root = storageDir ?? defaultDemianStorageDir();
32734
+ const kept = [...records].sort((a, b2) => b2.updatedAt - a.updatedAt).slice(0, MAX_STORED_CONVERSATIONS).map((record) => ({ ...record, id: sanitizeConversationId(record.id), modelHistory: cleanModelHistory(record.modelHistory) }));
32735
+ await mkdir14(path32.join(root, CONVERSATIONS_DIR), { recursive: true });
32736
+ for (const record of kept) {
32737
+ await writeJson(conversationRecordPath(root, record.id), {
32738
+ version: 1,
32739
+ id: record.id,
32740
+ title: record.title,
32741
+ createdAt: record.createdAt,
32742
+ updatedAt: record.updatedAt,
32743
+ cwd: record.cwd,
32744
+ modelHistory: record.modelHistory,
32745
+ snapshot: record.snapshot
32746
+ });
32747
+ }
32748
+ await writeJson(path32.join(root, CONVERSATION_INDEX_FILE), {
32749
+ version: 1,
32750
+ selectedSessionId,
32751
+ conversations: kept.map((record) => ({
32752
+ id: record.id,
32753
+ title: record.title,
32754
+ updatedAt: record.updatedAt,
32755
+ cwd: record.cwd,
32756
+ path: conversationDir(root, record.id)
32757
+ }))
32758
+ });
32759
+ }
32760
+ async function deleteConversationRecord(storageDir, id) {
32761
+ const root = storageDir ?? defaultDemianStorageDir();
32762
+ await rm4(conversationDir(root, id), { recursive: true, force: true });
32763
+ }
32764
+ function findConversation(records, target) {
32765
+ const value = target?.trim();
32766
+ if (!value) return void 0;
32767
+ const index = Number(value);
32768
+ if (Number.isInteger(index) && index >= 1 && index <= records.length) return records[index - 1];
32769
+ const normalized = value.toLowerCase();
32770
+ return records.find((record) => record.id === value || record.id.startsWith(value)) ?? records.find((record) => record.title.toLowerCase() === normalized) ?? records.find((record) => record.title.toLowerCase().includes(normalized));
32771
+ }
32772
+ function sortConversationRecords(records, cwd) {
32773
+ const resolvedCwd = path32.resolve(cwd);
32774
+ return [...records].sort((a, b2) => {
32775
+ const aCurrent = isConversationForCwd(a, resolvedCwd) ? 0 : 1;
32776
+ const bCurrent = isConversationForCwd(b2, resolvedCwd) ? 0 : 1;
32777
+ if (aCurrent !== bCurrent) return aCurrent - bCurrent;
32778
+ const statusDelta = conversationStatusRank(conversationRuntimeStatus(a, resolvedCwd)) - conversationStatusRank(conversationRuntimeStatus(b2, resolvedCwd));
32779
+ if (statusDelta !== 0) return statusDelta;
32780
+ return b2.updatedAt - a.updatedAt;
32781
+ });
32782
+ }
32783
+ function conversationRuntimeStatus(record, cwd) {
32784
+ if (record.snapshot?.inputMode === "running") return "running";
32785
+ return isConversationForCwd(record, cwd) ? "ready" : "stored";
32786
+ }
32787
+ function isConversationForCwd(record, cwd) {
32788
+ return path32.resolve(record.cwd) === path32.resolve(cwd);
32789
+ }
32790
+ function conversationSummaryLine(record, index, activeId) {
32791
+ const active = record.id === activeId ? "*" : " ";
32792
+ const age = formatRelativeAge(Date.now() - record.updatedAt);
32793
+ return `${active} ${index + 1}. ${record.title} ${record.id} ${age} ${record.cwd}`;
32794
+ }
32795
+ function conversationStatusRank(status) {
32796
+ if (status === "running") return 0;
32797
+ if (status === "ready") return 1;
32798
+ return 2;
32799
+ }
32800
+ function titleFromHistory(history, fallback = "New session") {
32801
+ const firstUser = history.find((message) => message.role === "user" && typeof message.content === "string");
32802
+ return firstLine(firstUser?.content ?? fallback);
32803
+ }
32804
+ function cleanModelHistory(history) {
32805
+ return history.filter((message) => !isInternalModelHistoryMessage(message)).slice(-MAX_MODEL_HISTORY);
32806
+ }
32807
+ function conversationDir(storageDir, id) {
32808
+ return path32.join(storageDir, CONVERSATIONS_DIR, sanitizeConversationId(id));
32809
+ }
32810
+ function conversationRecordPath(storageDir, id) {
32811
+ return path32.join(conversationDir(storageDir, id), CONVERSATION_FILE);
32812
+ }
32813
+ async function listConversationRecordIds(storageDir) {
32814
+ try {
32815
+ const entries = await readdir4(path32.join(storageDir, CONVERSATIONS_DIR), { withFileTypes: true });
32816
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => sanitizeConversationId(entry.name)).filter(Boolean);
32817
+ } catch {
32818
+ return [];
32819
+ }
32820
+ }
32821
+ async function readJson(filePath) {
32822
+ try {
32823
+ return JSON.parse(await readFile16(filePath, "utf8"));
32824
+ } catch {
32825
+ return void 0;
32826
+ }
32827
+ }
32828
+ async function writeJson(filePath, value) {
32829
+ await mkdir14(path32.dirname(filePath), { recursive: true });
32830
+ await writeFile13(filePath, `${JSON.stringify(value, null, 2)}
32831
+ `, "utf8");
32832
+ }
32833
+ function normalizeConversationRecord(value, fallbackId) {
32834
+ if (!value || typeof value !== "object") return void 0;
32835
+ const object2 = value;
32836
+ const id = sanitizeConversationId(typeof object2.id === "string" ? object2.id : fallbackId);
32837
+ if (!id) return void 0;
32838
+ const history = Array.isArray(object2.modelHistory) ? object2.modelHistory.filter(isMessage) : [];
32839
+ const snapshot = object2.snapshot && typeof object2.snapshot === "object" && !Array.isArray(object2.snapshot) ? object2.snapshot : void 0;
32840
+ return {
32841
+ id,
32842
+ title: typeof object2.title === "string" && object2.title.trim() ? object2.title.trim() : titleFromHistory(history),
32843
+ createdAt: numberOrNow(object2.createdAt),
32844
+ updatedAt: numberOrNow(object2.updatedAt),
32845
+ cwd: typeof object2.cwd === "string" && object2.cwd.trim() ? object2.cwd : process.cwd(),
32846
+ modelHistory: cleanModelHistory(history),
32847
+ snapshot
32848
+ };
32849
+ }
32850
+ function isMessage(value) {
32851
+ if (!value || typeof value !== "object") return false;
32852
+ const message = value;
32853
+ if (message.role === "user") return typeof message.content === "string" || Array.isArray(message.content);
32854
+ if (message.role === "assistant") return message.content === void 0 || message.content === null || typeof message.content === "string" || Array.isArray(message.toolCalls);
32855
+ if (message.role === "tool") return typeof message.toolCallId === "string" && typeof message.name === "string" && typeof message.content === "string";
32856
+ if (message.role === "system") return typeof message.content === "string";
32857
+ return false;
32858
+ }
32859
+ function isInternalModelHistoryMessage(message) {
32860
+ return message.role === "user" && typeof message.content === "string" && /^You are running as sub agent:/i.test(message.content.trim());
32861
+ }
32862
+ function sanitizeConversationId(id) {
32863
+ return String(id || "conversation").replace(/[^a-zA-Z0-9._-]+/g, "_") || "conversation";
32864
+ }
32865
+ function numberOrNow(value) {
32866
+ return typeof value === "number" && Number.isFinite(value) ? value : Date.now();
32867
+ }
32868
+ function firstLine(value) {
32869
+ const line = value.replace(/\s+/g, " ").trim();
32870
+ if (!line) return "New session";
32871
+ return line.length > 60 ? `${line.slice(0, 57)}...` : line;
32872
+ }
32873
+ function formatRelativeAge(ms2) {
32874
+ const seconds = Math.max(0, Math.floor(ms2 / 1e3));
32875
+ if (seconds < 60) return `${seconds}s ago`;
32876
+ const minutes = Math.floor(seconds / 60);
32877
+ if (minutes < 60) return `${minutes}m ago`;
32878
+ const hours = Math.floor(minutes / 60);
32879
+ if (hours < 48) return `${hours}h ago`;
32880
+ return `${Math.floor(hours / 24)}d ago`;
32881
+ }
32882
+
32480
32883
  // src/ui/tui/controller.ts
32481
32884
  async function runTuiSession(flags, store2) {
32482
- const cwd = path32.resolve(flags.cwd ?? process.cwd());
32885
+ const cwd = path33.resolve(flags.cwd ?? process.cwd());
32483
32886
  const eventBus = new EventBus();
32484
32887
  eventBus.subscribe((event) => store2.handleEvent(event));
32485
32888
  const loadedConfig2 = await loadConfig({ cwd, configPath: flags.configPath });
@@ -32511,10 +32914,20 @@ async function runTuiSession(flags, store2) {
32511
32914
  }
32512
32915
  });
32513
32916
  let nextPrompt = flags.prompt;
32514
- let history = flags.initialHistory ? [...flags.initialHistory] : [];
32917
+ const conversationsEnabled = flags.conversationManagement === true;
32918
+ const conversationState = conversationsEnabled ? await initializeConversationState(cwd, flags) : void 0;
32919
+ let currentConversation = conversationState?.current;
32920
+ const conversationRecords = conversationState?.records ?? /* @__PURE__ */ new Map();
32921
+ const shouldShowStartupSessionSelector = conversationsEnabled && !flags.prompt.trim() && !flags.sessionId;
32922
+ let history = flags.initialHistory ? [...flags.initialHistory] : currentConversation?.modelHistory ? [...currentConversation.modelHistory] : [];
32515
32923
  let lastExternalSessionKey;
32516
- const externalSessions = new ClaudeCodeSessionMap();
32517
- const rootSessionId = flags.sessionId ?? createRootSessionId();
32924
+ let externalSessions = new ClaudeCodeSessionMap();
32925
+ const runtimeByConversation = /* @__PURE__ */ new Map();
32926
+ let rootSessionId = currentConversation?.id ?? flags.sessionId ?? createRootSessionId();
32927
+ if (currentConversation && !shouldShowStartupSessionSelector) {
32928
+ store2.restoreConversationSnapshot(currentConversation.snapshot, { sessionId: currentConversation.id, title: currentConversation.title, cwd: currentConversation.cwd });
32929
+ runtimeByConversation.set(currentConversation.id, { externalSessions, lastExternalSessionKey });
32930
+ }
32518
32931
  const goalTitleGenerator = async (input2) => {
32519
32932
  const selection = store2.currentSelection();
32520
32933
  const runtime = resolveProviderRuntimeConfig(config, selection);
@@ -32532,6 +32945,14 @@ async function runTuiSession(flags, store2) {
32532
32945
  if (resolved.kind === "external-agent") return input2.objective.trim().split(/\s+/).slice(0, 8).join(" ");
32533
32946
  return summarizeGoalTitleWithProvider({ ...input2, provider: resolved.provider, model: resolved.model });
32534
32947
  };
32948
+ if (shouldShowStartupSessionSelector) {
32949
+ const selected = await store2.requestSessionSelection(startupSessionOptions());
32950
+ if (selected.kind === "exit" || store2.exitRequested()) {
32951
+ await saveCurrentSelection();
32952
+ return 0;
32953
+ }
32954
+ await applyStartupSessionSelection(selected);
32955
+ }
32535
32956
  for (; ; ) {
32536
32957
  const prompt = await store2.requestPrompt(nextPrompt);
32537
32958
  nextPrompt = void 0;
@@ -32540,11 +32961,17 @@ async function runTuiSession(flags, store2) {
32540
32961
  return 0;
32541
32962
  }
32542
32963
  if (!prompt) return 0;
32964
+ const sessionCommand = parseSessionCommand(prompt);
32965
+ if (sessionCommand) {
32966
+ await handleSessionCommand(sessionCommand);
32967
+ continue;
32968
+ }
32543
32969
  if (isCompactCommand(prompt)) {
32544
32970
  const result = compactInteractiveHistory(history, config.context.main);
32545
32971
  history = result.messages;
32546
32972
  store2.prepareForPrompt("waiting for next message");
32547
32973
  store2.markHistoryCompacted(result);
32974
+ await persistCurrentConversation();
32548
32975
  continue;
32549
32976
  }
32550
32977
  const mode = resolveAgentMode(config, flags.mode);
@@ -32696,6 +33123,7 @@ async function runTuiSession(flags, store2) {
32696
33123
  history = interactiveHistoryFromRunMessages(result.messages);
32697
33124
  lastExternalSessionKey = externalSessionKey ?? lastExternalSessionKey;
32698
33125
  }
33126
+ await persistCurrentConversation();
32699
33127
  } catch (error) {
32700
33128
  store2.setStopTask(void 0);
32701
33129
  if (activeAbort?.signal.aborted) {
@@ -32708,6 +33136,7 @@ async function runTuiSession(flags, store2) {
32708
33136
  store2.setStopTask(void 0);
32709
33137
  }
32710
33138
  if (store2.exitRequested()) {
33139
+ await persistCurrentConversation();
32711
33140
  await saveCurrentSelection();
32712
33141
  return 0;
32713
33142
  }
@@ -32716,6 +33145,161 @@ async function runTuiSession(flags, store2) {
32716
33145
  async function saveCurrentSelection() {
32717
33146
  await saveSelection(store2.currentSelection());
32718
33147
  }
33148
+ async function handleSessionCommand(command) {
33149
+ if (!conversationsEnabled) {
33150
+ store2.showSystemMessage("Sessions", "Session management is available in demian-cli TUI, but this embedded runtime lets the host manage sessions.");
33151
+ store2.prepareForPrompt("waiting for next message");
33152
+ return;
33153
+ }
33154
+ if (!currentConversation) {
33155
+ currentConversation = createConversationRecord({ id: flags.sessionId ?? createRootSessionId(), cwd });
33156
+ conversationRecords.set(currentConversation.id, currentConversation);
33157
+ }
33158
+ if (command.action === "help") {
33159
+ store2.showSystemMessage("Sessions", sessionUsage());
33160
+ store2.prepareForPrompt("waiting for next message");
33161
+ return;
33162
+ }
33163
+ if (command.action === "list") {
33164
+ store2.showSystemMessage("Sessions", sessionListMessage());
33165
+ store2.prepareForPrompt("waiting for next message");
33166
+ return;
33167
+ }
33168
+ if (command.action === "current") {
33169
+ store2.showSystemMessage("Current Session", conversationSummaryLine(currentConversation, sortedConversations().findIndex((item) => item.id === currentConversation?.id), currentConversation.id));
33170
+ store2.prepareForPrompt("waiting for next message");
33171
+ return;
33172
+ }
33173
+ if (command.action === "new") {
33174
+ await persistCurrentConversation();
33175
+ const next = createConversationRecord({ id: createRootSessionId(), cwd, title: command.title });
33176
+ conversationRecords.set(next.id, next);
33177
+ await switchConversation(next);
33178
+ store2.prepareForPrompt(`new session: ${next.title}`);
33179
+ await persistCurrentConversation();
33180
+ return;
33181
+ }
33182
+ if (command.action === "switch") {
33183
+ const target = findConversation(sortedConversations(), command.target);
33184
+ if (!target) {
33185
+ store2.showSystemMessage("Sessions", `No session matched "${command.target ?? ""}".
33186
+
33187
+ ${sessionListMessage()}`);
33188
+ store2.prepareForPrompt("waiting for next message");
33189
+ return;
33190
+ }
33191
+ await persistCurrentConversation();
33192
+ await switchConversation(target);
33193
+ store2.prepareForPrompt(`switched session: ${target.title}`);
33194
+ await persistCurrentConversation();
33195
+ return;
33196
+ }
33197
+ if (command.action === "delete") {
33198
+ const target = findConversation(sortedConversations(), command.target);
33199
+ if (!target) {
33200
+ store2.showSystemMessage("Sessions", `No session matched "${command.target ?? ""}".`);
33201
+ store2.prepareForPrompt("waiting for next message");
33202
+ return;
33203
+ }
33204
+ conversationRecords.delete(target.id);
33205
+ runtimeByConversation.delete(target.id);
33206
+ await deleteConversationRecord(flags.conversationStorageDir, target.id);
33207
+ if (currentConversation?.id === target.id) {
33208
+ const next = sortedConversations()[0] ?? createConversationRecord({ id: createRootSessionId(), cwd });
33209
+ conversationRecords.set(next.id, next);
33210
+ await switchConversation(next);
33211
+ }
33212
+ store2.prepareForPrompt(`deleted session: ${target.title}`);
33213
+ await persistCurrentConversation();
33214
+ return;
33215
+ }
33216
+ if (command.action === "rename") {
33217
+ const title = command.title?.trim();
33218
+ if (!title) {
33219
+ store2.showSystemMessage("Sessions", "Usage: /session rename <title>");
33220
+ store2.prepareForPrompt("waiting for next message");
33221
+ return;
33222
+ }
33223
+ currentConversation.title = title;
33224
+ currentConversation.updatedAt = Date.now();
33225
+ store2.prepareForPrompt(`renamed session: ${title}`);
33226
+ await persistCurrentConversation();
33227
+ return;
33228
+ }
33229
+ if (command.action === "clear") {
33230
+ history = [];
33231
+ currentConversation.modelHistory = [];
33232
+ currentConversation.snapshot = void 0;
33233
+ currentConversation.title = "New session";
33234
+ currentConversation.updatedAt = Date.now();
33235
+ store2.clearConversation(currentConversation.title);
33236
+ store2.prepareForPrompt("cleared current conversation");
33237
+ await persistCurrentConversation();
33238
+ }
33239
+ }
33240
+ function startupSessionOptions() {
33241
+ const records = sortedConversations();
33242
+ const sessionOptions = records.map((record) => ({
33243
+ kind: "session",
33244
+ id: record.id,
33245
+ title: record.title,
33246
+ cwd: record.cwd,
33247
+ status: conversationRuntimeStatus(record, cwd),
33248
+ updatedAt: record.updatedAt,
33249
+ currentWorkspace: path33.resolve(record.cwd) === cwd
33250
+ }));
33251
+ return [...sessionOptions, { kind: "new", title: "New session", cwd, status: "ready", currentWorkspace: true }];
33252
+ }
33253
+ async function applyStartupSessionSelection(selection) {
33254
+ if (selection.kind === "new") {
33255
+ const next = createConversationRecord({ id: createRootSessionId(), cwd });
33256
+ conversationRecords.set(next.id, next);
33257
+ await switchConversation(next);
33258
+ store2.prepareForPrompt("new session ready");
33259
+ await saveConversationRecords(flags.conversationStorageDir, [...conversationRecords.values()], next.id);
33260
+ return;
33261
+ }
33262
+ const target = conversationRecords.get(selection.id) ?? findConversation(sortedConversations(), selection.id);
33263
+ if (!target) {
33264
+ const next = createConversationRecord({ id: createRootSessionId(), cwd });
33265
+ conversationRecords.set(next.id, next);
33266
+ await switchConversation(next);
33267
+ store2.prepareForPrompt("new session ready");
33268
+ await saveConversationRecords(flags.conversationStorageDir, [...conversationRecords.values()], next.id);
33269
+ return;
33270
+ }
33271
+ await switchConversation(target);
33272
+ store2.prepareForPrompt(`session ready: ${target.title}`);
33273
+ await saveConversationRecords(flags.conversationStorageDir, [...conversationRecords.values()], target.id);
33274
+ }
33275
+ async function switchConversation(record) {
33276
+ currentConversation = record;
33277
+ rootSessionId = record.id;
33278
+ history = [...record.modelHistory];
33279
+ const runtime = runtimeByConversation.get(record.id) ?? { externalSessions: new ClaudeCodeSessionMap(), lastExternalSessionKey: void 0 };
33280
+ externalSessions = runtime.externalSessions;
33281
+ lastExternalSessionKey = runtime.lastExternalSessionKey;
33282
+ runtimeByConversation.set(record.id, runtime);
33283
+ store2.restoreConversationSnapshot(record.snapshot, { sessionId: record.id, title: record.title, cwd: record.cwd || cwd });
33284
+ }
33285
+ async function persistCurrentConversation() {
33286
+ if (!conversationsEnabled || !currentConversation) return;
33287
+ currentConversation.modelHistory = cleanModelHistory(history);
33288
+ if (currentConversation.title === "New session" && currentConversation.modelHistory.length > 0) currentConversation.title = titleFromHistory(currentConversation.modelHistory);
33289
+ currentConversation.snapshot = store2.conversationSnapshot();
33290
+ currentConversation.updatedAt = Date.now();
33291
+ conversationRecords.set(currentConversation.id, currentConversation);
33292
+ runtimeByConversation.set(currentConversation.id, { externalSessions, lastExternalSessionKey });
33293
+ await saveConversationRecords(flags.conversationStorageDir, [...conversationRecords.values()], currentConversation.id);
33294
+ }
33295
+ function sortedConversations() {
33296
+ return sortConversationRecords([...conversationRecords.values()], cwd);
33297
+ }
33298
+ function sessionListMessage() {
33299
+ const records = sortedConversations();
33300
+ if (!records.length) return "No saved sessions.";
33301
+ return ["Saved sessions:", ...records.map((record, index) => conversationSummaryLine(record, index, currentConversation?.id)), "", "Use /session switch <number|id|title> to open one."].join("\n");
33302
+ }
32719
33303
  async function saveSelection(selection) {
32720
33304
  if (!shouldPersistSelection(selection)) return;
32721
33305
  const key = preferenceKey(selection);
@@ -32727,18 +33311,46 @@ async function runTuiSession(flags, store2) {
32727
33311
  function isGoalStateClearingAction(action) {
32728
33312
  return action === "status" || action === "pause" || action === "resume";
32729
33313
  }
33314
+ async function initializeConversationState(cwd, flags) {
33315
+ const loaded = await loadConversationRecords(flags.conversationStorageDir);
33316
+ const records = new Map(loaded.records.map((record) => [record.id, record]));
33317
+ const shouldResumeSelected = !flags.prompt.trim();
33318
+ const orderedRecords = sortConversationRecords([...records.values()], cwd);
33319
+ const preferred = (flags.sessionId ? findConversation(orderedRecords, flags.sessionId) : void 0) ?? (shouldResumeSelected && loaded.selectedSessionId ? findConversation(orderedRecords, loaded.selectedSessionId) : void 0) ?? (shouldResumeSelected ? orderedRecords[0] : void 0);
33320
+ const current = preferred ?? (flags.prompt.trim() || flags.sessionId || flags.initialHistory?.length ? createConversationRecord({
33321
+ id: flags.sessionId ?? createRootSessionId(),
33322
+ cwd,
33323
+ history: flags.initialHistory
33324
+ }) : void 0);
33325
+ if (current && flags.initialHistory?.length) current.modelHistory = cleanModelHistory(flags.initialHistory);
33326
+ if (current) records.set(current.id, current);
33327
+ return { records, current };
33328
+ }
33329
+ function sessionUsage() {
33330
+ return [
33331
+ "Usage:",
33332
+ " /session list",
33333
+ " /session new [title]",
33334
+ " /session switch <number|id|title>",
33335
+ " /session rename <title>",
33336
+ " /session delete <number|id|title>",
33337
+ " /session clear",
33338
+ "",
33339
+ "Aliases: /sessions, /conversation, /conversations, /conv"
33340
+ ].join("\n");
33341
+ }
32730
33342
 
32731
33343
  // src/config-watcher.ts
32732
33344
  import { EventEmitter } from "node:events";
32733
33345
  import fs11 from "node:fs";
32734
- import path34 from "node:path";
33346
+ import path35 from "node:path";
32735
33347
 
32736
33348
  // src/config-scaffold.ts
32737
- import { chmod as chmod3, mkdir as mkdir14, readFile as readFile16, rename as rename6, stat as stat8, writeFile as writeFile13 } from "node:fs/promises";
33349
+ import { chmod as chmod3, mkdir as mkdir15, readFile as readFile17, rename as rename6, stat as stat8, writeFile as writeFile14 } from "node:fs/promises";
32738
33350
  import os13 from "node:os";
32739
- import path33 from "node:path";
33351
+ import path34 from "node:path";
32740
33352
  function defaultUserConfigPath() {
32741
- return path33.join(os13.homedir(), ".demian", "config.json");
33353
+ return path34.join(os13.homedir(), ".demian", "config.json");
32742
33354
  }
32743
33355
  function defaultUserConfig(defaultProvider = detectDefaultProvider()) {
32744
33356
  return {
@@ -32859,7 +33471,7 @@ async function createUserConfig(options2 = {}) {
32859
33471
  `;
32860
33472
  if (options2.print) return { path: filePath, created: false, content };
32861
33473
  const existed = await exists(filePath);
32862
- if (existed && !options2.force) return { path: filePath, created: false, content: await readFile16(filePath, "utf8"), existed: true };
33474
+ if (existed && !options2.force) return { path: filePath, created: false, content: await readFile17(filePath, "utf8"), existed: true };
32863
33475
  await writeJsonAtomic(filePath, content);
32864
33476
  return { path: filePath, created: true, content, existed };
32865
33477
  }
@@ -32922,7 +33534,7 @@ async function updateConfigDefaults(options2) {
32922
33534
  }
32923
33535
  async function readConfigObject(filePath) {
32924
33536
  if (!await exists(filePath)) await createUserConfig({ path: filePath });
32925
- const raw = JSON.parse(await readFile16(filePath, "utf8"));
33537
+ const raw = JSON.parse(await readFile17(filePath, "utf8"));
32926
33538
  raw.version ??= 2;
32927
33539
  raw.providers = objectValue(raw.providers);
32928
33540
  return raw;
@@ -32983,9 +33595,9 @@ function apiKeyAuthFields(options2, defaultApiKeyEnv) {
32983
33595
  };
32984
33596
  }
32985
33597
  async function writeJsonAtomic(filePath, content) {
32986
- await mkdir14(path33.dirname(filePath), { recursive: true, mode: 448 });
33598
+ await mkdir15(path34.dirname(filePath), { recursive: true, mode: 448 });
32987
33599
  const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
32988
- await writeFile13(temp, content, { mode: 384 });
33600
+ await writeFile14(temp, content, { mode: 384 });
32989
33601
  await rename6(temp, filePath);
32990
33602
  await chmod3(filePath, 384).catch(() => void 0);
32991
33603
  }
@@ -33036,9 +33648,9 @@ var ConfigWatcher = class extends EventEmitter {
33036
33648
  this.#started = true;
33037
33649
  this.#lastSeen = this.#snapshot();
33038
33650
  try {
33039
- fs11.mkdirSync(path34.dirname(this.#filePath), { recursive: true, mode: 448 });
33040
- this.#watcher = fs11.watch(path34.dirname(this.#filePath), { persistent: false }, (_eventType, filename) => {
33041
- if (filename && filename !== path34.basename(this.#filePath)) return;
33651
+ fs11.mkdirSync(path35.dirname(this.#filePath), { recursive: true, mode: 448 });
33652
+ this.#watcher = fs11.watch(path35.dirname(this.#filePath), { persistent: false }, (_eventType, filename) => {
33653
+ if (filename && filename !== path35.basename(this.#filePath)) return;
33042
33654
  this.#schedule();
33043
33655
  });
33044
33656
  this.#watcher.on("error", () => void 0);
@@ -33180,6 +33792,7 @@ if (process.argv.includes("--config-path")) {
33180
33792
  process.exit(0);
33181
33793
  }
33182
33794
  var options = parseOptions();
33795
+ var SNAPSHOT_DEBOUNCE_MS = 80;
33183
33796
  var store = new TuiStore();
33184
33797
  var diffState = normalizeDiffState(options.initialSnapshot?.diff);
33185
33798
  var restoredSnapshot = normalizeInitialPublicState(options.initialSnapshot);
@@ -33305,14 +33918,14 @@ async function undoDiffAction(actionId) {
33305
33918
  if (!action) throw new Error("No diff action is available to undo.");
33306
33919
  if (!canUndoDiffAction(diffState, action.id)) throw new Error("Only the latest change for a file can be undone.");
33307
33920
  if (!hasBeforeSnapshot(action)) throw new Error("This diff was not recorded with enough content to undo safely.");
33308
- const target = path35.resolve(options.cwd, action.path);
33309
- const relative = path35.relative(options.cwd, target);
33310
- if (relative.startsWith("..") || path35.isAbsolute(relative)) throw new Error("Refusing to undo a file outside the workspace.");
33921
+ const target = path36.resolve(options.cwd, action.path);
33922
+ const relative = path36.relative(options.cwd, target);
33923
+ if (relative.startsWith("..") || path36.isAbsolute(relative)) throw new Error("Refusing to undo a file outside the workspace.");
33311
33924
  if (!action.beforeExists) {
33312
- await rm4(target, { force: true });
33925
+ await rm5(target, { force: true });
33313
33926
  } else {
33314
- await mkdir15(path35.dirname(target), { recursive: true });
33315
- await writeFile14(target, action.beforeContent ?? "", "utf8");
33927
+ await mkdir16(path36.dirname(target), { recursive: true });
33928
+ await writeFile15(target, action.beforeContent ?? "", "utf8");
33316
33929
  }
33317
33930
  markDiffActionUndone(action.id);
33318
33931
  send({ type: "diffUndoCompleted", actionId: action.id, path: action.path });
@@ -33405,7 +34018,7 @@ function scheduleSnapshot() {
33405
34018
  snapshotTimer = setTimeout(() => {
33406
34019
  snapshotTimer = void 0;
33407
34020
  send({ type: "snapshot", state: publicState(store.snapshot()) });
33408
- }, 33);
34021
+ }, SNAPSHOT_DEBOUNCE_MS);
33409
34022
  }
33410
34023
  function sendSnapshotNow() {
33411
34024
  if (snapshotTimer) {