codemini-cli 0.6.3 → 0.6.5

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