codemini-cli 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/codemini-web/dist/assets/{AboutDialog-jgqGjQgl.js → AboutDialog-MRopwNIL.js} +2 -2
  2. package/codemini-web/dist/assets/CodeWikiPanel-UpK5xGE3.js +1 -0
  3. package/codemini-web/dist/assets/ConfigDialog-CNl28wsj.js +1 -0
  4. package/codemini-web/dist/assets/GitDiffDialog-gSysUg2J.js +3 -0
  5. package/codemini-web/dist/assets/{MemoryDialog-BhxQgG0I.js → MemoryDialog-DFUmo3Kl.js} +3 -3
  6. package/codemini-web/dist/assets/MessageBubble-CGnnViv0.js +12 -0
  7. package/codemini-web/dist/assets/PatchDiff-B8rwvEg5.js +230 -0
  8. package/codemini-web/dist/assets/ProjectSelector-BF59M1zb.js +1 -0
  9. package/codemini-web/dist/assets/{SkillDialog-DxS43NpR.js → SkillDialog-CQTjbSiw.js} +4 -4
  10. package/codemini-web/dist/assets/SoulDialog-BLjUGqqB.js +1 -0
  11. package/codemini-web/dist/assets/chevron-right--85xg7qk.js +1 -0
  12. package/codemini-web/dist/assets/{chunk-BO2N2NFS-Budy_hfO.js → chunk-BO2N2NFS-6uELoidu.js} +6 -6
  13. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-CQS1PAvD.js → highlighted-body-OFNGDK62-gb1UMBZ5.js} +1 -1
  14. package/codemini-web/dist/assets/index-1xqD0R5t.css +2 -0
  15. package/codemini-web/dist/assets/index-CDXQGwPs.js +65 -0
  16. package/codemini-web/dist/assets/{input-CNQgbKe6.js → input-Ca8O_061.js} +1 -1
  17. package/codemini-web/dist/assets/lib-BXWizt13.js +1 -0
  18. package/codemini-web/dist/assets/mermaid-GHXKKRXX-ROliF8Yd.js +1 -0
  19. package/codemini-web/dist/assets/{pencil-Ce_LFiEh.js → pencil-BhT11Ztp.js} +1 -1
  20. package/codemini-web/dist/assets/{refresh-cw-BKL-AZu5.js → refresh-cw-D7R5Lth6.js} +1 -1
  21. package/codemini-web/dist/assets/select-DBvcHBzs.js +1 -0
  22. package/codemini-web/dist/assets/{trash-2-KmAlCwXd.js → trash-2-BfNZcWfX.js} +1 -1
  23. package/codemini-web/dist/index.html +2 -2
  24. package/codemini-web/lib/runtime-bridge.js +325 -296
  25. package/codemini-web/server.js +310 -243
  26. package/package.json +1 -1
  27. package/src/core/agent-loop.js +188 -97
  28. package/src/core/chat-runtime.js +674 -571
  29. package/src/core/config-store.js +11 -3
  30. package/src/core/git-oplog-change-tracker.js +387 -0
  31. package/src/core/non-git-backup.js +116 -0
  32. package/src/core/paths.js +123 -123
  33. package/src/core/session-store.js +148 -99
  34. package/src/core/tools.js +499 -456
  35. package/src/tui/chat-app.js +196 -56
  36. package/codemini-web/dist/assets/CodeWikiPanel-EPuoerNv.js +0 -1
  37. package/codemini-web/dist/assets/ConfigDialog-B5IGZCc9.js +0 -1
  38. package/codemini-web/dist/assets/GitDiffDialog-Bb_Tw5ZK.js +0 -222
  39. package/codemini-web/dist/assets/MessageBubble-wUff4GP4.js +0 -6
  40. package/codemini-web/dist/assets/ProjectSelector-C0leTf6f.js +0 -1
  41. package/codemini-web/dist/assets/SoulDialog-XDTEGWvH.js +0 -1
  42. package/codemini-web/dist/assets/chevron-right-Dbzw7YzA.js +0 -1
  43. package/codemini-web/dist/assets/index-D0EGtNPr.js +0 -65
  44. package/codemini-web/dist/assets/index-wOUf3WkN.css +0 -2
  45. package/codemini-web/dist/assets/lib-BOngVP_M.js +0 -11
  46. package/codemini-web/dist/assets/lib-DrOTTm_N.js +0 -1
  47. package/codemini-web/dist/assets/mermaid-GHXKKRXX-DrBu5KyC.js +0 -1
  48. package/codemini-web/dist/assets/select-BZXfigic.js +0 -1
