@hifullmoon/aicommit 2.6.0 → 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,18 @@ 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
+
13
+ ## [2.6.1] - 2026-09-14
14
+
15
+ ### Fixed
16
+
17
+ - Split mode now streams thinking during each large-change planning batch, with batch progress in the spinner; interactive review still shows only the final plan's reasoning.
18
+
7
19
  ## [2.6.0] - 2026-09-14
8
20
 
9
21
  ### Added
@@ -233,7 +245,9 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
233
245
  - Added file-level split planning and execution with Git-state concurrency checks.
234
246
  - Added provider presets and user/project configuration boundaries.
235
247
 
236
- [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.0...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
250
+ [2.6.1]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.1
237
251
  [2.6.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.0
238
252
  [2.5.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.5.0
239
253
  [2.4.1]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.4.1
package/README.md CHANGED
@@ -455,7 +455,7 @@ The staged index (or complete split-mode working tree, including untracked file
455
455
 
456
456
  Reasoning defaults to `on` with `medium` effort. It is mapped natively for OpenAI reasoning models, DeepSeek, OpenRouter, and MiniMax; models that do not expose reasoning continue normally and show an unavailable notice instead of failing. Official OpenAI endpoints validate the selected effort against the model generation before sending the request, so unsupported combinations such as `o3 --no-reasoning` or `gpt-5.1 --reasoning=max` fail locally with a clear list of supported levels. DeepSeek's current `deepseek-v4-flash` and `deepseek-v4-pro` models receive `thinking: { "type": "enabled" }` plus `reasoning_effort`; `medium`/`xhigh` are normalized to DeepSeek's `high` level.
457
457
 
458
- When reasoning mode is `on` (including via `--reasoning=<level>`), aicommit requests a streaming response and displays reasoning as it arrives. In `auto` mode it also displays thinking if the provider emits it, without forcing reasoning on. Split mode streams the final planning request's thinking, including when large changes use batched analysis; the review prompt keeps that reasoning available. Intermediate analysis batches show progress rather than mixing several reasoning streams. The live panel requires an interactive terminal, and JSON output never includes reasoning. The live view follows the newest two terminal lines by default; press `Ctrl+O` to expand or collapse the accumulated text during generation or review. Long expanded output is kept inside the terminal viewport—use `PageUp`/`PageDown` to read every page. Holding `Ctrl+O` counts as one toggle, so key repeat cannot leave duplicate panels behind. Output is sanitized and capped by `reasoning.maxDisplayChars`; providers that do not expose reasoning show a short unavailable notice.
458
+ When reasoning mode is `on` (including via `--reasoning=<level>`), aicommit requests a streaming response and displays reasoning as it arrives. In `auto` mode it also displays thinking if the provider emits it, without forcing reasoning on. Split mode streams thinking from each sequential planning batch with a batch label; the review prompt keeps only the final plan's reasoning. Earlier deep-analysis chunks show progress. The live panel requires an interactive terminal, and JSON output never includes reasoning. The live view follows the newest two terminal lines by default; press `Ctrl+O` to expand or collapse the accumulated text during generation or review. Long expanded output is kept inside the terminal viewport—use `PageUp`/`PageDown` to read every page. Holding `Ctrl+O` counts as one toggle, so key repeat cannot leave duplicate panels behind. Output is sanitized and capped by `reasoning.maxDisplayChars`; providers that do not expose reasoning show a short unavailable notice.
459
459
 
460
460
  ```json
461
461
  {
package/README.zh-CN.md CHANGED
@@ -457,7 +457,7 @@ exec zsh
457
457
 
458
458
  推理默认为 `on`,强度为 `medium`。OpenAI 推理模型、DeepSeek、OpenRouter 和 MiniMax 使用各自原生映射;不暴露推理能力的模型会正常继续,并显示“不可用”提示,而不是失败。官方 OpenAI 端点会在发送请求前根据模型代际校验所选强度,因此 `o3 --no-reasoning` 或 `gpt-5.1 --reasoning=max` 等不支持的组合会在本地失败,并清楚列出支持的级别。DeepSeek 当前的 `deepseek-v4-flash` 和 `deepseek-v4-pro` 会收到 `thinking: { "type": "enabled" }` 与 `reasoning_effort`;`medium` / `xhigh` 会归一化为 DeepSeek 的 `high` 级别。
459
459
 
460
- 推理模式为 `on` 时(包括通过 `--reasoning=<level>` 启用),AICommit 会请求流式响应并实时显示推理;`auto` 模式下若 Provider 主动返回 think,也会显示,但不会强制开启推理。split 模式会显示最终规划请求的 think,大变更走分批分析时也一样,并在审阅界面保留这段内容;前面的分析批次只显示进度,避免多个推理流混在一起。实时面板需要交互式终端,JSON 输出始终不包含推理。默认实时视图跟随终端最新两行;生成或审阅期间按 `Ctrl+O` 可展开或收起累计文本。较长的展开输出会限制在终端视口内,可用 `PageUp` / `PageDown` 阅读所有页面。长按 `Ctrl+O` 只计为一次切换,避免按键重复留下多个面板。输出会经过清理,并受 `reasoning.maxDisplayChars` 限制;不暴露推理的 Provider 会显示简短的不可用提示。
460
+ 推理模式为 `on` 时(包括通过 `--reasoning=<level>` 启用),AICommit 会请求流式响应并实时显示推理;`auto` 模式下若 Provider 主动返回 think,也会显示,但不会强制开启推理。split 模式会按批次实时显示规划请求的 think,并标明当前批次;审阅界面只保留最终规划的推理。更早的深度分析分块仍只显示进度。实时面板需要交互式终端,JSON 输出始终不包含推理。默认实时视图跟随终端最新两行;生成或审阅期间按 `Ctrl+O` 可展开或收起累计文本。较长的展开输出会限制在终端视口内,可用 `PageUp` / `PageDown` 阅读所有页面。长按 `Ctrl+O` 只计为一次切换,避免按键重复留下多个面板。输出会经过清理,并受 `reasoning.maxDisplayChars` 限制;不暴露推理的 Provider 会显示简短的不可用提示。
461
461
 
462
462
  ```json
463
463
  {
@@ -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.0
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.0",
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": {
@@ -736,8 +736,29 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
736
736
  coverage.metadataOnlyFiles = coverage.totalFiles - coverage.sampledFiles;
737
737
  }
738
738
  const next = [];
739
- for (const batch of batches) {
739
+ for (const [batchIndex, batch] of batches.entries()) {
740
740
  const ids = batch.map((x) => x.id);
741
+ const finalPlan = batches.length === 1;
742
+ stream?.onProgress?.(
743
+ finalPlan
744
+ ? 'Planning final split merge ...'
745
+ : `Planning split batch ${batchIndex + 1}/${batches.length} ...`,
746
+ );
747
+ let startedThinking = false;
748
+ const batchStream = stream && {
749
+ onReasoningDelta(chunk) {
750
+ if (!startedThinking && chunk) {
751
+ startedThinking = true;
752
+ if (!finalPlan || level > 0) {
753
+ const stage = finalPlan ? 'final merge' : `batch ${batchIndex + 1}/${batches.length}`;
754
+ stream.onReasoningDelta?.(`\n\n[Planning ${stage}]\n`);
755
+ }
756
+ }
757
+ stream.onReasoningDelta?.(chunk);
758
+ },
759
+ // Only the final merge belongs in the subsequent review prompt.
760
+ onReasoningComplete: finalPlan ? (text) => stream.onReasoningComplete?.(text) : undefined,
761
+ };
741
762
  const groups = await jsonCall(
742
763
  config,
743
764
  `Each commit message must follow this policy: ${JSON.stringify(policy)}.\nGroup related changes into logical commits, including related implementation and tests across directories. Return [{"ids":[input IDs],"summary":"factual combined change summary","subject":"commit subject","body":"optional commit body"}]. Assign every input ID exactly once. Do not merge unrelated changes just to reduce group count.`,
@@ -750,9 +771,7 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
750
771
  'Analysis plan is missing a commit subject.',
751
772
  );
752
773
  },
753
- // Earlier batches may run many requests. Surface thinking from the
754
- // final plan, which is the reasoning relevant to the review screen.
755
- batches.length === 1 ? stream : null,
774
+ batchStream,
756
775
  );
757
776
  for (const group of groups) {
758
777
  next.push({
@@ -17,13 +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
  ? {
25
+ onProgress(status) {
26
+ progressStatus = status;
27
+ if (liveReasoning) liveReasoning.setProgress(status);
28
+ else if (spinner.isSpinning) spinner.text = chalk.dim(status);
29
+ },
23
30
  onReasoningDelta(chunk) {
31
+ if (!interactive) return;
24
32
  if (!liveReasoning) {
25
33
  spinner.stop();
26
- liveReasoning = startReasoningStream(reasoning.maxDisplayChars, chunk);
34
+ liveReasoning = startReasoningStream(
35
+ reasoning.maxDisplayChars,
36
+ chunk,
37
+ progressStatus,
38
+ );
27
39
  return;
28
40
  }
29
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/split.js CHANGED
@@ -1797,12 +1797,14 @@ export async function splitFlow(
1797
1797
  let planReasoning = null;
1798
1798
  const planningStream = stream
1799
1799
  ? {
1800
+ onProgress(status) {
1801
+ stream.onProgress?.(status);
1802
+ },
1800
1803
  onReasoningDelta(chunk) {
1801
- planReasoning = (planReasoning || '') + chunk;
1802
1804
  stream.onReasoningDelta(chunk);
1803
1805
  },
1804
1806
  onReasoningComplete(text) {
1805
- if (text) planReasoning = text;
1807
+ planReasoning = text || null;
1806
1808
  },
1807
1809
  }
1808
1810
  : null;
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);