ccmanager 4.0.4 → 4.1.1

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.
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import type { Key } from 'ink';
2
3
  export interface ConfirmationOption {
3
4
  label: string;
4
5
  value: string;
@@ -14,9 +15,7 @@ interface ConfirmationProps {
14
15
  hint?: React.ReactNode;
15
16
  onCancel?: () => void;
16
17
  onEscape?: () => void;
17
- onCustomInput?: (input: string, key: {
18
- [key: string]: boolean;
19
- }) => boolean;
18
+ onCustomInput?: (input: string, key: Key) => boolean;
20
19
  }
21
20
  /**
22
21
  * Reusable confirmation component with SelectInput UI pattern
@@ -60,6 +60,10 @@ const makeKey = (overrides = {}) => ({
60
60
  backspace: false,
61
61
  delete: false,
62
62
  meta: false,
63
+ super: false,
64
+ hyper: false,
65
+ capsLock: false,
66
+ numLock: false,
63
67
  ...overrides,
64
68
  });
65
69
  describe('PresetSelector component', () => {
@@ -65,23 +65,11 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
65
65
  stdout.write('\x1b[?7l');
66
66
  // Clear screen when entering session
67
67
  stdout.write('\x1B[2J\x1B[H');
68
- // Handle session restoration by writing all buffered output at once.
69
- // This is faster than chunk-by-chunk replay and preserves scrollback.
70
- const handleSessionRestore = (restoredSession) => {
68
+ // Restore the current terminal state from the headless xterm snapshot.
69
+ const handleSessionRestore = (restoredSession, restoreSnapshot) => {
71
70
  if (restoredSession.id === session.id) {
72
- if (restoredSession.outputHistory.length === 0)
73
- return;
74
- // Concatenate all history buffers and write at once for better performance
75
- const allHistory = Buffer.concat(restoredSession.outputHistory);
76
- const historyStr = allHistory.toString('utf8');
77
- // Normalize the output
78
- const normalized = normalizeLineEndings(historyStr);
79
- // Remove leading clear screen sequences to avoid double-clear
80
- const cleaned = normalized
81
- .replace(/^\x1B\[2J/g, '')
82
- .replace(/^\x1B\[H/g, '');
83
- if (cleaned.length > 0) {
84
- stdout.write(cleaned);
71
+ if (restoreSnapshot.length > 0) {
72
+ stdout.write(restoreSnapshot);
85
73
  }
86
74
  }
87
75
  };
@@ -102,8 +90,6 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
102
90
  };
103
91
  sessionManager.on('sessionData', handleSessionData);
104
92
  sessionManager.on('sessionExit', handleSessionExit);
105
- // Mark session as active (this will trigger the restore event)
106
- sessionManager.setSessionActive(session.id, true);
107
93
  // Immediately resize the PTY and terminal to current dimensions
108
94
  // This fixes rendering issues when terminal width changed while in menu
109
95
  // https://github.com/kbwo/ccmanager/issues/2
@@ -120,6 +106,9 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
120
106
  catch {
121
107
  /* empty */
122
108
  }
109
+ // Mark session as active after resizing so the restore snapshot matches
110
+ // the current terminal dimensions.
111
+ sessionManager.setSessionActive(session.id, true);
123
112
  // Handle terminal resize
