@machina.ai/cell-cli 1.64.0-rc2 → 1.66.0-rc1

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 (194) hide show
  1. package/dist/index.js +14 -9
  2. package/dist/index.js.map +1 -1
  3. package/dist/package.json +4 -4
  4. package/dist/src/acp/acpSession.js +9 -13
  5. package/dist/src/acp/acpSession.js.map +1 -1
  6. package/dist/src/acp/acpSession.test.js +88 -0
  7. package/dist/src/acp/acpSession.test.js.map +1 -1
  8. package/dist/src/commands/mcp/add.js +14 -6
  9. package/dist/src/commands/mcp/add.js.map +1 -1
  10. package/dist/src/commands/mcp/add.test.js +61 -0
  11. package/dist/src/commands/mcp/add.test.js.map +1 -1
  12. package/dist/src/commands/mcp/remove.js +14 -1
  13. package/dist/src/commands/mcp/remove.js.map +1 -1
  14. package/dist/src/commands/mcp/remove.test.js +63 -0
  15. package/dist/src/commands/mcp/remove.test.js.map +1 -1
  16. package/dist/src/config/config.js +12 -0
  17. package/dist/src/config/config.js.map +1 -1
  18. package/dist/src/config/config.test.js +17 -0
  19. package/dist/src/config/config.test.js.map +1 -1
  20. package/dist/src/config/envDefaults.d.ts +20 -0
  21. package/dist/src/config/envDefaults.js +29 -0
  22. package/dist/src/config/envDefaults.js.map +1 -0
  23. package/dist/src/config/envDefaults.test.d.ts +6 -0
  24. package/dist/src/config/envDefaults.test.js +23 -0
  25. package/dist/src/config/envDefaults.test.js.map +1 -0
  26. package/dist/src/config/extension-manager.js +13 -4
  27. package/dist/src/config/extension-manager.js.map +1 -1
  28. package/dist/src/config/extension.test.js +117 -0
  29. package/dist/src/config/extension.test.js.map +1 -1
  30. package/dist/src/config/extensions/github.test.js +34 -0
  31. package/dist/src/config/extensions/github.test.js.map +1 -1
  32. package/dist/src/config/extensions/github_fetch.js +110 -18
  33. package/dist/src/config/extensions/github_fetch.js.map +1 -1
  34. package/dist/src/config/extensions/github_fetch.test.js +251 -36
  35. package/dist/src/config/extensions/github_fetch.test.js.map +1 -1
  36. package/dist/src/config/settings.d.ts +1 -0
  37. package/dist/src/config/settings.js +148 -58
  38. package/dist/src/config/settings.js.map +1 -1
  39. package/dist/src/config/settings.test.js +300 -0
  40. package/dist/src/config/settings.test.js.map +1 -1
  41. package/dist/src/config/settingsSchema.d.ts +1 -1
  42. package/dist/src/config/settingsSchema.js +6 -1
  43. package/dist/src/config/settingsSchema.js.map +1 -1
  44. package/dist/src/gemini.test.js +69 -0
  45. package/dist/src/gemini.test.js.map +1 -1
  46. package/dist/src/generated/git-commit.d.ts +2 -2
  47. package/dist/src/generated/git-commit.js +2 -2
  48. package/dist/src/interactiveCli.js +8 -3
  49. package/dist/src/interactiveCli.js.map +1 -1
  50. package/dist/src/services/CommandService.js +1 -1
  51. package/dist/src/services/CommandService.js.map +1 -1
  52. package/dist/src/services/CommandService.test.js +18 -1
  53. package/dist/src/services/CommandService.test.js.map +1 -1
  54. package/dist/src/services/SlashCommandConflictHandler.d.ts +1 -0
  55. package/dist/src/services/SlashCommandConflictHandler.js +18 -2
  56. package/dist/src/services/SlashCommandConflictHandler.js.map +1 -1
  57. package/dist/src/services/SlashCommandConflictHandler.test.js +85 -8
  58. package/dist/src/services/SlashCommandConflictHandler.test.js.map +1 -1
  59. package/dist/src/ui/AppContainer.js +254 -12
  60. package/dist/src/ui/AppContainer.js.map +1 -1
  61. package/dist/src/ui/AppContainer.test.js +362 -6
  62. package/dist/src/ui/AppContainer.test.js.map +1 -1
  63. package/dist/src/ui/auth/AuthDialog.js +21 -5
  64. package/dist/src/ui/auth/AuthDialog.js.map +1 -1
  65. package/dist/src/ui/auth/AuthDialog.test.js +83 -0
  66. package/dist/src/ui/auth/AuthDialog.test.js.map +1 -1
  67. package/dist/src/ui/auth/useAuth.js +7 -1
  68. package/dist/src/ui/auth/useAuth.js.map +1 -1
  69. package/dist/src/ui/auth/useAuth.test.js +9 -0
  70. package/dist/src/ui/auth/useAuth.test.js.map +1 -1
  71. package/dist/src/ui/commands/marketplaceCommand.js +92 -4
  72. package/dist/src/ui/commands/marketplaceCommand.js.map +1 -1
  73. package/dist/src/ui/commands/marketplaceCommand.test.js +98 -1
  74. package/dist/src/ui/commands/marketplaceCommand.test.js.map +1 -1
  75. package/dist/src/ui/components/AsciiArt.d.ts +6 -6
  76. package/dist/src/ui/components/AsciiArt.js +6 -6
  77. package/dist/src/ui/components/CliSpinner.d.ts +3 -2
  78. package/dist/src/ui/components/CliSpinner.js +27 -3
  79. package/dist/src/ui/components/CliSpinner.js.map +1 -1
  80. package/dist/src/ui/components/CliSpinner.test.js +42 -3
  81. package/dist/src/ui/components/CliSpinner.test.js.map +1 -1
  82. package/dist/src/ui/components/Composer.js +3 -3
  83. package/dist/src/ui/components/Composer.js.map +1 -1
  84. package/dist/src/ui/components/Composer.test.js +39 -1
  85. package/dist/src/ui/components/Composer.test.js.map +1 -1
  86. package/dist/src/ui/components/DetailedMessagesDisplay.js +5 -1
  87. package/dist/src/ui/components/DetailedMessagesDisplay.js.map +1 -1
  88. package/dist/src/ui/components/DetailedMessagesDisplay.test.js +19 -1
  89. package/dist/src/ui/components/DetailedMessagesDisplay.test.js.map +1 -1
  90. package/dist/src/ui/components/Footer.js +1 -1
  91. package/dist/src/ui/components/Footer.js.map +1 -1
  92. package/dist/src/ui/components/GeminiRespondingSpinner.d.ts +1 -1
  93. package/dist/src/ui/components/GeminiRespondingSpinner.js +9 -2
  94. package/dist/src/ui/components/GeminiRespondingSpinner.js.map +1 -1
  95. package/dist/src/ui/components/GeminiSpinner.d.ts +1 -1
  96. package/dist/src/ui/components/GeminiSpinner.js +18 -7
  97. package/dist/src/ui/components/GeminiSpinner.js.map +1 -1
  98. package/dist/src/ui/components/HistoryItemDisplay.js +5 -2
  99. package/dist/src/ui/components/HistoryItemDisplay.js.map +1 -1
  100. package/dist/src/ui/components/HistoryItemDisplay.test.js +21 -0
  101. package/dist/src/ui/components/HistoryItemDisplay.test.js.map +1 -1
  102. package/dist/src/ui/components/LoadingIndicator.d.ts +1 -1
  103. package/dist/src/ui/components/LoadingIndicator.js +4 -2
  104. package/dist/src/ui/components/LoadingIndicator.js.map +1 -1
  105. package/dist/src/ui/components/MainContent.js +47 -9
  106. package/dist/src/ui/components/MainContent.js.map +1 -1
  107. package/dist/src/ui/components/MainContent.test.js +87 -1
  108. package/dist/src/ui/components/MainContent.test.js.map +1 -1
  109. package/dist/src/ui/components/MemoryUsageDisplay.js +8 -4
  110. package/dist/src/ui/components/MemoryUsageDisplay.js.map +1 -1
  111. package/dist/src/ui/components/ModelDialog.js +30 -16
  112. package/dist/src/ui/components/ModelDialog.js.map +1 -1
  113. package/dist/src/ui/components/ModelDialog.test.js +32 -24
  114. package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
  115. package/dist/src/ui/components/StatusRow.d.ts +7 -6
  116. package/dist/src/ui/components/StatusRow.js +24 -10
  117. package/dist/src/ui/components/StatusRow.js.map +1 -1
  118. package/dist/src/ui/components/ValidationDialog.js +13 -6
  119. package/dist/src/ui/components/ValidationDialog.js.map +1 -1
  120. package/dist/src/ui/components/ValidationDialog.test.js +40 -0
  121. package/dist/src/ui/components/ValidationDialog.test.js.map +1 -1
  122. package/dist/src/ui/components/messages/DenseToolMessage.js +17 -12
  123. package/dist/src/ui/components/messages/DenseToolMessage.js.map +1 -1
  124. package/dist/src/ui/components/messages/DenseToolMessage.test.js +71 -0
  125. package/dist/src/ui/components/messages/DenseToolMessage.test.js.map +1 -1
  126. package/dist/src/ui/components/messages/ToolResultDisplay.js +14 -11
  127. package/dist/src/ui/components/messages/ToolResultDisplay.js.map +1 -1
  128. package/dist/src/ui/components/messages/ToolResultDisplay.test.js +32 -0
  129. package/dist/src/ui/components/messages/ToolResultDisplay.test.js.map +1 -1
  130. package/dist/src/ui/components/shared/MaxSizedBox.js +8 -2
  131. package/dist/src/ui/components/shared/MaxSizedBox.js.map +1 -1
  132. package/dist/src/ui/components/shared/MaxSizedBox.test.js +23 -1
  133. package/dist/src/ui/components/shared/MaxSizedBox.test.js.map +1 -1
  134. package/dist/src/ui/contexts/ToolActionsContext.js +7 -1
  135. package/dist/src/ui/contexts/ToolActionsContext.js.map +1 -1
  136. package/dist/src/ui/contexts/ToolActionsContext.test.js +54 -1
  137. package/dist/src/ui/contexts/ToolActionsContext.test.js.map +1 -1
  138. package/dist/src/ui/hooks/atCommandProcessor.d.ts +2 -1
  139. package/dist/src/ui/hooks/atCommandProcessor.js +15 -15
  140. package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
  141. package/dist/src/ui/hooks/useAgentStream.d.ts +9 -2
  142. package/dist/src/ui/hooks/useAgentStream.js +173 -8
  143. package/dist/src/ui/hooks/useAgentStream.js.map +1 -1
  144. package/dist/src/ui/hooks/useAgentStream.test.js +104 -1
  145. package/dist/src/ui/hooks/useAgentStream.test.js.map +1 -1
  146. package/dist/src/ui/hooks/useGeminiStream.d.ts +1 -1
  147. package/dist/src/ui/hooks/useGeminiStream.js +174 -59
  148. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  149. package/dist/src/ui/hooks/useGeminiStream.test.js +230 -3
  150. package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
  151. package/dist/src/ui/hooks/useLoadingIndicator.d.ts +2 -1
  152. package/dist/src/ui/hooks/useLoadingIndicator.js +3 -3
  153. package/dist/src/ui/hooks/useLoadingIndicator.js.map +1 -1
  154. package/dist/src/ui/hooks/useLoadingIndicator.test.js +23 -4
  155. package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
  156. package/dist/src/ui/hooks/useReverseSearchCompletion.js +10 -6
  157. package/dist/src/ui/hooks/useReverseSearchCompletion.js.map +1 -1
  158. package/dist/src/ui/hooks/useReverseSearchCompletion.test.js +49 -0
  159. package/dist/src/ui/hooks/useReverseSearchCompletion.test.js.map +1 -1
  160. package/dist/src/ui/key/keyBindings.js +19 -6
  161. package/dist/src/ui/key/keyBindings.js.map +1 -1
  162. package/dist/src/ui/key/keyBindings.test.js +106 -0
  163. package/dist/src/ui/key/keyBindings.test.js.map +1 -1
  164. package/dist/src/ui/utils/formatters.d.ts +2 -0
  165. package/dist/src/ui/utils/formatters.js +13 -5
  166. package/dist/src/ui/utils/formatters.js.map +1 -1
  167. package/dist/src/ui/utils/formatters.test.js +18 -0
  168. package/dist/src/ui/utils/formatters.test.js.map +1 -1
  169. package/dist/src/utils/activityLogger.js +109 -33
  170. package/dist/src/utils/activityLogger.js.map +1 -1
  171. package/dist/src/utils/activityLogger.test.js +441 -0
  172. package/dist/src/utils/activityLogger.test.js.map +1 -1
  173. package/dist/src/utils/handleAutoUpdate.d.ts +4 -1
  174. package/dist/src/utils/handleAutoUpdate.js +45 -3
  175. package/dist/src/utils/handleAutoUpdate.js.map +1 -1
  176. package/dist/src/utils/handleAutoUpdate.test.js +280 -2
  177. package/dist/src/utils/handleAutoUpdate.test.js.map +1 -1
  178. package/dist/src/utils/nodeVersion.d.ts +11 -0
  179. package/dist/src/utils/nodeVersion.js +42 -0
  180. package/dist/src/utils/nodeVersion.js.map +1 -0
  181. package/dist/src/utils/nodeVersion.test.d.ts +6 -0
  182. package/dist/src/utils/nodeVersion.test.js +35 -0
  183. package/dist/src/utils/nodeVersion.test.js.map +1 -0
  184. package/dist/src/utils/processUtils.d.ts +23 -0
  185. package/dist/src/utils/processUtils.js +72 -0
  186. package/dist/src/utils/processUtils.js.map +1 -1
  187. package/dist/src/utils/processUtils.test.js +95 -1
  188. package/dist/src/utils/processUtils.test.js.map +1 -1
  189. package/dist/src/utils/relaunch.js +8 -6
  190. package/dist/src/utils/relaunch.js.map +1 -1
  191. package/dist/src/utils/relaunch.test.js +107 -1
  192. package/dist/src/utils/relaunch.test.js.map +1 -1
  193. package/dist/tsconfig.tsbuildinfo +1 -1
  194. package/package.json +4 -4
