@hifullmoon/aicommit 2.6.1 → 2.6.2

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.6.2] - 2026-09-14
8
+
9
+ ### Fixed
10
+
11
+ - MiniMax streaming now accepts both cumulative snapshots and ordinary deltas without duplicating thinking or corrupting split plans. The live thinking panel also updates batch progress after the spinner stops.
12
+
7
13
  ## [2.6.1] - 2026-09-14
8
14
 
9
15
  ### Fixed
@@ -239,7 +245,8 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
239
245
  - Added file-level split planning and execution with Git-state concurrency checks.
240
246
  - Added provider presets and user/project configuration boundaries.
241
247
 
242
- [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.1...HEAD
248
+ [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.2...HEAD
249
+ [2.6.2]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.2
243
250
  [2.6.1]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.1
244
251
  [2.6.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.0
245
252
  [2.5.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.5.0
@@ -33,7 +33,7 @@ The release workflow uses npm Trusted Publishing without a long-lived `NPM_TOKEN
33
33
  ```bash
34
34
  workdir=$(mktemp -d)
35
35
  cd "$workdir"
36
- npm install --package-lock-only @hifullmoon/aicommit@2.6.1
36
+ npm install --package-lock-only @hifullmoon/aicommit@2.6.2
37
37
  npm audit signatures
38
38
  ```
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hifullmoon/aicommit",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "Safe, local-first AI commit message generator for Git workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,16 +17,25 @@ export async function runModelTask({
17
17
  }) {
18
18
  const spinner = ora({ text: chalk.dim(spinnerText), color: 'cyan' }).start();
19
19
  let liveReasoning;
20
+ let progressStatus = 'Thinking…';
21
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
20
22
  const stream =
21
23
  reasoning && reasoning.mode !== 'off' && !machineOutput
22
24
  ? {
23
25
  onProgress(status) {
24
- if (spinner.isSpinning) spinner.text = chalk.dim(status);
26
+ progressStatus = status;
27
+ if (liveReasoning) liveReasoning.setProgress(status);
28
+ else if (spinner.isSpinning) spinner.text = chalk.dim(status);
25
29
  },
26
30
  onReasoningDelta(chunk) {
31
+ if (!interactive) return;
27
32
  if (!liveReasoning) {
28
33
  spinner.stop();
29
- liveReasoning = startReasoningStream(reasoning.maxDisplayChars, chunk);
34
+ liveReasoning = startReasoningStream(
35
+ reasoning.maxDisplayChars,
36
+ chunk,
37
+ progressStatus,
38
+ );
30
39
  return;
31
40
  }
32
41
  liveReasoning.append(chunk);
@@ -236,7 +236,7 @@ function transport(config, adapter, state) {
236
236
  if (
237
237
  (response.headers.get('content-type') || '').toLowerCase().includes('text/event-stream')
238
238
  )
239
- return normalizeEventStream(response);
239
+ return normalizeEventStream(response, adapter.id);
240
240
  let data;
241
241
  try {
242
242
  data = await response.json();
@@ -89,14 +89,50 @@ function reasoningDelta(delta) {
89
89
  return primary + details;
90
90
  }
91
91
 
92
- function normalizeChunk(value) {
92
+ // MiniMax may stream ordinary deltas or the cumulative snapshots shown in its
93
+ // OpenAI-compatible example. Detect a snapshot only when it extends
94
+ // the text already received; otherwise retain the chunk verbatim so repeated
95
+ // ordinary deltas are not lost.
96
+ function miniMaxIncrement(value, state) {
97
+ if (typeof value !== 'string' || !value) return value;
98
+ const prior = state.text;
99
+ if (prior && value.startsWith(prior)) {
100
+ if (value.length > prior.length) {
101
+ state.text = value;
102
+ state.cumulative = true;
103
+ return value.slice(prior.length);
104
+ }
105
+ if (state.cumulative) return '';
106
+ }
107
+ state.text += value;
108
+ state.cumulative = false;
109
+ return value;
110
+ }
111
+
112
+ function normalizeChunk(value, miniMaxChoices = null) {
93
113
  if (!value || !Array.isArray(value.choices)) return value;
94
114
  for (const choice of value.choices) {
95
115
  if (!choice || typeof choice !== 'object') continue;
96
116
  choice.finish_reason = normalizeFinishReason(choice.finish_reason);
97
117
  const delta = choice.delta ?? choice.message;
98
118
  if (!delta || typeof delta !== 'object') continue;
99
- choice.delta = { ...delta, reasoning_content: reasoningDelta(delta) };
119
+ const reasoning = reasoningDelta(delta);
120
+ if (miniMaxChoices) {
121
+ const index = choice.index ?? 0;
122
+ if (!miniMaxChoices.has(index))
123
+ miniMaxChoices.set(index, {
124
+ content: { text: '', cumulative: false },
125
+ reasoning: { text: '', cumulative: false },
126
+ });
127
+ const state = miniMaxChoices.get(index);
128
+ choice.delta = {
129
+ ...delta,
130
+ content: miniMaxIncrement(delta.content, state.content),
131
+ reasoning_content: miniMaxIncrement(reasoning, state.reasoning),
132
+ };
133
+ } else {
134
+ choice.delta = { ...delta, reasoning_content: reasoning };
135
+ }
100
136
  // Retain reasoning_details for Pi's replay metadata while also exposing all
101
137
  // its textual segments as ordinary thinking deltas, including legacy shapes.
102
138
  }
@@ -106,10 +142,11 @@ function normalizeChunk(value) {
106
142
  // eventsource-parser handles UTF-8-decoded SSE framing; this boundary adjusts
107
143
  // only vendor fields. Pi still owns model-result assembly and finish validation.
108
144
  // Web Stream piping preserves backpressure, cancellation and body read errors.
109
- export function normalizeEventStream(response) {
145
+ export function normalizeEventStream(response, provider = '') {
110
146
  if (!response.body)
111
147
  throw fail(ERROR_CATEGORIES.RESPONSE_FORMAT, 'Streaming response did not include a body.');
112
148
  let done = false;
149
+ const miniMaxChoices = provider === 'minimax' ? new Map() : null;
113
150
  const encoder = new globalThis.TextEncoder();
114
151
  const body = response.body
115
152
  .pipeThrough(new globalThis.TextDecoderStream())
@@ -132,7 +169,7 @@ export function normalizeEventStream(response) {
132
169
  { cause },
133
170
  );
134
171
  }
135
- data = JSON.stringify(normalizeChunk(value));
172
+ data = JSON.stringify(normalizeChunk(value, miniMaxChoices));
136
173
  }
137
174
  controller.enqueue(
138
175
  encoder.encode(`${event.event ? `event: ${event.event}\n` : ''}data: ${data}\n\n`),
package/src/ui.js CHANGED
@@ -169,6 +169,7 @@ function expandedLineCapacity(reservedRows) {
169
169
 
170
170
  const reasoningStreamPrompt = createPrompt((config, done) => {
171
171
  const [text, setText] = useState(config.initialText || '');
172
+ const [progressText, setProgressText] = useState(config.initialProgress || 'Thinking…');
172
173
  const [expanded, setExpanded] = useState(false);
173
174
  const [reasoningOffset, setReasoningOffset] = useState(0);
174
175
  const [status, setStatus] = useState('streaming');
@@ -178,6 +179,7 @@ const reasoningStreamPrompt = createPrompt((config, done) => {
178
179
  const maxOffset = Math.max(0, completeView.totalLines - pageSize);
179
180
 
180
181
  config.controller.update = setText;
182
+ config.controller.updateProgress = setProgressText;
181
183
  config.controller.finish = () => {
182
184
  setStatus('done');
183
185
  done(text);
@@ -220,14 +222,18 @@ const reasoningStreamPrompt = createPrompt((config, done) => {
220
222
  maxExpandedLines: pageSize,
221
223
  offset: reasoningOffset,
222
224
  });
223
- return `${panel}\n${chalk.dim(' Thinking… Ctrl+C cancel')}\x1B[?25l`;
225
+ return `${panel}\n${chalk.dim(` ${sanitizeTerminalText(progressText)} Ctrl+C cancel`)}\x1B[?25l`;
224
226
  });
225
227
 
226
228
  // Start a redraw-safe live reasoning view. Updates are batched to one frame
227
229
  // every ~32ms so token-sized SSE chunks do not cause hundreds of terminal
228
230
  // redraws per second. The complete text remains available to the subsequent
229
231
  // review prompt after this temporary view is cleared.
230
- export function startReasoningStream(maxChars = 12000, initialText = '') {
232
+ export function startReasoningStream(
233
+ maxChars = 12000,
234
+ initialText = '',
235
+ initialProgress = 'Thinking…',
236
+ ) {
231
237
  const controller = {};
232
238
  let accumulated = String(initialText || '');
233
239
  let timer = null;
@@ -238,6 +244,7 @@ export function startReasoningStream(maxChars = 12000, initialText = '') {
238
244
  append(chunk) {
239
245
  accumulated += String(chunk || '');
240
246
  },
247
+ setProgress() {},
241
248
  async stop() {
242
249
  stopped = true;
243
250
  return accumulated;
@@ -246,7 +253,7 @@ export function startReasoningStream(maxChars = 12000, initialText = '') {
246
253
  }
247
254
 
248
255
  const prompt = reasoningStreamPrompt(
249
- { controller, maxChars, initialText: accumulated },
256
+ { controller, maxChars, initialText: accumulated, initialProgress },
250
257
  { clearPromptOnDone: true },
251
258
  );
252
259
 
@@ -256,6 +263,9 @@ export function startReasoningStream(maxChars = 12000, initialText = '') {
256
263
  };
257
264
 
258
265
  return {
266
+ setProgress(status) {
267
+ if (!stopped) controller.updateProgress?.(status);
268
+ },
259
269
  append(chunk) {
260
270
  if (stopped || !chunk) return;
261
271
  accumulated += String(chunk);