ccmanager 3.5.0 → 3.5.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.
Files changed (82) hide show
  1. package/dist/cli.js +3 -2
  2. package/dist/components/App.d.ts +1 -0
  3. package/dist/components/App.js +17 -39
  4. package/dist/components/App.test.js +12 -44
  5. package/dist/components/Configuration.js +10 -15
  6. package/dist/components/ConfigureCommand.js +18 -106
  7. package/dist/components/ConfigureCustomCommand.js +2 -17
  8. package/dist/components/ConfigureOther.js +5 -23
  9. package/dist/components/ConfigureOther.test.js +7 -31
  10. package/dist/components/ConfigureShortcuts.js +4 -31
  11. package/dist/components/ConfigureStatusHooks.js +5 -44
  12. package/dist/components/ConfigureStatusHooks.test.js +7 -31
  13. package/dist/components/ConfigureTimeout.js +2 -14
  14. package/dist/components/ConfigureWorktree.js +4 -47
  15. package/dist/components/ConfigureWorktreeHooks.js +5 -37
  16. package/dist/components/ConfigureWorktreeHooks.test.js +8 -33
  17. package/dist/components/Confirmation.js +9 -21
  18. package/dist/components/CustomCommandSummary.js +2 -5
  19. package/dist/components/DeleteConfirmation.js +10 -47
  20. package/dist/components/DeleteWorktree.js +14 -42
  21. package/dist/components/DeleteWorktree.test.js +6 -6
  22. package/dist/components/LoadingSpinner.js +3 -6
  23. package/dist/components/LoadingSpinner.test.js +22 -22
  24. package/dist/components/Menu.d.ts +1 -0
  25. package/dist/components/Menu.js +10 -42
  26. package/dist/components/Menu.recent-projects.test.js +8 -8
  27. package/dist/components/Menu.test.js +10 -10
  28. package/dist/components/MergeWorktree.js +16 -88
  29. package/dist/components/MergeWorktree.test.js +5 -5
  30. package/dist/components/NewWorktree.js +25 -105
  31. package/dist/components/NewWorktree.test.js +8 -8
  32. package/dist/components/PresetSelector.js +3 -9
  33. package/dist/components/ProjectList.js +9 -38
  34. package/dist/components/ProjectList.recent-projects.test.js +7 -7
  35. package/dist/components/ProjectList.test.js +37 -37
  36. package/dist/components/RemoteBranchSelector.js +2 -21
  37. package/dist/components/RemoteBranchSelector.test.js +8 -8
  38. package/dist/components/TextInputWrapper.d.ts +5 -0
  39. package/dist/components/TextInputWrapper.js +138 -11
  40. package/dist/constants/statusIcons.d.ts +2 -1
  41. package/dist/constants/statusIcons.js +13 -4
  42. package/dist/constants/statusIcons.test.js +41 -11
  43. package/dist/contexts/ConfigEditorContext.d.ts +1 -1
  44. package/dist/contexts/ConfigEditorContext.js +3 -2
  45. package/dist/services/autoApprovalVerifier.js +1 -8
  46. package/dist/services/bunTerminal.js +41 -136
  47. package/dist/services/config/configEditor.js +2 -12
  48. package/dist/services/config/globalConfigManager.js +4 -24
  49. package/dist/services/config/projectConfigManager.js +3 -18
  50. package/dist/services/globalSessionOrchestrator.js +3 -12
  51. package/dist/services/globalSessionOrchestrator.test.js +1 -8
  52. package/dist/services/projectManager.js +13 -68
  53. package/dist/services/sessionManager.d.ts +1 -1
  54. package/dist/services/sessionManager.effect.test.js +9 -37
  55. package/dist/services/sessionManager.js +12 -28
  56. package/dist/services/sessionManager.test.js +48 -40
  57. package/dist/services/shortcutManager.js +7 -13
  58. package/dist/services/stateDetector/base.d.ts +1 -1
  59. package/dist/services/stateDetector/claude.d.ts +1 -1
  60. package/dist/services/stateDetector/claude.js +11 -4
  61. package/dist/services/stateDetector/claude.test.js +47 -24
  62. package/dist/services/stateDetector/cline.d.ts +1 -1
  63. package/dist/services/stateDetector/cline.js +1 -1
  64. package/dist/services/stateDetector/codex.d.ts +1 -1
  65. package/dist/services/stateDetector/codex.js +1 -1
  66. package/dist/services/stateDetector/cursor.d.ts +1 -1
  67. package/dist/services/stateDetector/cursor.js +1 -1
  68. package/dist/services/stateDetector/gemini.d.ts +1 -1
  69. package/dist/services/stateDetector/gemini.js +1 -1
  70. package/dist/services/stateDetector/github-copilot.d.ts +1 -1
  71. package/dist/services/stateDetector/github-copilot.js +1 -1
  72. package/dist/services/stateDetector/opencode.d.ts +1 -1
  73. package/dist/services/stateDetector/opencode.js +1 -1
  74. package/dist/services/stateDetector/types.d.ts +1 -1
  75. package/dist/services/worktreeConfigManager.js +3 -8
  76. package/dist/services/worktreeService.js +2 -12
  77. package/dist/types/index.js +4 -12
  78. package/dist/utils/logger.js +12 -33
  79. package/dist/utils/mutex.d.ts +1 -1
  80. package/dist/utils/mutex.js +4 -19
  81. package/dist/utils/worktreeUtils.js +4 -4
  82. package/package.json +12 -12
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from 'ink-testing-library';
3
3
  import { describe, it, expect, vi, beforeEach } from 'vitest';
