mixdog 0.9.60 → 0.9.61

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.60",
3
+ "version": "0.9.61",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -409,6 +409,32 @@ test('abort before a newline settles the accumulated partial response', async ()
409
409
  assert.equal(assistants[0].text, 'partial without newline');
410
410
  });
411
411
 
412
+ test('abort after tool boundaries does not replay committed progress as one giant assistant row', async () => {
413
+ const harness = makeTurnHarness(async (_text, options) => {
414
+ options.onAssistantText('first progress');
415
+ await options.onToolCall(1, [{
416
+ id: 'call_1',
417
+ name: 'shell',
418
+ input: { command: 'first' },
419
+ }]);
420
+ options.onToolResult({ tool_call_id: 'call_1', content: 'ok' });
421
+ options.onAssistantText('second progress');
422
+ await options.onToolCall(2, [{
423
+ id: 'call_2',
424
+ name: 'shell',
425
+ input: { command: 'second' },
426
+ }]);
427
+ const error = new Error('interrupted');
428
+ error.name = 'SessionClosedError';
429
+ throw error;
430
+ });
431
+
432
+ assert.equal(await harness.runTurn('go'), 'cancelled');
433
+ assert.equal(harness.getState().streamingTail, null);
434
+ const assistants = harness.getState().items.filter((item) => item.kind === 'assistant');
435
+ assert.deepEqual(assistants.map((item) => item.text), ['first progress', 'second progress']);
436
+ });
437
+
412
438
  // Codifies pre-existing parity: the old in-items completed-line snapshot survived starved recovery.
413
439
  test('starved Esc recovery settles a non-empty tail exactly once', async () => {
414
440
  let state = {
@@ -27537,9 +27537,9 @@ function createRunTurn(bag) {
27537
27537
  flushStreamBatch();
27538
27538
  if (error?.name === "SessionClosedError") {
27539
27539
  cancelled = true;
27540
- if (assistantText.trim()) {
27541
- const id = currentAssistantId || ensureAssistant(assistantText);
27542
- settleStreamingTail(id, { text: currentAssistantText || assistantText });
27540
+ if (currentAssistantText.trim()) {
27541
+ const id = currentAssistantId || ensureAssistant(currentAssistantText);
27542
+ settleStreamingTail(id, { text: currentAssistantText });
27543
27543
  }
27544
27544
  flushToolResults([], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true, cancelled: true });
27545
27545
  finalizeToolHeaders();
@@ -1278,9 +1278,13 @@ export function createRunTurn(bag) {
1278
1278
  flushStreamBatch(); // ensure any batched text lands before the error notice
1279
1279
  if (error?.name === 'SessionClosedError') {
1280
1280
  cancelled = true;
1281
- if (assistantText.trim()) {
1282
- const id = currentAssistantId || ensureAssistant(assistantText);
1283
- settleStreamingTail(id, { text: currentAssistantText || assistantText });
1281
+ // Tool boundaries already sealed prior progress segments into their
1282
+ // own rows. On abort, preserve only the still-open segment; replaying
1283
+ // turn-global assistantText creates one giant duplicate after the
1284
+ // cancelled tool card.
1285
+ if (currentAssistantText.trim()) {
1286
+ const id = currentAssistantId || ensureAssistant(currentAssistantText);
1287
+ settleStreamingTail(id, { text: currentAssistantText });
1284
1288
  }
1285
1289
  // Finalize pending tool cards so they don't stay "Running..." forever
1286
1290
  // after cancellation. Without this, the spinner vanishes and TurnDone