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
@@ -47,177 +47,177 @@ function updateToolInSegments(segments, toolId, updater) {
47
47
  });
48
48
  }
49
49
 
50
- function appendTextSegment(segments, delta, isStreaming = true) {
51
- const value = String(delta || '');
52
- if (!value) return segments || [];
53
- const current = Array.isArray(segments) ? segments : [];
50
+ function appendTextSegment(segments, delta, isStreaming = true) {
51
+ const value = String(delta || '');
52
+ if (!value) return segments || [];
53
+ const current = Array.isArray(segments) ? segments : [];
54
54
  const last = current[current.length - 1];
55
55
  if (last?.type === 'text') {
56
56
  return [
57
57
  ...current.slice(0, -1),
58
58
  { ...last, text: `${last.text || ''}${value}`, isStreaming }
59
59
  ];
60
- }
61
- return [...current, { type: 'text', text: value, isStreaming }];
62
- }
63
-
64
- function replaceTextSegment(segments, text, isStreaming = false) {
65
- const value = String(text || '');
66
- const current = Array.isArray(segments) ? segments : [];
67
- const index = current.findLastIndex((seg) => seg?.type === 'text');
68
- if (index === -1) return value ? [...current, { type: 'text', text: value, isStreaming }] : current;
69
- return current.map((seg, i) => (
70
- i === index ? { ...seg, text: value, isStreaming } : seg
71
- ));
72
- }
73
-
74
- function appendThinkingSegment(segments, delta, isStreaming = true) {
75
- const value = String(delta || '');
76
- if (!value) return segments || [];
77
- const current = Array.isArray(segments) ? segments : [];
78
- const now = new Date().toISOString();
79
- const nowMs = Date.parse(now);
80
- const last = current[current.length - 1];
81
- if (last?.type === 'thinking') {
82
- const startedAt = last.startedAt || now;
83
- return [
84
- ...current.slice(0, -1),
85
- {
86
- ...last,
87
- text: `${last.text || ''}${value}`,
88
- isStreaming,
89
- startedAt,
90
- endedAt: isStreaming ? null : (last.endedAt || now),
91
- durationMs: Math.max(Number(last.durationMs || 0), nowMs - Date.parse(startedAt))
92
- }
93
- ];
94
- }
95
- return [...current, { type: 'thinking', text: value, isStreaming, startedAt: now, endedAt: isStreaming ? null : now, durationMs: isStreaming ? 0 : null }];
96
- }
97
-
98
- function resolveThinkingDurationMs(seg, endedAt) {
99
- const explicit = Number(seg?.durationMs);
100
- const startMs = Date.parse(seg?.startedAt || '');
101
- const endMs = Date.parse(seg?.endedAt || endedAt || '');
102
- const measured = Number.isFinite(startMs) && Number.isFinite(endMs) ? Math.max(0, endMs - startMs) : null;
103
- if (Number.isFinite(explicit) && measured != null) return Math.max(explicit, measured);
104
- if (Number.isFinite(explicit)) return Math.max(0, explicit);
105
- return measured;
106
- }
107
-
108
- function finishThinkingSegments(segments) {
109
- const endedAt = new Date().toISOString();
110
- return (Array.isArray(segments) ? segments : []).map((seg) => (
111
- seg.type === 'thinking'
112
- ? {
113
- ...seg,
114
- isStreaming: false,
115
- endedAt: seg.endedAt || endedAt,
116
- durationMs: resolveThinkingDurationMs(seg, endedAt)
117
- }
118
- : seg
119
- ));
120
- }
121
-
122
- function getReasoningTextFromAssistantMessage(message = {}) {
123
- if (typeof message.reasoning_content === 'string' && message.reasoning_content.trim()) {
124
- return message.reasoning_content.trim();
125
- }
126
- if (!Array.isArray(message.reasoning_details)) return '';
127
- return message.reasoning_details
128
- .map((block) => {
129
- if (!block || typeof block !== 'object') return '';
130
- if (block.type === 'thinking') return block.thinking || block.text || '';
131
- if (block.type === 'reasoning' || block.type === 'reasoning_content') return block.text || block.reasoning_content || '';
132
- if (block.type === 'redacted_thinking') return '[redacted thinking]';
133
- return '';
134
- })
135
- .filter(Boolean)
136
- .join('\n\n')
137
- .trim();
138
- }
139
-
140
- function hasThinkingSegment(segments) {
141
- return (Array.isArray(segments) ? segments : []).some((seg) => seg.type === 'thinking' && String(seg.text || '').trim());
142
- }
143
-
144
- function normalizeUiUsage(usage) {
145
- if (!usage || typeof usage !== 'object') return null;
146
- const out = {};
147
- for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'cachedInputTokens', 'cacheMissInputTokens', 'cacheWriteInputTokens', 'reasoningOutputTokens', 'requests']) {
148
- const value = Number(usage?.[key]);
149
- if (Number.isFinite(value)) out[key] = Math.max(0, Math.round(value));
150
- }
151
- return Object.keys(out).length ? out : null;
152
- }
153
-
154
- function mergeUiUsage(left, right) {
155
- const a = normalizeUiUsage(left);
156
- const b = normalizeUiUsage(right);
157
- if (!a) return b;
158
- if (!b) return a;
159
- const out = {};
160
- for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'cachedInputTokens', 'cacheMissInputTokens', 'cacheWriteInputTokens', 'reasoningOutputTokens', 'requests']) {
161
- out[key] = Math.max(0, Math.round(Number(a[key] || 0) + Number(b[key] || 0)));
162
- }
163
- return out;
164
- }
165
-
166
- function toCodeWikiGenerateProgress(event) {
167
- if (!event?.type) return null;
168
- const now = new Date().toISOString();
169
- if (event.type === 'plan:steps') {
170
- return {
171
- type: 'codewiki:generate_progress',
172
- phase: 'steps',
173
- timestamp: now,
174
- steps: (Array.isArray(event.steps) ? event.steps : []).map((step, index) => ({
175
- index: Number(step.index || index + 1),
176
- title: step.title || '',
177
- role: step.role || 'general',
178
- status: step.status || 'pending'
179
- }))
180
- };
181
- }
182
- if (event.type === 'plan:step_start') {
183
- return {
184
- type: 'codewiki:generate_progress',
185
- phase: 'step_start',
186
- timestamp: now,
187
- step: Number(event.step || 0),
188
- total: Number(event.total || 0),
189
- role: event.role || 'general',
190
- title: event.title || '',
191
- status: 'running'
192
- };
193
- }
194
- if (event.type === 'plan:step_done' || event.type === 'plan:progress') {
195
- return {
196
- type: 'codewiki:generate_progress',
197
- phase: event.type === 'plan:step_done' ? 'step_done' : 'step_progress',
198
- timestamp: now,
199
- step: Number(event.step || 0),
200
- total: Number(event.total || 0),
201
- role: event.role || 'general',
202
- title: event.title || '',
203
- status: event.status || (event.type === 'plan:step_done' ? 'done' : 'running'),
204
- summary: event.summary || ''
205
- };
206
- }
207
- if (event.type === 'skill:start' || event.type === 'skill:end' || event.type === 'skill:error') {
208
- return {
209
- type: 'codewiki:generate_progress',
210
- phase: event.type.replace('skill:', 'skill_'),
211
- timestamp: now,
212
- name: event.name || 'project-requirements',
213
- status: event.type === 'skill:error' ? 'failed' : event.type === 'skill:end' ? 'done' : 'running',
214
- summary: event.summary || ''
215
- };
216
- }
217
- return null;
218
- }
219
-
220
- function createPlanStepUiMessage(event) {
60
+ }
61
+ return [...current, { type: 'text', text: value, isStreaming }];
62
+ }
63
+
64
+ function replaceTextSegment(segments, text, isStreaming = false) {
65
+ const value = String(text || '');
66
+ const current = Array.isArray(segments) ? segments : [];
67
+ const index = current.findLastIndex((seg) => seg?.type === 'text');
68
+ if (index === -1) return value ? [...current, { type: 'text', text: value, isStreaming }] : current;
69
+ return current.map((seg, i) => (
70
+ i === index ? { ...seg, text: value, isStreaming } : seg
71
+ ));
72
+ }
73
+
74
+ function appendThinkingSegment(segments, delta, isStreaming = true) {
75
+ const value = String(delta || '');
76
+ if (!value) return segments || [];
77
+ const current = Array.isArray(segments) ? segments : [];
78
+ const now = new Date().toISOString();
79
+ const nowMs = Date.parse(now);
80
+ const last = current[current.length - 1];
81
+ if (last?.type === 'thinking') {
82
+ const startedAt = last.startedAt || now;
83
+ return [
84
+ ...current.slice(0, -1),
85
+ {
86
+ ...last,
87
+ text: `${last.text || ''}${value}`,
88
+ isStreaming,
89
+ startedAt,
90
+ endedAt: isStreaming ? null : (last.endedAt || now),
91
+ durationMs: Math.max(Number(last.durationMs || 0), nowMs - Date.parse(startedAt))
92
+ }
93
+ ];
94
+ }
95
+ return [...current, { type: 'thinking', text: value, isStreaming, startedAt: now, endedAt: isStreaming ? null : now, durationMs: isStreaming ? 0 : null }];
96
+ }
97
+
98
+ function resolveThinkingDurationMs(seg, endedAt) {
99
+ const explicit = Number(seg?.durationMs);
100
+ const startMs = Date.parse(seg?.startedAt || '');
101
+ const endMs = Date.parse(seg?.endedAt || endedAt || '');
102
+ const measured = Number.isFinite(startMs) && Number.isFinite(endMs) ? Math.max(0, endMs - startMs) : null;
103
+ if (Number.isFinite(explicit) && measured != null) return Math.max(explicit, measured);
104
+ if (Number.isFinite(explicit)) return Math.max(0, explicit);
105
+ return measured;
106
+ }
107
+
108
+ function finishThinkingSegments(segments) {
109
+ const endedAt = new Date().toISOString();
110
+ return (Array.isArray(segments) ? segments : []).map((seg) => (
111
+ seg.type === 'thinking'
112
+ ? {
113
+ ...seg,
114
+ isStreaming: false,
115
+ endedAt: seg.endedAt || endedAt,
116
+ durationMs: resolveThinkingDurationMs(seg, endedAt)
117
+ }
118
+ : seg
119
+ ));
120
+ }
121
+
122
+ function getReasoningTextFromAssistantMessage(message = {}) {
123
+ if (typeof message.reasoning_content === 'string' && message.reasoning_content.trim()) {
124
+ return message.reasoning_content.trim();
125
+ }
126
+ if (!Array.isArray(message.reasoning_details)) return '';
127
+ return message.reasoning_details
128
+ .map((block) => {
129
+ if (!block || typeof block !== 'object') return '';
130
+ if (block.type === 'thinking') return block.thinking || block.text || '';
131
+ if (block.type === 'reasoning' || block.type === 'reasoning_content') return block.text || block.reasoning_content || '';
132
+ if (block.type === 'redacted_thinking') return '[redacted thinking]';
133
+ return '';
134
+ })
135
+ .filter(Boolean)
136
+ .join('\n\n')
137
+ .trim();
138
+ }
139
+
140
+ function hasThinkingSegment(segments) {
141
+ return (Array.isArray(segments) ? segments : []).some((seg) => seg.type === 'thinking' && String(seg.text || '').trim());
142
+ }
143
+
144
+ function normalizeUiUsage(usage) {
145
+ if (!usage || typeof usage !== 'object') return null;
146
+ const out = {};
147
+ for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'cachedInputTokens', 'cacheMissInputTokens', 'cacheWriteInputTokens', 'reasoningOutputTokens', 'requests']) {
148
+ const value = Number(usage?.[key]);
149
+ if (Number.isFinite(value)) out[key] = Math.max(0, Math.round(value));
150
+ }
151
+ return Object.keys(out).length ? out : null;
152
+ }
153
+
154
+ function mergeUiUsage(left, right) {
155
+ const a = normalizeUiUsage(left);
156
+ const b = normalizeUiUsage(right);
157
+ if (!a) return b;
158
+ if (!b) return a;
159
+ const out = {};
160
+ for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'cachedInputTokens', 'cacheMissInputTokens', 'cacheWriteInputTokens', 'reasoningOutputTokens', 'requests']) {
161
+ out[key] = Math.max(0, Math.round(Number(a[key] || 0) + Number(b[key] || 0)));
162
+ }
163
+ return out;
164
+ }
165
+
166
+ function toCodeWikiGenerateProgress(event) {
167
+ if (!event?.type) return null;
168
+ const now = new Date().toISOString();
169
+ if (event.type === 'plan:steps') {
170
+ return {
171
+ type: 'codewiki:generate_progress',
172
+ phase: 'steps',
173
+ timestamp: now,
174
+ steps: (Array.isArray(event.steps) ? event.steps : []).map((step, index) => ({
175
+ index: Number(step.index || index + 1),
176
+ title: step.title || '',
177
+ role: step.role || 'general',
178
+ status: step.status || 'pending'
179
+ }))
180
+ };
181
+ }
182
+ if (event.type === 'plan:step_start') {
183
+ return {
184
+ type: 'codewiki:generate_progress',
185
+ phase: 'step_start',
186
+ timestamp: now,
187
+ step: Number(event.step || 0),
188
+ total: Number(event.total || 0),
189
+ role: event.role || 'general',
190
+ title: event.title || '',
191
+ status: 'running'
192
+ };
193
+ }
194
+ if (event.type === 'plan:step_done' || event.type === 'plan:progress') {
195
+ return {
196
+ type: 'codewiki:generate_progress',
197
+ phase: event.type === 'plan:step_done' ? 'step_done' : 'step_progress',
198
+ timestamp: now,
199
+ step: Number(event.step || 0),
200
+ total: Number(event.total || 0),
201
+ role: event.role || 'general',
202
+ title: event.title || '',
203
+ status: event.status || (event.type === 'plan:step_done' ? 'done' : 'running'),
204
+ summary: event.summary || ''
205
+ };
206
+ }
207
+ if (event.type === 'skill:start' || event.type === 'skill:end' || event.type === 'skill:error') {
208
+ return {
209
+ type: 'codewiki:generate_progress',
210
+ phase: event.type.replace('skill:', 'skill_'),
211
+ timestamp: now,
212
+ name: event.name || 'project-requirements',
213
+ status: event.type === 'skill:error' ? 'failed' : event.type === 'skill:end' ? 'done' : 'running',
214
+ summary: event.summary || ''
215
+ };
216
+ }
217
+ return null;
218
+ }
219
+
220
+ function createPlanStepUiMessage(event) {
221
221
  return {
222
222
  id: `plan-step-${event.step}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
223
223
  role: event.role || 'general',
@@ -360,81 +360,86 @@ export class RuntimeBridge {
360
360
  }
361
361
  break;
362
362
  }
363
- case 'assistant:delta': {
364
- const delta = stripPlanProgressText(event.text);
365
- if (this.#uiActiveMsgId && delta) {
366
- this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
367
- ...message,
368
- segments: appendTextSegment(finishThinkingSegments(message.segments), delta, true)
369
- }));
370
- }
371
- break;
372
- }
373
- case 'assistant:reasoning_delta': {
374
- if (this.#uiActiveMsgId && event.text) {
375
- this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
376
- ...message,
377
- segments: appendThinkingSegment(message.segments, event.text, true)
378
- }));
379
- }
380
- break;
381
- }
382
- case 'assistant:response': {
383
- const reasoningText = getReasoningTextFromAssistantMessage(event.assistantMessage);
384
- if (this.#uiActiveMsgId && reasoningText) {
385
- this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
386
- ...message,
387
- segments: hasThinkingSegment(message.segments)
388
- ? message.segments
389
- : appendThinkingSegment(message.segments, reasoningText, false)
390
- }));
391
- }
392
- if (this.#uiActiveMsgId && event.text) {
393
- const text = stripPlanProgressText(event.text);
394
- this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
395
- ...message,
396
- segments: text
397
- ? replaceTextSegment(finishThinkingSegments(message.segments), text, false)
398
- : message.segments
399
- }));
400
- }
401
- if (this.#uiActiveMsgId) {
402
- this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
403
- ...message,
404
- segments: finishThinkingSegments(message.segments),
405
- usage: mergeUiUsage(message.usage, event.usage || event.assistantMessage?.usage) || message.usage || null
406
- }));
407
- }
408
- break;
409
- }
363
+ case 'assistant:delta': {
364
+ const delta = stripPlanProgressText(event.text);
365
+ if (this.#uiActiveMsgId && delta) {
366
+ this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
367
+ ...message,
368
+ segments: appendTextSegment(finishThinkingSegments(message.segments), delta, true)
369
+ }));
370
+ }
371
+ break;
372
+ }
373
+ case 'assistant:reasoning_delta': {
374
+ if (this.#uiActiveMsgId && event.text) {
375
+ this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
376
+ ...message,
377
+ segments: appendThinkingSegment(message.segments, event.text, true)
378
+ }));
379
+ }
380
+ break;
381
+ }
382
+ case 'assistant:response': {
383
+ const reasoningText = getReasoningTextFromAssistantMessage(event.assistantMessage);
384
+ if (this.#uiActiveMsgId && reasoningText) {
385
+ this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
386
+ ...message,
387
+ segments: hasThinkingSegment(message.segments)
388
+ ? message.segments
389
+ : appendThinkingSegment(message.segments, reasoningText, false)
390
+ }));
391
+ }
392
+ if (this.#uiActiveMsgId && event.text) {
393
+ const text = stripPlanProgressText(event.text);
394
+ this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
395
+ ...message,
396
+ segments: text
397
+ ? replaceTextSegment(finishThinkingSegments(message.segments), text, false)
398
+ : message.segments
399
+ }));
400
+ }
401
+ if (this.#uiActiveMsgId) {
402
+ this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
403
+ ...message,
404
+ segments: finishThinkingSegments(message.segments),
405
+ usage: mergeUiUsage(message.usage, event.usage || event.assistantMessage?.usage) || message.usage || null
406
+ }));
407
+ }
408
+ break;
409
+ }
410
410
  case 'tool:start': {
411
- if (this.#uiActiveMsgId) {
412
- const toolCard = { id: event.id, name: event.name, arguments: event.arguments, status: 'running', durationMs: null, summary: '', result: '' };
413
- this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
414
- ...message,
415
- segments: addToolToSegments(finishThinkingSegments(message.segments), toolCard)
416
- }));
417
- }
411
+ if (this.#uiActiveMsgId) {
412
+ const toolCard = { id: event.id, name: event.name, arguments: event.arguments, status: 'running', durationMs: null, summary: '', result: '' };
413
+ this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
414
+ ...message,
415
+ segments: addToolToSegments(finishThinkingSegments(message.segments), toolCard)
416
+ }));
417
+ }
418
418
  break;
419
419
  }
420
420
  case 'tool:end': {
421
421
  if (this.#uiActiveMsgId) {
422
+ const eventChanges = Array.isArray(event.fileChanges) && event.fileChanges.length
423
+ ? event.fileChanges
424
+ : (event.fileChange?.path ? [event.fileChange] : []);
422
425
  this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
423
426
  ...message,
424
- fileChanges: event.fileChange?.path
425
- ? [...(Array.isArray(message.fileChanges) ? message.fileChanges : []), event.fileChange]
427
+ fileChanges: eventChanges.length
428
+ ? [...(Array.isArray(message.fileChanges) ? message.fileChanges : []), ...eventChanges]
426
429
  : (Array.isArray(message.fileChanges) ? message.fileChanges : []),
427
430
  segments: updateToolInSegments(message.segments, event.id, (card) => ({
428
431
  ...card,
429
432
  status: 'done',
430
433
  durationMs: event.durationMs,
431
434
  summary: event.summary || card.summary,
432
- ...(event.fileChange ? { fileChange: event.fileChange } : {})
435
+ ...(event.resultMeta ? { resultMeta: event.resultMeta } : {}),
436
+ ...(event.fileChange ? { fileChange: event.fileChange } : {}),
437
+ ...(eventChanges.length ? { fileChanges: eventChanges } : {})
433
438
  }))
434
439
  }));
435
440
  }
436
- break;
437
- }
441
+ break;
442
+ }
438
443
  case 'tool:result': {
439
444
  if (this.#uiActiveMsgId) {
440
445
  this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
@@ -548,7 +553,7 @@ export class RuntimeBridge {
548
553
  return this.#runtime.consumeStartupEvents();
549
554
  }
550
555
 
551
- handleSubmit(line, options = {}) {
556
+ handleSubmit(line, options = {}) {
552
557
  if (this.#busy) return { error: true, message: 'A request is already in progress' };
553
558
  this.#resetUiTranscriptIfSessionChanged();
554
559
  const trimmed = String(line || '').trim();
@@ -566,13 +571,13 @@ export class RuntimeBridge {
566
571
  this.#recordUiEvent(event);
567
572
  this.#broadcast(event);
568
573
  }, options).then((result) => {
569
- if (this.#uiActiveMsgId) {
570
- this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
571
- ...message,
572
- segments: finishThinkingSegments(message.segments)
573
- .map((seg) => seg.type === 'text' ? { ...seg, isStreaming: false } : seg)
574
- }));
575
- }
574
+ if (this.#uiActiveMsgId) {
575
+ this.#updateUiMessage(this.#uiActiveMsgId, (message) => ({
576
+ ...message,
577
+ segments: finishThinkingSegments(message.segments)
578
+ .map((seg) => seg.type === 'text' ? { ...seg, isStreaming: false } : seg)
579
+ }));
580
+ }
576
581
  this.#uiActiveMsgId = null;
577
582
  this.#uiPlanStepIds = new Map();
578
583
  this.#broadcast({ type: 'submit:done', result: { type: result.type, aborted: result.aborted, text: result.text } });
@@ -587,39 +592,39 @@ export class RuntimeBridge {
587
592
  this.#busy = false;
588
593
  this.#broadcastRuntimeState();
589
594
  });
590
- return { accepted: true };
591
- }
592
-
593
- handleCodeWikiGenerate(line) {
594
- if (this.#busy) return { error: true, message: 'A request is already in progress' };
595
- this.#busy = true;
596
- this.#broadcastRuntimeState();
597
- const emitProgress = (event) => {
598
- const progress = toCodeWikiGenerateProgress(event);
599
- if (progress) this.#broadcast(progress);
600
- };
601
- this.#runtime.submit(line, emitProgress, { codeWikiGenerate: true }).then((result) => {
602
- this.#broadcast({
603
- type: 'codewiki:generate_done',
604
- result: {
605
- type: result?.type || 'assistant',
606
- aborted: !!result?.aborted,
607
- text: result?.text || ''
608
- }
609
- });
610
- }).catch((err) => {
611
- this.#broadcast({
612
- type: 'codewiki:generate_error',
613
- message: err?.message || 'CodeWiki generation failed'
614
- });
615
- }).finally(() => {
616
- this.#busy = false;
617
- this.#broadcastRuntimeState();
618
- });
619
- return { accepted: true };
620
- }
621
-
622
- async handleCodeWikiAsk(line, onEvent = null) {
595
+ return { accepted: true };
596
+ }
597
+
598
+ handleCodeWikiGenerate(line) {
599
+ if (this.#busy) return { error: true, message: 'A request is already in progress' };
600
+ this.#busy = true;
601
+ this.#broadcastRuntimeState();
602
+ const emitProgress = (event) => {
603
+ const progress = toCodeWikiGenerateProgress(event);
604
+ if (progress) this.#broadcast(progress);
605
+ };
606
+ this.#runtime.submit(line, emitProgress, { codeWikiGenerate: true }).then((result) => {
607
+ this.#broadcast({
608
+ type: 'codewiki:generate_done',
609
+ result: {
610
+ type: result?.type || 'assistant',
611
+ aborted: !!result?.aborted,
612
+ text: result?.text || ''
613
+ }
614
+ });
615
+ }).catch((err) => {
616
+ this.#broadcast({
617
+ type: 'codewiki:generate_error',
618
+ message: err?.message || 'CodeWiki generation failed'
619
+ });
620
+ }).finally(() => {
621
+ this.#busy = false;
622
+ this.#broadcastRuntimeState();
623
+ });
624
+ return { accepted: true };
625
+ }
626
+
627
+ async handleCodeWikiAsk(line, onEvent = null) {
623
628
  if (this.#busy) return { error: true, message: 'A request is already in progress' };
624
629
  this.#busy = true;
625
630
  const emit = (event) => {
@@ -652,27 +657,34 @@ export class RuntimeBridge {
652
657
  return this.#runtime.abort();
653
658
  }
654
659
 
655
- async setExecutionMode(mode) {
656
- if (this.#busy) return false;
657
- const ok = await this.#runtime.setExecutionMode(mode);
658
- if (ok) this.#broadcast({ type: 'mode:changed', mode, ...this.getState() });
659
- return ok;
660
- }
661
-
662
- async reloadConfig(options = {}) {
663
- return this.#runtime.reloadConfig?.(options);
664
- }
665
-
666
- async reloadCommandsAndSkills() {
667
- const ok = await this.#runtime.reloadCommandsAndSkills?.();
668
- if (ok) this.#broadcastRuntimeState();
660
+ async setExecutionMode(mode) {
661
+ if (this.#busy) return false;
662
+ const ok = await this.#runtime.setExecutionMode(mode);
663
+ if (ok) this.#broadcast({ type: 'mode:changed', mode, ...this.getState() });
669
664
  return ok;
670
665
  }
671
666
 
672
- handleApproval(id, approved) {
673
- return this.#approval.resolve(id, approved);
667
+ async setApprovalMode(mode) {
668
+ if (this.#busy) return false;
669
+ const ok = await this.#runtime.setApprovalMode?.(mode);
670
+ if (ok) this.#broadcast({ type: 'approval-mode:changed', approvalMode: mode, ...this.getState() });
671
+ return ok;
674
672
  }
675
673
 
674
+ async reloadConfig(options = {}) {
675
+ return this.#runtime.reloadConfig?.(options);
676
+ }
677
+
678
+ async reloadCommandsAndSkills() {
679
+ const ok = await this.#runtime.reloadCommandsAndSkills?.();
680
+ if (ok) this.#broadcastRuntimeState();
681
+ return ok;
682
+ }
683
+
684
+ handleApproval(id, approved) {
685
+ return this.#approval.resolve(id, approved);
686
+ }
687
+
676
688
  getState() {
677
689
  const state = this.#runtime.getRuntimeState();
678
690
  const serializableState = typeof state?.toJSON === 'function' ? state.toJSON() : state;
@@ -691,33 +703,50 @@ export class RuntimeBridge {
691
703
  return messages
692
704
  .filter(m => m.role !== 'system')
693
705
  .map(m => ({
694
- role: m.role,
695
- content: typeof m.content === 'string' ? m.content : (Array.isArray(m.content) ? m.content.map(c => c.text || '').join('') : ''),
696
- reasoningContent: typeof m.reasoning_content === 'string' ? m.reasoning_content : '',
697
- reasoningDetails: Array.isArray(m.reasoning_details) ? m.reasoning_details : [],
698
- reasoningStartedAt: m.reasoning_started_at || null,
699
- reasoningEndedAt: m.reasoning_ended_at || null,
700
- reasoningDurationMs: Number.isFinite(Number(m.reasoning_duration_ms)) ? Number(m.reasoning_duration_ms) : null,
701
- toolCalls: m.tool_calls || [],
702
- fileChanges: Array.isArray(m.file_changes) ? m.file_changes : [],
703
- toolCallId: m.tool_call_id || null,
704
- toolSummary: m.role === 'tool' ? summarizeHistoricalToolMessage(m) : null,
706
+ role: m.role,
707
+ content: typeof m.content === 'string' ? m.content : (Array.isArray(m.content) ? m.content.map(c => c.text || '').join('') : ''),
708
+ reasoningContent: typeof m.reasoning_content === 'string' ? m.reasoning_content : '',
709
+ reasoningDetails: Array.isArray(m.reasoning_details) ? m.reasoning_details : [],
710
+ reasoningStartedAt: m.reasoning_started_at || null,
711
+ reasoningEndedAt: m.reasoning_ended_at || null,
712
+ reasoningDurationMs: Number.isFinite(Number(m.reasoning_duration_ms)) ? Number(m.reasoning_duration_ms) : null,
713
+ toolCalls: m.tool_calls || [],
714
+ fileChanges: Array.isArray(m.file_changes) ? m.file_changes : [],
715
+ toolCallId: m.tool_call_id || null,
716
+ toolSummary: m.role === 'tool' ? summarizeHistoricalToolMessage(m) : null,
705
717
  toolDurationMs: Number.isFinite(Number(m.tool_duration_ms)) ? Number(m.tool_duration_ms) : null,
706
718
  toolStatus: m.tool_status || null,
719
+ toolResultMeta: m.tool_result_meta || null,
707
720
  toolFileChange: m.tool_file_change || null,
721
+ toolFileChanges: Array.isArray(m.tool_file_changes) ? m.tool_file_changes : [],
708
722
  planTranscript: Array.isArray(m.plan_transcript) ? m.plan_transcript : null,
709
723
  usage: normalizeUiUsage(m.usage),
710
724
  at: m.at || null
711
725
  }));
712
726
  }
713
727
 
714
- getSessionCompactMeta() {
728
+ getSessionCompactMeta() {
715
729
  const compact = this.#runtime.getSessionCompact();
716
730
  if (!compact) return null;
717
731
  return { boundaryIndex: compact.boundaryIndex, mode: compact.mode, timestamp: compact.timestamp };
718
- }
719
-
720
- async getUiMessages() {
732
+ }
733
+
734
+ getChangeSets() {
735
+ return this.#runtime.getChangeSets?.() || [];
736
+ }
737
+
738
+ getChangeSetPatch(id) {
739
+ return this.#runtime.getChangeSetPatch?.(id) || '';
740
+ }
741
+
742
+ async undoChangeSet(id) {
743
+ if (this.#busy) return { error: true, message: 'A request is already in progress' };
744
+ const result = await this.#runtime.undoChangeSet?.(id);
745
+ this.#broadcast({ type: 'change:undone', result });
746
+ return result || { error: true, message: 'Git change oplog is not available' };
747
+ }
748
+
749
+ async getUiMessages() {
721
750
  this.#resetUiTranscriptIfSessionChanged();
722
751
  if (this.#uiMessages.length > 0) return this.#uiMessages;
723
752
  const sessionId = this.getSessionId();