4
4
  // Mock bunTerminal to avoid native module loading issues
@@ -81,22 +81,22 @@ describe('ProjectList', () => {
81
81
  vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(mockProjects));
82
82
  });
83
83
  it('should render project list with correct title', () => {
84
- const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
84
+ const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
85
85
  expect(lastFrame()).toContain('CCManager - Multi-Project Mode');
86
86
  expect(lastFrame()).toContain('Select a project:');
87
87
  });
88
88
  it('should display loading state initially', () => {
89
89
  // Create an Effect that never completes to keep loading state
90
90
  vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.async(() => { }));
91
- const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
91
+ const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
92
92
  expect(lastFrame()).toContain('Loading projects...');
93
93
  });
94
94
  it('should display projects after loading', async () => {
95
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
95
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
96
96
  // Wait a bit for async operations
97
97
  await new Promise(resolve => setTimeout(resolve, 100));
98
98
  // Force rerender
99
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
99
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
100
100
  // Wait for SelectInput to render with our mock
101
101
  await vi.waitFor(() => {
102
102
  const frame = lastFrame();
@@ -108,16 +108,16 @@ describe('ProjectList', () => {
108
108
  expect(frame).toContain('2 ❯ project3');
109
109
  });
110
110
  it('should display error when provided', () => {
111
- const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Failed to load projects", onDismissError: mockOnDismissError }));
111
+ const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Failed to load projects", onDismissError: mockOnDismissError }));
112
112
  expect(lastFrame()).toContain('Error: Failed to load projects');
113
113
  expect(lastFrame()).toContain('Press any key to dismiss');
114
114
  });
115
115
  it('should handle project selection via menu', async () => {
116
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
116
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
117
117
  // Wait a bit for async operations
118
118
  await new Promise(resolve => setTimeout(resolve, 100));
119
119
  // Force rerender
120
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
120
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
121
121
  // Wait for component to update after async loading
122
122
  await vi.waitFor(() => {
123
123
  const frame = lastFrame();
@@ -132,11 +132,11 @@ describe('ProjectList', () => {
132
132
  expect(frame).toContain('Exit');
133
133
  });
134
134
  it('should display number shortcuts for first 10 projects', async () => {
135
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
135
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
136
136
  // Wait a bit for async operations
137
137
  await new Promise(resolve => setTimeout(resolve, 100));
138
138
  // Force rerender
139
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
139
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
140
140
  // Wait for projects to load
141
141
  await vi.waitFor(() => {
142
142
  const frame = lastFrame();
@@ -149,11 +149,11 @@ describe('ProjectList', () => {
149
149
  expect(frame).toContain('2 ❯ project3');
150
150
  });
151
151
  it('should display exit option in menu', async () => {
152
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
152
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
153
153
  // Wait a bit for async operations
154
154
  await new Promise(resolve => setTimeout(resolve, 100));
155
155
  // Force rerender
156
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
156
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
157
157
  // Wait for projects to load
158
158
  await vi.waitFor(() => {
159
159
  const frame = lastFrame();
@@ -165,7 +165,7 @@ describe('ProjectList', () => {
165
165
  expect(frame).toContain('Exit');
166
166
  });
167
167
  it('should display refresh option in menu', async () => {
168
- const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
168
+ const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
169
169
  // Wait for projects to load
170
170
  await vi.waitFor(() => {
171
171
  return lastFrame()?.includes('project1') ?? false;
@@ -177,11 +177,11 @@ describe('ProjectList', () => {
177
177
  });
178
178
  it('should show empty state when no projects found', async () => {
179
179
  vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed([]));
180
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
180
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
181
181
  // Wait for loading to finish
182
182
  await new Promise(resolve => setTimeout(resolve, 100));
183
183
  // Force rerender
184
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
184
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
185
185
  // Wait for projects to load
186
186
  await vi.waitFor(() => {
187
187
  const frame = lastFrame();
@@ -190,7 +190,7 @@ describe('ProjectList', () => {
190
190
  expect(lastFrame()).toContain('No git repositories found in /projects');
191
191
  });
192
192
  it('should display error message when error prop is provided', () => {
193
- const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
193
+ const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
194
194
  expect(lastFrame()).toContain('Error: Test error');
195
195
  expect(lastFrame()).toContain('Press any key to dismiss');
196
196
  });
@@ -204,7 +204,7 @@ describe('ProjectList', () => {
204
204
  // Need to set up stdin.setRawMode for the test
205
205
  const originalSetRawMode = process.stdin.setRawMode;
206
206
  process.stdin.setRawMode = vi.fn();
207
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
207
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
208
208
  // Wait for projects to load
209
209
  await vi.waitFor(() => {
210
210
  return lastFrame()?.includes('project1') ?? false;
@@ -233,7 +233,7 @@ describe('ProjectList', () => {
233
233
  // Wait a bit for state update
234
234
  await new Promise(resolve => setTimeout(resolve, 50));
235
235
  // Force rerender to see updated state
236
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
236
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
237
237
  // Should show search input
238
238
  expect(lastFrame()).toContain('Search:');
239
239
  // Restore original
@@ -245,7 +245,7 @@ describe('ProjectList', () => {
245
245
  mockUseInput.mockImplementation(handler => {
246
246
  inputHandler = handler;
247
247
  });
248
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
248
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
249
249
  // Wait for projects to load
250
250
  await vi.waitFor(() => {
251
251
  return lastFrame()?.includes('project1') ?? false;
@@ -270,7 +270,7 @@ describe('ProjectList', () => {
270
270
  end: false,
271
271
  });
272
272
  // Force rerender with search active and query
273
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
273
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
274
274
  // Simulate typing "project2" in search
275
275
  // This would be handled by the TextInput component
276
276
  // We'll test the filtering logic separately
@@ -284,7 +284,7 @@ describe('ProjectList', () => {
284
284
  // Need to set up stdin.setRawMode for the test
285
285
  const originalSetRawMode = process.stdin.setRawMode;
286
286
  process.stdin.setRawMode = vi.fn();
287
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
287
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
288
288
  // Wait for projects to load
289
289
  await vi.waitFor(() => {
290
290
  return lastFrame()?.includes('project1') ?? false;
@@ -311,7 +311,7 @@ describe('ProjectList', () => {
311
311
  // Wait a bit for state update
312
312
  await new Promise(resolve => setTimeout(resolve, 50));
313
313
  // Force rerender
314
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
314
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
315
315
  // Should be in search mode
316
316
  expect(lastFrame()).toContain('Search:');
317
317
  // Press ESC
@@ -336,7 +336,7 @@ describe('ProjectList', () => {
336
336
  // Wait a bit for state update
337
337
  await new Promise(resolve => setTimeout(resolve, 50));
338
338
  // Force rerender
339
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
339
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
340
340
  // Should exit search mode
341
341
  expect(lastFrame()).not.toContain('Search:');
342
342
  // Restore original
@@ -351,7 +351,7 @@ describe('ProjectList', () => {
351
351
  // Need to set up stdin.setRawMode for the test
352
352
  const originalSetRawMode = process.stdin.setRawMode;
353
353
  process.stdin.setRawMode = vi.fn();
354
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
354
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
355
355
  // Press "/" key
356
356
  inputHandler('/', {
357
357
  escape: false,
@@ -374,7 +374,7 @@ describe('ProjectList', () => {
374
374
  // Wait a bit for state update
375
375
  await new Promise(resolve => setTimeout(resolve, 50));
376
376
  // Force rerender
377
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
377
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: "Test error", onDismissError: mockOnDismissError }));
378
378
  // Should not show search input, should dismiss error instead
379
379
  expect(lastFrame()).not.toContain('Search:');
380
380
  expect(mockOnDismissError).toHaveBeenCalled();
@@ -390,7 +390,7 @@ describe('ProjectList', () => {
390
390
  // Need to set up stdin.setRawMode for the test
391
391
  const originalSetRawMode = process.stdin.setRawMode;
392
392
  process.stdin.setRawMode = vi.fn();
393
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
393
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
394
394
  // Wait for projects to load
395
395
  await vi.waitFor(() => {
396
396
  return lastFrame()?.includes('project1') ?? false;
@@ -417,7 +417,7 @@ describe('ProjectList', () => {
417
417
  // Wait a bit for state update
418
418
  await new Promise(resolve => setTimeout(resolve, 50));
419
419
  // Force rerender
420
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
420
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
421
421
  // Should be in search mode
422
422
  expect(lastFrame()).toContain('Search:');
423
423
  // Press Enter
@@ -442,7 +442,7 @@ describe('ProjectList', () => {
442
442
  // Wait a bit for state update
443
443
  await new Promise(resolve => setTimeout(resolve, 50));
444
444
  // Force rerender
445
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
445
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
446
446
  // Should exit search mode
447
447
  expect(lastFrame()).not.toContain('Search:');
448
448
  // Should not have called onSelectProject
@@ -459,7 +459,7 @@ describe('ProjectList', () => {
459
459
  // Need to set up stdin.setRawMode for the test
460
460
  const originalSetRawMode = process.stdin.setRawMode;
461
461
  process.stdin.setRawMode = vi.fn();
462
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
462
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
463
463
  // Wait for projects to load
464
464
  await vi.waitFor(() => {
465
465
  return lastFrame()?.includes('project1') ?? false;
@@ -525,7 +525,7 @@ describe('ProjectList', () => {
525
525
  });
526
526
  await new Promise(resolve => setTimeout(resolve, 50));
527
527
  // Force rerender
528
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
528
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
529
529
  // Should display all projects (filter cleared)
530
530
  expect(lastFrame()).toContain('project1');
531
531
  expect(lastFrame()).toContain('project2');
@@ -544,11 +544,11 @@ describe('ProjectList', () => {
544
544
  cause: 'Directory not accessible',
545
545
  });
546
546
  vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.fail(fileSystemError));
547
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
547
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
548
548
  // Wait for loading to finish
549
549
  await new Promise(resolve => setTimeout(resolve, 100));
550
550
  // Force rerender
551
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
551
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
552
552
  // Wait for projects to attempt loading
553
553
  await vi.waitFor(() => {
554
554
  const frame = lastFrame();
@@ -569,10 +569,10 @@ describe('ProjectList', () => {
569
569
  vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(
570
570
  // @ts-expect-error - Test uses wrong error type (should be FileSystemError)
571
571
  Effect.fail(gitError));
572
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
572
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
573
573
  // Wait for projects to attempt loading
574
574
  await vi.waitFor(() => {
575
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
575
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
576
576
  return !lastFrame()?.includes('Loading projects...');
577
577
  });
578
578
  // Should display error message
@@ -586,7 +586,7 @@ describe('ProjectList', () => {
586
586
  }, 500);
587
587
  return Effect.sync(() => clearTimeout(timeout));
588
588
  }));
589
- const { unmount, lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
589
+ const { unmount, lastFrame } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
590
590
  // Wait a bit to ensure promise is pending
591
591
  await new Promise(resolve => setTimeout(resolve, 100));
592
592
  // Component should still be loading
@@ -600,11 +600,11 @@ describe('ProjectList', () => {
600
600
  });
601
601
  it('should successfully load projects using Effect execution', async () => {
602
602
  vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(mockProjects));
603
- const { lastFrame, rerender } = render(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
603
+ const { lastFrame, rerender } = render(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
604
604
  // Wait for loading to finish
605
605
  await new Promise(resolve => setTimeout(resolve, 100));
606
606
  // Force rerender
607
- rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
607
+ rerender(_jsx(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
608
608
  // Wait for projects to load
609
609
  await vi.waitFor(() => {
610
610
  const frame = lastFrame();
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import { shortcutManager } from '../services/shortcutManager.js';
@@ -23,25 +23,6 @@ const RemoteBranchSelector = ({ branchName, matches, onSelect, onCancel, }) => {
23
23
  onCancel();
24
24
  }
25
25
  });
26
- return (React.createElement(Box, { flexDirection: "column" },
27
- React.createElement(Box, { marginBottom: 1 },
28
- React.createElement(Text, { bold: true, color: "yellow" }, "\u26A0\uFE0F Ambiguous Branch Reference")),
29
- React.createElement(Box, { marginBottom: 1 },
30
- React.createElement(Text, null,
31
- "Branch ",
32
- React.createElement(Text, { color: "cyan" },
33
- "'",
34
- branchName,
35
- "'"),
36
- " exists in multiple remotes.")),
37
- React.createElement(Box, { marginBottom: 1 },
38
- React.createElement(Text, { dimColor: true }, "Please select which remote branch you want to use as the base:")),
39
- React.createElement(SelectInput, { items: selectItems, onSelect: handleSelectItem, initialIndex: 0 }),
40
- React.createElement(Box, { marginTop: 1 },
41
- React.createElement(Text, { dimColor: true },
42
- "Press \u2191\u2193 to navigate, Enter to select,",
43
- ' ',
44
- shortcutManager.getShortcutDisplay('cancel'),
45
- " to cancel"))));
26
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "yellow", children: "\u26A0\uFE0F Ambiguous Branch Reference" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Branch ", _jsxs(Text, { color: "cyan", children: ["'", branchName, "'"] }), " exists in multiple remotes."] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Please select which remote branch you want to use as the base:" }) }), _jsx(SelectInput, { items: selectItems, onSelect: handleSelectItem, initialIndex: 0 }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press \u2191\u2193 to navigate, Enter to select,", ' ', shortcutManager.getShortcutDisplay('cancel'), " to cancel"] }) })] }));
46
27
  };
47
28
  export default RemoteBranchSelector;
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from 'ink-testing-library';
3
3
  import RemoteBranchSelector from './RemoteBranchSelector.js';
4
4
  import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
@@ -59,7 +59,7 @@ describe('RemoteBranchSelector Component', () => {
59
59
  vi.clearAllMocks();
60
60
  });
61
61
  it('should render warning title and branch name', () => {
62
- const { lastFrame } = render(React.createElement(RemoteBranchSelector, { branchName: mockBranchName, matches: mockMatches, onSelect: onSelect, onCancel: onCancel }));
62
+ const { lastFrame } = render(_jsx(RemoteBranchSelector, { branchName: mockBranchName, matches: mockMatches, onSelect: onSelect, onCancel: onCancel }));
63
63
  const output = lastFrame();
64
64
  expect(output).toContain('⚠️ Ambiguous Branch Reference');
65
65
  // The component renders the branch name and checks for the message
@@ -67,18 +67,18 @@ describe('RemoteBranchSelector Component', () => {
67
67
  expect(output).toContain('exists in multiple remotes.');
68
68
  });
69
69
  it('should render all remote branch options', () => {
70
- const { lastFrame } = render(React.createElement(RemoteBranchSelector, { branchName: mockBranchName, matches: mockMatches, onSelect: onSelect, onCancel: onCancel }));
70
+ const { lastFrame } = render(_jsx(RemoteBranchSelector, { branchName: mockBranchName, matches: mockMatches, onSelect: onSelect, onCancel: onCancel }));
71
71
  const output = lastFrame();
72
72
  expect(output).toContain('origin/feature/awesome-feature (from origin)');
73
73
  expect(output).toContain('upstream/feature/awesome-feature (from upstream)');
74
74
  });
75
75
  it('should render cancel option', () => {
76
- const { lastFrame } = render(React.createElement(RemoteBranchSelector, { branchName: mockBranchName, matches: mockMatches, onSelect: onSelect, onCancel: onCancel }));
76
+ const { lastFrame } = render(_jsx(RemoteBranchSelector, { branchName: mockBranchName, matches: mockMatches, onSelect: onSelect, onCancel: onCancel }));
77
77
  const output = lastFrame();
78
78
  expect(output).toContain('← Cancel');
79
79
  });
80
80
  it('should display help text with shortcut information', () => {
81
- const { lastFrame } = render(React.createElement(RemoteBranchSelector, { branchName: mockBranchName, matches: mockMatches, onSelect: onSelect, onCancel: onCancel }));
81
+ const { lastFrame } = render(_jsx(RemoteBranchSelector, { branchName: mockBranchName, matches: mockMatches, onSelect: onSelect, onCancel: onCancel }));
82
82
  const output = lastFrame();
83
83
  expect(output).toContain('Press ↑↓ to navigate, Enter to select, ESC to cancel');
84
84
  });
@@ -90,7 +90,7 @@ describe('RemoteBranchSelector Component', () => {
90
90
  fullRef: 'origin/feature/single-feature',
91
91
  },
92
92
  ];
93
- const { lastFrame } = render(React.createElement(RemoteBranchSelector, { branchName: "feature/single-feature", matches: singleMatch, onSelect: onSelect, onCancel: onCancel }));
93
+ const { lastFrame } = render(_jsx(RemoteBranchSelector, { branchName: "feature/single-feature", matches: singleMatch, onSelect: onSelect, onCancel: onCancel }));
94
94
  const output = lastFrame();
95
95
  expect(output).toContain('origin/feature/single-feature (from origin)');
96
96
  expect(output).not.toContain('upstream');
@@ -108,7 +108,7 @@ describe('RemoteBranchSelector Component', () => {
108
108
  fullRef: 'fork/feature/sub/complex-branch-name',
109
109
  },
110
110
  ];
111
- const { lastFrame } = render(React.createElement(RemoteBranchSelector, { branchName: "feature/sub/complex-branch-name", matches: complexMatches, onSelect: onSelect, onCancel: onCancel }));
111
+ const { lastFrame } = render(_jsx(RemoteBranchSelector, { branchName: "feature/sub/complex-branch-name", matches: complexMatches, onSelect: onSelect, onCancel: onCancel }));
112
112
  const output = lastFrame();
113
113
  expect(output).toContain('origin/feature/sub/complex-branch-name (from origin)');
114
114
  expect(output).toContain('fork/feature/sub/complex-branch-name (from fork)');
@@ -129,7 +129,7 @@ describe('RemoteBranchSelector Component', () => {
129
129
  fullRef: 'company/test-branch',
130
130
  },
131
131
  ];
132
- const { lastFrame } = render(React.createElement(RemoteBranchSelector, { branchName: "test-branch", matches: manyMatches, onSelect: onSelect, onCancel: onCancel }));
132
+ const { lastFrame } = render(_jsx(RemoteBranchSelector, { branchName: "test-branch", matches: manyMatches, onSelect: onSelect, onCancel: onCancel }));
133
133
  const output = lastFrame();
134
134
  // Verify all remotes are shown
135
135
  expect(output).toContain('origin/test-branch (from origin)');
@@ -9,5 +9,10 @@ interface TextInputWrapperProps {
9
9
  showCursor?: boolean;
10
10
  highlightPastedText?: boolean;
11
11
  }
12
+ /**
13
+ * Custom text input component that handles rapid input correctly.
14
+ * This is a replacement for ink-text-input that uses refs for immediate
15
+ * state updates, which is necessary for text expansion tools like Espanso.
16
+ */
12
17
  declare const TextInputWrapper: React.FC<TextInputWrapperProps>;
13
18
  export default TextInputWrapper;
@@ -1,15 +1,142 @@
1
- import React from 'react';
2
- import TextInput from 'ink-text-input';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef } from 'react';
3
+ import { Text, useInput } from 'ink';
3
4
  import stripAnsi from 'strip-ansi';
4
- const TextInputWrapper = ({ value, onChange, ...props }) => {
5
- const handleChange = (newValue) => {
6
- // First strip all ANSI escape sequences
7
- let cleanedValue = stripAnsi(newValue);
8
- // Then specifically remove bracketed paste mode markers that might remain
9
- // These sometimes appear as literal text after ANSI stripping
10
- cleanedValue = cleanedValue.replace(/\[200~/g, '').replace(/\[201~/g, '');
11
- onChange(cleanedValue);
5
+ /**
6
+ * Custom text input component that handles rapid input correctly.
7
+ * This is a replacement for ink-text-input that uses refs for immediate
8
+ * state updates, which is necessary for text expansion tools like Espanso.
9
+ */
10
+ const TextInputWrapper = ({ value, onChange, onSubmit, placeholder = '', focus = true, mask, showCursor = true, }) => {
11
+ // Use ref to track the actual current value for immediate updates
12
+ // This is critical for handling rapid input from tools like Espanso
13
+ const valueRef = useRef(value);
14
+ const cursorRef = useRef(value.length);
15
+ // State for triggering re-renders
16
+ const [, forceUpdate] = useState({});
17
+ // Sync refs when value prop changes from parent
18
+ useEffect(() => {
19
+ valueRef.current = value;
20
+ // Adjust cursor if it's beyond the new value length
21
+ if (cursorRef.current > value.length) {
22
+ cursorRef.current = value.length;
23
+ }
24
+ }, [value]);
25
+ const cleanValue = (val) => {
26
+ let cleaned = stripAnsi(val);
27
+ cleaned = cleaned.replace(/\[200~/g, '').replace(/\[201~/g, '');
28
+ return cleaned;
12
29
  };
13
- return React.createElement(TextInput, { value: value, onChange: handleChange, ...props });
30
+ // Process backspace characters that might be embedded in input string
31
+ // This handles cases where text expansion tools send backspaces as characters
32
+ const processBackspaces = (input, currentValue, cursor) => {
33
+ let newValue = currentValue;
34
+ let newCursor = cursor;
35
+ let remaining = '';
36
+ for (let i = 0; i < input.length; i++) {
37
+ const char = input[i];
38
+ const charCode = char?.charCodeAt(0);
39
+ // Check for backspace characters (ASCII 8 or 127)
40
+ if (charCode === 8 || charCode === 127) {
41
+ if (newCursor > 0) {
42
+ newValue =
43
+ newValue.slice(0, newCursor - 1) + newValue.slice(newCursor);
44
+ newCursor--;
45
+ }
46
+ }
47
+ else {
48
+ // Regular character - add to remaining
49
+ remaining += char;
50
+ }
51
+ }
52
+ return { value: newValue, cursor: newCursor, remainingInput: remaining };
53
+ };
54
+ useInput((input, key) => {
55
+ // Ignore certain keys
56
+ if (key.upArrow ||
57
+ key.downArrow ||
58
+ (key.ctrl && input === 'c') ||
59
+ key.tab ||
60
+ (key.shift && key.tab)) {
61
+ return;
62
+ }
63
+ // Handle Enter/Return
64
+ if (key.return) {
65
+ if (onSubmit) {
66
+ onSubmit(valueRef.current);
67
+ }
68
+ return;
69
+ }
70
+ let currentValue = valueRef.current;
71
+ let cursor = cursorRef.current;
72
+ if (key.leftArrow) {
73
+ if (showCursor && cursor > 0) {
74
+ cursorRef.current = cursor - 1;
75
+ forceUpdate({});
76
+ }
77
+ return;
78
+ }
79
+ if (key.rightArrow) {
80
+ if (showCursor && cursor < currentValue.length) {
81
+ cursorRef.current = cursor + 1;
82
+ forceUpdate({});
83
+ }
84
+ return;
85
+ }
86
+ if (key.backspace || key.delete) {
87
+ if (cursor > 0) {
88
+ const nextValue = currentValue.slice(0, cursor - 1) + currentValue.slice(cursor);
89
+ valueRef.current = nextValue;
90
+ cursorRef.current = cursor - 1;
91
+ onChange(nextValue);
92
+ forceUpdate({});
93
+ }
94
+ return;
95
+ }
96
+ // Process input that might contain embedded backspace characters
97
+ // (some text expansion tools send backspaces as part of the input string)
98
+ const { value: processedValue, cursor: processedCursor, remainingInput, } = processBackspaces(input, currentValue, cursor);
99
+ currentValue = processedValue;
100
+ cursor = processedCursor;
101
+ // Add remaining characters (non-backspace)
102
+ if (remainingInput) {
103
+ const cleanedInput = cleanValue(remainingInput);
104
+ if (cleanedInput) {
105
+ currentValue =
106
+ currentValue.slice(0, cursor) +
107
+ cleanedInput +
108
+ currentValue.slice(cursor);
109
+ cursor = cursor + cleanedInput.length;
110
+ }
111
+ }
112
+ // Update refs immediately (synchronously)
113
+ valueRef.current = currentValue;
114
+ cursorRef.current = cursor;
115
+ // Notify parent of value change
116
+ onChange(currentValue);
117
+ // Force re-render to update display
118
+ forceUpdate({});
119
+ }, { isActive: focus });
120
+ // Render the text with cursor
121
+ const displayValue = mask
122
+ ? mask.repeat(valueRef.current.length)
123
+ : valueRef.current;
124
+ const cursor = cursorRef.current;
125
+ if (!showCursor || !focus) {
126
+ return (_jsx(Text, { children: displayValue.length > 0 ? (displayValue) : placeholder ? (_jsx(Text, { dimColor: true, children: placeholder })) : null }));
127
+ }
128
+ // Show cursor
129
+ if (displayValue.length === 0) {
130
+ // Show placeholder with cursor on first char
131
+ if (placeholder) {
132
+ return (_jsxs(Text, { children: [_jsx(Text, { inverse: true, children: placeholder[0] || ' ' }), _jsx(Text, { dimColor: true, children: placeholder.slice(1) })] }));
133
+ }
134
+ return _jsx(Text, { inverse: true, children: " " });
135
+ }
136
+ // Render value with cursor
137
+ const beforeCursor = displayValue.slice(0, cursor);
138
+ const atCursor = displayValue[cursor] || ' ';
139
+ const afterCursor = displayValue.slice(cursor + 1);
140
+ return (_jsxs(Text, { children: [beforeCursor, _jsx(Text, { inverse: true, children: atCursor }), afterCursor] }));
14
141
  };
15
142
  export default TextInputWrapper;
@@ -13,6 +13,7 @@ export declare const STATUS_LABELS: {
13
13
  export declare const STATUS_TAGS: {
14
14
  readonly BACKGROUND_TASK: "\u001B[2m[BG]\u001B[0m";
15
15
  };
16
+ export declare const getBackgroundTaskTag: (count: number) => string;
16
17
  export declare const MENU_ICONS: {
17
18
  readonly NEW_WORKTREE: "⊕";
18
19
  readonly MERGE_WORKTREE: "⇄";
@@ -20,4 +21,4 @@ export declare const MENU_ICONS: {
20
21
  readonly CONFIGURE_SHORTCUTS: "⌨";
21
22
  readonly EXIT: "⏻";
22
23
  };
23
- export declare const getStatusDisplay: (status: SessionState, hasBackgroundTask?: boolean) => string;
24
+ export declare const getStatusDisplay: (status: SessionState, backgroundTaskCount?: number) => string;
@@ -12,6 +12,16 @@ export const STATUS_LABELS = {
12
12
  export const STATUS_TAGS = {
13
13
  BACKGROUND_TASK: '\x1b[2m[BG]\x1b[0m',
14
14
  };
15
+ export const getBackgroundTaskTag = (count) => {
16
+ if (count <= 0) {
17
+ return '';
18
+ }
19
+ if (count === 1) {
20
+ return STATUS_TAGS.BACKGROUND_TASK;
21
+ }
22
+ // count >= 2: show [BG:N]
23
+ return `\x1b[2m[BG:${count}]\x1b[0m`;
24
+ };
15
25
  export const MENU_ICONS = {
16
26
  NEW_WORKTREE: '⊕',
17
27
  MERGE_WORKTREE: '⇄',
@@ -31,9 +41,8 @@ const getBaseStatusDisplay = (status) => {
31
41
  return `${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE}`;
32
42
  }
33
43
  };
34
- export const getStatusDisplay = (status, hasBackgroundTask = false) => {
44
+ export const getStatusDisplay = (status, backgroundTaskCount = 0) => {
35
45
  const display = getBaseStatusDisplay(status);
36
- return hasBackgroundTask
37
- ? `${display} ${STATUS_TAGS.BACKGROUND_TASK}`
38
- : display;
46
+ const bgTag = getBackgroundTaskTag(backgroundTaskCount);
47
+ return bgTag ? `${display} ${bgTag}` : display;
39
48
  };