124
113
  const handleResize = () => {
125
114
  const cols = process.stdout.columns || 80;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink-testing-library';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { EventEmitter } from 'events';
5
+ const testState = vi.hoisted(() => ({
6
+ stdout: null,
7
+ }));
8
+ class MockStdout extends EventEmitter {
9
+ write = vi.fn();
10
+ }
11
+ vi.mock('ink', async () => {
12
+ const actual = await vi.importActual('ink');
13
+ return {
14
+ ...actual,
15
+ useStdout: vi.fn(() => ({ stdout: testState.stdout })),
16
+ };
17
+ });
18
+ import Session from './Session.js';
19
+ describe('Session', () => {
20
+ const originalColumns = process.stdout.columns;
21
+ const originalRows = process.stdout.rows;
22
+ const originalIsTTY = process.stdin.isTTY;
23
+ beforeEach(() => {
24
+ testState.stdout = new MockStdout();
25
+ Object.defineProperty(process.stdout, 'columns', {
26
+ value: 120,
27
+ configurable: true,
28
+ });
29
+ Object.defineProperty(process.stdout, 'rows', {
30
+ value: 40,
31
+ configurable: true,
32
+ });
33
+ Object.defineProperty(process.stdin, 'isTTY', {
34
+ value: false,
35
+ configurable: true,
36
+ });
37
+ });
38
+ afterEach(() => {
39
+ testState.stdout = null;
40
+ Object.defineProperty(process.stdout, 'columns', {
41
+ value: originalColumns,
42
+ configurable: true,
43
+ });
44
+ Object.defineProperty(process.stdout, 'rows', {
45
+ value: originalRows,
46
+ configurable: true,
47
+ });
48
+ Object.defineProperty(process.stdin, 'isTTY', {
49
+ value: originalIsTTY,
50
+ configurable: true,
51
+ });
52
+ vi.restoreAllMocks();
53
+ });
54
+ it('resizes before activating and writes restore snapshots verbatim', async () => {
55
+ const listeners = new Map();
56
+ const processResize = vi.fn();
57
+ const processWrite = vi.fn();
58
+ const terminalResize = vi.fn();
59
+ const setSessionActive = vi.fn((sessionId, active) => {
60
+ if (sessionId === session.id && active) {
61
+ for (const handler of listeners.get('sessionRestore') ?? []) {
62
+ handler(session, '\nrestored');
63
+ }
64
+ }
65
+ });
66
+ const session = {
67
+ id: 'session-1',
68
+ process: {
69
+ write: processWrite,
70
+ resize: processResize,
71
+ },
72
+ terminal: {
73
+ resize: terminalResize,
74
+ },
75
+ stateMutex: {
76
+ getSnapshot: () => ({ state: 'idle' }),
77
+ },
78
+ };
79
+ const sessionManager = {
80
+ on: vi.fn((event, handler) => {
81
+ const handlers = listeners.get(event) ?? new Set();
82
+ handlers.add(handler);
83
+ listeners.set(event, handlers);
84
+ return sessionManager;
85
+ }),
86
+ off: vi.fn((event, handler) => {
87
+ listeners.get(event)?.delete(handler);
88
+ return sessionManager;
89
+ }),
90
+ setSessionActive,
91
+ cancelAutoApproval: vi.fn(),
92
+ };
93
+ render(_jsx(Session, { session: session, sessionManager: sessionManager, onReturnToMenu: vi.fn() }));
94
+ await new Promise(resolve => setTimeout(resolve, 0));
95
+ expect(processResize).toHaveBeenCalledWith(120, 40);
96
+ expect(terminalResize).toHaveBeenCalledWith(120, 40);
97
+ expect(processResize.mock.invocationCallOrder[0] ?? 0).toBeLessThan(setSessionActive.mock.invocationCallOrder[0] ?? 0);
98
+ expect(terminalResize.mock.invocationCallOrder[0] ?? 0).toBeLessThan(setSessionActive.mock.invocationCallOrder[0] ?? 0);
99
+ expect(testState.stdout?.write).toHaveBeenNthCalledWith(3, '\nrestored');
100
+ });
101
+ });
@@ -52,16 +52,31 @@ vi.mock('./config/configReader.js', () => ({
52
52
  setAutoApprovalEnabled: vi.fn(),
53
53
  },
54
54
  }));