@@ -170,6 +170,7 @@ vi.mock('../utils/events.js');
170
170
  vi.mock('../utils/handleAutoUpdate.js');
171
171
  vi.mock('./utils/ConsolePatcher.js');
172
172
  vi.mock('../utils/cleanup.js');
173
+ import { appEvents, AppEvent } from '../utils/events.js';
173
174
  import { useHistory } from './hooks/useHistoryManager.js';
174
175
  import { useThemeCommand } from './hooks/useThemeCommand.js';
175
176
  import { useAuthCommand } from './auth/useAuth.js';
@@ -196,9 +197,16 @@ import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
196
197
  import { useKeypress } from './hooks/useKeypress.js';
197
198
  import * as useKeypressModule from './hooks/useKeypress.js';
198
199
  import { useSuspend } from './hooks/useSuspend.js';
200
+ import { useTerminalSize } from './hooks/useTerminalSize.js';
199
201
  import { writeToStdout, enableMouseEvents, disableMouseEvents, } from '@google/gemini-cli-core';
200
202
  import {} from '../config/extension-manager.js';
201
203
  import { WARNING_PROMPT_DURATION_MS, EXPAND_HINT_DURATION_MS, } from './constants.js';
204
+ // In the default inline mode, refreshStatic must only clear the visible
205
+ // viewport. It must never emit `\x1b[3J` (Erase Saved Lines), which purges the
206
+ // terminal's native scrollback history.
207
+ const CLEAR_VIEWPORT = ansiEscapes.eraseScreen + ansiEscapes.cursorTo(0, 0);
208
+ const ERASE_SCROLLBACK = '\x1b[3J';
209
+ const getScrollbackPurgingWrites = () => mocks.mockStdout.write.mock.calls.filter((call) => String(call[0]).includes(ERASE_SCROLLBACK));
202
210
  describe('AppContainer State Management', () => {
203
211
  let mockConfig;
204
212
  let mockSettings;
@@ -238,6 +246,7 @@ describe('AppContainer State Management', () => {
238
246
  const mockedUseTerminalTheme = useTerminalTheme;
239
247
  const mockedUseShellInactivityStatus = useShellInactivityStatus;
240
248
  const mockedUseFocusState = useFocus;
249
+ const mockedUseTerminalSize = useTerminalSize;
241
250
  const DEFAULT_GEMINI_STREAM_MOCK = {
242
251
  streamingState: 'idle',
243
252
  submitQuery: vi.fn(),
@@ -371,6 +380,10 @@ describe('AppContainer State Management', () => {
371
380
  mockedUseSuspend.mockReturnValue({
372
381
  handleSuspend: vi.fn(),
373
382
  });
383
+ mockedUseTerminalSize.mockReturnValue({
384
+ columns: 80,
385
+ rows: 24,
386
+ });
374
387
  mockedUseHookDisplayState.mockReturnValue([]);
375
388
  mockedUseTerminalTheme.mockReturnValue(undefined);
376
389
  mockedUseShellInactivityStatus.mockReturnValue({
@@ -1700,6 +1713,51 @@ describe('AppContainer State Management', () => {
1700
1713
  expect(mockHandleSlashCommand).toHaveBeenCalledWith('/quit', undefined, undefined, false);
1701
1714
  unmount();
1702
1715
  });
1716
+ it('should await in-flight cancelOngoingRequest promise before quitting on rapid double press', async () => {
1717
+ let resolveCancel;
1718
+ const cancelPromise = new Promise((resolve) => {
1719
+ resolveCancel = resolve;
1720
+ });
1721
+ mockCancelOngoingRequest.mockReturnValue(cancelPromise);
1722
+ await setupKeypressTest();
1723
+ pressKey('\x03', 2); // Rapid double Ctrl+C
1724
+ expect(mockCancelOngoingRequest).toHaveBeenCalledTimes(2);
1725
+ // Should not have called /quit yet while cancelPromise is still pending
1726
+ expect(mockHandleSlashCommand).not.toHaveBeenCalled();
1727
+ await act(async () => {
1728
+ resolveCancel();
1729
+ await cancelPromise;
1730
+ });
1731
+ expect(mockHandleSlashCommand).toHaveBeenCalledWith('/quit', undefined, undefined, false);
1732
+ unmount();
1733
+ });
1734
+ it('should bound waiting on a hanging cancelOngoingRequest to 1s via Promise.race on rapid double press', async () => {
1735
+ mockCancelOngoingRequest.mockReturnValue(new Promise(() => { }));
1736
+ await setupKeypressTest();
1737
+ pressKey('\x03', 2); // Rapid double Ctrl+C
1738
+ expect(mockHandleSlashCommand).not.toHaveBeenCalled();
1739
+ await act(async () => {
1740
+ await vi.advanceTimersByTimeAsync(1000);
1741
+ });
1742
+ expect(mockHandleSlashCommand).toHaveBeenCalledWith('/quit', undefined, undefined, false);
1743
+ unmount();
1744
+ });
1745
+ it('should safely handle rejected cancelOngoingRequest promise and still quit on rapid double press', async () => {
1746
+ let rejectCancel;
1747
+ const cancelPromise = new Promise((_, reject) => {
1748
+ rejectCancel = reject;
1749
+ });
1750
+ mockCancelOngoingRequest.mockReturnValue(cancelPromise);
1751
+ await setupKeypressTest();
1752
+ pressKey('\x03', 2); // Rapid double Ctrl+C
1753
+ expect(mockHandleSlashCommand).not.toHaveBeenCalled();
1754
+ await act(async () => {
1755
+ rejectCancel(new Error('Cancel failed'));
1756
+ await cancelPromise.catch(() => { });
1757
+ });
1758
+ expect(mockHandleSlashCommand).toHaveBeenCalledWith('/quit', undefined, undefined, false);
1759
+ unmount();
1760
+ });
1703
1761
  it('should reset press count after a timeout', async () => {
1704
1762
  await setupKeypressTest();
1705
1763
  pressKey('\x03'); // Ctrl+C
@@ -2472,9 +2530,209 @@ describe('AppContainer State Management', () => {
2472
2530
  await act(async () => {
2473
2531
  await new Promise((resolve) => setTimeout(resolve, 100));
2474
2532
  });
2475
- // Check that clearTerminal was NOT written to stdout
2476
- const clearTerminalCalls = mocks.mockStdout.write.mock.calls.filter((call) => call[0] === ansiEscapes.clearTerminal);
2477
- expect(clearTerminalCalls).toHaveLength(0);
2533
+ // Check that refreshStatic did NOT clear the viewport
2534
+ const clearViewportCalls = mocks.mockStdout.write.mock.calls.filter((call) => call[0] === CLEAR_VIEWPORT);
2535
+ expect(clearViewportCalls).toHaveLength(0);
2536
+ unmount();
2537
+ });
2538
+ it('keeps constrainHeight true when OpenDebugConsole event is triggered', async () => {
2539
+ const { unmount } = await act(async () => renderAppContainer());
2540
+ expect(capturedUIState).toBeTruthy();
2541
+ expect(capturedUIState.showErrorDetails).toBe(false);
2542
+ expect(capturedUIState.constrainHeight).toBe(true);
2543
+ const openDebugConsoleHandler = vi
2544
+ .mocked(appEvents.on)
2545
+ .mock.calls.find((call) => call[0] === AppEvent.OpenDebugConsole)?.[1];
2546
+ expect(openDebugConsoleHandler).toBeDefined();
2547
+ act(() => {
2548
+ openDebugConsoleHandler();
2549
+ });
2550
+ expect(capturedUIState.showErrorDetails).toBe(true);
2551
+ expect(capturedUIState.constrainHeight).toBe(true);
2552
+ unmount();
2553
+ });
2554
+ it('debounces refreshStatic when terminal width changes in default inline mode, and ignores height-only changes', async () => {
2555
+ vi.useFakeTimers();
2556
+ try {
2557
+ vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);
2558
+ vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(false);
2559
+ const { rerender, unmount } = await act(async () => renderAppContainer());
2560
+ expect(capturedUIState).toBeTruthy();
2561
+ const initialRemountKey = capturedUIState.historyRemountKey;
2562
+ mocks.mockStdout.write.mockClear();
2563
+ // Initial mount should not trigger refreshStatic after 100ms
2564
+ act(() => {
2565
+ vi.advanceTimersByTime(100);
2566
+ });
2567
+ expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(CLEAR_VIEWPORT);
2568
+ expect(capturedUIState.historyRemountKey).toBe(initialRemountKey);
2569
+ // Simulate height-only resize (e.g. mobile virtual keyboard toggle)
2570
+ mockedUseTerminalSize.mockReturnValue({ columns: 80, rows: 16 });
2571
+ await act(async () => {
2572
+ rerender(getAppContainer());
2573
+ });
2574
+ act(() => {
2575
+ vi.advanceTimersByTime(100);
2576
+ });
2577
+ expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(CLEAR_VIEWPORT);
2578
+ expect(capturedUIState.historyRemountKey).toBe(initialRemountKey);
2579
+ // Simulate rapid consecutive horizontal resizes (columns: 80 -> 70 -> 60)
2580
+ mockedUseTerminalSize.mockReturnValue({ columns: 70, rows: 16 });
2581
+ await act(async () => {
2582
+ rerender(getAppContainer());
2583
+ });
2584
+ act(() => {
2585
+ vi.advanceTimersByTime(60);
2586
+ });
2587
+ expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(CLEAR_VIEWPORT);
2588
+ // Second resize before 100ms elapses should reset the debounce timer
2589
+ mockedUseTerminalSize.mockReturnValue({ columns: 60, rows: 16 });
2590
+ await act(async () => {
2591
+ rerender(getAppContainer());
2592
+ });
2593
+ act(() => {
2594
+ vi.advanceTimersByTime(60);
2595
+ });
2596
+ expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(CLEAR_VIEWPORT);
2597
+ expect(capturedUIState.historyRemountKey).toBe(initialRemountKey);
2598
+ // Remaining 40ms completes the 100ms debounce from the last width change
2599
+ act(() => {
2600
+ vi.advanceTimersByTime(40);
2601
+ });
2602
+ expect(mocks.mockStdout.write).toHaveBeenCalledWith(CLEAR_VIEWPORT);
2603
+ expect(capturedUIState.historyRemountKey).toBe(initialRemountKey + 1);
2604
+ unmount();
2605
+ }
2606
+ finally {
2607
+ vi.useRealTimers();
2608
+ }
2609
+ });
2610
+ it('corks stdout during horizontal resize, discards intermediate buffered writes, and wraps refreshStatic in synchronized output', async () => {
2611
+ vi.useFakeTimers();
2612
+ const mockStdoutWithCork = mocks.mockStdout;
2613
+ let resizeListener;
2614
+ const bufferedCallback = vi.fn();
2615
+ const writableState = {
2616
+ corked: 0,
2617
+ length: 0,
2618
+ writelen: 0,
2619
+ buffered: [],
2620
+ bufferedIndex: 0,
2621
+ };
2622
+ mockStdoutWithCork.columns = 80;
2623
+ mockStdoutWithCork._writableState = writableState;
2624
+ mockStdoutWithCork.cork = vi.fn(() => {
2625
+ writableState.corked += 1;
2626
+ });
2627
+ mockStdoutWithCork.uncork = vi.fn(() => {
2628
+ writableState.corked = Math.max(0, writableState.corked - 1);
2629
+ });
2630
+ mockStdoutWithCork.prependListener = vi.fn((event, cb) => {
2631
+ if (event === 'resize') {
2632
+ resizeListener = cb;
2633
+ }
2634
+ });
2635
+ mockStdoutWithCork.off = vi.fn();
2636
+ try {
2637
+ vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);
2638
+ vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(false);
2639
+ const { rerender, unmount } = await act(async () => renderAppContainer());
2640
+ expect(resizeListener).toBeTypeOf('function');
2641
+ mocks.mockStdout.write.mockClear();
2642
+ mockStdoutWithCork.cork.mockClear();
2643
+ mockStdoutWithCork.uncork.mockClear();
2644
+ // Simulate horizontal resize event before Ink.resized runs
2645
+ mockStdoutWithCork.columns = 60;
2646
+ resizeListener();
2647
+ expect(mockStdoutWithCork.cork).toHaveBeenCalledTimes(1);
2648
+ expect(writableState.corked).toBe(1);
2649
+ // Simulate intermediate buffered frame written while corked during resize drag
2650
+ // alongside an active in-flight write at index 0 (bufferedIndex = 1, writelen = 10)
2651
+ const inFlightCallback = vi.fn();
2652
+ const discardedChunk = 'intermediate frame';
2653
+ writableState.buffered.push({
2654
+ chunk: 'in-flight',
2655
+ encoding: 'utf8',
2656
+ callback: inFlightCallback,
2657
+ }, {
2658
+ chunk: discardedChunk,
2659
+ encoding: 'utf8',
2660
+ callback: bufferedCallback,
2661
+ });
2662
+ writableState.bufferedIndex = 1;
2663
+ writableState.writelen = 10;
2664
+ writableState.length = 10 + Buffer.byteLength(discardedChunk, 'utf8');
2665
+ mockedUseTerminalSize.mockReturnValue({ columns: 60, rows: 24 });
2666
+ await act(async () => {
2667
+ rerender(getAppContainer());
2668
+ });
2669
+ // Advance 80ms (timer for 60 cols has 20ms left), then emit a new raw
2670
+ // stdout resize event (columns = 50) BEFORE React commits the new size.
2671
+ act(() => {
2672
+ vi.advanceTimersByTime(80);
2673
+ });
2674
+ mockStdoutWithCork.columns = 50;
2675
+ resizeListener();
2676
+ // Advancing past the original 100ms mark (20ms more) must NOT flush or
2677
+ // uncork stdout for the stale 60-col width.
2678
+ act(() => {
2679
+ vi.advanceTimersByTime(30);
2680
+ });
2681
+ expect(mocks.mockStdout.write).not.toHaveBeenCalled();
2682
+ expect(mockStdoutWithCork.uncork).not.toHaveBeenCalled();
2683
+ expect(writableState.corked).toBe(1);
2684
+ // Now React commits the 50-col width and settles for 100ms
2685
+ mockedUseTerminalSize.mockReturnValue({ columns: 50, rows: 24 });
2686
+ await act(async () => {
2687
+ rerender(getAppContainer());
2688
+ });
2689
+ act(() => {
2690
+ vi.advanceTimersByTime(100);
2691
+ });
2692
+ // Intermediate buffered frame should have been discarded before the viewport clear,
2693
+ // truncating buffered to startIndex (1) and preserving in-flight write state
2694
+ expect(inFlightCallback).not.toHaveBeenCalled();
2695
+ expect(bufferedCallback).toHaveBeenCalledWith(null);
2696
+ expect(writableState.buffered).toHaveLength(1);
2697
+ expect(writableState.bufferedIndex).toBe(1);
2698
+ expect(writableState.length).toBe(10);
2699
+ expect(writableState.writelen).toBe(10);
2700
+ // The viewport clear should be wrapped in DEC Synchronized Output (\x1b[?2026h ... \x1b[?2026l) and uncorked in useLayoutEffect
2701
+ expect(mocks.mockStdout.write).toHaveBeenNthCalledWith(1, '\x1b[?2026h');
2702
+ expect(mocks.mockStdout.write).toHaveBeenNthCalledWith(2, CLEAR_VIEWPORT);
2703
+ expect(mocks.mockStdout.write).toHaveBeenNthCalledWith(3, '\x1b[?2026l');
2704
+ expect(getScrollbackPurgingWrites()).toHaveLength(0);
2705
+ expect(mockStdoutWithCork.uncork).toHaveBeenCalledTimes(1);
2706
+ expect(writableState.corked).toBe(0);
2707
+ unmount();
2708
+ }
2709
+ finally {
2710
+ delete mockStdoutWithCork.columns;
2711
+ delete mockStdoutWithCork.cork;
2712
+ delete mockStdoutWithCork.uncork;
2713
+ delete mockStdoutWithCork.prependListener;
2714
+ delete mockStdoutWithCork.off;
2715
+ delete mockStdoutWithCork._writableState;
2716
+ vi.useRealTimers();
2717
+ }
2718
+ });
2719
+ it('refreshStatic clears only the viewport and preserves terminal scrollback in default inline mode', async () => {
2720
+ vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);
2721
+ vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(false);
2722
+ const { unmount } = await act(async () => renderAppContainer());
2723
+ expect(capturedUIActions).toBeTruthy();
2724
+ const initialRemountKey = capturedUIState.historyRemountKey;
2725
+ mocks.mockStdout.write.mockClear();
2726
+ act(() => {
2727
+ capturedUIActions.refreshStatic();
2728
+ });
2729
+ // Only the visible viewport is erased so earlier turns remain available
2730
+ // in the terminal's native scrollback.
2731
+ expect(mocks.mockStdout.write).toHaveBeenCalledWith(CLEAR_VIEWPORT);
2732
+ expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(ansiEscapes.clearTerminal);
2733
+ expect(getScrollbackPurgingWrites()).toHaveLength(0);
2734
+ // Static history is still remounted so it is re-rendered after the clear.
2735
+ expect(capturedUIState.historyRemountKey).toBe(initialRemountKey + 1);
2478
2736
  unmount();
2479
2737
  });
2480
2738
  });
@@ -2517,8 +2775,8 @@ describe('AppContainer State Management', () => {
2517
2775
  await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
2518
2776
  // Should be reset
2519
2777
  expect(capturedUIState.constrainHeight).toBe(true);
2520
- // Should NOT refresh static's clearTerminal in alternate buffer
2521
- expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(ansiEscapes.clearTerminal);
2778
+ // Should NOT refresh static's viewport clear in alternate buffer
2779
+ expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(CLEAR_VIEWPORT);
2522
2780
  unmount();
2523
2781
  });
2524
2782
  });
@@ -2730,7 +2988,7 @@ describe('AppContainer State Management', () => {
2730
2988
  });
2731
2989
  // Should remain expanded and not clear terminal
2732
2990
  expect(capturedUIState.constrainHeight).toBe(false);
2733
- expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(ansiEscapes.clearTerminal);
2991
+ expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(CLEAR_VIEWPORT);
2734
2992
  // Simulate Escape key to exit expanded view
2735
2993
  act(() => {
2736
2994
  stdin.write('\x1b');
@@ -2743,6 +3001,104 @@ describe('AppContainer State Management', () => {
2743
3001
  });
2744
3002
  unmount();
2745
3003
  });
3004
+ it('does not reset the hint timer when an overflowing ID is removed while others remain', async () => {
3005
+ const { unmount } = await act(async () => renderAppContainer());
3006
+ await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
3007
+ act(() => {
3008
+ capturedOverflowActions.addOverflowingId('test-id-1');
3009
+ capturedOverflowActions.addOverflowingId('test-id-2');
3010
+ });
3011
+ act(() => {
3012
+ vi.advanceTimersByTime(1);
3013
+ });
3014
+ await waitFor(() => {
3015
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
3016
+ });
3017
+ // Advance half the duration
3018
+ act(() => {
3019
+ vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
3020
+ });
3021
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
3022
+ // Removing one overflowing ID should NOT reset the hint timer
3023
+ act(() => {
3024
+ capturedOverflowActions.removeOverflowingId('test-id-2');
3025
+ });
3026
+ act(() => {
3027
+ vi.advanceTimersByTime(1);
3028
+ });
3029
+ // Advance the remaining half of the original timer
3030
+ act(() => {
3031
+ vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 - 1);
3032
+ });
3033
+ await waitFor(() => {
3034
+ expect(capturedUIState.showIsExpandableHint).toBe(false);
3035
+ });
3036
+ unmount();
3037
+ });
3038
+ it('does not clear terminal when expanding with Ctrl+O if there are no last-turn tool calls in history', async () => {
3039
+ const { stdin, unmount } = await act(async () => renderAppContainer());
3040
+ mocks.mockStdout.write.mockClear();
3041
+ // Expand with Ctrl+O when history has no last-turn tool calls
3042
+ act(() => {
3043
+ stdin.write('\x0f');
3044
+ });
3045
+ expect(capturedUIState.constrainHeight).toBe(false);
3046
+ expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(CLEAR_VIEWPORT);
3047
+ // Collapse with Ctrl+O should still refresh static and clear terminal
3048
+ act(() => {
3049
+ stdin.write('\x0f');
3050
+ });
3051
+ expect(capturedUIState.constrainHeight).toBe(true);
3052
+ expect(mocks.mockStdout.write).toHaveBeenCalledWith(CLEAR_VIEWPORT);
3053
+ unmount();
3054
+ });
3055
+ it('clears terminal when expanding with Ctrl+O if there are last-turn tool calls in history', async () => {
3056
+ useHistory.mockReturnValue({
3057
+ history: [
3058
+ { id: 1, type: 'user', text: 'run something' },
3059
+ {
3060
+ id: 2,
3061
+ type: 'tool_group',
3062
+ tools: [
3063
+ {
3064
+ callId: 'call-1',
3065
+ name: 'test_tool',
3066
+ description: 'desc',
3067
+ status: CoreToolCallStatus.Success,
3068
+ resultDisplay: 'result',
3069
+ confirmationDetails: undefined,
3070
+ },
3071
+ ],
3072
+ },
3073
+ ],
3074
+ addItem: vi.fn(),
3075
+ updateItem: vi.fn(),
3076
+ clearItems: vi.fn(),
3077
+ loadHistory: vi.fn(),
3078
+ });
3079
+ const { stdin, unmount } = await act(async () => renderAppContainer());
3080
+ mocks.mockStdout.write.mockClear();
3081
+ act(() => {
3082
+ stdin.write('\x0f');
3083
+ });
3084
+ expect(capturedUIState.constrainHeight).toBe(false);
3085
+ expect(mocks.mockStdout.write).toHaveBeenCalledWith(CLEAR_VIEWPORT);
3086
+ unmount();
3087
+ });
3088
+ it('passes pauseUpdates=true to useLoadingIndicator when expanded in standard mode', async () => {
3089
+ const { stdin, unmount } = await act(async () => renderAppContainer());
3090
+ expect(useLoadingIndicator).toHaveBeenLastCalledWith(expect.objectContaining({
3091
+ pauseUpdates: false,
3092
+ }));
3093
+ act(() => {
3094
+ stdin.write('\x0f');
3095
+ });
3096
+ expect(capturedUIState.constrainHeight).toBe(false);
3097
+ expect(useLoadingIndicator).toHaveBeenLastCalledWith(expect.objectContaining({
3098
+ pauseUpdates: true,
3099
+ }));
3100
+ unmount();
3101
+ });
2746
3102
  });
2747
3103
  describe('Permission Handling', () => {
2748
3104
  it('shows permission dialog when checkPermissions returns paths', async () => {