@ohzw/worktree-command-tui 0.1.1 → 0.1.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.
Files changed (46) hide show
  1. package/README.md +91 -26
  2. package/dist/app.d.ts +1 -1
  3. package/dist/app.js +242 -114
  4. package/dist/components/ActionPanel.d.ts +4 -2
  5. package/dist/components/ActionPanel.js +87 -135
  6. package/dist/components/ContextBar.d.ts +4 -1
  7. package/dist/components/ContextBar.js +29 -3
  8. package/dist/components/Header.js +5 -1
  9. package/dist/components/HelpWindow.d.ts +7 -0
  10. package/dist/components/HelpWindow.js +29 -0
  11. package/dist/components/LogPanel.d.ts +10 -3
  12. package/dist/components/LogPanel.js +240 -33
  13. package/dist/components/WorktreeList.js +20 -40
  14. package/dist/core/command-runner.d.ts +11 -0
  15. package/dist/core/command-runner.js +59 -7
  16. package/dist/core/config-lifecycle.d.ts +25 -0
  17. package/dist/core/config-lifecycle.js +160 -0
  18. package/dist/core/config.d.ts +2 -3
  19. package/dist/core/config.js +0 -48
  20. package/dist/core/git-metadata.d.ts +25 -0
  21. package/dist/core/git-metadata.js +84 -0
  22. package/dist/core/git-worktrees.d.ts +2 -1
  23. package/dist/core/git-worktrees.js +30 -11
  24. package/dist/core/github-metadata.d.ts +21 -0
  25. package/dist/core/github-metadata.js +153 -0
  26. package/dist/core/init.d.ts +3 -2
  27. package/dist/core/init.js +9 -57
  28. package/dist/core/log-reader.d.ts +7 -0
  29. package/dist/core/log-reader.js +59 -0
  30. package/dist/core/posix-process.d.ts +2 -2
  31. package/dist/core/posix-process.js +19 -4
  32. package/dist/core/process-control.d.ts +2 -2
  33. package/dist/core/process-control.js +5 -2
  34. package/dist/core/runtime-state.d.ts +42 -0
  35. package/dist/core/runtime-state.js +125 -0
  36. package/dist/core/runtime.d.ts +19 -39
  37. package/dist/core/runtime.js +112 -216
  38. package/dist/core/session-store.js +22 -7
  39. package/dist/core/tui-interaction.d.ts +31 -0
  40. package/dist/core/tui-interaction.js +59 -0
  41. package/dist/core/worktree-projection.d.ts +76 -0
  42. package/dist/core/worktree-projection.js +132 -0
  43. package/dist/main.js +6 -5
  44. package/dist/terminal/viewport.d.ts +15 -0
  45. package/dist/terminal/viewport.js +49 -0
  46. package/package.json +1 -1