55
+ vi.mock('@xterm/addon-serialize', () => ({
56
+ SerializeAddon: vi.fn().mockImplementation(function () {
57
+ return {
58
+ serialize: vi.fn(() => ''),
59
+ activate: vi.fn(),
60
+ dispose: vi.fn(),
61
+ };
62
+ }),
63
+ }));
55
64
  vi.mock('@xterm/headless', () => ({
56
65
  default: {
57
66
  Terminal: vi.fn().mockImplementation(function () {
58
67
  return {
68
+ rows: 24,
69
+ cols: 80,
59
70
  buffer: {
60
71
  active: {
72
+ type: 'normal',
73
+ baseY: 0,
61
74
  length: 0,
62
75
  getLine: vi.fn(),
63
76
  },
64
77
  },
78
+ loadAddon: vi.fn(),
79
+ resize: vi.fn(),
65
80
  write: vi.fn(),
66
81
  };
67
82
  }),
@@ -35,6 +35,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
35
35
  private updateSessionState;
36
36
  constructor();
37
37
  private createTerminal;
38
+ private getRestoreSnapshot;
38
39
  private createSessionInternal;
39
40
  /**
40
41
  * Create session with command preset using Effect-based error handling
@@ -33,17 +33,32 @@ vi.mock('./config/configReader.js', () => ({
33
33
  getStatusHooks: vi.fn(() => ({})),
34
34
  },
35
35
  }));
36
+ vi.mock('@xterm/addon-serialize', () => ({
37
+ SerializeAddon: vi.fn().mockImplementation(function () {
38
+ return {
39
+ serialize: vi.fn(() => ''),
40
+ activate: vi.fn(),
41
+ dispose: vi.fn(),
42
+ };
43
+ }),
44
+ }));
36
45
  // Mock Terminal
37
46
  vi.mock('@xterm/headless', () => ({
38
47
  default: {
39
48
  Terminal: vi.fn().mockImplementation(function () {
40
49
  return {
50
+ rows: 24,
51
+ cols: 80,
41
52
  buffer: {
42
53
  active: {
54
+ type: 'normal',
55
+ baseY: 0,
43
56
  length: 0,
44
57
  getLine: vi.fn(),
45
58
  },
46
59
  },
60
+ loadAddon: vi.fn(),
61
+ resize: vi.fn(),
47
62
  write: vi.fn(),
48
63
  };
49
64
  }),
@@ -1,6 +1,7 @@
1
1
  import { spawn } from './bunTerminal.js';
2
2
  import { EventEmitter } from 'events';
3
3
  import pkg from '@xterm/headless';
4
+ import { SerializeAddon } from '@xterm/addon-serialize';
4
5
  import { spawn as childSpawn } from 'child_process';
5
6
  import { configReader } from './config/configReader.js';
6
7
  import { executeStatusHook } from '../utils/hookExecutor.js';
@@ -17,6 +18,7 @@ import { injectTeammateMode } from '../utils/commandArgs.js';
17
18
  import { preparePresetLaunch } from '../utils/presetPrompt.js';
18
19
  const { Terminal } = pkg;
19
20
  const TERMINAL_CONTENT_MAX_LINES = 300;
21
+ const TERMINAL_SCROLLBACK_LINES = 5000;
20
22
  export class SessionManager extends EventEmitter {
21
23
  sessions;
22
24
  waitingWithBottomBorder = new Map();
@@ -192,14 +194,22 @@ export class SessionManager extends EventEmitter {
192
194
  return new Terminal({
193
195
  cols: process.stdout.columns || 80,
194
196
  rows: process.stdout.rows || 24,
197
+ scrollback: TERMINAL_SCROLLBACK_LINES,
195
198
  allowProposedApi: true,
196
199
  logLevel: 'off',
197
200
  });
198
201
  }
202
+ getRestoreSnapshot(session) {
203
+ return session.serializer.serialize({
204
+ scrollback: 0,
205
+ });
206
+ }
199
207
  async createSessionInternal(worktreePath, ptyProcess, options = {}) {
200
208
  const existingSessions = this.getSessionsForWorktree(worktreePath);
201
209
  const maxNumber = existingSessions.reduce((max, s) => Math.max(max, s.sessionNumber), 0);
202
210
  const terminal = this.createTerminal();
211
+ const serializer = new SerializeAddon();
212
+ terminal.loadAddon(serializer);
203
213
  const detectionStrategy = options.detectionStrategy ?? 'claude';
204
214
  const stateDetector = createStateDetector(detectionStrategy);
205
215
  const session = {
@@ -212,10 +222,10 @@ export class SessionManager extends EventEmitter {
212
222
  lastAccessedAt: Date.now(),
213
223
  process: ptyProcess,
214
224
  output: [],
215
- outputHistory: [],
216
225
  lastActivity: new Date(),
217
226
  isActive: false,
218
227
  terminal,
228
+ serializer,
219
229
  stateCheckInterval: undefined, // Will be set in setupBackgroundHandler
220
230
  isPrimaryCommand: options.isPrimaryCommand ?? true,
221
231
  presetName: options.presetName,
@@ -291,24 +301,6 @@ export class SessionManager extends EventEmitter {
291
301
  session.process.onData((data) => {
292
302
  // Write data to virtual terminal
293
303
  session.terminal.write(data);
294
- // Check for screen clear escape sequence (e.g., from /clear command)
295
- // When detected, clear the output history to prevent replaying old content on restore
296
- // This helps avoid excessive scrolling when restoring sessions with large output history
297
- if (data.includes('\x1B[2J')) {
298
- session.outputHistory = [];
299
- }
300
- // Store in output history as Buffer
301
- const buffer = Buffer.from(data, 'utf8');
302
- session.outputHistory.push(buffer);
303
- // Limit memory usage - keep max 10MB of output history
304
- const MAX_HISTORY_SIZE = 10 * 1024 * 1024; // 10MB
305
- let totalSize = session.outputHistory.reduce((sum, buf) => sum + buf.length, 0);
306
- while (totalSize > MAX_HISTORY_SIZE && session.outputHistory.length > 0) {
307
- const removed = session.outputHistory.shift();
308
- if (removed) {
309
- totalSize -= removed.length;
310
- }
311
- }
312
304
  session.lastActivity = new Date();
313
305
  // Only emit data events when session is active
314
306
  if (session.isActive) {
@@ -479,9 +471,9 @@ export class SessionManager extends EventEmitter {
479
471
  session.isActive = active;
480
472
  if (active) {
481
473
  session.lastAccessedAt = Date.now();
482
- // Emit a restore event with the output history if available
483
- if (session.outputHistory.length > 0) {
484
- this.emit('sessionRestore', session);
474
+ const restoreSnapshot = this.getRestoreSnapshot(session);
475
+ if (restoreSnapshot.length > 0) {
476
+ this.emit('sessionRestore', session, restoreSnapshot);
485
477
  }
486
478
  }
487
479
  }
@@ -37,19 +37,38 @@ vi.mock('./config/configReader.js', () => ({
37
37
  setAutoApprovalEnabled: vi.fn(),
38
38
  },
39
39
  }));
40
+ vi.mock('@xterm/addon-serialize', () => ({
41
+ SerializeAddon: vi.fn().mockImplementation(function () {
42
+ return {
43
+ serialize: vi.fn(() => ''),
44
+ activate: vi.fn(),
45
+ dispose: vi.fn(),
46
+ };
47
+ }),
48
+ }));
40
49
  // Mock Terminal
41
50
  vi.mock('@xterm/headless', () => ({
42
51
  default: {
43
52
  Terminal: vi.fn(function () {
44
53
  return {
54
+ rows: 24,
55
+ cols: 80,
45
56
  buffer: {
46
57
  active: {
58
+ type: 'normal',
59
+ baseY: 0,
47
60
  length: 0,
48
61
  getLine: vi.fn(function () {
49
62
  return null;
50
63
  }),
51
64
  },
52
65
  },
66
+ loadAddon: vi.fn(function () {
67
+ return undefined;
68
+ }),
69
+ resize: vi.fn(function () {
70
+ return undefined;
71
+ }),
53
72
  write: vi.fn(function () {
54
73
  return undefined;
55
74
  }),
@@ -740,63 +759,37 @@ describe('SessionManager', () => {
740
759
  expect(session.isPrimaryCommand).toBe(false);
741
760
  });
742
761
  });
743
- describe('clearHistoryOnClear', () => {
744
- it('should clear output history when screen clear escape sequence is detected', async () => {
745
- // Setup
762
+ describe('session restore snapshots', () => {
763
+ it('should emit serialized terminal output when activating a session', async () => {
746
764
  vi.mocked(configReader.getDefaultPreset).mockReturnValue({
747
765
  id: '1',
748
766
  name: 'Main',
749
767
  command: 'claude',
750
768
  });
751
769
  vi.mocked(spawn).mockReturnValue(mockPty);
752
- // Create session
753
770
  const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
754
- // Simulate some data output
755
- mockPty.emit('data', 'Hello World');
756
- mockPty.emit('data', 'More data');
757
- // Verify output history has data
758
- expect(session.outputHistory.length).toBe(2);
759
- // Simulate screen clear escape sequence
760
- mockPty.emit('data', '\x1B[2J');
761
- // Verify output history was cleared and only contains the clear sequence
762
- expect(session.outputHistory.length).toBe(1);
763
- expect(session.outputHistory[0]?.toString()).toBe('\x1B[2J');
764
- });
765
- it('should not clear output history for normal data', async () => {
766
- // Setup
771
+ const serializeMock = vi
772
+ .spyOn(session.serializer, 'serialize')
773
+ .mockReturnValue('\u001b[31mrestored\u001b[0m');
774
+ const restoreHandler = vi.fn();
775
+ sessionManager.on('sessionRestore', restoreHandler);
776
+ sessionManager.setSessionActive(session.id, true);
777
+ expect(serializeMock).toHaveBeenCalledWith({ scrollback: 0 });
778
+ expect(restoreHandler).toHaveBeenCalledWith(session, '\u001b[31mrestored\u001b[0m');
779
+ });
780
+ it('should skip restore event when serialized output is empty', async () => {
767
781
  vi.mocked(configReader.getDefaultPreset).mockReturnValue({
768
782
  id: '1',
769
783
  name: 'Main',
770
784
  command: 'claude',
771
785
  });
772
786
  vi.mocked(spawn).mockReturnValue(mockPty);
773
- // Create session
774
- const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
775
- // Simulate normal data output without screen clear
776
- mockPty.emit('data', 'Hello World');
777
- mockPty.emit('data', 'More data');
778
- mockPty.emit('data', 'Even more data');
779
- // Verify output history contains all data
780
- expect(session.outputHistory.length).toBe(3);
781
- });
782
- it('should clear history when screen clear is part of larger data chunk', async () => {
783
- // Setup
784
- vi.mocked(configReader.getDefaultPreset).mockReturnValue({
785
- id: '1',
786
- name: 'Main',
787
- command: 'claude',
788
- });
789
- vi.mocked(spawn).mockReturnValue(mockPty);
790
- // Create session
791
787
  const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
792
- // Simulate some data output
793
- mockPty.emit('data', 'Hello World');
794
- mockPty.emit('data', 'More data');
795
- // Simulate screen clear as part of larger data chunk (e.g., from /clear command)
796
- mockPty.emit('data', 'prefix\x1B[2Jsuffix');
797
- // Verify output history was cleared and only contains the new chunk
798
- expect(session.outputHistory.length).toBe(1);
799
- expect(session.outputHistory[0]?.toString()).toBe('prefix\x1B[2Jsuffix');
788
+ vi.spyOn(session.serializer, 'serialize').mockReturnValue('');
789
+ const restoreHandler = vi.fn();
790
+ sessionManager.on('sessionRestore', restoreHandler);
791
+ sessionManager.setSessionActive(session.id, true);
792
+ expect(restoreHandler).not.toHaveBeenCalled();
800
793
  });
801
794
  });
802
795
  describe('static methods', () => {
@@ -1,5 +1,6 @@
1
1
  import type { IPty } from '../services/bunTerminal.js';
2
2
  import type pkg from '@xterm/headless';
3
+ import type { SerializeAddon } from '@xterm/addon-serialize';
3
4
  import { GitStatus } from '../utils/gitStatus.js';
4
5
  import { Mutex, SessionStateData } from '../utils/mutex.js';
5
6
  import type { StateDetector } from '../services/stateDetector/types.js';
@@ -25,10 +26,10 @@ export interface Session {
25
26
  lastAccessedAt: number;
26
27
  process: IPty;
27
28
  output: string[];
28
- outputHistory: Buffer[];
29
29
  lastActivity: Date;
30
30
  isActive: boolean;
31
31
  terminal: Terminal;
32
+ serializer: SerializeAddon;
32
33
  stateCheckInterval: NodeJS.Timeout | undefined;
33
34
  isPrimaryCommand: boolean;
34
35
  presetName: string | undefined;
@@ -382,8 +382,8 @@ describe('hookExecutor Integration Tests', () => {
382
382
  lastAccessedAt: Date.now(),
383
383
  process: {},
384
384
  terminal: {},
385
+ serializer: {},
385
386
  output: [],
386
- outputHistory: [],
387
387
  stateCheckInterval: undefined,
388
388
  isPrimaryCommand: true,
389
389
  presetName: undefined,
@@ -441,8 +441,8 @@ describe('hookExecutor Integration Tests', () => {
441
441
  lastAccessedAt: Date.now(),
442
442
  process: {},
443
443
  terminal: {},
444
+ serializer: {},
444
445
  output: [],
445
- outputHistory: [],
446
446
  stateCheckInterval: undefined,
447
447
  isPrimaryCommand: true,
448
448
  presetName: undefined,
@@ -498,8 +498,8 @@ describe('hookExecutor Integration Tests', () => {
498
498
  lastAccessedAt: Date.now(),
499
499
  process: {},
500
500
  terminal: {},
501
+ serializer: {},
501
502
  output: [],
502
- outputHistory: [],
503
503
  stateCheckInterval: undefined,
504
504
  isPrimaryCommand: true,
505
505
  presetName: undefined,
@@ -557,8 +557,8 @@ describe('hookExecutor Integration Tests', () => {
557
557
  lastAccessedAt: Date.now(),
558
558
  process: {},
559
559
  terminal: {},
560
+ serializer: {},
560
561
  output: [],
561
- outputHistory: [],
562
562
  stateCheckInterval: undefined,
563
563
  isPrimaryCommand: true,
564
564
  presetName: undefined,
@@ -129,10 +129,10 @@ describe('prepareSessionItems', () => {
129
129
  lastAccessedAt: Date.now(),
130
130
  process: {},
131
131
  output: [],
132
- outputHistory: [],
133
132
  lastActivity: new Date(),
134
133
  isActive: true,
135
134
  terminal: {},
135
+ serializer: {},
136
136
  stateCheckInterval: undefined,
137
137
  isPrimaryCommand: true,
138
138
  presetName: undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "4.0.4",
3
+ "version": "4.1.1",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",
@@ -41,11 +41,11 @@
41
41
  "bin"
42
42
  ],
43
43
  "optionalDependencies": {
44
- "@kodaikabasawa/ccmanager-darwin-arm64": "4.0.4",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "4.0.4",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "4.0.4",
47
- "@kodaikabasawa/ccmanager-linux-x64": "4.0.4",
48
- "@kodaikabasawa/ccmanager-win32-x64": "4.0.4"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "4.1.1",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "4.1.1",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "4.1.1",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "4.1.1",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "4.1.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "prettier": "@vdemedes/prettier-config",
70
70
  "dependencies": {
71
+ "@xterm/addon-serialize": "^0.14.0",
71
72
  "@xterm/headless": "^6.0.0",
72
73
  "effect": "^3.18.2",
73
74
  "ink": "^6.6.0",