@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.
Files changed (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/auth/gitlab.js +12 -5
  3. package/dist/auth/gitlab.js.map +1 -1
  4. package/dist/commands.d.ts +5 -0
  5. package/dist/commands.js +90 -1
  6. package/dist/commands.js.map +1 -1
  7. package/dist/components/BranchList.js +417 -281
  8. package/dist/components/BranchList.js.map +1 -1
  9. package/dist/components/ColorPill.js +3 -3
  10. package/dist/components/ColorPill.js.map +1 -1
  11. package/dist/components/CommandBar.d.ts +8 -0
  12. package/dist/components/CommandBar.js +142 -0
  13. package/dist/components/CommandBar.js.map +1 -0
  14. package/dist/components/DetailPanel.js +2 -2
  15. package/dist/components/DetailPanel.js.map +1 -1
  16. package/dist/components/ErrorBoundary.d.ts +1 -1
  17. package/dist/components/Header.js +6 -2
  18. package/dist/components/Header.js.map +1 -1
  19. package/dist/components/HelpScreen.js +3 -2
  20. package/dist/components/HelpScreen.js.map +1 -1
  21. package/dist/components/OverlayPanel.d.ts +1 -1
  22. package/dist/components/PullRequestList.js +118 -18
  23. package/dist/components/PullRequestList.js.map +1 -1
  24. package/dist/components/Settings.js +5 -0
  25. package/dist/components/Settings.js.map +1 -1
  26. package/dist/components/StatusScreen.js +16 -8
  27. package/dist/components/StatusScreen.js.map +1 -1
  28. package/dist/components/TableLayout.d.ts +26 -10
  29. package/dist/components/TableLayout.js +67 -146
  30. package/dist/components/TableLayout.js.map +1 -1
  31. package/dist/components/WorkItemForm.js +5 -0
  32. package/dist/components/WorkItemForm.js.map +1 -1
  33. package/dist/components/WorkItemList.js +119 -93
  34. package/dist/components/WorkItemList.js.map +1 -1
  35. package/dist/git.d.ts +8 -0
  36. package/dist/git.js.map +1 -1
  37. package/dist/implement.js +7 -3
  38. package/dist/implement.js.map +1 -1
  39. package/dist/stores/backendDataStore.d.ts +6 -0
  40. package/dist/stores/backendDataStore.js +81 -1
  41. package/dist/stores/backendDataStore.js.map +1 -1
  42. package/dist/stores/navigationStore.d.ts +4 -0
  43. package/dist/stores/navigationStore.js +10 -0
  44. package/dist/stores/navigationStore.js.map +1 -1
  45. package/dist/stores/uiStore.d.ts +12 -0
  46. package/dist/stores/uiStore.js.map +1 -1
  47. package/dist/test-helpers.d.ts +7 -0
  48. package/dist/test-helpers.js +208 -0
  49. package/dist/test-helpers.js.map +1 -0
  50. package/drizzle/meta/_journal.json +1 -1
  51. package/package.json +1 -1
@@ -1,160 +1,336 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useCallback, useMemo } from 'react';
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 Spinner from 'ink-spinner';
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 { listBranches, listWorktrees, getCurrentBranch, checkoutBranch, hasUncommittedChanges, createBranch, } from '../git.js';
9
- import { deleteBranch, mergeBranch, removeWorktree, fetchAll, pushBranch, } from '../git-async.js';
10
- import { linkBranchToItem } from '../branch-links.js';
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 items = useBackendDataStore((s) => s.items);
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 [rows, setRows] = useState([]);
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 [filterText, setFilterText] = useState('');
26
- const loadBranches = useCallback(() => {
27
- const branches = listBranches(cwd);
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
- loadBranches();
53
- setFetching(true);
54
- fetchAll(cwd)
55
- .then(() => {
56
- loadBranches(); // reload with updated remote info
57
- })
58
- .catch(() => {
59
- // fetch failed (no remote, offline, etc) — ignore
60
- })
61
- .finally(() => {
62
- setFetching(false);
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, filteredRows.length - 1));
75
- if (clampedCursor !== cursor && filteredRows.length > 0) {
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), 3000);
125
+ const timer = setTimeout(() => setToastMessage(null), 10000);
83
126
  return () => clearTimeout(timer);
84
127
  }, [toastMessage]);