@@ -1,5 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { getOrderedNonActiveTags, projectAction, projectNote, projectPullRequest, projectUpstream, projectWorkingTree, sanitizeInlineText, } from '../core/worktree-projection.js';
4
+ import { getScrollbarThumbRows, sliceListViewport } from '../terminal/viewport.js';
5
+ export function getActionVariant(selectedRow, activePath) {
6
+ return projectAction(selectedRow, activePath).severity;
7
+ }
8
+ function formatUpstream(selectedRow) {
9
+ const upstream = projectUpstream(selectedRow);
10
+ if (upstream.kind === 'unavailable') {
11
+ return 'unavailable';
12
+ }
13
+ if (upstream.kind === 'none') {
14
+ return '-';
15
+ }
16
+ return `${upstream.branch} (↑${upstream.ahead} ↓${upstream.behind})`;
17
+ }
3
18
  function getTagColor(tag) {
4
19
  if (tag === 'active') {
5
20
  return 'green';
@@ -15,145 +30,89 @@ function getTagColor(tag) {
15
30
  }
16
31
  return 'magenta';
17
32
  }
18
- export function getActionVariant(selectedRow, activePath) {
19
- if (selectedRow.invalidReason) {
20
- return 'error';
21
- }
22
- if (selectedRow.path === activePath) {
23
- return 'success';
24
- }
25
- if ((selectedRow.workingTree?.conflicts ?? 0) > 0) {
26
- return 'error';
27
- }
28
- if ((selectedRow.workingTree?.staged ?? 0) > 0
29
- || (selectedRow.workingTree?.unstaged ?? 0) > 0
30
- || (selectedRow.workingTree?.untracked ?? 0) > 0) {
31
- return 'info';
32
- }
33
- return 'info';
34
- }
35
- function getNoteVariant(selectedRow) {
36
- if (selectedRow.invalidReason || selectedRow.tags.includes('external')) {
37
- return selectedRow.invalidReason ? 'error' : 'info';
38
- }
39
- if ((selectedRow.workingTree?.conflicts ?? 0) > 0) {
40
- return 'error';
41
- }
42
- if ((selectedRow.workingTree?.staged ?? 0) > 0
43
- || (selectedRow.workingTree?.unstaged ?? 0) > 0
44
- || (selectedRow.workingTree?.untracked ?? 0) > 0) {
45
- return 'info';
46
- }
47
- return 'info';
33
+ function getTagLabel(tag) {
34
+ return tag === 'main' ? 'root' : tag;
48
35
  }
49
- function sanitizeInlineText(value) {
50
- return value
51
- .replace(/[\r\n\t\u2028\u2029]+/g, ' ')
52
- .replace(/[\u0000-\u001f\u007f-\u009f]/g, '')
53
- .replace(/\p{Cf}/gu, '')
54
- .replace(/\s+/g, ' ')
55
- .trim();
56
- }
57
- function formatUpstream(selectedRow) {
58
- if (selectedRow.upstreamUnavailable) {
59
- return 'unavailable';
36
+ function getWorkingTreePartLabel(kind) {
37
+ if (kind === 'staged') {
38
+ return 'index';
60
39
  }
61
- if (!selectedRow.upstream) {
62
- return '-';
40
+ if (kind === 'unstaged') {
41
+ return 'worktree';
63
42
  }
64
- return `${sanitizeInlineText(selectedRow.upstream.branch)} (↑${selectedRow.upstream.ahead} ↓${selectedRow.upstream.behind})`;
43
+ return kind;
65
44
  }
66
45
  function formatWorkingTree(selectedRow) {
67
- if (!selectedRow.workingTree) {
46
+ const workingTree = projectWorkingTree(selectedRow);
47
+ if (workingTree.kind === 'unavailable') {
68
48
  return 'unavailable';
69
49
  }
70
- const { staged, unstaged, untracked, conflicts } = selectedRow.workingTree;
71
- if (staged === 0 && unstaged === 0 && untracked === 0 && conflicts === 0) {
50
+ if (workingTree.kind === 'clean') {
72
51
  return 'clean';
73
52
  }
74
- const parts = [];
75
- if (staged > 0) {
76
- parts.push(`index ${staged}`);
77
- }
78
- if (unstaged > 0) {
79
- parts.push(`worktree ${unstaged}`);
80
- }
81
- if (untracked > 0) {
82
- parts.push(`untracked ${untracked}`);
83
- }
84
- if (conflicts > 0) {
85
- parts.push(`conflicts ${conflicts}`);
86
- }
53
+ const parts = workingTree.parts.map(part => `${getWorkingTreePartLabel(part.kind)} ${part.count}`);
87
54
  return `dirty (${parts.join(' · ')})`;
88
55
  }
56
+ function formatUtcDateTime(timestampMs) {
57
+ if (timestampMs === undefined || !Number.isFinite(timestampMs)) {
58
+ return '-';
59
+ }
60
+ const iso = new Date(timestampMs).toISOString();
61
+ return `${iso.slice(0, 10)} ${iso.slice(11, 19)} UTC`;
62
+ }
89
63
  function formatPullRequest(selectedRow) {
90
- if (!selectedRow.pullRequest || selectedRow.pullRequest.kind === 'none') {
64
+ const pullRequest = projectPullRequest(selectedRow);
65
+ if (pullRequest.kind === 'none') {
91
66
  return 'none';
92
67
  }
93
- if (selectedRow.pullRequest.kind === 'unavailable') {
68
+ if (pullRequest.kind === 'unavailable') {
94
69
  return 'unavailable';
95
70
  }
96
- const draft = selectedRow.pullRequest.isDraft ? 'draft/' : '';
97
- return `#${selectedRow.pullRequest.number} ${draft}${selectedRow.pullRequest.state.toLowerCase()} → ${sanitizeInlineText(selectedRow.pullRequest.baseBranch)}`;
71
+ const draft = pullRequest.isDraft ? 'draft/' : '';
72
+ const stateText = pullRequest.state.toLowerCase();
73
+ return `#${pullRequest.number} ${draft}${stateText} → ${pullRequest.baseBranch}`;
98
74
  }
99
- function getPullRequestLabel(selectedRow) {
100
- if (selectedRow.pullRequest?.kind === 'found' && selectedRow.pullRequest.state !== 'OPEN') {
101
- return 'Last PR';
102
- }
103
- return 'PR';
104
- }
105
- function getPullRequestTitleLabel(selectedRow) {
106
- if (selectedRow.pullRequest?.kind === 'found' && selectedRow.pullRequest.state !== 'OPEN') {
107
- return 'Last PR Title';
108
- }
109
- return 'PR Title';
110
- }
111
- export function getPullRequestColor(selectedRow) {
112
- if (!selectedRow.pullRequest || selectedRow.pullRequest.kind === 'none') {
113
- return undefined;
114
- }
115
- if (selectedRow.pullRequest.kind === 'unavailable') {
75
+ function getPullRequestColorFromProjection(pullRequest) {
76
+ if (pullRequest.kind === 'unavailable') {
116
77
  return 'red';
117
78
  }
118
- if (selectedRow.pullRequest.state !== 'OPEN') {
79
+ if (pullRequest.kind !== 'found' || pullRequest.isHistorical) {
119
80
  return undefined;
120
81
  }
121
- return selectedRow.pullRequest.isDraft ? 'yellow' : 'green';
82
+ return pullRequest.isDraft ? 'yellow' : 'green';
83
+ }
84
+ export function getPullRequestColor(selectedRow) {
85
+ return getPullRequestColorFromProjection(projectPullRequest(selectedRow));
86
+ }
87
+ function getPullRequestLabel(pullRequest) {
88
+ return pullRequest.kind === 'found' && pullRequest.isHistorical ? 'Last PR' : 'PR';
89
+ }
90
+ function getPullRequestTitleLabel(pullRequest) {
91
+ return pullRequest.kind === 'found' && pullRequest.isHistorical ? 'Last PR Title' : 'PR Title';
92
+ }
93
+ function getPullRequestDimColor(pullRequest) {
94
+ return pullRequest.kind === 'found' && pullRequest.isHistorical;
122
95
  }
123
96
  function getActionMessage(selectedRow, activePath) {
124
- if (selectedRow.invalidReason) {
97
+ const action = projectAction(selectedRow, activePath);
98
+ if (action.kind === 'blocked') {
125
99
  return 'Cannot start this worktree.';
126
100
  }
127
- if (selectedRow.path === activePath) {
101
+ if (action.kind === 'active') {
128
102
  return 'Already active. Press s to stop the current session.';
129
103
  }
130
104
  return 'Press Enter to start here and switch the active session.';
131
105
  }
132
106
  function getNotes(selectedRow) {
133
- if (selectedRow.invalidReason) {
134
- return selectedRow.invalidReason;
107
+ const note = projectNote(selectedRow);
108
+ if (note.kind === 'invalid') {
109
+ return note.invalidReason;
135
110
  }
136
- if (selectedRow.tags.includes('external')) {
111
+ if (note.kind === 'external') {
137
112
  return 'External worktree managed outside the main checkout path.';
138
113
  }
139
114
  return 'Ready to launch with the configured command in this worktree.';
140
115
  }
141
- function getOrderedTags(tags) {
142
- const tagPriority = {
143
- active: 0,
144
- main: 1,
145
- external: 2,
146
- invalid: 3,
147
- };
148
- return [...tags].sort((a, b) => {
149
- const aPriority = tagPriority[a] ?? 10;
150
- const bPriority = tagPriority[b] ?? 10;
151
- if (aPriority === bPriority) {
152
- return a.localeCompare(b);
153
- }
154
- return aPriority - bPriority;
155
- });
156
- }
157
116
  function getVariantColor(variant) {
158
117
  if (variant === 'success') {
159
118
  return 'green';
@@ -178,59 +137,52 @@ function section(label) {
178
137
  function divider() {
179
138
  return { text: ' ', dimColor: true };
180
139
  }
181
- function getPanelLines(selectedRow, activePath, compactDetails) {
140
+ function getPanelLines(selectedRow, activePath, setupAvailable, compactDetails) {
182
141
  if (!selectedRow) {
183
142
  return [{ text: 'No worktrees found.', dimColor: true }];
184
143
  }
185
144
  const lines = [section('Identity')];
186
145
  const showFullPath = !compactDetails && selectedRow.shortPath !== selectedRow.path;
187
146
  const showTags = !compactDetails;
188
- const pullRequestTitle = selectedRow.pullRequest?.kind === 'found' && !compactDetails
189
- ? sanitizeInlineText(selectedRow.pullRequest.title)
190
- : null;
147
+ const pullRequest = projectPullRequest(selectedRow);
148
+ const pullRequestTitle = pullRequest.kind === 'found' && !compactDetails ? pullRequest.title : null;
191
149
  lines.push({ text: `Branch: ${sanitizeInlineText(selectedRow.branch)}`, bold: true }, { text: `Path: ${sanitizeInlineText(selectedRow.shortPath)}` });
192
150
  if (showFullPath) {
193
151
  lines.push({ text: `Full Path: ${sanitizeInlineText(selectedRow.path)}` });
194
152
  }
195
153
  lines.push({ text: `HEAD: ${selectedRow.headSha || '-'}` });
154
+ lines.push({ text: `Branch Created: ${formatUtcDateTime(selectedRow.branchCreatedAtMs)}` });
196
155
  if (showTags) {
197
- for (const tag of getOrderedTags(selectedRow.tags.filter(tag => tag !== 'active'))) {
198
- lines.push({ text: tag.toUpperCase(), color: getTagColor(tag) });
156
+ for (const { tag } of getOrderedNonActiveTags(selectedRow.tags)) {
157
+ lines.push({ text: getTagLabel(tag).toUpperCase(), color: getTagColor(tag) });
199
158
  }
200
159
  }
201
160
  lines.push(divider(), section('Git / PR'), { text: `Upstream: ${formatUpstream(selectedRow)}` }, { text: `Status: ${formatWorkingTree(selectedRow)}` }, {
202
- text: `${getPullRequestLabel(selectedRow)}: ${formatPullRequest(selectedRow)}`,
203
- color: getPullRequestColor(selectedRow),
204
- dimColor: selectedRow.pullRequest?.kind === 'found' && selectedRow.pullRequest.state !== 'OPEN',
161
+ text: `${getPullRequestLabel(pullRequest)}: ${formatPullRequest(selectedRow)}`,
162
+ color: getPullRequestColorFromProjection(pullRequest),
163
+ dimColor: getPullRequestDimColor(pullRequest),
205
164
  });
206
165
  if (pullRequestTitle) {
207
- lines.push({ text: `${getPullRequestTitleLabel(selectedRow)}: ${pullRequestTitle}`, dimColor: selectedRow.pullRequest?.kind === 'found' && selectedRow.pullRequest.state !== 'OPEN' });
166
+ lines.push({ text: `${getPullRequestTitleLabel(pullRequest)}: ${pullRequestTitle}`, dimColor: getPullRequestDimColor(pullRequest) });
208
167
  }
209
168
  const actionVariant = getActionVariant(selectedRow, activePath);
210
- const noteVariant = getNoteVariant(selectedRow);
211
- lines.push(divider(), section('Action'), { text: `${getVariantIcon(actionVariant)} ${getActionMessage(selectedRow, activePath)}`, color: getVariantColor(actionVariant) }, section('Notes'), { text: `${getVariantIcon(noteVariant)} ${getNotes(selectedRow)}`, color: getVariantColor(noteVariant) });
169
+ const noteVariant = projectNote(selectedRow).severity;
170
+ lines.push(divider(), section('Action'), { text: `${getVariantIcon(actionVariant)} ${getActionMessage(selectedRow, activePath)}`, color: getVariantColor(actionVariant) });
171
+ if (setupAvailable) {
172
+ lines.push({ text: 'ℹ Press i to run setup in this worktree.', color: 'blue' });
173
+ }
174
+ lines.push(section('Notes'), { text: `${getVariantIcon(noteVariant)} ${getNotes(selectedRow)}`, color: getVariantColor(noteVariant) });
212
175
  return lines;
213
176
  }
214
- function getScrollbarThumbRows(totalLines, viewportHeight, scrollOffset) {
215
- if (totalLines <= viewportHeight) {
216
- return new Set();
217
- }
218
- const thumbSize = Math.max(1, Math.floor((viewportHeight / totalLines) * viewportHeight));
219
- const maxScrollOffset = Math.max(1, totalLines - viewportHeight);
220
- const thumbStart = Math.round((scrollOffset / maxScrollOffset) * (viewportHeight - thumbSize));
221
- return new Set(Array.from({ length: thumbSize }, (_, index) => thumbStart + index));
222
- }
223
- export function ActionPanel({ selectedRow, activePath, stacked, width, height, compactDetails, scrollOffset = 0, }) {
224
- const lines = getPanelLines(selectedRow, activePath, compactDetails ?? false);
225
- const contentViewportHeight = height === undefined ? undefined : Math.max(1, height - 3);
226
- const maxScrollOffset = contentViewportHeight === undefined ? 0 : Math.max(0, lines.length - contentViewportHeight);
227
- const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
228
- const visibleLines = contentViewportHeight === undefined
229
- ? lines
230
- : lines.slice(effectiveScrollOffset, effectiveScrollOffset + contentViewportHeight);
231
- const showScrollbar = contentViewportHeight !== undefined && lines.length > contentViewportHeight;
177
+ export function ActionPanel({ selectedRow, activePath, setupAvailable, stacked, width, height, compactDetails, scrollOffset = 0, }) {
178
+ const lines = getPanelLines(selectedRow, activePath, setupAvailable, compactDetails ?? false);
179
+ const viewport = height === undefined ? undefined : sliceListViewport(lines, height - 3, scrollOffset);
180
+ const contentViewportHeight = viewport?.viewportHeight;
181
+ const effectiveScrollOffset = viewport?.scrollOffset ?? 0;
182
+ const visibleLines = viewport?.visibleItems ?? lines;
183
+ const showScrollbar = viewport !== undefined && lines.length > viewport.viewportHeight;
232
184
  const scrollbarThumbRows = showScrollbar
233
- ? getScrollbarThumbRows(lines.length, contentViewportHeight, effectiveScrollOffset)
185
+ ? getScrollbarThumbRows(lines.length, viewport.viewportHeight, viewport.scrollOffset)
234
186
  : new Set();
235
187
  return (_jsxs(Box, { width: width, height: height, flexGrow: stacked ? 0 : 1, flexShrink: 1, borderStyle: "round", borderColor: "magenta", flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, color: "magenta", wrap: "truncate-end", children: "Selection / Action" }), _jsx(Box, { height: contentViewportHeight, flexDirection: "column", overflow: "hidden", children: visibleLines.map((line, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { color: line.color, dimColor: line.dimColor, bold: line.bold, wrap: "truncate-end", children: line.text }) }), showScrollbar ? (_jsx(Text, { color: scrollbarThumbRows.has(index) ? 'magenta' : 'gray', dimColor: !scrollbarThumbRows.has(index), children: scrollbarThumbRows.has(index) ? '█' : '│' })) : null] }, `${effectiveScrollOffset + index}-${line.text}`))) })] }));
236
188
  }
@@ -1,5 +1,8 @@
1
1
  import React from 'react';
2
2
  import type { AppStatus } from '../core/runtime.js';
3
- export declare function ContextBar({ status }: {
3
+ export declare function ContextBar({ status, setupAvailable, editorAvailable, confirmationOpen, }: {
4
4
  status: AppStatus;
5
+ setupAvailable: boolean;
6
+ editorAvailable: boolean;
7
+ confirmationOpen: boolean;
5
8
  }): React.JSX.Element;
@@ -1,9 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
2
3
  import { Box, Text } from 'ink';
3
4
  import { Spinner } from '@inkjs/ui';
5
+ import { sanitizeInlineText } from '../core/worktree-projection.js';
4
6
  const KIND_TO_ICON = {
5
7
  idle: 'ℹ',
6
8
  starting: '⚠',
9
+ 'setting-up': '⚠',
7
10
  running: '✓',
8
11
  stopping: '⚠',
9
12
  error: '✘',
@@ -11,11 +14,34 @@ const KIND_TO_ICON = {
11
14
  const KIND_TO_COLOR = {
12
15
  idle: 'blue',
13
16
  starting: 'yellow',
17
+ 'setting-up': 'yellow',
14
18
  running: 'green',
15
19
  stopping: 'yellow',
16
20
  error: 'red',
17
21
  };
18
- export function ContextBar({ status }) {
19
- const isBusy = status.kind === 'starting' || status.kind === 'stopping';
20
- return (_jsxs(Box, { borderStyle: "round", borderColor: KIND_TO_COLOR[status.kind], flexDirection: "column", paddingX: 1, children: [isBusy ? (_jsx(Spinner, { label: `Status: ${status.kind} — ${status.message}` })) : (_jsxs(Text, { color: KIND_TO_COLOR[status.kind], wrap: "truncate-end", children: [KIND_TO_ICON[status.kind], " Status: ", status.kind, " \u2014 ", status.message] })), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Keys: \u2191\u2193/jk move g/G first/last Wheel/PgUp/PgDn list & selection scroll [/] log scroll L full-screen logs Enter start/switch s stop r refresh q quit" })] }));
22
+ function buildKeyHints(setupAvailable, editorAvailable, confirmationOpen) {
23
+ if (confirmationOpen) {
24
+ return [
25
+ { binding: 'd/y', label: 'Confirm' },
26
+ { binding: 'Esc/n/q', label: 'Cancel' },
27
+ ];
28
+ }
29
+ const hints = [
30
+ { binding: '↑↓/jk', label: 'Move' },
31
+ { binding: 'Enter', label: 'Switch' },
32
+ ];
33
+ if (setupAvailable) {
34
+ hints.push({ binding: 'i', label: 'Setup' });
35
+ }
36
+ if (editorAvailable) {
37
+ hints.push({ binding: 'e', label: 'Editor' });
38
+ }
39
+ hints.push({ binding: 'o', label: 'Open PR' }, { binding: 'd', label: 'Delete' }, { binding: 'L', label: 'Logs' }, { binding: 's', label: 'Stop' }, { binding: 'r', label: 'Refresh' }, { binding: '?', label: 'Help' }, { binding: 'q', label: 'Quit' });
40
+ return hints;
41
+ }
42
+ export function ContextBar({ status, setupAvailable, editorAvailable, confirmationOpen, }) {
43
+ const isBusy = status.kind === 'setting-up' || status.kind === 'starting' || status.kind === 'stopping';
44
+ const keyHints = buildKeyHints(setupAvailable, editorAvailable, confirmationOpen);
45
+ const statusMessage = sanitizeInlineText(status.message);
46
+ return (_jsxs(Box, { borderStyle: "round", borderColor: KIND_TO_COLOR[status.kind], flexDirection: "column", paddingX: 1, children: [isBusy ? (_jsx(Spinner, { label: `Status: ${status.kind} — ${statusMessage}` })) : (_jsxs(Text, { color: KIND_TO_COLOR[status.kind], wrap: "truncate-end", children: [KIND_TO_ICON[status.kind], " Status: ", status.kind, " \u2014 ", statusMessage] })), _jsx(Text, { wrap: "truncate-end", children: keyHints.map((hint, hintIndex) => (_jsxs(React.Fragment, { children: [hintIndex === 0 ? null : _jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: "white", children: hint.binding }), _jsxs(Text, { dimColor: true, children: [" ", hint.label] })] }, hint.binding))) })] }));
21
47
  }
@@ -1,5 +1,9 @@
1
1
  import { jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { sanitizeInlineText } from '../core/worktree-projection.js';
3
4
  export function Header({ repoName, namespace, activeBranch, }) {
4
- return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "blue", wrap: "truncate-end", children: ["Worktree Command TUI \u00B7 Repo: ", repoName] }), _jsxs(Text, { color: "green", wrap: "truncate-end", children: ["Active: ", activeBranch ?? '-'] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["Namespace: ", namespace] })] }));
5
+ const safeRepoName = sanitizeInlineText(repoName);
6
+ const safeNamespace = sanitizeInlineText(namespace);
7
+ const safeActiveBranch = activeBranch === null ? '-' : sanitizeInlineText(activeBranch);
8
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "blue", wrap: "truncate-end", children: ["Worktree Command TUI \u00B7 Repo: ", safeRepoName] }), _jsxs(Text, { color: "green", wrap: "truncate-end", children: ["Active: ", safeActiveBranch] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["Namespace: ", safeNamespace] })] }));
5
9
  }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ export declare function HelpWindow({ setupAvailable, editorAvailable, width, height }: {
3
+ setupAvailable: boolean;
4
+ editorAvailable: boolean;
5
+ width: number;
6
+ height: number;
7
+ }): React.JSX.Element;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ function buildHelpLines(setupAvailable, editorAvailable) {
4
+ const lines = [
5
+ { section: 'Movement', binding: '↑↓/jk', description: 'move selection' },
6
+ { section: 'Movement', binding: 'g/G', description: 'first/last' },
7
+ { section: 'Scroll', binding: 'Wheel', description: 'pane under cursor' },
8
+ { section: 'Scroll', binding: 'PageUp/PageDn', description: 'selection page' },
9
+ { section: 'Logs', binding: 'L', description: 'full-screen logs' },
10
+ { section: 'Logs', binding: '[/]', description: 'scroll log' },
11
+ { section: 'Actions', binding: 'Enter', description: 'start/switch worktree' },
12
+ ];
13
+ if (setupAvailable) {
14
+ lines.push({ section: 'Actions', binding: 'i', description: 'setup selected worktree' });
15
+ }
16
+ if (editorAvailable) {
17
+ lines.push({ section: 'Actions', binding: 'e', description: 'open selected worktree in editor' });
18
+ }
19
+ lines.push({ section: 'Actions', binding: 'o', description: 'open selected pull request' }, { section: 'Actions', binding: 'd', description: 'arm worktree deletion' }, { section: 'Actions', binding: 's', description: 'stop active session' }, { section: 'Actions', binding: 'r', description: 'refresh' }, { section: 'Actions', binding: 'q', description: 'quit' }, { section: 'Help', binding: 'Esc/q/?', description: 'close help' }, { section: 'Help', binding: 'd/y', description: 'confirm delete after arming' }, { section: 'Help', binding: 'Esc/n/q', description: 'cancel delete confirmation' });
20
+ return lines;
21
+ }
22
+ export function HelpWindow({ setupAvailable, editorAvailable, width, height }) {
23
+ let previousSection = '';
24
+ return (_jsxs(Box, { width: width, height: height, borderStyle: "round", borderColor: "blue", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "blue", wrap: "truncate-end", children: "Keyboard Help" }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: "Primary shortcuts are shown in the status footer. Advanced shortcuts live here." }), buildHelpLines(setupAvailable, editorAvailable).map(line => {
25
+ const section = line.section === previousSection ? '' : line.section;
26
+ previousSection = line.section;
27
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 10, children: _jsx(Text, { dimColor: true, wrap: "truncate-end", children: section }) }), _jsx(Box, { width: 16, children: _jsx(Text, { color: "white", wrap: "truncate-end", children: line.binding }) }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: line.description })] }, `${line.section}-${line.binding}`));
28
+ })] }));
29
+ }
@@ -1,8 +1,15 @@
1
1
  import type { AppLogEntry } from '../core/runtime.js';
2
- type LineSpec = {
3
- text: string;
4
- color?: 'cyan';
2
+ type LogColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray';
3
+ type LineStyle = {
4
+ color?: LogColor;
5
5
  dimColor?: boolean;
6
+ bold?: boolean;
7
+ };
8
+ type LineSegment = {
9
+ text: string;
10
+ } & LineStyle;
11
+ type LineSpec = {
12
+ segments: LineSegment[];
6
13
  };
7
14
  export declare function buildLogLines(logs: AppLogEntry[]): LineSpec[];
8
15
  export declare function LogPanel({ logs, width, height, scrollOffset, title, }: {