@sascha384/tic 4.8.0 → 5.0.0
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/.claude-plugin/plugin.json +1 -1
- package/dist/auth/gitlab.js +12 -5
- package/dist/auth/gitlab.js.map +1 -1
- package/dist/commands.d.ts +5 -0
- package/dist/commands.js +90 -1
- package/dist/commands.js.map +1 -1
- package/dist/components/BranchList.js +417 -281
- package/dist/components/BranchList.js.map +1 -1
- package/dist/components/ColorPill.js +3 -3
- package/dist/components/ColorPill.js.map +1 -1
- package/dist/components/CommandBar.d.ts +8 -0
- package/dist/components/CommandBar.js +142 -0
- package/dist/components/CommandBar.js.map +1 -0
- package/dist/components/DetailPanel.js +2 -2
- package/dist/components/DetailPanel.js.map +1 -1
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/Header.js +6 -2
- package/dist/components/Header.js.map +1 -1
- package/dist/components/HelpScreen.js +3 -2
- package/dist/components/HelpScreen.js.map +1 -1
- package/dist/components/OverlayPanel.d.ts +1 -1
- package/dist/components/PullRequestList.js +118 -18
- package/dist/components/PullRequestList.js.map +1 -1
- package/dist/components/Settings.js +5 -0
- package/dist/components/Settings.js.map +1 -1
- package/dist/components/StatusScreen.js +16 -8
- package/dist/components/StatusScreen.js.map +1 -1
- package/dist/components/TableLayout.d.ts +26 -10
- package/dist/components/TableLayout.js +67 -146
- package/dist/components/TableLayout.js.map +1 -1
- package/dist/components/WorkItemForm.js +5 -0
- package/dist/components/WorkItemForm.js.map +1 -1
- package/dist/components/WorkItemList.js +119 -93
- package/dist/components/WorkItemList.js.map +1 -1
- package/dist/git.d.ts +8 -0
- package/dist/git.js.map +1 -1
- package/dist/implement.js +7 -3
- package/dist/implement.js.map +1 -1
- package/dist/stores/backendDataStore.d.ts +6 -0
- package/dist/stores/backendDataStore.js +81 -1
- package/dist/stores/backendDataStore.js.map +1 -1
- package/dist/stores/navigationStore.d.ts +4 -0
- package/dist/stores/navigationStore.js +10 -0
- package/dist/stores/navigationStore.js.map +1 -1
- package/dist/stores/uiStore.d.ts +12 -0
- package/dist/stores/uiStore.js.map +1 -1
- package/dist/test-helpers.d.ts +7 -0
- package/dist/test-helpers.js +208 -0
- package/dist/test-helpers.js.map +1 -0
- package/drizzle/meta/_journal.json +1 -1
- package/package.json +1 -1
|
@@ -1,160 +1,336 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useState, useEffect,
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import
|
|
4
|
+
import { TableLayout } from './TableLayout.js';
|
|
5
5
|
import { useThemeStore } from '../stores/themeStore.js';
|
|
6
|
-
import { useNavigationStore } from '../stores/navigationStore.js';
|
|
7
|
-
import { useBackendDataStore } from '../stores/backendDataStore.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
6
|
+
import { navigationStore, useNavigationStore, } from '../stores/navigationStore.js';
|
|
7
|
+
import { backendDataStore, useBackendDataStore, } from '../stores/backendDataStore.js';
|
|
8
|
+
import { uiStore, useUIStore } from '../stores/uiStore.js';
|
|
9
|
+
import { getCurrentBranch, checkoutBranch, hasUncommittedChanges, createBranch, } from '../git.js';
|
|
10
|
+
import { deleteBranch, mergeBranch, removeWorktree, pushBranch, } from '../git-async.js';
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { CommandBar } from './CommandBar.js';
|
|
13
|
+
import { OverlayPanel } from './OverlayPanel.js';
|
|
14
|
+
import { useTerminalWidth } from '../hooks/useTerminalWidth.js';
|
|
15
|
+
import { getVisibleCommands, } from '../commands.js';
|
|
16
|
+
function relativeTime(isoDate) {
|
|
17
|
+
if (!isoDate)
|
|
18
|
+
return '';
|
|
19
|
+
const diff = Date.now() - new Date(isoDate).getTime();
|
|
20
|
+
const mins = Math.floor(diff / 60000);
|
|
21
|
+
if (mins < 60)
|
|
22
|
+
return `${mins}m ago`;
|
|
23
|
+
const hours = Math.floor(mins / 60);
|
|
24
|
+
if (hours < 24)
|
|
25
|
+
return `${hours}h ago`;
|
|
26
|
+
const days = Math.floor(hours / 24);
|
|
27
|
+
return `${days}d ago`;
|
|
28
|
+
}
|
|
29
|
+
function buildBranchColumns(accent, muted, mutedDim) {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
key: 'branch',
|
|
33
|
+
header: 'Branch',
|
|
34
|
+
width: -1, // flex
|
|
35
|
+
required: true,
|
|
36
|
+
render: (row, selected) => {
|
|
37
|
+
const isTic = row.branch.name.startsWith('tic/');
|
|
38
|
+
const prefix = row.branch.current ? '* ' : ' ';
|
|
39
|
+
return (_jsxs(Text, { color: selected ? accent : isTic ? accent : undefined, bold: selected || isTic, wrap: "truncate", children: [prefix, row.branch.name] }));
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'item',
|
|
44
|
+
header: 'Item',
|
|
45
|
+
width: 30,
|
|
46
|
+
hidePriority: 3,
|
|
47
|
+
render: (row, selected) => {
|
|
48
|
+
const display = row.linkedItem
|
|
49
|
+
? `#${row.linkedItem.id} ${row.linkedItem.title}`
|
|
50
|
+
: '';
|
|
51
|
+
return (_jsx(Text, { color: row.linkedItem ? (selected ? undefined : muted) : undefined, dimColor: !row.linkedItem ? mutedDim : undefined, wrap: "truncate", children: display }));
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'worktree',
|
|
56
|
+
header: 'Worktree',
|
|
57
|
+
width: 10,
|
|
58
|
+
hidePriority: 1,
|
|
59
|
+
render: (row) => _jsx(Text, { children: row.worktree ? '\u2713' : '' }),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: 'remote',
|
|
63
|
+
header: 'Remote',
|
|
64
|
+
width: 10,
|
|
65
|
+
hidePriority: 2,
|
|
66
|
+
render: (row) => {
|
|
67
|
+
if (!row.branch.upstream)
|
|
68
|
+
return _jsx(Text, { children: "--" });
|
|
69
|
+
const parts = [];
|
|
70
|
+
if (row.branch.ahead > 0)
|
|
71
|
+
parts.push(`\u2191${row.branch.ahead}`);
|
|
72
|
+
if (row.branch.behind > 0)
|
|
73
|
+
parts.push(`\u2193${row.branch.behind}`);
|
|
74
|
+
return _jsx(Text, { children: parts.length > 0 ? parts.join(' ') : '\u2713' });
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: 'time',
|
|
79
|
+
header: 'Last Commit',
|
|
80
|
+
width: 10,
|
|
81
|
+
hidePriority: 0,
|
|
82
|
+
render: (row, selected) => (_jsx(Text, { color: selected ? undefined : muted, dimColor: !selected ? mutedDim : undefined, children: relativeTime(row.branch.lastCommitDate) })),
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
12
86
|
export function BranchList() {
|
|
13
87
|
const { accent, muted, mutedDim, warning } = useThemeStore((s) => s.colors);
|
|
14
88
|
const navigate = useNavigationStore((s) => s.navigate);
|
|
15
89
|
const navigateToHelp = useNavigationStore((s) => s.navigateToHelp);
|
|
16
|
-
const
|
|
90
|
+
const selectedBranchName = useNavigationStore((s) => s.selectedBranchName);
|
|
91
|
+
const rows = useBackendDataStore((s) => s.branches);
|
|
92
|
+
const prCapabilities = useBackendDataStore((s) => s.prCapabilities);
|
|
93
|
+
const capabilities = useBackendDataStore((s) => s.capabilities);
|
|
17
94
|
const cwd = process.cwd();
|
|
18
|
-
const
|
|
95
|
+
const termWidth = useTerminalWidth();
|
|
96
|
+
const branchColumns = useMemo(() => buildBranchColumns(accent, muted, mutedDim), [accent, muted, mutedDim]);
|
|
19
97
|
const [cursor, setCursor] = useState(0);
|
|
20
|
-
const [fetching, setFetching] = useState(false);
|
|
21
|
-
const [confirmation, setConfirmation] = useState(null);
|
|
22
98
|
const [toastMessage, setToastMessage] = useState(null);
|
|
23
99
|
const [inputMode, setInputMode] = useState('normal');
|
|
24
100
|
const [inputValue, setInputValue] = useState('');
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const worktrees = listWorktrees(cwd);
|
|
29
|
-
const branchRows = branches.map((b) => {
|
|
30
|
-
const linked = linkBranchToItem(b.name, items);
|
|
31
|
-
const wt = worktrees.find((w) => w.branch === b.name) ?? null;
|
|
32
|
-
return {
|
|
33
|
-
branch: b,
|
|
34
|
-
linkedItem: linked ? { id: linked.id, title: linked.title } : null,
|
|
35
|
-
worktree: wt,
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
// Sort: current branch first, then tic/ branches, then alphabetical
|
|
39
|
-
branchRows.sort((a, b) => {
|
|
40
|
-
if (a.branch.current !== b.branch.current)
|
|
41
|
-
return a.branch.current ? -1 : 1;
|
|
42
|
-
const aIsTic = a.branch.name.startsWith('tic/');
|
|
43
|
-
const bIsTic = b.branch.name.startsWith('tic/');
|
|
44
|
-
if (aIsTic !== bIsTic)
|
|
45
|
-
return aIsTic ? -1 : 1;
|
|
46
|
-
return a.branch.name.localeCompare(b.branch.name);
|
|
47
|
-
});
|
|
48
|
-
setRows(branchRows);
|
|
49
|
-
}, [cwd, items]);
|
|
50
|
-
// Initial load + background fetch
|
|
101
|
+
const activeOverlay = useUIStore((s) => s.activeOverlay);
|
|
102
|
+
const { openOverlay, closeOverlay } = uiStore.getState();
|
|
103
|
+
// Trigger background fetch on mount
|
|
51
104
|
useEffect(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
}, [cwd, loadBranches]);
|
|
65
|
-
// Filtered rows
|
|
66
|
-
const filteredRows = useMemo(() => {
|
|
67
|
-
if (!filterText)
|
|
68
|
-
return rows;
|
|
69
|
-
const lower = filterText.toLowerCase();
|
|
70
|
-
return rows.filter((r) => r.branch.name.toLowerCase().includes(lower) ||
|
|
71
|
-
r.linkedItem?.title.toLowerCase().includes(lower));
|
|
72
|
-
}, [rows, filterText]);
|
|
105
|
+
backendDataStore.getState().refreshBranches();
|
|
106
|
+
}, []);
|
|
107
|
+
// Set initial cursor from navigation
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (selectedBranchName) {
|
|
110
|
+
const idx = rows.findIndex((r) => r.branch.name === selectedBranchName);
|
|
111
|
+
if (idx >= 0)
|
|
112
|
+
setCursor(idx);
|
|
113
|
+
navigationStore.getState().selectBranch(null);
|
|
114
|
+
}
|
|
115
|
+
}, [selectedBranchName, rows]);
|
|
73
116
|
// Clamp cursor
|
|
74
|
-
const clampedCursor = Math.max(0, Math.min(cursor,
|
|
75
|
-
if (clampedCursor !== cursor &&
|
|
117
|
+
const clampedCursor = Math.max(0, Math.min(cursor, rows.length - 1));
|
|
118
|
+
if (clampedCursor !== cursor && rows.length > 0) {
|
|
76
119
|
setCursor(clampedCursor);
|
|
77
120
|
}
|
|
78
121
|
// Auto-clear toast
|
|
79
122
|
useEffect(() => {
|
|
80
123
|
if (!toastMessage)
|
|
81
124
|
return;
|
|
82
|
-
const timer = setTimeout(() => setToastMessage(null),
|
|
125
|
+
const timer = setTimeout(() => setToastMessage(null), 10000);
|
|
83
126
|
return () => clearTimeout(timer);
|
|
84
127
|
}, [toastMessage]);
|
|
85
128
|
const showToast = (msg) => setToastMessage(msg);
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
129
|
+
const reloadBranches = () => backendDataStore.getState().loadBranches();
|
|
130
|
+
const currentRow = rows[clampedCursor];
|
|
131
|
+
// --- Action functions (shared between useInput and command palette) ---
|
|
132
|
+
const doSwitch = useCallback(() => {
|
|
133
|
+
if (!currentRow)
|
|
134
|
+
return;
|
|
135
|
+
if (currentRow.branch.current) {
|
|
136
|
+
showToast('Already on this branch');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (hasUncommittedChanges(cwd)) {
|
|
140
|
+
showToast('Uncommitted changes — stash or commit first');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
checkoutBranch(currentRow.branch.name, cwd);
|
|
145
|
+
showToast(`Switched to ${currentRow.branch.name}`);
|
|
146
|
+
reloadBranches();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
showToast(err instanceof Error ? err.message : 'Checkout failed');
|
|
150
|
+
}
|
|
151
|
+
}, [currentRow, cwd]);
|
|
152
|
+
const doWorktree = useCallback(() => {
|
|
153
|
+
if (!currentRow)
|
|
154
|
+
return;
|
|
155
|
+
if (!currentRow.worktree) {
|
|
156
|
+
showToast('No worktree for this branch');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const shell = process.env['SHELL'] ?? '/bin/sh';
|
|
160
|
+
// Strip Node.js debug env vars so child processes don't inherit debugger settings
|
|
161
|
+
const env = { ...process.env };
|
|
162
|
+
delete env['NODE_OPTIONS'];
|
|
163
|
+
delete env['NODE_INSPECT_PUBLISH_UID'];
|
|
164
|
+
process.stdin.setRawMode?.(false);
|
|
165
|
+
spawnSync(shell, [], {
|
|
166
|
+
cwd: currentRow.worktree.path,
|
|
167
|
+
stdio: 'inherit',
|
|
168
|
+
env,
|
|
169
|
+
});
|
|
170
|
+
process.stdin.setRawMode?.(true);
|
|
171
|
+
console.clear();
|
|
172
|
+
reloadBranches();
|
|
173
|
+
}, [currentRow]);
|
|
174
|
+
const doDelete = useCallback(() => {
|
|
175
|
+
if (!currentRow)
|
|
176
|
+
return;
|
|
177
|
+
if (currentRow.branch.current) {
|
|
178
|
+
showToast('Cannot delete current branch');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
openOverlay({
|
|
182
|
+
type: 'branch-delete-confirm',
|
|
183
|
+
branch: currentRow.branch.name,
|
|
184
|
+
worktreePath: currentRow.worktree?.path ?? null,
|
|
185
|
+
});
|
|
186
|
+
}, [currentRow, openOverlay]);
|
|
187
|
+
const doMerge = useCallback(() => {
|
|
188
|
+
if (!currentRow)
|
|
189
|
+
return;
|
|
190
|
+
if (currentRow.branch.current) {
|
|
191
|
+
showToast('Cannot merge current branch into itself');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const currentBranch = getCurrentBranch(cwd) ?? 'current branch';
|
|
195
|
+
openOverlay({
|
|
196
|
+
type: 'branch-merge-confirm',
|
|
197
|
+
branch: currentRow.branch.name,
|
|
198
|
+
into: currentBranch,
|
|
199
|
+
});
|
|
200
|
+
}, [currentRow, cwd, openOverlay]);
|
|
201
|
+
const doPush = useCallback(() => {
|
|
202
|
+
if (!currentRow)
|
|
203
|
+
return;
|
|
204
|
+
void (async () => {
|
|
205
|
+
try {
|
|
206
|
+
showToast(`Pushing ${currentRow.branch.name}...`);
|
|
207
|
+
await pushBranch(currentRow.branch.name, cwd);
|
|
208
|
+
showToast(`Pushed ${currentRow.branch.name}`);
|
|
209
|
+
reloadBranches();
|
|
144
210
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return;
|
|
211
|
+
catch (err) {
|
|
212
|
+
showToast(err instanceof Error ? err.message : 'Push failed');
|
|
148
213
|
}
|
|
149
|
-
|
|
214
|
+
})();
|
|
215
|
+
}, [currentRow, cwd]);
|
|
216
|
+
const doCreatePr = useCallback(() => {
|
|
217
|
+
if (!currentRow)
|
|
218
|
+
return;
|
|
219
|
+
const { prCapabilities: prCaps, createPullRequest } = backendDataStore.getState();
|
|
220
|
+
if (!prCaps.create) {
|
|
221
|
+
showToast('Backend does not support PR creation');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (currentRow.branch.current) {
|
|
225
|
+
showToast('Cannot create PR from current branch');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const title = currentRow.linkedItem
|
|
229
|
+
? currentRow.linkedItem.title
|
|
230
|
+
: currentRow.branch.name;
|
|
231
|
+
const linkedItems = currentRow.linkedItem ? [currentRow.linkedItem.id] : [];
|
|
232
|
+
void createPullRequest({
|
|
233
|
+
title,
|
|
234
|
+
sourceBranch: currentRow.branch.name,
|
|
235
|
+
linkedItems,
|
|
236
|
+
})
|
|
237
|
+
.then((pr) => {
|
|
238
|
+
showToast(`PR #${String(pr.number)} created`);
|
|
239
|
+
backendDataStore
|
|
240
|
+
.getState()
|
|
241
|
+
.loadPullRequests()
|
|
242
|
+
.catch(() => { });
|
|
243
|
+
})
|
|
244
|
+
.catch((err) => {
|
|
245
|
+
showToast(err instanceof Error ? err.message : 'Failed to create PR');
|
|
246
|
+
});
|
|
247
|
+
}, [currentRow]);
|
|
248
|
+
const doRefresh = useCallback(() => {
|
|
249
|
+
backendDataStore.getState().refreshBranches();
|
|
250
|
+
}, []);
|
|
251
|
+
const doCreateBranch = useCallback(() => {
|
|
252
|
+
setInputMode('new-branch');
|
|
253
|
+
setInputValue('');
|
|
254
|
+
}, []);
|
|
255
|
+
// --- Command palette ---
|
|
256
|
+
const commandContext = {
|
|
257
|
+
screen: 'branch-list',
|
|
258
|
+
markedCount: 0,
|
|
259
|
+
hasSelectedItem: false,
|
|
260
|
+
capabilities,
|
|
261
|
+
types: [],
|
|
262
|
+
activeType: null,
|
|
263
|
+
hasSyncManager: false,
|
|
264
|
+
gitAvailable: true,
|
|
265
|
+
hasActiveFilters: false,
|
|
266
|
+
hasSavedViews: false,
|
|
267
|
+
hasSelectedBranch: currentRow !== undefined,
|
|
268
|
+
isCurrentBranch: currentRow?.branch.current ?? false,
|
|
269
|
+
hasWorktree: currentRow?.worktree !== undefined && currentRow?.worktree !== null,
|
|
270
|
+
hasPrCreateCapability: prCapabilities.create,
|
|
271
|
+
hasSelectedPr: false,
|
|
272
|
+
};
|
|
273
|
+
const paletteCommands = useMemo(() => getVisibleCommands(commandContext), [
|
|
274
|
+
commandContext.hasSelectedBranch,
|
|
275
|
+
commandContext.isCurrentBranch,
|
|
276
|
+
commandContext.hasWorktree,
|
|
277
|
+
prCapabilities.create,
|
|
278
|
+
]);
|
|
279
|
+
const handleCommandSelect = useCallback((cmd) => {
|
|
280
|
+
closeOverlay();
|
|
281
|
+
switch (cmd.id) {
|
|
282
|
+
case 'branch-switch':
|
|
283
|
+
doSwitch();
|
|
284
|
+
break;
|
|
285
|
+
case 'branch-create':
|
|
286
|
+
doCreateBranch();
|
|
287
|
+
break;
|
|
288
|
+
case 'branch-delete':
|
|
289
|
+
doDelete();
|
|
290
|
+
break;
|
|
291
|
+
case 'branch-merge':
|
|
292
|
+
doMerge();
|
|
293
|
+
break;
|
|
294
|
+
case 'branch-push':
|
|
295
|
+
doPush();
|
|
296
|
+
break;
|
|
297
|
+
case 'branch-create-pr':
|
|
298
|
+
doCreatePr();
|
|
299
|
+
break;
|
|
300
|
+
case 'branch-worktree':
|
|
301
|
+
doWorktree();
|
|
302
|
+
break;
|
|
303
|
+
case 'branch-refresh':
|
|
304
|
+
doRefresh();
|
|
305
|
+
break;
|
|
306
|
+
case 'branch-back':
|
|
307
|
+
navigate('list');
|
|
308
|
+
break;
|
|
309
|
+
case 'help':
|
|
310
|
+
navigateToHelp();
|
|
311
|
+
break;
|
|
150
312
|
}
|
|
151
|
-
|
|
313
|
+
}, [
|
|
314
|
+
closeOverlay,
|
|
315
|
+
doSwitch,
|
|
316
|
+
doCreateBranch,
|
|
317
|
+
doDelete,
|
|
318
|
+
doMerge,
|
|
319
|
+
doPush,
|
|
320
|
+
doCreatePr,
|
|
321
|
+
doWorktree,
|
|
322
|
+
doRefresh,
|
|
323
|
+
navigate,
|
|
324
|
+
navigateToHelp,
|
|
325
|
+
]);
|
|
326
|
+
useInput((input, key) => {
|
|
327
|
+
if (activeOverlay)
|
|
328
|
+
return;
|
|
329
|
+
// --- Input mode (new branch name) ---
|
|
152
330
|
if (inputMode !== 'normal') {
|
|
153
331
|
if (key.escape) {
|
|
154
332
|
setInputMode('normal');
|
|
155
333
|
setInputValue('');
|
|
156
|
-
if (inputMode === 'search')
|
|
157
|
-
setFilterText('');
|
|
158
334
|
return;
|
|
159
335
|
}
|
|
160
336
|
if (key.return) {
|
|
@@ -162,39 +338,28 @@ export function BranchList() {
|
|
|
162
338
|
try {
|
|
163
339
|
createBranch(inputValue.trim(), cwd);
|
|
164
340
|
showToast(`Created branch ${inputValue.trim()}`);
|
|
165
|
-
|
|
341
|
+
reloadBranches();
|
|
166
342
|
}
|
|
167
343
|
catch (err) {
|
|
168
344
|
showToast(err instanceof Error ? err.message : 'Failed to create branch');
|
|
169
345
|
}
|
|
170
346
|
}
|
|
171
|
-
if (inputMode === 'search') {
|
|
172
|
-
setFilterText(inputValue);
|
|
173
|
-
}
|
|
174
347
|
setInputMode('normal');
|
|
175
348
|
setInputValue('');
|
|
176
349
|
return;
|
|
177
350
|
}
|
|
178
351
|
if (key.backspace || key.delete) {
|
|
179
352
|
setInputValue((v) => v.slice(0, -1));
|
|
180
|
-
if (inputMode === 'search')
|
|
181
|
-
setFilterText(inputValue.slice(0, -1));
|
|
182
353
|
return;
|
|
183
354
|
}
|
|
184
355
|
if (input && !key.ctrl && !key.meta) {
|
|
185
356
|
setInputValue((v) => v + input);
|
|
186
|
-
if (inputMode === 'search')
|
|
187
|
-
setFilterText(inputValue + input);
|
|
188
357
|
return;
|
|
189
358
|
}
|
|
190
359
|
return;
|
|
191
360
|
}
|
|
192
361
|
// --- Normal mode ---
|
|
193
362
|
if (key.escape) {
|
|
194
|
-
if (filterText) {
|
|
195
|
-
setFilterText('');
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
363
|
navigate('list');
|
|
199
364
|
return;
|
|
200
365
|
}
|
|
@@ -202,180 +367,151 @@ export function BranchList() {
|
|
|
202
367
|
navigateToHelp();
|
|
203
368
|
return;
|
|
204
369
|
}
|
|
370
|
+
if (input === '/') {
|
|
371
|
+
openOverlay({ type: 'command-bar' });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
205
374
|
// Navigation
|
|
206
|
-
if (
|
|
207
|
-
setCursor((c) => Math.min(c + 1,
|
|
375
|
+
if (key.downArrow) {
|
|
376
|
+
setCursor((c) => Math.min(c + 1, rows.length - 1));
|
|
208
377
|
return;
|
|
209
378
|
}
|
|
210
|
-
if (
|
|
379
|
+
if (key.upArrow) {
|
|
211
380
|
setCursor((c) => Math.max(c - 1, 0));
|
|
212
381
|
return;
|
|
213
382
|
}
|
|
214
383
|
if (!currentRow)
|
|
215
384
|
return;
|
|
216
|
-
// Switch to branch
|
|
217
385
|
if (key.return) {
|
|
218
|
-
|
|
219
|
-
showToast('Already on this branch');
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (hasUncommittedChanges(cwd)) {
|
|
223
|
-
showToast('Uncommitted changes — stash or commit first');
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
try {
|
|
227
|
-
checkoutBranch(currentRow.branch.name, cwd);
|
|
228
|
-
showToast(`Switched to ${currentRow.branch.name}`);
|
|
229
|
-
loadBranches();
|
|
230
|
-
}
|
|
231
|
-
catch (err) {
|
|
232
|
-
showToast(err instanceof Error ? err.message : 'Checkout failed');
|
|
233
|
-
}
|
|
386
|
+
doSwitch();
|
|
234
387
|
return;
|
|
235
388
|
}
|
|
236
|
-
// Open worktree shell
|
|
237
389
|
if (input === 'w') {
|
|
238
|
-
|
|
239
|
-
showToast('No worktree for this branch');
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
// Spawn shell in worktree directory
|
|
243
|
-
const shell = process.env['SHELL'] ?? '/bin/sh';
|
|
244
|
-
process.stdin.setRawMode?.(false);
|
|
245
|
-
spawnSync(shell, [], {
|
|
246
|
-
cwd: currentRow.worktree.path,
|
|
247
|
-
stdio: 'inherit',
|
|
248
|
-
env: { ...process.env },
|
|
249
|
-
});
|
|
250
|
-
process.stdin.setRawMode?.(true);
|
|
251
|
-
loadBranches();
|
|
390
|
+
doWorktree();
|
|
252
391
|
return;
|
|
253
392
|
}
|
|
254
|
-
// Delete branch
|
|
255
393
|
if (input === 'd') {
|
|
256
|
-
|
|
257
|
-
showToast('Cannot delete current branch');
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
setConfirmation({
|
|
261
|
-
type: 'delete',
|
|
262
|
-
branch: currentRow.branch.name,
|
|
263
|
-
worktreePath: currentRow.worktree?.path ?? null,
|
|
264
|
-
unmerged: false,
|
|
265
|
-
});
|
|
394
|
+
doDelete();
|
|
266
395
|
return;
|
|
267
396
|
}
|
|
268
|
-
// Merge branch
|
|
269
397
|
if (input === 'm') {
|
|
270
|
-
|
|
271
|
-
showToast('Cannot merge current branch into itself');
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
const currentBranch = getCurrentBranch(cwd) ?? 'current branch';
|
|
275
|
-
setConfirmation({
|
|
276
|
-
type: 'merge',
|
|
277
|
-
branch: currentRow.branch.name,
|
|
278
|
-
into: currentBranch,
|
|
279
|
-
});
|
|
398
|
+
doMerge();
|
|
280
399
|
return;
|
|
281
400
|
}
|
|
282
|
-
// Push branch
|
|
283
401
|
if (input === 'P') {
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
showToast(`Pushing ${currentRow.branch.name}...`);
|
|
287
|
-
await pushBranch(currentRow.branch.name, cwd);
|
|
288
|
-
showToast(`Pushed ${currentRow.branch.name}`);
|
|
289
|
-
loadBranches();
|
|
290
|
-
}
|
|
291
|
-
catch (err) {
|
|
292
|
-
showToast(err instanceof Error ? err.message : 'Push failed');
|
|
293
|
-
}
|
|
294
|
-
})();
|
|
402
|
+
doPush();
|
|
295
403
|
return;
|
|
296
404
|
}
|
|
297
|
-
// Create PR (reuse existing flow)
|
|
298
405
|
if (input === 'p') {
|
|
299
|
-
|
|
300
|
-
// For now, show toast — full integration in Task 7
|
|
301
|
-
showToast('PR creation — use p from list view');
|
|
406
|
+
doCreatePr();
|
|
302
407
|
return;
|
|
303
408
|
}
|
|
304
|
-
// Refresh
|
|
305
409
|
if (input === 'r') {
|
|
306
|
-
|
|
307
|
-
fetchAll(cwd)
|
|
308
|
-
.then(() => loadBranches())
|
|
309
|
-
.catch(() => { })
|
|
310
|
-
.finally(() => setFetching(false));
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
// New branch
|
|
314
|
-
if (input === 'n') {
|
|
315
|
-
setInputMode('new-branch');
|
|
316
|
-
setInputValue('');
|
|
410
|
+
doRefresh();
|
|
317
411
|
return;
|
|
318
412
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
setInputMode('search');
|
|
322
|
-
setInputValue('');
|
|
413
|
+
if (input === 'c') {
|
|
414
|
+
doCreateBranch();
|
|
323
415
|
return;
|
|
324
416
|
}
|
|
325
417
|
});
|
|
326
|
-
// --- Time formatting helper ---
|
|
327
|
-
const relativeTime = (isoDate) => {
|
|
328
|
-
if (!isoDate)
|
|
329
|
-
return '';
|
|
330
|
-
const diff = Date.now() - new Date(isoDate).getTime();
|
|
331
|
-
const mins = Math.floor(diff / 60000);
|
|
332
|
-
if (mins < 60)
|
|
333
|
-
return `${mins}m ago`;
|
|
334
|
-
const hours = Math.floor(mins / 60);
|
|
335
|
-
if (hours < 24)
|
|
336
|
-
return `${hours}h ago`;
|
|
337
|
-
const days = Math.floor(hours / 24);
|
|
338
|
-
return `${days}d ago`;
|
|
339
|
-
};
|
|
340
418
|
// --- Render ---
|
|
341
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: accent, children: "Branches" }), _jsxs(Text, { color: muted, dimColor: mutedDim, children: [' ', "(",
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
419
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: accent, children: "Branches" }), _jsxs(Text, { color: muted, dimColor: mutedDim, children: [' ', "(", rows.length, ")"] })] }), inputMode === 'new-branch' && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: accent, children: "New branch name: " }), _jsx(Text, { children: inputValue }), _jsx(Text, { color: muted, dimColor: mutedDim, children: "\u2588" })] })), rows.length === 0 ? (_jsx(Box, { children: _jsx(Text, { color: muted, dimColor: mutedDim, children: "No branches" }) })) : (_jsx(TableLayout, { items: rows, columns: branchColumns, cursor: clampedCursor, terminalWidth: termWidth, getKey: (row) => row.branch.name })), toastMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: warning, children: toastMessage }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: muted, dimColor: mutedDim, children: ["\u2191/\u2193 navigate ", '\u00b7', " Enter switch ", '\u00b7', " d delete ", '\u00b7', " m merge ", '\u00b7', " p PR ", '\u00b7', " P push ", '\u00b7', " c new ", '\u00b7', " w worktree ", '\u00b7', " r refresh ", '\u00b7', " / search ", '\u00b7', " Esc back", ' ', '\u00b7', " ? help"] }) }), activeOverlay?.type === 'command-bar' && (_jsx(CommandBar, { commands: paletteCommands, onCommand: handleCommandSelect, onCancel: closeOverlay })), activeOverlay?.type === 'branch-delete-confirm' && (_jsx(OverlayPanel, { title: `Delete "${activeOverlay.branch}"?${activeOverlay.worktreePath ? ' (worktree will also be removed)' : ''}`, items: [
|
|
420
|
+
{ id: 'yes', label: 'Yes, delete', value: 'yes' },
|
|
421
|
+
{ id: 'no', label: 'Cancel', value: 'no' },
|
|
422
|
+
], onSelect: (item) => {
|
|
423
|
+
if (item.value === 'yes') {
|
|
424
|
+
const { branch, worktreePath } = activeOverlay;
|
|
425
|
+
closeOverlay();
|
|
426
|
+
void (async () => {
|
|
427
|
+
try {
|
|
428
|
+
if (worktreePath) {
|
|
429
|
+
await removeWorktree(worktreePath, cwd, true);
|
|
430
|
+
}
|
|
431
|
+
await deleteBranch(branch, cwd, false);
|
|
432
|
+
showToast(`Deleted ${branch}`);
|
|
433
|
+
reloadBranches();
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
437
|
+
if (msg.includes('not fully merged')) {
|
|
438
|
+
openOverlay({
|
|
439
|
+
type: 'branch-force-delete-confirm',
|
|
440
|
+
branch,
|
|
441
|
+
worktreePath,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
showToast(msg.split('\n')[0] ?? 'Error');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
})();
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
closeOverlay();
|
|
452
|
+
}
|
|
453
|
+
}, onCancel: () => closeOverlay() })), activeOverlay?.type === 'branch-force-delete-confirm' && (_jsx(OverlayPanel, { title: `"${activeOverlay.branch}" is not fully merged. Force delete?`, items: [
|
|
454
|
+
{ id: 'yes', label: 'Yes, force delete', value: 'yes' },
|
|
455
|
+
{ id: 'no', label: 'Cancel', value: 'no' },
|
|
456
|
+
], onSelect: (item) => {
|
|
457
|
+
if (item.value === 'yes') {
|
|
458
|
+
const { branch, worktreePath } = activeOverlay;
|
|
459
|
+
closeOverlay();
|
|
460
|
+
void (async () => {
|
|
461
|
+
try {
|
|
462
|
+
if (worktreePath) {
|
|
463
|
+
await removeWorktree(worktreePath, cwd, true);
|
|
464
|
+
}
|
|
465
|
+
await deleteBranch(branch, cwd, true);
|
|
466
|
+
showToast(`Deleted ${branch}`);
|
|
467
|
+
reloadBranches();
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
showToast(err instanceof Error
|
|
471
|
+
? (err.message.split('\n')[0] ?? 'Error')
|
|
472
|
+
: 'Error');
|
|
473
|
+
}
|
|
474
|
+
})();
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
closeOverlay();
|
|
478
|
+
}
|
|
479
|
+
}, onCancel: () => closeOverlay() })), activeOverlay?.type === 'branch-merge-confirm' && (_jsx(OverlayPanel, { title: `Merge "${activeOverlay.branch}" into "${activeOverlay.into}"?`, items: [
|
|
480
|
+
{ id: 'yes', label: 'Yes, merge', value: 'yes' },
|
|
481
|
+
{ id: 'no', label: 'Cancel', value: 'no' },
|
|
482
|
+
], onSelect: (item) => {
|
|
483
|
+
if (item.value === 'yes') {
|
|
484
|
+
const { branch } = activeOverlay;
|
|
485
|
+
closeOverlay();
|
|
486
|
+
void (async () => {
|
|
487
|
+
try {
|
|
488
|
+
const result = await mergeBranch(branch, cwd);
|
|
489
|
+
if (result.success) {
|
|
490
|
+
showToast(`Merged ${branch} into ${activeOverlay.into}`);
|
|
491
|
+
const wt = rows.find((r) => r.branch.name === branch)?.worktree ??
|
|
492
|
+
null;
|
|
493
|
+
openOverlay({
|
|
494
|
+
type: 'branch-delete-confirm',
|
|
495
|
+
branch,
|
|
496
|
+
worktreePath: wt?.path ?? null,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
else if (result.hasConflicts) {
|
|
500
|
+
showToast('Merge conflicts — resolve in terminal');
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
showToast(`Merge failed: ${result.message}`);
|
|
504
|
+
}
|
|
505
|
+
reloadBranches();
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
showToast(err instanceof Error ? err.message : 'Merge failed');
|
|
509
|
+
}
|
|
510
|
+
})();
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
closeOverlay();
|
|
514
|
+
}
|
|
515
|
+
}, onCancel: () => closeOverlay() }))] }));
|
|
380
516
|
}
|
|
381
517
|
//# sourceMappingURL=BranchList.js.map
|