85
128
  const showToast = (msg) => setToastMessage(msg);
86
- const currentRow = filteredRows[clampedCursor];
87
- useInput((input, key) => {
88
- // --- Confirmation mode ---
89
- if (confirmation) {
90
- if (input === 'y' || input === 'Y') {
91
- const conf = confirmation;
92
- setConfirmation(null);
93
- void (async () => {
94
- try {
95
- if (conf.type === 'delete' || conf.type === 'force-delete') {
96
- const force = conf.type === 'force-delete';
97
- if (conf.worktreePath) {
98
- await removeWorktree(conf.worktreePath, cwd, true);
99
- }
100
- await deleteBranch(conf.branch, cwd, force);
101
- showToast(`Deleted ${conf.branch}`);
102
- loadBranches();
103
- }
104
- else if (conf.type === 'merge') {
105
- const result = await mergeBranch(conf.branch, cwd);
106
- if (result.success) {
107
- showToast(`Merged ${conf.branch} into ${conf.into}`);
108
- // Offer to delete merged branch
109
- const wt = rows.find((r) => r.branch.name === conf.branch)?.worktree ??
110
- null;
111
- setConfirmation({
112
- type: 'delete',
113
- branch: conf.branch,
114
- worktreePath: wt?.path ?? null,
115
- unmerged: false,
116
- });
117
- }
118
- else if (result.hasConflicts) {
119
- showToast('Merge conflicts — resolve in terminal');
120
- }
121
- else {
122
- showToast(`Merge failed: ${result.message}`);
123
- }
124
- loadBranches();
125
- }
126
- }
127
- catch (err) {
128
- const msg = err instanceof Error ? err.message : String(err);
129
- if (conf.type === 'delete' &&
130
- !conf.unmerged &&
131
- msg.includes('not fully merged')) {
132
- setConfirmation({
133
- type: 'force-delete',
134
- branch: conf.branch,
135
- worktreePath: conf.worktreePath,
136
- });
137
- }
138
- else {
139
- showToast(msg.split('\n')[0] ?? 'Error');
140
- }
141
- }
142
- })();
143
- return;
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
- if (input === 'n' || input === 'N' || key.escape) {
146
- setConfirmation(null);
147
- return;
211
+ catch (err) {
212
+ showToast(err instanceof Error ? err.message : 'Push failed');
148
213
  }
149
- return; // block other input during confirmation
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
- // --- Input modes (new branch name, search) ---
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
- loadBranches();
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 (input === 'j' || key.downArrow) {
207
- setCursor((c) => Math.min(c + 1, filteredRows.length - 1));
375
+ if (key.downArrow) {
376
+ setCursor((c) => Math.min(c + 1, rows.length - 1));
208
377
  return;
209
378
  }
210
- if (input === 'k' || key.upArrow) {
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
- if (currentRow.branch.current) {
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
- if (!currentRow.worktree) {
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
- if (currentRow.branch.current) {
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
- if (currentRow.branch.current) {
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
- void (async () => {
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
- // Navigate back to list and trigger PR creation for this branch
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
- setFetching(true);
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
- // Search
320
- if (input === '/') {
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: [' ', "(", filteredRows.length, ")"] }), fetching && (_jsxs(Text, { color: warning, children: [' ', _jsx(Spinner, { type: "dots" }), " Fetching..."] })), filterText && (_jsxs(Text, { color: muted, dimColor: mutedDim, children: [' ', "filter: ", filterText] }))] }), 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" })] })), inputMode === 'search' && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: accent, children: "/" }), _jsx(Text, { children: inputValue }), _jsx(Text, { color: muted, dimColor: mutedDim, children: "\u2588" })] })), confirmation && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: warning, children: [confirmation.type === 'delete' &&
342
- `Delete branch "${confirmation.branch}"?` +
343
- (confirmation.worktreePath
344
- ? ` This will also remove worktree at ${confirmation.worktreePath}.`
345
- : ''), confirmation.type === 'force-delete' &&
346
- `Branch "${confirmation.branch}" is not fully merged. Force delete?` +
347
- (confirmation.worktreePath
348
- ? ` This will also remove worktree at ${confirmation.worktreePath}.`
349
- : ''), confirmation.type === 'merge' &&
350
- `Merge "${confirmation.branch}" into "${confirmation.into}"?`, ' (y/n)'] }) })), filteredRows.length === 0 ? (_jsx(Box, { children: _jsx(Text, { color: muted, dimColor: mutedDim, children: "No branches" }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: 32, children: _jsx(Text, { bold: true, color: muted, dimColor: mutedDim, children: "Branch" }) }), _jsx(Box, { width: 30, children: _jsx(Text, { bold: true, color: muted, dimColor: mutedDim, children: "Item" }) }), _jsx(Box, { width: 20, children: _jsx(Text, { bold: true, color: muted, dimColor: mutedDim, children: "Worktree" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { bold: true, color: muted, dimColor: mutedDim, children: "Remote" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { bold: true, color: muted, dimColor: mutedDim, children: "Last Commit" }) })] }), filteredRows.map((row, index) => {
351
- const isSelected = index === clampedCursor;
352
- const isTic = row.branch.name.startsWith('tic/');
353
- const prefix = row.branch.current ? '* ' : ' ';
354
- const branchDisplay = prefix + row.branch.name;
355
- const truncBranch = branchDisplay.length > 30
356
- ? branchDisplay.slice(0, 30) + '\u2026'
357
- : branchDisplay;
358
- const itemDisplay = row.linkedItem
359
- ? `#${row.linkedItem.id} ${row.linkedItem.title}`
360
- : '';
361
- const truncItem = itemDisplay.length > 28
362
- ? itemDisplay.slice(0, 28) + '\u2026'
363
- : itemDisplay;
364
- const wtDisplay = row.worktree ? '\u2713' : '';
365
- let remoteDisplay = '--';
366
- if (row.branch.upstream) {
367
- const parts = [];
368
- if (row.branch.ahead > 0)
369
- parts.push(`\u2191${row.branch.ahead}`);
370
- if (row.branch.behind > 0)
371
- parts.push(`\u2193${row.branch.behind}`);
372
- remoteDisplay = parts.length > 0 ? parts.join(' ') : '\u2713';
373
- }
374
- return (_jsxs(Box, { children: [_jsx(Box, { width: 32, children: _jsx(Text, { inverse: isSelected, bold: isSelected || isTic, color: isTic && !isSelected ? accent : undefined, children: truncBranch }) }), _jsx(Box, { width: 30, children: _jsx(Text, { inverse: isSelected, color: row.linkedItem
375
- ? isSelected
376
- ? undefined
377
- : muted
378
- : undefined, dimColor: !row.linkedItem ? mutedDim : undefined, children: truncItem }) }), _jsx(Box, { width: 20, children: _jsx(Text, { inverse: isSelected, children: wtDisplay }) }), _jsx(Box, { width: 10, children: _jsx(Text, { inverse: isSelected, children: remoteDisplay }) }), _jsx(Box, { width: 10, children: _jsx(Text, { inverse: isSelected, color: muted, dimColor: mutedDim, children: relativeTime(row.branch.lastCommitDate) }) })] }, row.branch.name));
379
- })] })), toastMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: warning, children: toastMessage }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: muted, dimColor: mutedDim, children: ["j/k navigate ", '\u00b7', " Enter switch ", '\u00b7', " d delete ", '\u00b7', " m merge ", '\u00b7', " P push ", '\u00b7', " n new ", '\u00b7', " w worktree", ' ', '\u00b7', " r refresh ", '\u00b7', " / search ", '\u00b7', " Esc back", ' ', '\u00b7', " ? help"] }) })] }));
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