ccmanager 3.5.1 → 3.5.3
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.js +2 -2
- 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.effect.test.js +9 -37
- package/dist/services/sessionManager.js +3 -18
- package/dist/services/sessionManager.test.js +9 -37
- package/dist/services/shortcutManager.js +7 -13
- 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.js +3 -18
- package/dist/utils/worktreeUtils.js +3 -3
- package/package.json +13 -13
|
@@ -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)');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import TextInput from 'ink-text-input';
|
|
3
3
|
import stripAnsi from 'strip-ansi';
|
|
4
4
|
const TextInputWrapper = ({ value, onChange, ...props }) => {
|
|
@@ -10,6 +10,6 @@ const TextInputWrapper = ({ value, onChange, ...props }) => {
|
|
|
10
10
|
cleanedValue = cleanedValue.replace(/\[200~/g, '').replace(/\[201~/g, '');
|
|
11
11
|
onChange(cleanedValue);
|
|
12
12
|
};
|
|
13
|
-
return
|
|
13
|
+
return _jsx(TextInput, { value: value, onChange: handleChange, ...props });
|
|
14
14
|
};
|
|
15
15
|
export default TextInputWrapper;
|
|
@@ -12,7 +12,7 @@ interface ConfigEditorProviderProps {
|
|
|
12
12
|
* Uses singleton config editors to ensure config changes are
|
|
13
13
|
* immediately visible to all components.
|
|
14
14
|
*/
|
|
15
|
-
export declare function ConfigEditorProvider({ scope, children, }: ConfigEditorProviderProps):
|
|
15
|
+
export declare function ConfigEditorProvider({ scope, children, }: ConfigEditorProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
16
16
|
/**
|
|
17
17
|
* Hook to access ConfigEditor from context.
|
|
18
18
|
* Must be used within a ConfigEditorProvider.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
2
3
|
import { ConfigEditor } from '../services/config/configEditor.js';
|
|
3
4
|
const ConfigEditorContext = createContext(null);
|
|
4
5
|
/**
|
|
@@ -9,7 +10,7 @@ const ConfigEditorContext = createContext(null);
|
|
|
9
10
|
*/
|
|
10
11
|
export function ConfigEditorProvider({ scope, children, }) {
|
|
11
12
|
const configEditor = useMemo(() => new ConfigEditor(scope), [scope]);
|
|
12
|
-
return (
|
|
13
|
+
return (_jsx(ConfigEditorContext.Provider, { value: configEditor, children: children }));
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Hook to access ConfigEditor from context.
|
|
@@ -44,14 +44,7 @@ const buildPrompt = (terminalOutput) => PROMPT_TEMPLATE.replace(PLACEHOLDER.term
|
|
|
44
44
|
* user permission is required before proceeding
|
|
45
45
|
*/
|
|
46
46
|
export class AutoApprovalVerifier {
|
|
47
|
-
|
|
48
|
-
Object.defineProperty(this, "model", {
|
|
49
|
-
enumerable: true,
|
|
50
|
-
configurable: true,
|
|
51
|
-
writable: true,
|
|
52
|
-
value: 'haiku'
|
|
53
|
-
});
|
|
54
|
-
}
|
|
47
|
+
model = 'haiku';
|
|
55
48
|
createExecOptions(signal) {
|
|
56
49
|
return {
|
|
57
50
|
encoding: 'utf8',
|
|
@@ -9,118 +9,26 @@
|
|
|
9
9
|
* BunTerminal class that wraps Bun's built-in Terminal API.
|
|
10
10
|
*/
|
|
11
11
|
class BunTerminal {
|
|
12
|
+
_pid = -1;
|
|
13
|
+
_cols;
|
|
14
|
+
_rows;
|
|
15
|
+
_process;
|
|
16
|
+
_closed = false;
|
|
17
|
+
_dataListeners = [];
|
|
18
|
+
_exitListeners = [];
|
|
19
|
+
_subprocess = null;
|
|
20
|
+
_terminal = null;
|
|
21
|
+
_decoder = new globalThis.TextDecoder('utf-8');
|
|
22
|
+
// Buffering to combine fragmented data chunks from the same event loop
|
|
23
|
+
_dataBuffer = '';
|
|
24
|
+
_flushTimer = null;
|
|
25
|
+
_syncOutputMode = false;
|
|
26
|
+
// Synchronized output escape sequences (used by Ink and other TUI frameworks)
|
|
27
|
+
static SYNC_OUTPUT_START = '\x1b[?2026h';
|
|
28
|
+
static SYNC_OUTPUT_END = '\x1b[?2026l';
|
|
29
|
+
static FLUSH_DELAY_MS = 8; // ~2 frames at 60fps for batching
|
|
30
|
+
static SYNC_TIMEOUT_MS = 100; // Timeout for sync mode
|
|
12
31
|
constructor(file, args, options) {
|
|
13
|
-
Object.defineProperty(this, "_pid", {
|
|
14
|
-
enumerable: true,
|
|
15
|
-
configurable: true,
|
|
16
|
-
writable: true,
|
|
17
|
-
value: -1
|
|
18
|
-
});
|
|
19
|
-
Object.defineProperty(this, "_cols", {
|
|
20
|
-
enumerable: true,
|
|
21
|
-
configurable: true,
|
|
22
|
-
writable: true,
|
|
23
|
-
value: void 0
|
|
24
|
-
});
|
|
25
|
-
Object.defineProperty(this, "_rows", {
|
|
26
|
-
enumerable: true,
|
|
27
|
-
configurable: true,
|
|
28
|
-
writable: true,
|
|
29
|
-
value: void 0
|
|
30
|
-
});
|
|
31
|
-
Object.defineProperty(this, "_process", {
|
|
32
|
-
enumerable: true,
|
|
33
|
-
configurable: true,
|
|
34
|
-
writable: true,
|
|
35
|
-
value: void 0
|
|
36
|
-
});
|
|
37
|
-
Object.defineProperty(this, "_closed", {
|
|
38
|
-
enumerable: true,
|
|
39
|
-
configurable: true,
|
|
40
|
-
writable: true,
|
|
41
|
-
value: false
|
|
42
|
-
});
|
|
43
|
-
Object.defineProperty(this, "_dataListeners", {
|
|
44
|
-
enumerable: true,
|
|
45
|
-
configurable: true,
|
|
46
|
-
writable: true,
|
|
47
|
-
value: []
|
|
48
|
-
});
|
|
49
|
-
Object.defineProperty(this, "_exitListeners", {
|
|
50
|
-
enumerable: true,
|
|
51
|
-
configurable: true,
|
|
52
|
-
writable: true,
|
|
53
|
-
value: []
|
|
54
|
-
});
|
|
55
|
-
Object.defineProperty(this, "_subprocess", {
|
|
56
|
-
enumerable: true,
|
|
57
|
-
configurable: true,
|
|
58
|
-
writable: true,
|
|
59
|
-
value: null
|
|
60
|
-
});
|
|
61
|
-
Object.defineProperty(this, "_terminal", {
|
|
62
|
-
enumerable: true,
|
|
63
|
-
configurable: true,
|
|
64
|
-
writable: true,
|
|
65
|
-
value: null
|
|
66
|
-
});
|
|
67
|
-
Object.defineProperty(this, "_decoder", {
|
|
68
|
-
enumerable: true,
|
|
69
|
-
configurable: true,
|
|
70
|
-
writable: true,
|
|
71
|
-
value: new globalThis.TextDecoder('utf-8')
|
|
72
|
-
});
|
|
73
|
-
// Buffering to combine fragmented data chunks from the same event loop
|
|
74
|
-
Object.defineProperty(this, "_dataBuffer", {
|
|
75
|
-
enumerable: true,
|
|
76
|
-
configurable: true,
|
|
77
|
-
writable: true,
|
|
78
|
-
value: ''
|
|
79
|
-
});
|
|
80
|
-
Object.defineProperty(this, "_flushTimer", {
|
|
81
|
-
enumerable: true,
|
|
82
|
-
configurable: true,
|
|
83
|
-
writable: true,
|
|
84
|
-
value: null
|
|
85
|
-
});
|
|
86
|
-
Object.defineProperty(this, "_syncOutputMode", {
|
|
87
|
-
enumerable: true,
|
|
88
|
-
configurable: true,
|
|
89
|
-
writable: true,
|
|
90
|
-
value: false
|
|
91
|
-
});
|
|
92
|
-
Object.defineProperty(this, "onData", {
|
|
93
|
-
enumerable: true,
|
|
94
|
-
configurable: true,
|
|
95
|
-
writable: true,
|
|
96
|
-
value: (listener) => {
|
|
97
|
-
this._dataListeners.push(listener);
|
|
98
|
-
return {
|
|
99
|
-
dispose: () => {
|
|
100
|
-
const index = this._dataListeners.indexOf(listener);
|
|
101
|
-
if (index !== -1) {
|
|
102
|
-
this._dataListeners.splice(index, 1);
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
Object.defineProperty(this, "onExit", {
|
|
109
|
-
enumerable: true,
|
|
110
|
-
configurable: true,
|
|
111
|
-
writable: true,
|
|
112
|
-
value: (listener) => {
|
|
113
|
-
this._exitListeners.push(listener);
|
|
114
|
-
return {
|
|
115
|
-
dispose: () => {
|
|
116
|
-
const index = this._exitListeners.indexOf(listener);
|
|
117
|
-
if (index !== -1) {
|
|
118
|
-
this._exitListeners.splice(index, 1);
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
32
|
this._cols = options.cols ?? 80;
|
|
125
33
|
this._rows = options.rows ?? 24;
|
|
126
34
|
this._process = file;
|
|
@@ -278,6 +186,28 @@ class BunTerminal {
|
|
|
278
186
|
get process() {
|
|
279
187
|
return this._process;
|
|
280
188
|
}
|
|
189
|
+
onData = (listener) => {
|
|
190
|
+
this._dataListeners.push(listener);
|
|
191
|
+
return {
|
|
192
|
+
dispose: () => {
|
|
193
|
+
const index = this._dataListeners.indexOf(listener);
|
|
194
|
+
if (index !== -1) {
|
|
195
|
+
this._dataListeners.splice(index, 1);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
onExit = (listener) => {
|
|
201
|
+
this._exitListeners.push(listener);
|
|
202
|
+
return {
|
|
203
|
+
dispose: () => {
|
|
204
|
+
const index = this._exitListeners.indexOf(listener);
|
|
205
|
+
if (index !== -1) {
|
|
206
|
+
this._exitListeners.splice(index, 1);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
};
|
|
281
211
|
write(data) {
|
|
282
212
|
if (this._closed || !this._terminal) {
|
|
283
213
|
return;
|
|
@@ -317,31 +247,6 @@ class BunTerminal {
|
|
|
317
247
|
}
|
|
318
248
|
}
|
|
319
249
|
}
|
|
320
|
-
// Synchronized output escape sequences (used by Ink and other TUI frameworks)
|
|
321
|
-
Object.defineProperty(BunTerminal, "SYNC_OUTPUT_START", {
|
|
322
|
-
enumerable: true,
|
|
323
|
-
configurable: true,
|
|
324
|
-
writable: true,
|
|
325
|
-
value: '\x1b[?2026h'
|
|
326
|
-
});
|
|
327
|
-
Object.defineProperty(BunTerminal, "SYNC_OUTPUT_END", {
|
|
328
|
-
enumerable: true,
|
|
329
|
-
configurable: true,
|
|
330
|
-
writable: true,
|
|
331
|
-
value: '\x1b[?2026l'
|
|
332
|
-
});
|
|
333
|
-
Object.defineProperty(BunTerminal, "FLUSH_DELAY_MS", {
|
|
334
|
-
enumerable: true,
|
|
335
|
-
configurable: true,
|
|
336
|
-
writable: true,
|
|
337
|
-
value: 8
|
|
338
|
-
}); // ~2 frames at 60fps for batching
|
|
339
|
-
Object.defineProperty(BunTerminal, "SYNC_TIMEOUT_MS", {
|
|
340
|
-
enumerable: true,
|
|
341
|
-
configurable: true,
|
|
342
|
-
writable: true,
|
|
343
|
-
value: 100
|
|
344
|
-
}); // Timeout for sync mode
|
|
345
250
|
/**
|
|
346
251
|
* Spawn a new PTY process using Bun's built-in Terminal API.
|
|
347
252
|
*
|