@@ -33,12 +33,21 @@ import { forgetMemory, listMemories, rememberMemory, searchMemories, captureToIn
33
33
  import { runDreamConsolidation } from './dream-consolidate.js';
34
34
  import { normalizePlanState } from './plan-state.js';
35
35
  import { countActiveTodos, normalizeTodos } from './todo-state.js';
36
- import {
37
- attachReflectTargets,
38
- buildReflectSkillDraft,
39
- parseReflectScope,
40
- writeReflectSkillDraft
41
- } from './reflect-skill.js';
36
+ import {
37
+ attachReflectTargets,
38
+ buildReflectSkillDraft,
39
+ parseReflectScope,
40
+ writeReflectSkillDraft
41
+ } from './reflect-skill.js';
42
+ import {
43
+ beginGitOplogCapture,
44
+ captureGitOplogChanges,
45
+ createGitOplogChangeTracker,
46
+ listGitOplogChanges,
47
+ readGitOplogPatch,
48
+ undoGitOplogChange
49
+ } from './git-oplog-change-tracker.js';
50
+ import { createNonGitBackupManager } from './non-git-backup.js';
42
51
 
43
52
  const STREAM_SAVE_DEBOUNCE_MS = 120;
44
53
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -83,283 +92,283 @@ function slugify(input) {
83
92
  return base || 'untitled';
84
93
  }
85
94
 
86
- function nowStamp() {
87
- return new Date().toISOString().replace(/[:.]/g, '-');
88
- }
89
-
90
- function numberFromPath(obj, pathParts) {
91
- let current = obj;
92
- for (const part of pathParts) {
93
- if (!current || typeof current !== 'object') return null;
94
- current = current[part];
95
- }
96
- const value = Number(current);
97
- return Number.isFinite(value) ? Math.max(0, value) : null;
98
- }
99
-
100
- function firstFiniteNumber(obj, paths) {
101
- for (const pathParts of paths) {
102
- const value = numberFromPath(obj, pathParts);
103
- if (value != null) return value;
104
- }
105
- return null;
106
- }
107
-
108
- function sumFiniteNumbers(obj, paths) {
109
- let sum = 0;
110
- let found = false;
111
- for (const pathParts of paths) {
112
- const value = numberFromPath(obj, pathParts);
113
- if (value != null) {
114
- sum += value;
115
- found = true;
116
- }
117
- }
118
- return found ? sum : null;
119
- }
120
-
121
- function collectRawUsage(usage) {
122
- if (!usage || typeof usage !== 'object') return [];
123
- if (Array.isArray(usage.raw)) {
124
- return usage.raw
125
- .filter((item) => item && typeof item === 'object')
126
- .map((item) => ({ ...item }));
127
- }
128
- return [{ ...usage }];
129
- }
130
-
131
- export function normalizeModelUsage(usage) {
132
- if (!usage || typeof usage !== 'object') return null;
133
- const promptCacheHitTokens = firstFiniteNumber(usage, [
134
- ['prompt_cache_hit_tokens'],
135
- ['promptCacheHitTokens'],
136
- ['cache_hit_tokens'],
137
- ['cacheHitTokens']
138
- ]);
139
- const promptCacheMissTokens = firstFiniteNumber(usage, [
140
- ['prompt_cache_miss_tokens'],
141
- ['promptCacheMissTokens'],
142
- ['cache_miss_tokens'],
143
- ['cacheMissTokens']
144
- ]);
145
- const explicitInputTokens = firstFiniteNumber(usage, [
146
- ['prompt_tokens'],
147
- ['input_tokens'],
148
- ['inputTokens'],
149
- ['promptTokens'],
150
- ['prompt_token_count'],
151
- ['promptTokenCount'],
152
- ['input_token_count'],
153
- ['inputTokenCount'],
154
- ['input_total_tokens'],
155
- ['total_input_tokens'],
156
- ['usage', 'prompt_tokens'],
157
- ['usage', 'input_tokens'],
158
- ['usage_metadata', 'prompt_token_count'],
159
- ['usage_metadata', 'input_token_count'],
160
- ['usageMetadata', 'promptTokenCount'],
161
- ['usageMetadata', 'inputTokenCount'],
162
- ['token_usage', 'prompt_tokens'],
163
- ['token_usage', 'input_tokens'],
164
- ['tokenUsage', 'promptTokens'],
165
- ['tokenUsage', 'inputTokens'],
166
- ['tokens', 'input_tokens'],
167
- ['tokens', 'inputTokens'],
168
- ['tokens', 'prompt_tokens'],
169
- ['tokens', 'promptTokens'],
170
- ['billed_units', 'input_tokens'],
171
- ['billedUnits', 'inputTokens']
172
- ]);
173
- const cacheReadInputTokens = firstFiniteNumber(usage, [
174
- ['cache_read_input_tokens'],
175
- ['cacheReadInputTokens'],
176
- ['cache_read_tokens'],
177
- ['cacheReadTokens']
178
- ]);
179
- const outputTokens = firstFiniteNumber(usage, [
180
- ['completion_tokens'],
181
- ['output_tokens'],
182
- ['outputTokens'],
183
- ['completionTokens'],
184
- ['completion_token_count'],
185
- ['completionTokenCount'],
186
- ['output_token_count'],
187
- ['outputTokenCount'],
188
- ['candidates_token_count'],
189
- ['candidatesTokenCount'],
190
- ['usage', 'completion_tokens'],
191
- ['usage', 'output_tokens'],
192
- ['usage_metadata', 'candidates_token_count'],
193
- ['usage_metadata', 'output_token_count'],
194
- ['usageMetadata', 'candidatesTokenCount'],
195
- ['usageMetadata', 'outputTokenCount'],
196
- ['token_usage', 'completion_tokens'],
197
- ['token_usage', 'output_tokens'],
198
- ['tokenUsage', 'completionTokens'],
199
- ['tokenUsage', 'outputTokens'],
200
- ['tokens', 'output_tokens'],
201
- ['tokens', 'outputTokens'],
202
- ['tokens', 'completion_tokens'],
203
- ['tokens', 'completionTokens'],
204
- ['billed_units', 'output_tokens'],
205
- ['billedUnits', 'outputTokens']
206
- ]);
207
- const explicitTotal = firstFiniteNumber(usage, [
208
- ['total_tokens'],
209
- ['totalTokens'],
210
- ['total_token_count'],
211
- ['totalTokenCount'],
212
- ['usage', 'total_tokens'],
213
- ['usage_metadata', 'total_token_count'],
214
- ['usageMetadata', 'totalTokenCount'],
215
- ['token_usage', 'total_tokens'],
216
- ['tokenUsage', 'totalTokens'],
217
- ['tokens', 'total_tokens'],
218
- ['tokens', 'totalTokens']
219
- ]);
220
- const cachedInputTokens = firstFiniteNumber(usage, [
221
- ['prompt_tokens_details', 'cached_tokens'],
222
- ['input_tokens_details', 'cached_tokens'],
223
- ['promptTokensDetails', 'cachedTokens'],
224
- ['inputTokensDetails', 'cachedTokens'],
225
- ['cache_read_input_tokens'],
226
- ['cacheReadInputTokens'],
227
- ['cache_read_tokens'],
228
- ['cacheReadTokens'],
229
- ['cached_tokens'],
230
- ['cachedTokens'],
231
- ['cached_input_tokens'],
232
- ['cachedInputTokens'],
233
- ['cached_content_token_count'],
234
- ['cachedContentTokenCount'],
235
- ['usage', 'prompt_tokens_details', 'cached_tokens'],
236
- ['usage', 'input_tokens_details', 'cached_tokens'],
237
- ['usage_metadata', 'cached_content_token_count'],
238
- ['usageMetadata', 'cachedContentTokenCount'],
239
- ['token_usage', 'prompt_tokens_details', 'cached_tokens'],
240
- ['tokenUsage', 'promptTokensDetails', 'cachedTokens'],
241
- ['tokens', 'cached_tokens'],
242
- ['tokens', 'cachedTokens'],
243
- ['prompt_cache_hit_tokens'],
244
- ['promptCacheHitTokens'],
245
- ['cache_hit_tokens'],
246
- ['cacheHitTokens']
247
- ]);
248
- const explicitCacheMissInputTokens = firstFiniteNumber(usage, [
249
- ['prompt_cache_miss_tokens'],
250
- ['promptCacheMissTokens'],
251
- ['cache_miss_tokens'],
252
- ['cacheMissTokens']
253
- ]);
254
- const cacheWriteInputTokens = firstFiniteNumber(usage, [
255
- ['cache_creation_input_tokens'],
256
- ['cacheCreationInputTokens'],
257
- ['cache_write_input_tokens'],
258
- ['cacheWriteInputTokens'],
259
- ['cache_creation_tokens'],
260
- ['cacheCreationTokens'],
261
- ['usage', 'cache_creation_input_tokens'],
262
- ['usage', 'cache_write_input_tokens'],
263
- ['token_usage', 'cache_creation_input_tokens'],
264
- ['tokenUsage', 'cacheCreationInputTokens']
265
- ]) ?? sumFiniteNumbers(usage, [
266
- ['cache_creation', 'ephemeral_5m_input_tokens'],
267
- ['cache_creation', 'ephemeral_1h_input_tokens'],
268
- ['cacheCreation', 'ephemeral5mInputTokens'],
269
- ['cacheCreation', 'ephemeral1hInputTokens'],
270
- ['usage', 'cache_creation', 'ephemeral_5m_input_tokens'],
271
- ['usage', 'cache_creation', 'ephemeral_1h_input_tokens']
272
- ]);
273
- const hasAnthropicSplitCacheInput = explicitInputTokens != null
274
- && (cacheReadInputTokens != null || cacheWriteInputTokens != null)
275
- && promptCacheHitTokens == null;
276
- const cacheMissInputTokens = explicitCacheMissInputTokens ?? (
277
- hasAnthropicSplitCacheInput
278
- ? Number(explicitInputTokens || 0) + Number(cacheWriteInputTokens || 0)
279
- : null
280
- );
281
- const inputTokens = explicitInputTokens != null
282
- ? Number(explicitInputTokens || 0)
283
- + (hasAnthropicSplitCacheInput ? Number(cacheReadInputTokens || 0) + Number(cacheWriteInputTokens || 0) : 0)
284
- : (
285
- promptCacheHitTokens != null || promptCacheMissTokens != null
286
- ? Number(promptCacheHitTokens || 0) + Number(promptCacheMissTokens || 0)
287
- : null
288
- );
289
- const reasoningOutputTokens = firstFiniteNumber(usage, [
290
- ['completion_tokens_details', 'reasoning_tokens'],
291
- ['output_tokens_details', 'reasoning_tokens'],
292
- ['completionTokensDetails', 'reasoningTokens'],
293
- ['outputTokensDetails', 'reasoningTokens'],
294
- ['reasoning_tokens'],
295
- ['reasoningTokens'],
296
- ['thoughts_token_count'],
297
- ['thoughtsTokenCount'],
298
- ['usage', 'completion_tokens_details', 'reasoning_tokens'],
299
- ['usage_metadata', 'thoughts_token_count'],
300
- ['usageMetadata', 'thoughtsTokenCount']
301
- ]);
302
- const totalTokens = explicitTotal ?? (
303
- inputTokens != null || outputTokens != null
304
- ? Number(inputTokens || 0) + Number(outputTokens || 0)
305
- : null
306
- );
307
- if (
308
- inputTokens == null &&
309
- outputTokens == null &&
310
- totalTokens == null &&
311
- cachedInputTokens == null &&
312
- cacheWriteInputTokens == null
313
- ) {
314
- return null;
315
- }
316
- return {
317
- inputTokens: Math.round(inputTokens || 0),
318
- outputTokens: Math.round(outputTokens || 0),
319
- totalTokens: Math.round(totalTokens || 0),
320
- cachedInputTokens: Math.round(cachedInputTokens || 0),
321
- cacheMissInputTokens: Math.round(cacheMissInputTokens || 0),
322
- cacheWriteInputTokens: Math.round(cacheWriteInputTokens || 0),
323
- reasoningOutputTokens: Math.round(reasoningOutputTokens || 0),
324
- requests: 1,
325
- raw: collectRawUsage(usage)
326
- };
327
- }
328
-
329
- function cloneModelUsage(usage) {
330
- if (!usage || typeof usage !== 'object') return null;
331
- return {
332
- inputTokens: Math.max(0, Math.round(Number(usage.inputTokens || 0))),
333
- outputTokens: Math.max(0, Math.round(Number(usage.outputTokens || 0))),
334
- totalTokens: Math.max(0, Math.round(Number(usage.totalTokens || 0))),
335
- cachedInputTokens: Math.max(0, Math.round(Number(usage.cachedInputTokens || 0))),
336
- cacheMissInputTokens: Math.max(0, Math.round(Number(usage.cacheMissInputTokens || 0))),
337
- cacheWriteInputTokens: Math.max(0, Math.round(Number(usage.cacheWriteInputTokens || 0))),
338
- reasoningOutputTokens: Math.max(0, Math.round(Number(usage.reasoningOutputTokens || 0))),
339
- requests: Math.max(0, Math.round(Number(usage.requests || 0))),
340
- raw: Array.isArray(usage.raw) ? usage.raw.map((item) => ({ ...item })) : []
341
- };
342
- }
343
-
344
- function mergeModelUsage(left, right) {
345
- const a = cloneModelUsage(left);
346
- const b = cloneModelUsage(right);
347
- if (!a) return b;
348
- if (!b) return a;
349
- return {
350
- inputTokens: a.inputTokens + b.inputTokens,
351
- outputTokens: a.outputTokens + b.outputTokens,
352
- totalTokens: a.totalTokens + b.totalTokens,
353
- cachedInputTokens: a.cachedInputTokens + b.cachedInputTokens,
354
- cacheMissInputTokens: a.cacheMissInputTokens + b.cacheMissInputTokens,
355
- cacheWriteInputTokens: a.cacheWriteInputTokens + b.cacheWriteInputTokens,
356
- reasoningOutputTokens: a.reasoningOutputTokens + b.reasoningOutputTokens,
357
- requests: a.requests + b.requests,
358
- raw: [...a.raw, ...b.raw]
359
- };
360
- }
361
-
362
- function prioritizeByPreferredOrder(items, preferredOrder) {
95
+ function nowStamp() {
96
+ return new Date().toISOString().replace(/[:.]/g, '-');
97
+ }
98
+
99
+ function numberFromPath(obj, pathParts) {
100
+ let current = obj;
101
+ for (const part of pathParts) {
102
+ if (!current || typeof current !== 'object') return null;
103
+ current = current[part];
104
+ }
105
+ const value = Number(current);
106
+ return Number.isFinite(value) ? Math.max(0, value) : null;
107
+ }
108
+
109
+ function firstFiniteNumber(obj, paths) {
110
+ for (const pathParts of paths) {
111
+ const value = numberFromPath(obj, pathParts);
112
+ if (value != null) return value;
113
+ }
114
+ return null;
115
+ }
116
+
117
+ function sumFiniteNumbers(obj, paths) {
118
+ let sum = 0;
119
+ let found = false;
120
+ for (const pathParts of paths) {
121
+ const value = numberFromPath(obj, pathParts);
122
+ if (value != null) {
123
+ sum += value;
124
+ found = true;
125
+ }
126
+ }
127
+ return found ? sum : null;
128
+ }
129
+
130
+ function collectRawUsage(usage) {
131
+ if (!usage || typeof usage !== 'object') return [];
132
+ if (Array.isArray(usage.raw)) {
133
+ return usage.raw
134
+ .filter((item) => item && typeof item === 'object')
135
+ .map((item) => ({ ...item }));
136
+ }
137
+ return [{ ...usage }];
138
+ }
139
+
140
+ export function normalizeModelUsage(usage) {
141
+ if (!usage || typeof usage !== 'object') return null;
142
+ const promptCacheHitTokens = firstFiniteNumber(usage, [
143
+ ['prompt_cache_hit_tokens'],
144
+ ['promptCacheHitTokens'],
145
+ ['cache_hit_tokens'],
146
+ ['cacheHitTokens']
147
+ ]);
148
+ const promptCacheMissTokens = firstFiniteNumber(usage, [
149
+ ['prompt_cache_miss_tokens'],
150
+ ['promptCacheMissTokens'],
151
+ ['cache_miss_tokens'],
152
+ ['cacheMissTokens']
153
+ ]);
154
+ const explicitInputTokens = firstFiniteNumber(usage, [
155
+ ['prompt_tokens'],
156
+ ['input_tokens'],
157
+ ['inputTokens'],
158
+ ['promptTokens'],
159
+ ['prompt_token_count'],
160
+ ['promptTokenCount'],
161
+ ['input_token_count'],
162
+ ['inputTokenCount'],
163
+ ['input_total_tokens'],
164
+ ['total_input_tokens'],
165
+ ['usage', 'prompt_tokens'],
166
+ ['usage', 'input_tokens'],
167
+ ['usage_metadata', 'prompt_token_count'],
168
+ ['usage_metadata', 'input_token_count'],
169
+ ['usageMetadata', 'promptTokenCount'],
170
+ ['usageMetadata', 'inputTokenCount'],
171
+ ['token_usage', 'prompt_tokens'],
172
+ ['token_usage', 'input_tokens'],
173
+ ['tokenUsage', 'promptTokens'],
174
+ ['tokenUsage', 'inputTokens'],
175
+ ['tokens', 'input_tokens'],
176
+ ['tokens', 'inputTokens'],
177
+ ['tokens', 'prompt_tokens'],
178
+ ['tokens', 'promptTokens'],
179
+ ['billed_units', 'input_tokens'],
180
+ ['billedUnits', 'inputTokens']
181
+ ]);
182
+ const cacheReadInputTokens = firstFiniteNumber(usage, [
183
+ ['cache_read_input_tokens'],
184
+ ['cacheReadInputTokens'],
185
+ ['cache_read_tokens'],
186
+ ['cacheReadTokens']
187
+ ]);
188
+ const outputTokens = firstFiniteNumber(usage, [
189
+ ['completion_tokens'],
190
+ ['output_tokens'],
191
+ ['outputTokens'],
192
+ ['completionTokens'],
193
+ ['completion_token_count'],
194
+ ['completionTokenCount'],
195
+ ['output_token_count'],
196
+ ['outputTokenCount'],
197
+ ['candidates_token_count'],
198
+ ['candidatesTokenCount'],
199
+ ['usage', 'completion_tokens'],
200
+ ['usage', 'output_tokens'],
201
+ ['usage_metadata', 'candidates_token_count'],
202
+ ['usage_metadata', 'output_token_count'],
203
+ ['usageMetadata', 'candidatesTokenCount'],
204
+ ['usageMetadata', 'outputTokenCount'],
205
+ ['token_usage', 'completion_tokens'],
206
+ ['token_usage', 'output_tokens'],
207
+ ['tokenUsage', 'completionTokens'],
208
+ ['tokenUsage', 'outputTokens'],
209
+ ['tokens', 'output_tokens'],
210
+ ['tokens', 'outputTokens'],
211
+ ['tokens', 'completion_tokens'],
212
+ ['tokens', 'completionTokens'],
213
+ ['billed_units', 'output_tokens'],
214
+ ['billedUnits', 'outputTokens']
215
+ ]);
216
+ const explicitTotal = firstFiniteNumber(usage, [
217
+ ['total_tokens'],
218
+ ['totalTokens'],
219
+ ['total_token_count'],
220
+ ['totalTokenCount'],
221
+ ['usage', 'total_tokens'],
222
+ ['usage_metadata', 'total_token_count'],
223
+ ['usageMetadata', 'totalTokenCount'],
224
+ ['token_usage', 'total_tokens'],
225
+ ['tokenUsage', 'totalTokens'],
226
+ ['tokens', 'total_tokens'],
227
+ ['tokens', 'totalTokens']
228
+ ]);
229
+ const cachedInputTokens = firstFiniteNumber(usage, [
230
+ ['prompt_tokens_details', 'cached_tokens'],
231
+ ['input_tokens_details', 'cached_tokens'],
232
+ ['promptTokensDetails', 'cachedTokens'],
233
+ ['inputTokensDetails', 'cachedTokens'],
234
+ ['cache_read_input_tokens'],
235
+ ['cacheReadInputTokens'],
236
+ ['cache_read_tokens'],
237
+ ['cacheReadTokens'],
238
+ ['cached_tokens'],
239
+ ['cachedTokens'],
240
+ ['cached_input_tokens'],
241
+ ['cachedInputTokens'],
242
+ ['cached_content_token_count'],
243
+ ['cachedContentTokenCount'],
244
+ ['usage', 'prompt_tokens_details', 'cached_tokens'],
245
+ ['usage', 'input_tokens_details', 'cached_tokens'],
246
+ ['usage_metadata', 'cached_content_token_count'],
247
+ ['usageMetadata', 'cachedContentTokenCount'],
248
+ ['token_usage', 'prompt_tokens_details', 'cached_tokens'],
249
+ ['tokenUsage', 'promptTokensDetails', 'cachedTokens'],
250
+ ['tokens', 'cached_tokens'],
251
+ ['tokens', 'cachedTokens'],
252
+ ['prompt_cache_hit_tokens'],
253
+ ['promptCacheHitTokens'],
254
+ ['cache_hit_tokens'],
255
+ ['cacheHitTokens']
256
+ ]);
257
+ const explicitCacheMissInputTokens = firstFiniteNumber(usage, [
258
+ ['prompt_cache_miss_tokens'],
259
+ ['promptCacheMissTokens'],
260
+ ['cache_miss_tokens'],
261
+ ['cacheMissTokens']
262
+ ]);
263
+ const cacheWriteInputTokens = firstFiniteNumber(usage, [
264
+ ['cache_creation_input_tokens'],
265
+ ['cacheCreationInputTokens'],
266
+ ['cache_write_input_tokens'],
267
+ ['cacheWriteInputTokens'],
268
+ ['cache_creation_tokens'],
269
+ ['cacheCreationTokens'],
270
+ ['usage', 'cache_creation_input_tokens'],
271
+ ['usage', 'cache_write_input_tokens'],
272
+ ['token_usage', 'cache_creation_input_tokens'],
273
+ ['tokenUsage', 'cacheCreationInputTokens']
274
+ ]) ?? sumFiniteNumbers(usage, [
275
+ ['cache_creation', 'ephemeral_5m_input_tokens'],
276
+ ['cache_creation', 'ephemeral_1h_input_tokens'],
277
+ ['cacheCreation', 'ephemeral5mInputTokens'],
278
+ ['cacheCreation', 'ephemeral1hInputTokens'],
279
+ ['usage', 'cache_creation', 'ephemeral_5m_input_tokens'],
280
+ ['usage', 'cache_creation', 'ephemeral_1h_input_tokens']
281
+ ]);
282
+ const hasAnthropicSplitCacheInput = explicitInputTokens != null
283
+ && (cacheReadInputTokens != null || cacheWriteInputTokens != null)
284
+ && promptCacheHitTokens == null;
285
+ const cacheMissInputTokens = explicitCacheMissInputTokens ?? (
286
+ hasAnthropicSplitCacheInput
287
+ ? Number(explicitInputTokens || 0) + Number(cacheWriteInputTokens || 0)
288
+ : null
289
+ );
290
+ const inputTokens = explicitInputTokens != null
291
+ ? Number(explicitInputTokens || 0)
292
+ + (hasAnthropicSplitCacheInput ? Number(cacheReadInputTokens || 0) + Number(cacheWriteInputTokens || 0) : 0)
293
+ : (
294
+ promptCacheHitTokens != null || promptCacheMissTokens != null
295
+ ? Number(promptCacheHitTokens || 0) + Number(promptCacheMissTokens || 0)
296
+ : null
297
+ );
298
+ const reasoningOutputTokens = firstFiniteNumber(usage, [
299
+ ['completion_tokens_details', 'reasoning_tokens'],
300
+ ['output_tokens_details', 'reasoning_tokens'],
301
+ ['completionTokensDetails', 'reasoningTokens'],
302
+ ['outputTokensDetails', 'reasoningTokens'],
303
+ ['reasoning_tokens'],
304
+ ['reasoningTokens'],
305
+ ['thoughts_token_count'],
306
+ ['thoughtsTokenCount'],
307
+ ['usage', 'completion_tokens_details', 'reasoning_tokens'],
308
+ ['usage_metadata', 'thoughts_token_count'],
309
+ ['usageMetadata', 'thoughtsTokenCount']
310
+ ]);
311
+ const totalTokens = explicitTotal ?? (
312
+ inputTokens != null || outputTokens != null
313
+ ? Number(inputTokens || 0) + Number(outputTokens || 0)
314
+ : null
315
+ );
316
+ if (
317
+ inputTokens == null &&
318
+ outputTokens == null &&
319
+ totalTokens == null &&
320
+ cachedInputTokens == null &&
321
+ cacheWriteInputTokens == null
322
+ ) {
323
+ return null;
324
+ }
325
+ return {
326
+ inputTokens: Math.round(inputTokens || 0),
327
+ outputTokens: Math.round(outputTokens || 0),
328
+ totalTokens: Math.round(totalTokens || 0),
329
+ cachedInputTokens: Math.round(cachedInputTokens || 0),
330
+ cacheMissInputTokens: Math.round(cacheMissInputTokens || 0),
331
+ cacheWriteInputTokens: Math.round(cacheWriteInputTokens || 0),
332
+ reasoningOutputTokens: Math.round(reasoningOutputTokens || 0),
333
+ requests: 1,
334
+ raw: collectRawUsage(usage)
335
+ };
336
+ }
337
+
338
+ function cloneModelUsage(usage) {
339
+ if (!usage || typeof usage !== 'object') return null;
340
+ return {
341
+ inputTokens: Math.max(0, Math.round(Number(usage.inputTokens || 0))),
342
+ outputTokens: Math.max(0, Math.round(Number(usage.outputTokens || 0))),
343
+ totalTokens: Math.max(0, Math.round(Number(usage.totalTokens || 0))),
344
+ cachedInputTokens: Math.max(0, Math.round(Number(usage.cachedInputTokens || 0))),
345
+ cacheMissInputTokens: Math.max(0, Math.round(Number(usage.cacheMissInputTokens || 0))),
346
+ cacheWriteInputTokens: Math.max(0, Math.round(Number(usage.cacheWriteInputTokens || 0))),
347
+ reasoningOutputTokens: Math.max(0, Math.round(Number(usage.reasoningOutputTokens || 0))),
348
+ requests: Math.max(0, Math.round(Number(usage.requests || 0))),
349
+ raw: Array.isArray(usage.raw) ? usage.raw.map((item) => ({ ...item })) : []
350
+ };
351
+ }
352
+
353
+ function mergeModelUsage(left, right) {
354
+ const a = cloneModelUsage(left);
355
+ const b = cloneModelUsage(right);
356
+ if (!a) return b;
357
+ if (!b) return a;
358
+ return {
359
+ inputTokens: a.inputTokens + b.inputTokens,
360
+ outputTokens: a.outputTokens + b.outputTokens,
361
+ totalTokens: a.totalTokens + b.totalTokens,
362
+ cachedInputTokens: a.cachedInputTokens + b.cachedInputTokens,
363
+ cacheMissInputTokens: a.cacheMissInputTokens + b.cacheMissInputTokens,
364
+ cacheWriteInputTokens: a.cacheWriteInputTokens + b.cacheWriteInputTokens,
365
+ reasoningOutputTokens: a.reasoningOutputTokens + b.reasoningOutputTokens,
366
+ requests: a.requests + b.requests,
367
+ raw: [...a.raw, ...b.raw]
368
+ };
369
+ }
370
+
371
+ function prioritizeByPreferredOrder(items, preferredOrder) {
363
372
  const source = Array.isArray(items) ? items : [];
364
373
  const priorities = new Map((Array.isArray(preferredOrder) ? preferredOrder : []).map((value, index) => [value, index]));
365
374
  return [...source].sort((left, right) => {
@@ -370,22 +379,22 @@ function prioritizeByPreferredOrder(items, preferredOrder) {
370
379
  });
371
380
  }
372
381
 
373
- function normalizeUiLocale(value) {
374
- return String(value || '').toLowerCase().startsWith('en') ? 'en' : 'zh';
375
- }
376
-
377
- function formatLocalDateTimeSlug(date = new Date()) {
378
- const value = date instanceof Date ? date : new Date(date);
379
- const year = value.getFullYear();
380
- const month = String(value.getMonth() + 1).padStart(2, '0');
381
- const day = String(value.getDate()).padStart(2, '0');
382
- const hour = String(value.getHours()).padStart(2, '0');
383
- const minute = String(value.getMinutes()).padStart(2, '0');
384
- const second = String(value.getSeconds()).padStart(2, '0');
385
- return `${year}-${month}-${day}-${hour}-${minute}-${second}`;
386
- }
387
-
388
- function getCompletionCopy(language = 'zh') {
382
+ function normalizeUiLocale(value) {
383
+ return String(value || '').toLowerCase().startsWith('en') ? 'en' : 'zh';
384
+ }
385
+
386
+ function formatLocalDateTimeSlug(date = new Date()) {
387
+ const value = date instanceof Date ? date : new Date(date);
388
+ const year = value.getFullYear();
389
+ const month = String(value.getMonth() + 1).padStart(2, '0');
390
+ const day = String(value.getDate()).padStart(2, '0');
391
+ const hour = String(value.getHours()).padStart(2, '0');
392
+ const minute = String(value.getMinutes()).padStart(2, '0');
393
+ const second = String(value.getSeconds()).padStart(2, '0');
394
+ return `${year}-${month}-${day}-${hour}-${minute}-${second}`;
395
+ }
396
+
397
+ function getCompletionCopy(language = 'zh') {
389
398
  const lang = normalizeUiLocale(language);
390
399
  return {
391
400
  zh: {
@@ -428,7 +437,8 @@ function getCompletionCopy(language = 'zh') {
428
437
  'sdk.provider': '可选:openai-compatible | anthropic',
429
438
  'ui.language': '可选:zh | en',
430
439
  'ui.reply_language': '可选:zh | en',
431
- 'execution.mode': '可选:auto | normal | plan',
440
+ 'execution.mode': '可选:normal | plan',
441
+ 'execution.approval_mode': '可选:review | auto | full_access',
432
442
  'shell.default': '常用:bash | powershell',
433
443
  'policy.safe_mode': '可选:true | false',
434
444
  'policy.allowed_paths': 'JSON 数组,例如 ["D:\\\\shared"]',
@@ -460,7 +470,8 @@ function getCompletionCopy(language = 'zh') {
460
470
  commands: '列出 slash/自定义命令',
461
471
  status: '查看运行状态(mode/model/session)',
462
472
  model: '查看或切换模型',
463
- mode: '设置执行模式:normal|auto|plan',
473
+ mode: '设置工作模式:normal|plan',
474
+ approval: '设置审阅权限:review|auto|full_access',
464
475
  compact: '压缩消息上下文',
465
476
  checkpoint: '创建/查看/加载检查点',
466
477
  spec: '在 .codemini/specs 中创建 spec',
@@ -483,7 +494,8 @@ function getCompletionCopy(language = 'zh') {
483
494
  generic: {
484
495
  configCommand: '配置命令',
485
496
  historyCommand: '历史会话命令',
486
- modeCommand: '切换执行模式',
497
+ modeCommand: '切换工作模式',
498
+ approvalCommand: '切换审阅权限',
487
499
  checkpointCommand: '检查点命令',
488
500
  specCommand: '创建 spec 文件',
489
501
  planCommand: '规划命令',
@@ -541,7 +553,8 @@ function getCompletionCopy(language = 'zh') {
541
553
  'sdk.provider': 'options: openai-compatible | anthropic',
542
554
  'ui.language': 'options: zh | en',
543
555
  'ui.reply_language': 'options: zh | en',
544
- 'execution.mode': 'options: auto | normal | plan',
556
+ 'execution.mode': 'options: normal | plan',
557
+ 'execution.approval_mode': 'options: review | auto | full_access',
545
558
  'shell.default': 'common: bash | powershell',
546
559
  'policy.safe_mode': 'options: true | false',
547
560
  'policy.allowed_paths': 'JSON array, for example ["D:\\\\shared"]',
@@ -573,7 +586,8 @@ function getCompletionCopy(language = 'zh') {
573
586
  commands: 'list slash/custom commands',
574
587
  status: 'show runtime status (mode/model/session)',
575
588
  model: 'show or switch model',
576
- mode: 'set execution mode: normal|auto|plan',
589
+ mode: 'set work mode: normal|plan',
590
+ approval: 'set approval mode: review|auto|full_access',
577
591
  compact: 'compress message context',
578
592
  checkpoint: 'create/list/load conversation checkpoints',
579
593
  spec: 'create a spec markdown file in .codemini/specs',
@@ -596,7 +610,8 @@ function getCompletionCopy(language = 'zh') {
596
610
  generic: {
597
611
  configCommand: 'config command',
598
612
  historyCommand: 'history command',
599
- modeCommand: 'switch execution mode',
613
+ modeCommand: 'switch work mode',
614
+ approvalCommand: 'switch approval mode',
600
615
  checkpointCommand: 'checkpoint command',
601
616
  specCommand: 'create a spec file',
602
617
  planCommand: 'planning command',
@@ -2577,7 +2592,7 @@ function summarizePromptBudgetAudit(audit) {
2577
2592
  return `prompt budget: ${totalTokens}/${maxContextTokens} est tokens (${pct}%)${components ? `; ${components}` : ''}`;
2578
2593
  }
2579
2594
 
2580
- function buildRuntimeStateSnapshot({ currentSession, config, model, executionMode, extraSession }) {
2595
+ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMode, extraSession }) {
2581
2596
  const activeParentMessages = Array.isArray(currentSession?.compact?.view) && currentSession.compact.view.length > 0
2582
2597
  ? currentSession.compact.view
2583
2598
  : currentSession?.messages || [];
@@ -2588,11 +2603,12 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
2588
2603
  const contextUsagePct = maxContextTokens > 0 ? Math.min(100, Math.max(0, (currentContextTokens / maxContextTokens) * 100)) : 0;
2589
2604
  const planState = currentSession?.planState;
2590
2605
  const snapshot = {
2591
- sessionId: currentSession?.id || '',
2592
- sessionTitle: currentSession?.title || '',
2593
- messageCount: Array.isArray(currentSession?.messages) ? currentSession.messages.length : 0,
2594
- mode: executionMode || config.execution?.mode || 'normal',
2595
- sdkProvider: config.sdk?.provider || 'openai-compatible',
2606
+ sessionId: currentSession?.id || '',
2607
+ sessionTitle: currentSession?.title || '',
2608
+ messageCount: Array.isArray(currentSession?.messages) ? currentSession.messages.length : 0,
2609
+ mode: executionMode === 'auto' ? 'normal' : (executionMode || config.execution?.mode || 'normal'),
2610
+ approvalMode: config.execution?.approval_mode || (executionMode === 'auto' ? 'auto' : 'review'),
2611
+ sdkProvider: config.sdk?.provider || 'openai-compatible',
2596
2612
  agentRole: 'general',
2597
2613
  model: model || config.model?.name || '',
2598
2614
  mainModel: config.model?.name || '',
@@ -2939,10 +2955,13 @@ async function askModel({
2939
2955
  signal,
2940
2956
  allowedTools,
2941
2957
  maxSteps: maxStepsOverride,
2942
- skipAnalysisNudge = false,
2943
- compactedForModel: compactedInput = null,
2944
- onCompactedUpdate = null
2945
- }) {
2958
+ skipAnalysisNudge = false,
2959
+ compactedForModel: compactedInput = null,
2960
+ onCompactedUpdate = null,
2961
+ changeTracker = null,
2962
+ backupManager = null,
2963
+ projectIsGit = Boolean(config?.runtime?.project_is_git)
2964
+ }) {
2946
2965
  let compacted = compactedInput;
2947
2966
  const modelInputText = typeof modelText === 'string' && modelText ? modelText : text;
2948
2967
  const maxContextTokens = effectiveMaxContextTokens(config);
@@ -3104,11 +3123,12 @@ async function askModel({
3104
3123
  scheduleSessionSave();
3105
3124
  },
3106
3125
  getPlanState: () => normalizePlanState(session.planState),
3107
- onPlanStateUpdate: (planState) => {
3108
- session.planState = normalizePlanState(planState);
3109
- scheduleSessionSave();
3110
- }
3111
- });
3126
+ onPlanStateUpdate: (planState) => {
3127
+ session.planState = normalizePlanState(planState);
3128
+ scheduleSessionSave();
3129
+ },
3130
+ backupManager
3131
+ });
3112
3132
 
3113
3133
  const filteredDefinitions = Array.isArray(allowedTools)
3114
3134
  ? definitions.filter((t) => allowedTools.includes(t.function?.name || t.name))
@@ -3147,38 +3167,45 @@ async function askModel({
3147
3167
 
3148
3168
  let activeAssistantIndex = -1;
3149
3169
  const pendingToolMeta = new Map();
3150
- const normalizeFileChange = (change) => {
3151
- if (!change || typeof change !== 'object') return null;
3152
- const path = String(change.path || '').trim();
3153
- if (!path) return null;
3154
- const action = String(change.action || '').trim();
3155
- return {
3156
- path,
3157
- action: action === 'create' || action === 'delete' ? action : 'edit',
3170
+ const normalizeFileChange = (change) => {
3171
+ if (!change || typeof change !== 'object') return null;
3172
+ const path = String(change.path || '').trim();
3173
+ if (!path) return null;
3174
+ const action = String(change.action || '').trim();
3175
+ return {
3176
+ path,
3177
+ action: action === 'create' || action === 'delete' ? action : 'edit',
3158
3178
  linesAdded: Number(change.linesAdded || 0),
3159
3179
  linesRemoved: Number(change.linesRemoved || 0),
3160
3180
  changedLine: Number(change.changedLine || 0),
3161
- diffPreview: String(change.diffPreview || '')
3181
+ diffPreview: String(change.diffPreview || ''),
3182
+ changeSetId: String(change.changeSetId || ''),
3183
+ patchRef: String(change.patchRef || '')
3162
3184
  };
3163
3185
  };
3164
- const fileChangeFingerprint = (change) => JSON.stringify({
3165
- path: change.path,
3166
- action: change.action,
3167
- linesAdded: Number(change.linesAdded || 0),
3168
- linesRemoved: Number(change.linesRemoved || 0),
3169
- changedLine: Number(change.changedLine || 0),
3170
- diffPreview: String(change.diffPreview || '')
3186
+ const fileChangeFingerprint = (change) => JSON.stringify({
3187
+ path: change.path,
3188
+ action: change.action,
3189
+ linesAdded: Number(change.linesAdded || 0),
3190
+ linesRemoved: Number(change.linesRemoved || 0),
3191
+ changedLine: Number(change.changedLine || 0),
3192
+ diffPreview: String(change.diffPreview || ''),
3193
+ changeSetId: String(change.changeSetId || ''),
3194
+ patchRef: String(change.patchRef || '')
3171
3195
  });
3172
- const appendUniqueFileChange = (message, fileChange) => {
3173
- const existing = Array.isArray(message.file_changes) ? message.file_changes : [];
3174
- const nextKey = fileChangeFingerprint(fileChange);
3175
- if (existing.some((change) => fileChangeFingerprint(normalizeFileChange(change) || {}) === nextKey)) {
3176
- message.file_changes = existing;
3177
- return;
3178
- }
3179
- message.file_changes = [...existing, fileChange];
3180
- };
3181
- const attachToolMetaToSessionCall = (toolId, meta = {}) => {
3196
+ const normalizeFileChanges = (changes) => (Array.isArray(changes) ? changes : [changes])
3197
+ .map(normalizeFileChange)
3198
+ .filter(Boolean);
3199
+ const appendUniqueFileChange = (message, fileChange) => {
3200
+ const existing = Array.isArray(message.file_changes) ? message.file_changes : [];
3201
+ const nextKey = fileChangeFingerprint(fileChange);
3202
+ if (existing.some((change) => fileChangeFingerprint(normalizeFileChange(change) || {}) === nextKey)) {
3203
+ message.file_changes = existing;
3204
+ return;
3205
+ }
3206
+ message.file_changes = [...existing, fileChange];
3207
+ };
3208
+ const attachToolMetaToSessionCall = (toolId, meta = {}) => {
3182
3209
  if (!toolId) return false;
3183
3210
  for (let i = session.messages.length - 1; i >= 0; i -= 1) {
3184
3211
  const msg = session.messages[i];
@@ -3188,11 +3215,14 @@ async function askModel({
3188
3215
  if (Number.isFinite(Number(meta.durationMs))) call.durationMs = Number(meta.durationMs);
3189
3216
  if (typeof meta.summary === 'string' && meta.summary.trim()) call.summary = meta.summary.trim();
3190
3217
  if (typeof meta.status === 'string' && meta.status.trim()) call.status = meta.status.trim();
3218
+ if (meta.resultMeta && typeof meta.resultMeta === 'object') call.resultMeta = meta.resultMeta;
3191
3219
  const fileChange = normalizeFileChange(meta.fileChange);
3192
3220
  if (fileChange) {
3193
3221
  call.fileChange = fileChange;
3194
- appendUniqueFileChange(msg, fileChange);
3195
3222
  }
3223
+ const fileChanges = normalizeFileChanges(meta.fileChanges && meta.fileChanges.length ? meta.fileChanges : fileChange);
3224
+ if (fileChanges.length) call.fileChanges = fileChanges;
3225
+ for (const change of fileChanges) appendUniqueFileChange(msg, change);
3196
3226
  msg.at = new Date().toISOString();
3197
3227
  return true;
3198
3228
  }
@@ -3204,64 +3234,64 @@ async function askModel({
3204
3234
  session.messages.push(stampedMessage('assistant', ''));
3205
3235
  activeAssistantIndex = session.messages.length - 1;
3206
3236
  if (persistSession) scheduleSessionSave();
3207
- } else if (event?.type === 'assistant:delta') {
3208
- if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
3209
- const current = session.messages[activeAssistantIndex];
3210
- const now = new Date();
3211
- if (current.reasoning_started_at && !current.reasoning_ended_at) {
3212
- current.reasoning_ended_at = now.toISOString();
3213
- current.reasoning_duration_ms = Math.max(
3214
- Number(current.reasoning_duration_ms || 0),
3215
- Date.parse(current.reasoning_ended_at) - Date.parse(current.reasoning_started_at)
3216
- );
3217
- }
3218
- current.content = `${current.content || ''}${event.text || ''}`;
3219
- current.at = now.toISOString();
3220
- if (persistSession) scheduleSessionSave();
3221
- }
3222
- } else if (event?.type === 'assistant:reasoning_delta') {
3223
- if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
3224
- const current = session.messages[activeAssistantIndex];
3225
- const now = new Date();
3226
- if (!current.reasoning_started_at) current.reasoning_started_at = now.toISOString();
3227
- current.reasoning_content = `${current.reasoning_content || ''}${event.text || ''}`;
3228
- current.reasoning_duration_ms = Math.max(
3229
- 0,
3230
- now.getTime() - Date.parse(current.reasoning_started_at)
3231
- );
3232
- current.at = now.toISOString();
3233
- if (persistSession) scheduleSessionSave();
3234
- }
3235
- } else if (event?.type === 'assistant:response') {
3236
- const eventUsage = normalizeModelUsage(event.usage || event.assistantMessage?.usage);
3237
- if (eventUsage) event.usage = eventUsage;
3238
- if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
3239
- const current = session.messages[activeAssistantIndex];
3240
- const now = new Date();
3241
- current.content = event.assistantMessage?.content ?? event.text ?? current.content;
3242
- if (typeof event.assistantMessage?.reasoning_content === 'string' && event.assistantMessage.reasoning_content) {
3243
- current.reasoning_content = event.assistantMessage.reasoning_content;
3244
- }
3237
+ } else if (event?.type === 'assistant:delta') {
3238
+ if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
3239
+ const current = session.messages[activeAssistantIndex];
3240
+ const now = new Date();
3241
+ if (current.reasoning_started_at && !current.reasoning_ended_at) {
3242
+ current.reasoning_ended_at = now.toISOString();
3243
+ current.reasoning_duration_ms = Math.max(
3244
+ Number(current.reasoning_duration_ms || 0),
3245
+ Date.parse(current.reasoning_ended_at) - Date.parse(current.reasoning_started_at)
3246
+ );
3247
+ }
3248
+ current.content = `${current.content || ''}${event.text || ''}`;
3249
+ current.at = now.toISOString();
3250
+ if (persistSession) scheduleSessionSave();
3251
+ }
3252
+ } else if (event?.type === 'assistant:reasoning_delta') {
3253
+ if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
3254
+ const current = session.messages[activeAssistantIndex];
3255
+ const now = new Date();
3256
+ if (!current.reasoning_started_at) current.reasoning_started_at = now.toISOString();
3257
+ current.reasoning_content = `${current.reasoning_content || ''}${event.text || ''}`;
3258
+ current.reasoning_duration_ms = Math.max(
3259
+ 0,
3260
+ now.getTime() - Date.parse(current.reasoning_started_at)
3261
+ );
3262
+ current.at = now.toISOString();
3263
+ if (persistSession) scheduleSessionSave();
3264
+ }
3265
+ } else if (event?.type === 'assistant:response') {
3266
+ const eventUsage = normalizeModelUsage(event.usage || event.assistantMessage?.usage);
3267
+ if (eventUsage) event.usage = eventUsage;
3268
+ if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
3269
+ const current = session.messages[activeAssistantIndex];
3270
+ const now = new Date();
3271
+ current.content = event.assistantMessage?.content ?? event.text ?? current.content;
3272
+ if (typeof event.assistantMessage?.reasoning_content === 'string' && event.assistantMessage.reasoning_content) {
3273
+ current.reasoning_content = event.assistantMessage.reasoning_content;
3274
+ }
3245
3275
  if (Array.isArray(event.assistantMessage?.reasoning_details) && event.assistantMessage.reasoning_details.length > 0) {
3246
3276
  current.reasoning_details = event.assistantMessage.reasoning_details;
3247
3277
  }
3248
- if (Array.isArray(event.assistantMessage?.tool_calls) && event.assistantMessage.tool_calls.length > 0) {
3249
- current.tool_calls = event.assistantMessage.tool_calls;
3250
- }
3251
- if (eventUsage) {
3252
- current.usage = mergeModelUsage(current.usage, eventUsage);
3253
- }
3254
- if ((current.reasoning_content || current.reasoning_details) && current.reasoning_started_at) {
3255
- current.reasoning_ended_at = current.reasoning_ended_at || now.toISOString();
3256
- current.reasoning_duration_ms = Math.max(
3257
- Number(current.reasoning_duration_ms || 0),
3258
- Date.parse(current.reasoning_ended_at) - Date.parse(current.reasoning_started_at)
3259
- );
3260
- }
3261
- current.at = now.toISOString();
3262
- if (persistSession) scheduleSessionSave();
3263
- } else {
3264
- const assistantMessage = event.assistantMessage && typeof event.assistantMessage === 'object'
3278
+ if (Array.isArray(event.assistantMessage?.tool_calls) && event.assistantMessage.tool_calls.length > 0) {
3279
+ current.tool_calls = event.assistantMessage.tool_calls;
3280
+ }
3281
+ if (eventUsage) {
3282
+ current.usage = mergeModelUsage(current.usage, eventUsage);
3283
+ }
3284
+ if ((current.reasoning_content || current.reasoning_details) && current.reasoning_started_at) {
3285
+ current.reasoning_ended_at = current.reasoning_ended_at || now.toISOString();
3286
+ current.reasoning_duration_ms = Math.max(
3287
+ Number(current.reasoning_duration_ms || 0),
3288
+ Date.parse(current.reasoning_ended_at) - Date.parse(current.reasoning_started_at)
3289
+ );
3290
+ }
3291
+ current.at = now.toISOString();
3292
+ if (persistSession) scheduleSessionSave();
3293
+ } else {
3294
+ const assistantMessage = event.assistantMessage && typeof event.assistantMessage === 'object'
3265
3295
  ? event.assistantMessage
3266
3296
  : { content: event.text || '' };
3267
3297
  session.messages.push(stampedMessage('assistant', assistantMessage.content || event.text || '', {
@@ -3271,38 +3301,40 @@ async function askModel({
3271
3301
  ...(Array.isArray(assistantMessage.reasoning_details) && assistantMessage.reasoning_details.length > 0
3272
3302
  ? { reasoning_details: assistantMessage.reasoning_details }
3273
3303
  : {}),
3274
- ...(Array.isArray(assistantMessage.tool_calls) && assistantMessage.tool_calls.length > 0
3275
- ? { tool_calls: assistantMessage.tool_calls }
3276
- : {}),
3277
- ...(eventUsage ? { usage: eventUsage } : {})
3278
- }));
3279
- if (persistSession) scheduleSessionSave();
3280
- }
3304
+ ...(Array.isArray(assistantMessage.tool_calls) && assistantMessage.tool_calls.length > 0
3305
+ ? { tool_calls: assistantMessage.tool_calls }
3306
+ : {}),
3307
+ ...(eventUsage ? { usage: eventUsage } : {})
3308
+ }));
3309
+ if (persistSession) scheduleSessionSave();
3310
+ }
3281
3311
  activeAssistantIndex = -1;
3282
- } else if (event?.type === 'tool:start') {
3283
- if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
3284
- const current = session.messages[activeAssistantIndex];
3285
- const now = new Date();
3286
- if (current.reasoning_started_at && !current.reasoning_ended_at) {
3287
- current.reasoning_ended_at = now.toISOString();
3288
- current.reasoning_duration_ms = Math.max(
3289
- Number(current.reasoning_duration_ms || 0),
3290
- Date.parse(current.reasoning_ended_at) - Date.parse(current.reasoning_started_at)
3291
- );
3292
- current.at = now.toISOString();
3293
- if (persistSession) scheduleSessionSave();
3294
- }
3295
- }
3296
- } else if (event?.type === 'tool:end' || event?.type === 'tool:error' || event?.type === 'tool:blocked') {
3312
+ } else if (event?.type === 'tool:start') {
3313
+ if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
3314
+ const current = session.messages[activeAssistantIndex];
3315
+ const now = new Date();
3316
+ if (current.reasoning_started_at && !current.reasoning_ended_at) {
3317
+ current.reasoning_ended_at = now.toISOString();
3318
+ current.reasoning_duration_ms = Math.max(
3319
+ Number(current.reasoning_duration_ms || 0),
3320
+ Date.parse(current.reasoning_ended_at) - Date.parse(current.reasoning_started_at)
3321
+ );
3322
+ current.at = now.toISOString();
3323
+ if (persistSession) scheduleSessionSave();
3324
+ }
3325
+ }
3326
+ } else if (event?.type === 'tool:end' || event?.type === 'tool:error' || event?.type === 'tool:blocked') {
3297
3327
  const toolId = String(event.id || '');
3298
3328
  if (toolId) {
3299
3329
  const meta = {
3300
3330
  durationMs: Number.isFinite(Number(event.durationMs)) ? Number(event.durationMs) : undefined,
3301
3331
  summary: typeof event.summary === 'string' ? event.summary : '',
3332
+ resultMeta: event.resultMeta && typeof event.resultMeta === 'object' ? event.resultMeta : null,
3302
3333
  fileChange: normalizeFileChange(event.fileChange),
3334
+ fileChanges: normalizeFileChanges(event.fileChanges),
3303
3335
  status:
3304
- event.type === 'tool:error'
3305
- ? 'error'
3336
+ event.type === 'tool:error'
3337
+ ? 'error'
3306
3338
  : event.type === 'tool:blocked'
3307
3339
  ? 'blocked'
3308
3340
  : 'done'
@@ -3319,9 +3351,11 @@ async function askModel({
3319
3351
  ...(Number.isFinite(Number(meta.durationMs)) ? { tool_duration_ms: Number(meta.durationMs) } : {}),
3320
3352
  ...(meta.summary ? { tool_summary: meta.summary } : {}),
3321
3353
  ...(meta.status ? { tool_status: meta.status } : {}),
3322
- ...(meta.fileChange ? { tool_file_change: meta.fileChange } : {})
3354
+ ...(meta.resultMeta ? { tool_result_meta: meta.resultMeta } : {}),
3355
+ ...(meta.fileChange ? { tool_file_change: meta.fileChange } : {}),
3356
+ ...(Array.isArray(meta.fileChanges) && meta.fileChanges.length ? { tool_file_changes: meta.fileChanges } : {})
3323
3357
  })
3324
- );
3358
+ );
3325
3359
  pendingToolMeta.delete(toolId);
3326
3360
  if (persistSession) scheduleSessionSave();
3327
3361
  }
@@ -3341,17 +3375,25 @@ async function askModel({
3341
3375
  toolHandlers: filteredHandlers,
3342
3376
  initialMessages: toOpenAIMessages(compacted ?? session.messages),
3343
3377
  onEvent: wrappedAgentEvent,
3344
- executionMode: executionMode || config.execution?.mode || 'normal',
3345
- alwaysAllowTools:
3378
+ executionMode: executionMode || config.execution?.mode || 'normal',
3379
+ approvalMode: config.execution?.approval_mode || 'review',
3380
+ projectIsGit: Boolean(projectIsGit || changeTracker?.enabled),
3381
+ alwaysAllowTools:
3346
3382
  alwaysAllowTools || config.execution?.always_allow_tools || ['run', 'read', 'write'],
3347
3383
  toolResultMaxChars: config.context?.tool_result_max_chars || 12000,
3348
3384
  toolFormatters: formatters,
3349
3385
  deferredDefinitions: filteredDeferred,
3350
3386
  requestToolApproval,
3351
- signal,
3352
- skipAnalysisNudge,
3353
- config,
3354
- requestCompletion: async ({ messages, tools, model: selectedModel }) => {
3387
+ signal,
3388
+ skipAnalysisNudge,
3389
+ config,
3390
+ changeTracker: changeTracker?.enabled
3391
+ ? {
3392
+ begin: (meta) => beginGitOplogCapture(changeTracker, meta),
3393
+ capture: (scope, meta) => captureGitOplogChanges(changeTracker, scope, meta)
3394
+ }
3395
+ : null,
3396
+ requestCompletion: async ({ messages, tools, model: selectedModel }) => {
3355
3397
  let started = false;
3356
3398
  const startAssistantStream = () => {
3357
3399
  if (!started) {
@@ -3370,15 +3412,15 @@ async function askModel({
3370
3412
  timeoutMs: config.gateway.timeout_ms || 1800000,
3371
3413
  maxRetries: config.gateway.max_retries ?? 2,
3372
3414
  signal,
3373
- onTextDelta: (delta) => {
3374
- startAssistantStream();
3375
- wrappedAgentEvent({ type: 'assistant:delta', text: delta });
3376
- },
3377
- onReasoningDelta: (delta) => {
3378
- startAssistantStream();
3379
- wrappedAgentEvent({ type: 'assistant:reasoning_delta', text: delta });
3380
- },
3381
- onToolCallDelta: (toolCall) => {
3415
+ onTextDelta: (delta) => {
3416
+ startAssistantStream();
3417
+ wrappedAgentEvent({ type: 'assistant:delta', text: delta });
3418
+ },
3419
+ onReasoningDelta: (delta) => {
3420
+ startAssistantStream();
3421
+ wrappedAgentEvent({ type: 'assistant:reasoning_delta', text: delta });
3422
+ },
3423
+ onToolCallDelta: (toolCall) => {
3382
3424
  startAssistantStream();
3383
3425
  wrappedAgentEvent({ type: 'assistant:tool_call_delta', toolCall });
3384
3426
  }
@@ -3505,7 +3547,7 @@ async function runSubAgentTask({
3505
3547
  }
3506
3548
  if (
3507
3549
  role !== 'summarizer' &&
3508
- ['assistant:start', 'assistant:delta', 'assistant:reasoning_delta', 'assistant:response', 'assistant:tool_call_delta'].includes(String(evt?.type || ''))
3550
+ ['assistant:start', 'assistant:delta', 'assistant:reasoning_delta', 'assistant:response', 'assistant:tool_call_delta'].includes(String(evt?.type || ''))
3509
3551
  ) {
3510
3552
  return;
3511
3553
  }
@@ -3528,7 +3570,7 @@ async function runSubAgentTask({
3528
3570
  systemPrompt: subSystemPrompt,
3529
3571
  onAgentEvent: wrappedOnAgentEvent,
3530
3572
  persistSession: false,
3531
- executionMode: 'auto',
3573
+ executionMode: 'normal',
3532
3574
  allowedTools: roleAllowedTools,
3533
3575
  skipAnalysisNudge: true,
3534
3576
  signal
@@ -3545,18 +3587,18 @@ async function runSubAgentTask({
3545
3587
  };
3546
3588
  }
3547
3589
 
3548
- function buildPlanStepTranscript({ stepRecord, stepIndex, totalSteps, messages }) {
3549
- const toolCardsById = new Map();
3550
- const toolCards = [];
3551
- const source = Array.isArray(messages) ? messages : [];
3552
- let usage = null;
3553
-
3554
- for (const msg of source) {
3555
- if (msg?.role === 'assistant' && msg.usage) {
3556
- usage = mergeModelUsage(usage, msg.usage);
3557
- }
3558
- if (msg?.role === 'assistant' && Array.isArray(msg.tool_calls)) {
3559
- for (const tc of msg.tool_calls) {
3590
+ function buildPlanStepTranscript({ stepRecord, stepIndex, totalSteps, messages }) {
3591
+ const toolCardsById = new Map();
3592
+ const toolCards = [];
3593
+ const source = Array.isArray(messages) ? messages : [];
3594
+ let usage = null;
3595
+
3596
+ for (const msg of source) {
3597
+ if (msg?.role === 'assistant' && msg.usage) {
3598
+ usage = mergeModelUsage(usage, msg.usage);
3599
+ }
3600
+ if (msg?.role === 'assistant' && Array.isArray(msg.tool_calls)) {
3601
+ for (const tc of msg.tool_calls) {
3560
3602
  const id = String(tc?.id || `tool-${toolCards.length + 1}`);
3561
3603
  if (toolCardsById.has(id)) continue;
3562
3604
  const card = {
@@ -3595,12 +3637,12 @@ function buildPlanStepTranscript({ stepRecord, stepIndex, totalSteps, messages }
3595
3637
  total: totalSteps,
3596
3638
  role: stepRecord.role || 'general',
3597
3639
  title: stepRecord.title || '',
3598
- status: stepRecord.failed ? 'failed' : 'done',
3599
- summary: stepRecord.failed ? stepRecord.failureReason : trimInline(stepRecord.output || '', 160),
3600
- segments,
3601
- ...(usage ? { usage } : {})
3602
- };
3603
- }
3640
+ status: stepRecord.failed ? 'failed' : 'done',
3641
+ summary: stepRecord.failed ? stepRecord.failureReason : trimInline(stepRecord.output || '', 160),
3642
+ segments,
3643
+ ...(usage ? { usage } : {})
3644
+ };
3645
+ }
3604
3646
 
3605
3647
  async function executePlanWithSubAgents({
3606
3648
  planState,
@@ -4013,13 +4055,13 @@ function renderProjectRequirementsSectionContract(ignoredSections = []) {
4013
4055
  return lines.join('\n');
4014
4056
  }
4015
4057
 
4016
- function buildProjectRequirementsSteps(renderedSkillPrompt, args = [], config = {}, reportSlug = formatLocalDateTimeSlug()) {
4017
- const options = parseProjectRequirementsOptions(args);
4018
- const userArgs = options.raw;
4019
- const requestedFocus = userArgs ? `User request/focus: ${userArgs}` : 'User request/focus: full workspace requirements report.';
4020
- const replyLanguageName = getReplyLanguageName(config);
4021
- const reportPath = `docs/requirements/${reportSlug}-project-requirements.html`;
4022
- const companionPath = `docs/requirements/${reportSlug}-project-requirements.md`;
4058
+ function buildProjectRequirementsSteps(renderedSkillPrompt, args = [], config = {}, reportSlug = formatLocalDateTimeSlug()) {
4059
+ const options = parseProjectRequirementsOptions(args);
4060
+ const userArgs = options.raw;
4061
+ const requestedFocus = userArgs ? `User request/focus: ${userArgs}` : 'User request/focus: full workspace requirements report.';
4062
+ const replyLanguageName = getReplyLanguageName(config);
4063
+ const reportPath = `docs/requirements/${reportSlug}-project-requirements.html`;
4064
+ const companionPath = `docs/requirements/${reportSlug}-project-requirements.md`;
4023
4065
  const reportContract = [
4024
4066
  requestedFocus,
4025
4067
  `Reply language: write generated report prose, UI labels inserted into the report, review notes, and final user-facing status in ${replyLanguageName} unless the user explicitly requested a different language. Do not translate REQUIREMENTS_* marker names or source code identifiers.`,
@@ -4341,11 +4383,11 @@ async function runProjectRequirementsPipeline({
4341
4383
  const options = parseProjectRequirementsOptions(parsedInput.args);
4342
4384
  const userFocus = options.raw;
4343
4385
  const goal = userFocus ? `project requirements report: ${userFocus}` : 'project requirements report';
4344
- const reportSlug = formatLocalDateTimeSlug();
4345
- const reportPath = `docs/requirements/${reportSlug}-project-requirements.html`;
4346
- const companionPath = `docs/requirements/${reportSlug}-project-requirements.md`;
4347
- const manifestPath = `docs/requirements/${reportSlug}-project-requirements.manifest.json`;
4348
- const steps = buildProjectRequirementsSteps(renderedSkillPrompt, parsedInput.args, config, reportSlug);
4386
+ const reportSlug = formatLocalDateTimeSlug();
4387
+ const reportPath = `docs/requirements/${reportSlug}-project-requirements.html`;
4388
+ const companionPath = `docs/requirements/${reportSlug}-project-requirements.md`;
4389
+ const manifestPath = `docs/requirements/${reportSlug}-project-requirements.manifest.json`;
4390
+ const steps = buildProjectRequirementsSteps(renderedSkillPrompt, parsedInput.args, config, reportSlug);
4349
4391
  const planFile = await writeMarkdownInProjectDir(
4350
4392
  'plans',
4351
4393
  'project-requirements-pipeline',
@@ -4655,10 +4697,10 @@ export async function createChatRuntime({
4655
4697
  currentSession.model = model;
4656
4698
  }
4657
4699
  const baseSystemPrompt = systemPrompt;
4658
- let executionMode = config.execution?.mode || 'normal';
4659
- if (hasPendingPlanApproval(currentSession)) {
4660
- executionMode = 'plan';
4661
- }
4700
+ let executionMode = config.execution?.mode === 'auto' ? 'normal' : (config.execution?.mode || 'normal');
4701
+ if (hasPendingPlanApproval(currentSession)) {
4702
+ executionMode = 'plan';
4703
+ }
4662
4704
  let compactState = null;
4663
4705
  const normalizeCompactThreshold = (value, fallback = 60) => {
4664
4706
  const num = Number(value);
@@ -4670,10 +4712,10 @@ export async function createChatRuntime({
4670
4712
  compactState.threshold = normalizeCompactThreshold(config.context?.preflight_trigger_pct, 60);
4671
4713
  };
4672
4714
  const syncRuntimeFromConfig = async ({ model: nextModel } = {}) => {
4673
- const configuredMode = String(config.execution?.mode || 'normal');
4674
- executionMode = hasPendingPlanApproval(currentSession)
4675
- ? 'plan'
4676
- : (['normal', 'auto', 'plan'].includes(configuredMode) ? configuredMode : 'normal');
4715
+ const configuredMode = String(config.execution?.mode || 'normal');
4716
+ executionMode = hasPendingPlanApproval(currentSession)
4717
+ ? 'plan'
4718
+ : (['normal', 'plan'].includes(configuredMode) ? configuredMode : 'normal');
4677
4719
  syncCompactStateFromConfig();
4678
4720
 
4679
4721
  const resolvedModel = String(nextModel || '').trim();
@@ -4686,15 +4728,29 @@ export async function createChatRuntime({
4686
4728
  }
4687
4729
  };
4688
4730
  const commands = await loadCommandsAndSkills();
4689
- const reloadCommandsAndSkills = async () => {
4690
- const next = await loadCommandsAndSkills();
4691
- commands.clear();
4692
- for (const [name, command] of next.entries()) {
4693
- commands.set(name, command);
4694
- }
4695
- };
4696
-
4697
- // Set up tool result store under session directory
4731
+ const reloadCommandsAndSkills = async () => {
4732
+ const next = await loadCommandsAndSkills();
4733
+ commands.clear();
4734
+ for (const [name, command] of next.entries()) {
4735
+ commands.set(name, command);
4736
+ }
4737
+ };
4738
+ let changeTracker = await createGitOplogChangeTracker({
4739
+ workspaceRoot: process.cwd(),
4740
+ sessionId: currentSession.id
4741
+ });
4742
+ let backupManager = changeTracker?.enabled
4743
+ ? null
4744
+ : await createNonGitBackupManager({
4745
+ workspaceRoot: process.cwd(),
4746
+ sessionId: currentSession.id
4747
+ }).catch(() => null);
4748
+ config.runtime = {
4749
+ ...(config.runtime || {}),
4750
+ project_is_git: Boolean(changeTracker?.enabled)
4751
+ };
4752
+
4753
+ // Set up tool result store under session directory
4698
4754
  const sessionResultsDir = path.join(getSessionsDir(), String(currentSession.id));
4699
4755
  setResultDir(sessionResultsDir);
4700
4756
  compactState = {
@@ -4762,7 +4818,8 @@ export async function createChatRuntime({
4762
4818
  'model.fast_name',
4763
4819
  'ui.language',
4764
4820
  'ui.reply_language',
4765
- 'execution.mode',
4821
+ 'execution.mode',
4822
+ 'execution.approval_mode',
4766
4823
  'shell.default',
4767
4824
  'sdk.provider',
4768
4825
  'gateway.timeout_ms',
@@ -4798,9 +4855,10 @@ export async function createChatRuntime({
4798
4855
  '/memory',
4799
4856
  '/capture',
4800
4857
  '/inbox',
4801
- '/dream',
4802
- '/mode',
4803
- '/plan',
4858
+ '/dream',
4859
+ '/mode',
4860
+ '/approval',
4861
+ '/plan',
4804
4862
  '/history',
4805
4863
  '/checkpoint',
4806
4864
  '/agents',
@@ -4819,7 +4877,8 @@ export async function createChatRuntime({
4819
4877
  { name: 'commands', description: completionCopy.commands.commands },
4820
4878
  { name: 'status', description: completionCopy.commands.status },
4821
4879
  { name: 'model', description: completionCopy.commands.model },
4822
- { name: 'mode', description: completionCopy.commands.mode },
4880
+ { name: 'mode', description: completionCopy.commands.mode },
4881
+ { name: 'approval', description: completionCopy.commands.approval },
4823
4882
  { name: 'compact', description: completionCopy.commands.compact },
4824
4883
  { name: 'checkpoint', description: completionCopy.commands.checkpoint },
4825
4884
  { name: 'spec', description: completionCopy.commands.spec },
@@ -4869,7 +4928,8 @@ export async function createChatRuntime({
4869
4928
 
4870
4929
  const historyTemplates = ['/history list', '/history current', '/history resume <session_id>'];
4871
4930
  const memoryTemplates = ['/memory list <scope>', '/memory search <scope> <query>', '/memory forget <scope> <id>'];
4872
- const modeTemplates = ['/mode normal', '/mode auto', '/mode plan'];
4931
+ const modeTemplates = ['/mode normal', '/mode plan'];
4932
+ const approvalTemplates = ['/approval review', '/approval auto', '/approval full_access'];
4873
4933
  const modelTemplates = ['/model current', '/model main', '/model fast', '/model set <name>'];
4874
4934
  const checkpointTemplates = [
4875
4935
  '/checkpoint create <name>',
@@ -4888,7 +4948,8 @@ export async function createChatRuntime({
4888
4948
  ...configTemplates,
4889
4949
  ...memoryTemplates,
4890
4950
  ...historyTemplates,
4891
- ...modeTemplates,
4951
+ ...modeTemplates,
4952
+ ...approvalTemplates,
4892
4953
  ...modelTemplates,
4893
4954
  ...checkpointTemplates,
4894
4955
  ...specTemplates,
@@ -4936,7 +4997,8 @@ export async function createChatRuntime({
4936
4997
  'config',
4937
4998
  'memory',
4938
4999
  'compact',
4939
- 'mode',
5000
+ 'mode',
5001
+ 'approval',
4940
5002
  'model',
4941
5003
  'checkpoint',
4942
5004
  'plan',
@@ -4956,7 +5018,8 @@ export async function createChatRuntime({
4956
5018
  }
4957
5019
  for (const template of memoryTemplates) registerSuggestion(template, completionCopy.generic.memoryCommand);
4958
5020
  for (const template of historyTemplates) registerSuggestion(template, completionCopy.generic.historyCommand);
4959
- for (const template of modeTemplates) registerSuggestion(template, completionCopy.generic.modeCommand);
5021
+ for (const template of modeTemplates) registerSuggestion(template, completionCopy.generic.modeCommand);
5022
+ for (const template of approvalTemplates) registerSuggestion(template, completionCopy.generic.approvalCommand);
4960
5023
  for (const template of modelTemplates) registerSuggestion(template, completionCopy.generic.modelCommand || completionCopy.commands.model);
4961
5024
  for (const template of checkpointTemplates) registerSuggestion(template, completionCopy.generic.checkpointCommand);
4962
5025
  for (const template of specTemplates) registerSuggestion(template, completionCopy.generic.specCommand);
@@ -5061,15 +5124,24 @@ export async function createChatRuntime({
5061
5124
  }
5062
5125
  return materializeSuggestions(modelTemplates);
5063
5126
  }
5064
- if (commandPart === 'mode') {
5065
- if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
5066
- const sub = tokens[1] || '';
5067
- return ['normal', 'auto', 'plan']
5068
- .filter((m) => m.startsWith(sub))
5069
- .map((m) => registerSuggestion(`/mode ${m}`, completionCopy.generic.modeCommand));
5070
- }
5071
- return materializeSuggestions(modeTemplates);
5072
- }
5127
+ if (commandPart === 'mode') {
5128
+ if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
5129
+ const sub = tokens[1] || '';
5130
+ return ['normal', 'plan']
5131
+ .filter((m) => m.startsWith(sub))
5132
+ .map((m) => registerSuggestion(`/mode ${m}`, completionCopy.generic.modeCommand));
5133
+ }
5134
+ return materializeSuggestions(modeTemplates);
5135
+ }
5136
+ if (commandPart === 'approval') {
5137
+ if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
5138
+ const sub = tokens[1] || '';
5139
+ return ['review', 'auto', 'full_access']
5140
+ .filter((m) => m.startsWith(sub))
5141
+ .map((m) => registerSuggestion(`/approval ${m}`, completionCopy.generic.approvalCommand));
5142
+ }
5143
+ return materializeSuggestions(approvalTemplates);
5144
+ }
5073
5145
  if (commandPart === 'checkpoint') {
5074
5146
  if (tokens.length <= 2 && !hasTrailingSpace) {
5075
5147
  const sub = tokens[1] || '';
@@ -5384,10 +5456,10 @@ export async function createChatRuntime({
5384
5456
  // 每次提交创建新的 AbortController,替代旧的
5385
5457
  activeAbortController = new AbortController();
5386
5458
  const { signal } = activeAbortController;
5387
- const activeReplySystemPrompt = await buildActiveSystemPrompt();
5388
- const parsedInput = parseInput(line);
5389
- const readOnlyCodeWiki = options?.readOnlyCodeWiki === true;
5390
- const codeWikiGenerate = options?.codeWikiGenerate === true;
5459
+ const activeReplySystemPrompt = await buildActiveSystemPrompt();
5460
+ const parsedInput = parseInput(line);
5461
+ const readOnlyCodeWiki = options?.readOnlyCodeWiki === true;
5462
+ const codeWikiGenerate = options?.codeWikiGenerate === true;
5391
5463
  const maybeAutoDreamFromRuntime = async () => {
5392
5464
  const threshold = Number(config?.memory?.auto_dream_threshold ?? 10);
5393
5465
  if (!(threshold > 0)) return null;
@@ -5421,7 +5493,7 @@ export async function createChatRuntime({
5421
5493
  }
5422
5494
  };
5423
5495
  try {
5424
- if (!readOnlyCodeWiki && !codeWikiGenerate && shouldPersistInputHistory(parsedInput)) {
5496
+ if (!readOnlyCodeWiki && !codeWikiGenerate && shouldPersistInputHistory(parsedInput)) {
5425
5497
  await appendInputHistory(line);
5426
5498
  }
5427
5499
  } catch {
@@ -5449,7 +5521,7 @@ export async function createChatRuntime({
5449
5521
  systemPrompt: readOnlySystemPrompt,
5450
5522
  onAgentEvent,
5451
5523
  requestToolApproval: activeRequestToolApproval,
5452
- executionMode: 'auto',
5524
+ executionMode: 'normal',
5453
5525
  alwaysAllowTools: CODEWIKI_READ_ONLY_TOOLS,
5454
5526
  allowedTools: CODEWIKI_READ_ONLY_TOOLS,
5455
5527
  persistSession: false,
@@ -5488,13 +5560,13 @@ export async function createChatRuntime({
5488
5560
  text: 'Commands: /help /exit /new /stop /commands /status /model /mode /compact /checkpoint /spec /plan /yes /no /edit /reject /agents /config /memory /capture /inbox /dream /reflect /history /debug /retry /<custom> !<shell>'
5489
5561
  };
5490
5562
  }
5491
- if (parsedInput.command === 'status') {
5492
- const todoCount = countActiveTodos(currentSession.todos);
5493
- return {
5494
- type: 'system',
5495
- text: `mode=${executionMode} | role=general | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | todos=${todoCount}`
5496
- };
5497
- }
5563
+ if (parsedInput.command === 'status') {
5564
+ const todoCount = countActiveTodos(currentSession.todos);
5565
+ return {
5566
+ type: 'system',
5567
+ text: `mode=${executionMode} | approval=${config.execution?.approval_mode || 'review'} | role=general | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | todos=${todoCount}`
5568
+ };
5569
+ }
5498
5570
  if (parsedInput.command === 'model') {
5499
5571
  const sub = String(parsedInput.args[0] || 'current').trim().toLowerCase();
5500
5572
  const mainModel = resolveDefaultModel(config);
@@ -5520,21 +5592,36 @@ export async function createChatRuntime({
5520
5592
  await saveSession(currentSession);
5521
5593
  return { type: 'system', text: `Model switched to: ${model}` };
5522
5594
  }
5523
- if (parsedInput.command === 'mode') {
5524
- const next = (parsedInput.args[0] || '').trim().toLowerCase();
5525
- if (!next) {
5526
- return { type: 'system', text: `Current mode: ${executionMode} (available: normal|auto|plan)` };
5527
- }
5528
- if (!['normal', 'auto', 'plan'].includes(next)) {
5529
- return { type: 'system', text: 'Usage: /mode <normal|auto|plan>' };
5530
- }
5531
- executionMode = next;
5532
- await setConfigValue('execution.mode', next);
5533
- config = await loadConfig();
5534
- const text = `Execution mode set to: ${next}`;
5535
- await persistLocalExchange(line, text);
5536
- return { type: 'system', text };
5537
- }
5595
+ if (parsedInput.command === 'mode') {
5596
+ const next = (parsedInput.args[0] || '').trim().toLowerCase();
5597
+ if (!next) {
5598
+ return { type: 'system', text: `Current work mode: ${executionMode} (available: normal|plan)` };
5599
+ }
5600
+ if (!['normal', 'plan'].includes(next)) {
5601
+ return { type: 'system', text: 'Usage: /mode <normal|plan>' };
5602
+ }
5603
+ executionMode = next;
5604
+ await setConfigValue('execution.mode', next);
5605
+ config = await loadConfig();
5606
+ const text = `Work mode set to: ${next}`;
5607
+ await persistLocalExchange(line, text);
5608
+ return { type: 'system', text };
5609
+ }
5610
+ if (parsedInput.command === 'approval') {
5611
+ const raw = (parsedInput.args[0] || '').trim().toLowerCase().replace(/-/g, '_');
5612
+ const next = raw === 'full' ? 'full_access' : raw;
5613
+ if (!next) {
5614
+ return { type: 'system', text: `Current approval mode: ${config.execution?.approval_mode || 'review'} (available: review|auto|full_access)` };
5615
+ }
5616
+ if (!['review', 'auto', 'full_access'].includes(next)) {
5617
+ return { type: 'system', text: 'Usage: /approval <review|auto|full_access>' };
5618
+ }
5619
+ await setConfigValue('execution.approval_mode', next);
5620
+ config = await loadConfig();
5621
+ const text = `Approval mode set to: ${next}`;
5622
+ await persistLocalExchange(line, text);
5623
+ return { type: 'system', text };
5624
+ }
5538
5625
  if (parsedInput.command === 'yes') {
5539
5626
  if (hasPendingReflectSkill(currentSession)) {
5540
5627
  const state = { ...currentSession.planState };
@@ -5551,7 +5638,7 @@ export async function createChatRuntime({
5551
5638
  workspaceRoot: process.cwd()
5552
5639
  });
5553
5640
  currentSession.planState = null;
5554
- executionMode = 'auto';
5641
+ executionMode = 'normal';
5555
5642
  if (onAgentEvent) onAgentEvent({ type: 'reflect:approval_cleared' });
5556
5643
  await reloadCommandsAndSkills();
5557
5644
  const text = `Reflect skill written and loaded: /${written.draft.name}\nPath: ${written.filePath}`;
@@ -5577,7 +5664,7 @@ export async function createChatRuntime({
5577
5664
  currentSession.planState = null;
5578
5665
  if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
5579
5666
  await removePlanFileIfPresent(planState);
5580
- executionMode = 'auto';
5667
+ executionMode = 'normal';
5581
5668
  await persistAssistantExchange(line, result.sessionText || result.text || '', {
5582
5669
  includeUser: false,
5583
5670
  extra: Array.isArray(result.transcript) ? { plan_transcript: result.transcript } : {}
@@ -5652,7 +5739,7 @@ export async function createChatRuntime({
5652
5739
  if (parsedInput.command === 'no') {
5653
5740
  if (hasPendingReflectSkill(currentSession)) {
5654
5741
  currentSession.planState = null;
5655
- executionMode = 'auto';
5742
+ executionMode = 'normal';
5656
5743
  if (onAgentEvent) onAgentEvent({ type: 'reflect:approval_cleared' });
5657
5744
  const text = 'Reflect skill draft discarded.';
5658
5745
  await persistLocalExchange(line, text, { includeUser: false });
@@ -5661,7 +5748,7 @@ export async function createChatRuntime({
5661
5748
  if (hasPendingPlanApproval(currentSession)) {
5662
5749
  currentSession.planState = null;
5663
5750
  if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
5664
- executionMode = 'auto';
5751
+ executionMode = 'normal';
5665
5752
  const text = 'Pending plan rejected and cleared.';
5666
5753
  await persistLocalExchange(line, text, { includeUser: false });
5667
5754
  return { type: 'system', text };
@@ -5676,7 +5763,7 @@ export async function createChatRuntime({
5676
5763
  currentSession.planState = null;
5677
5764
  if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
5678
5765
  await removePlanFileIfPresent(planState);
5679
- executionMode = 'auto';
5766
+ executionMode = 'normal';
5680
5767
  const text = 'Pending plan rejected and cleared.';
5681
5768
  await persistLocalExchange(line, text);
5682
5769
  return { type: 'system', text };
@@ -5826,7 +5913,7 @@ export async function createChatRuntime({
5826
5913
  currentSession.planState = null;
5827
5914
  if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
5828
5915
  await removePlanFileIfPresent(planState);
5829
- executionMode = 'auto';
5916
+ executionMode = 'normal';
5830
5917
  await persistAssistantExchange(line, result.sessionText || result.text || '', {
5831
5918
  includeUser: false,
5832
5919
  extra: Array.isArray(result.transcript) ? { plan_transcript: result.transcript } : {}
@@ -6273,14 +6360,14 @@ export async function createChatRuntime({
6273
6360
  return { type: 'system', text: rows.join('\n') };
6274
6361
  }
6275
6362
 
6276
- let custom = commands.get(parsedInput.command);
6277
- if (!custom) {
6278
- await reloadCommandsAndSkills();
6279
- custom = commands.get(parsedInput.command);
6280
- if (!custom) {
6281
- return { type: 'system', text: `Unknown slash command: /${parsedInput.command}` };
6282
- }
6283
- }
6363
+ let custom = commands.get(parsedInput.command);
6364
+ if (!custom) {
6365
+ await reloadCommandsAndSkills();
6366
+ custom = commands.get(parsedInput.command);
6367
+ if (!custom) {
6368
+ return { type: 'system', text: `Unknown slash command: /${parsedInput.command}` };
6369
+ }
6370
+ }
6284
6371
  if (custom.metadata.type === 'skill' && !isSkillEnabled(config, custom.name, custom)) {
6285
6372
  return { type: 'system', text: `Skill is disabled: ${custom.name}` };
6286
6373
  }
@@ -6373,7 +6460,7 @@ export async function createChatRuntime({
6373
6460
  activeSubSession = null;
6374
6461
  currentSession.planState = null;
6375
6462
  if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
6376
- executionMode = 'auto';
6463
+ executionMode = 'normal';
6377
6464
  await persistAssistantExchange(line, result.sessionText || result.text || '', {
6378
6465
  includeUser: false,
6379
6466
  extra: Array.isArray(result.transcript) ? { plan_transcript: result.transcript } : {}
@@ -6388,7 +6475,7 @@ export async function createChatRuntime({
6388
6475
  if (isRejectPlanText(parsedInput.text)) {
6389
6476
  currentSession.planState = null;
6390
6477
  if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
6391
- executionMode = 'auto';
6478
+ executionMode = 'normal';
6392
6479
  const text = 'Pending plan rejected and cleared.';
6393
6480
  await persistLocalExchange(line, text);
6394
6481
  return { type: 'system', text };
@@ -6582,11 +6669,13 @@ export async function createChatRuntime({
6582
6669
  systemPrompt: routedSystemPrompt,
6583
6670
  onAgentEvent,
6584
6671
  requestToolApproval: activeRequestToolApproval,
6585
- executionMode,
6586
- signal,
6587
- compactedForModel,
6588
- onCompactedUpdate: setCompactedView
6589
- });
6672
+ executionMode,
6673
+ signal,
6674
+ compactedForModel,
6675
+ onCompactedUpdate: setCompactedView,
6676
+ changeTracker,
6677
+ backupManager
6678
+ });
6590
6679
  await saveDirectMemoryPrompt(expandedText);
6591
6680
  await captureUserPromptForDream(expandedText);
6592
6681
  return { type: 'assistant', text: result.text, aborted: !!result.aborted };
@@ -6606,25 +6695,39 @@ export async function createChatRuntime({
6606
6695
  },
6607
6696
  consumeStartupEvents: () => startupEvents.splice(0, startupEvents.length),
6608
6697
  getInputHistory: () => loadInputHistory(),
6609
- getCurrentSessionId: () => currentSession.id,
6610
- getSessionMessages: () => currentSession.messages || [],
6611
- getSessionCompact: () => currentSession.compact || null,
6698
+ getCurrentSessionId: () => currentSession.id,
6699
+ getSessionMessages: () => currentSession.messages || [],
6700
+ getSessionCompact: () => currentSession.compact || null,
6701
+ getChangeSets: () => listGitOplogChanges(changeTracker),
6702
+ getChangeSetPatch: (id) => readGitOplogPatch(changeTracker, id),
6703
+ undoChangeSet: (id) => undoGitOplogChange(changeTracker, id),
6612
6704
  reloadConfig: async (options = {}) => {
6613
6705
  config = await loadConfig();
6706
+ config.runtime = {
6707
+ ...(config.runtime || {}),
6708
+ project_is_git: Boolean(changeTracker?.enabled)
6709
+ };
6614
6710
  await syncRuntimeFromConfig(options);
6615
6711
  return config;
6712
+ },
6713
+ reloadCommandsAndSkills: async () => {
6714
+ await reloadCommandsAndSkills();
6715
+ return true;
6716
+ },
6717
+ setExecutionMode: async (next) => {
6718
+ if (!['normal', 'plan'].includes(next)) return false;
6719
+ executionMode = next;
6720
+ await setConfigValue('execution.mode', next);
6721
+ config = await loadConfig();
6722
+ return true;
6616
6723
  },
6617
- reloadCommandsAndSkills: async () => {
6618
- await reloadCommandsAndSkills();
6724
+ setApprovalMode: async (next) => {
6725
+ const normalized = String(next || '').toLowerCase().replace(/-/g, '_');
6726
+ if (!['review', 'auto', 'full_access'].includes(normalized)) return false;
6727
+ await setConfigValue('execution.approval_mode', normalized);
6728
+ config = await loadConfig();
6619
6729
  return true;
6620
6730
  },
6621
- setExecutionMode: async (next) => {
6622
- if (!['normal', 'auto', 'plan'].includes(next)) return false;
6623
- executionMode = next;
6624
- await setConfigValue('execution.mode', next);
6625
- config = await loadConfig();
6626
- return true;
6627
- },
6628
6731
  setRequestToolApproval: (handler) => {
6629
6732
  activeRequestToolApproval = typeof handler === 'function' ? handler : null;
6630
6733
  return true;