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.
- package/dist/cli.js +3 -2
- package/dist/components/App.d.ts +1 -0
- package/dist/components/App.js +17 -39
- package/dist/components/App.test.js +12 -44
- package/dist/components/Configuration.js +10 -15
- package/dist/components/ConfigureCommand.js +18 -106
- package/dist/components/ConfigureCustomCommand.js +2 -17
- package/dist/components/ConfigureOther.js +5 -23
- package/dist/components/ConfigureOther.test.js +7 -31
- package/dist/components/ConfigureShortcuts.js +4 -31
- package/dist/components/ConfigureStatusHooks.js +5 -44
- package/dist/components/ConfigureStatusHooks.test.js +7 -31
- package/dist/components/ConfigureTimeout.js +2 -14
- package/dist/components/ConfigureWorktree.js +4 -47
- package/dist/components/ConfigureWorktreeHooks.js +5 -37
- package/dist/components/ConfigureWorktreeHooks.test.js +8 -33
- package/dist/components/Confirmation.js +9 -21
- package/dist/components/CustomCommandSummary.js +2 -5
- package/dist/components/DeleteConfirmation.js +10 -47
- package/dist/components/DeleteWorktree.js +14 -42
- package/dist/components/DeleteWorktree.test.js +6 -6
- package/dist/components/LoadingSpinner.js +3 -6
- package/dist/components/LoadingSpinner.test.js +22 -22
- package/dist/components/Menu.d.ts +1 -0
- package/dist/components/Menu.js +10 -42
- package/dist/components/Menu.recent-projects.test.js +8 -8
- package/dist/components/Menu.test.js +10 -10
- package/dist/components/MergeWorktree.js +16 -88
- package/dist/components/MergeWorktree.test.js +5 -5
- package/dist/components/NewWorktree.js +25 -105
- package/dist/components/NewWorktree.test.js +8 -8
- package/dist/components/PresetSelector.js +3 -9
- package/dist/components/ProjectList.js +9 -38
- package/dist/components/ProjectList.recent-projects.test.js +7 -7
- package/dist/components/ProjectList.test.js +37 -37
- package/dist/components/RemoteBranchSelector.js +2 -21
- package/dist/components/RemoteBranchSelector.test.js +8 -8
- package/dist/components/TextInputWrapper.d.ts +5 -0
- package/dist/components/TextInputWrapper.js +138 -11
- package/dist/constants/statusIcons.d.ts +2 -1
- package/dist/constants/statusIcons.js +13 -4
- package/dist/constants/statusIcons.test.js +41 -11
- package/dist/contexts/ConfigEditorContext.d.ts +1 -1
- package/dist/contexts/ConfigEditorContext.js +3 -2
- package/dist/services/autoApprovalVerifier.js +1 -8
- package/dist/services/bunTerminal.js +41 -136
- package/dist/services/config/configEditor.js +2 -12
- package/dist/services/config/globalConfigManager.js +4 -24
- package/dist/services/config/projectConfigManager.js +3 -18
- package/dist/services/globalSessionOrchestrator.js +3 -12
- package/dist/services/globalSessionOrchestrator.test.js +1 -8
- package/dist/services/projectManager.js +13 -68
- package/dist/services/sessionManager.d.ts +1 -1
- package/dist/services/sessionManager.effect.test.js +9 -37
- package/dist/services/sessionManager.js +12 -28
- package/dist/services/sessionManager.test.js +48 -40
- package/dist/services/shortcutManager.js +7 -13
- package/dist/services/stateDetector/base.d.ts +1 -1
- package/dist/services/stateDetector/claude.d.ts +1 -1
- package/dist/services/stateDetector/claude.js +11 -4
- package/dist/services/stateDetector/claude.test.js +47 -24
- package/dist/services/stateDetector/cline.d.ts +1 -1
- package/dist/services/stateDetector/cline.js +1 -1
- package/dist/services/stateDetector/codex.d.ts +1 -1
- package/dist/services/stateDetector/codex.js +1 -1
- package/dist/services/stateDetector/cursor.d.ts +1 -1
- package/dist/services/stateDetector/cursor.js +1 -1
- package/dist/services/stateDetector/gemini.d.ts +1 -1
- package/dist/services/stateDetector/gemini.js +1 -1
- package/dist/services/stateDetector/github-copilot.d.ts +1 -1
- package/dist/services/stateDetector/github-copilot.js +1 -1
- package/dist/services/stateDetector/opencode.d.ts +1 -1
- package/dist/services/stateDetector/opencode.js +1 -1
- package/dist/services/stateDetector/types.d.ts +1 -1
- package/dist/services/worktreeConfigManager.js +3 -8
- package/dist/services/worktreeService.js +2 -12
- package/dist/types/index.js +4 -12
- package/dist/utils/logger.js +12 -33
- package/dist/utils/mutex.d.ts +1 -1
- package/dist/utils/mutex.js +4 -19
- package/dist/utils/worktreeUtils.js +4 -4
- package/package.json +12 -12
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 (
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
2
|
-
import
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
44
|
+
export const getStatusDisplay = (status, backgroundTaskCount = 0) => {
|
|
35
45
|
const display = getBaseStatusDisplay(status);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
: display;
|
|
46
|
+
const bgTag = getBackgroundTaskTag(backgroundTaskCount);
|
|
47
|
+
return bgTag ? `${display} ${bgTag}` : display;
|
|
39
48
|
};
|