symphony-github 0.1.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 (166) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +341 -0
  3. package/config.example.yaml +101 -0
  4. package/dist/agents/launcher.d.ts +24 -0
  5. package/dist/agents/launcher.d.ts.map +1 -0
  6. package/dist/agents/launcher.js +152 -0
  7. package/dist/agents/launcher.js.map +1 -0
  8. package/dist/agents/registry.d.ts +10 -0
  9. package/dist/agents/registry.d.ts.map +1 -0
  10. package/dist/agents/registry.js +324 -0
  11. package/dist/agents/registry.js.map +1 -0
  12. package/dist/agents/runner.d.ts +58 -0
  13. package/dist/agents/runner.d.ts.map +1 -0
  14. package/dist/agents/runner.js +1190 -0
  15. package/dist/agents/runner.js.map +1 -0
  16. package/dist/app.d.ts +11 -0
  17. package/dist/app.d.ts.map +1 -0
  18. package/dist/app.js +829 -0
  19. package/dist/app.js.map +1 -0
  20. package/dist/components/ActivityView.d.ts +9 -0
  21. package/dist/components/ActivityView.d.ts.map +1 -0
  22. package/dist/components/ActivityView.js +73 -0
  23. package/dist/components/ActivityView.js.map +1 -0
  24. package/dist/components/Header.d.ts +12 -0
  25. package/dist/components/Header.d.ts.map +1 -0
  26. package/dist/components/Header.js +44 -0
  27. package/dist/components/Header.js.map +1 -0
  28. package/dist/components/IssueList.d.ts +10 -0
  29. package/dist/components/IssueList.d.ts.map +1 -0
  30. package/dist/components/IssueList.js +119 -0
  31. package/dist/components/IssueList.js.map +1 -0
  32. package/dist/components/Onboarding.d.ts +26 -0
  33. package/dist/components/Onboarding.d.ts.map +1 -0
  34. package/dist/components/Onboarding.js +948 -0
  35. package/dist/components/Onboarding.js.map +1 -0
  36. package/dist/components/PaneView.d.ts +9 -0
  37. package/dist/components/PaneView.d.ts.map +1 -0
  38. package/dist/components/PaneView.js +74 -0
  39. package/dist/components/PaneView.js.map +1 -0
  40. package/dist/components/StartupRecoveryView.d.ts +13 -0
  41. package/dist/components/StartupRecoveryView.d.ts.map +1 -0
  42. package/dist/components/StartupRecoveryView.js +85 -0
  43. package/dist/components/StartupRecoveryView.js.map +1 -0
  44. package/dist/components/StatusBar.d.ts +9 -0
  45. package/dist/components/StatusBar.d.ts.map +1 -0
  46. package/dist/components/StatusBar.js +70 -0
  47. package/dist/components/StatusBar.js.map +1 -0
  48. package/dist/components/TableView.d.ts +8 -0
  49. package/dist/components/TableView.d.ts.map +1 -0
  50. package/dist/components/TableView.js +87 -0
  51. package/dist/components/TableView.js.map +1 -0
  52. package/dist/config/index.d.ts +18 -0
  53. package/dist/config/index.d.ts.map +1 -0
  54. package/dist/config/index.js +357 -0
  55. package/dist/config/index.js.map +1 -0
  56. package/dist/git/merge.d.ts +23 -0
  57. package/dist/git/merge.d.ts.map +1 -0
  58. package/dist/git/merge.js +131 -0
  59. package/dist/git/merge.js.map +1 -0
  60. package/dist/git/utils.d.ts +34 -0
  61. package/dist/git/utils.d.ts.map +1 -0
  62. package/dist/git/utils.js +214 -0
  63. package/dist/git/utils.js.map +1 -0
  64. package/dist/git/worktree.d.ts +23 -0
  65. package/dist/git/worktree.d.ts.map +1 -0
  66. package/dist/git/worktree.js +116 -0
  67. package/dist/git/worktree.js.map +1 -0
  68. package/dist/index.d.ts +3 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +225 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/paths.d.ts +21 -0
  73. package/dist/paths.d.ts.map +1 -0
  74. package/dist/paths.js +59 -0
  75. package/dist/paths.js.map +1 -0
  76. package/dist/runModes.d.ts +7 -0
  77. package/dist/runModes.d.ts.map +1 -0
  78. package/dist/runModes.js +36 -0
  79. package/dist/runModes.js.map +1 -0
  80. package/dist/services/daemon.d.ts +85 -0
  81. package/dist/services/daemon.d.ts.map +1 -0
  82. package/dist/services/daemon.js +836 -0
  83. package/dist/services/daemon.js.map +1 -0
  84. package/dist/services/github.d.ts +101 -0
  85. package/dist/services/github.d.ts.map +1 -0
  86. package/dist/services/github.js +367 -0
  87. package/dist/services/github.js.map +1 -0
  88. package/dist/services/githubProgressReporter.d.ts +33 -0
  89. package/dist/services/githubProgressReporter.d.ts.map +1 -0
  90. package/dist/services/githubProgressReporter.js +272 -0
  91. package/dist/services/githubProgressReporter.js.map +1 -0
  92. package/dist/services/runtime.d.ts +43 -0
  93. package/dist/services/runtime.d.ts.map +1 -0
  94. package/dist/services/runtime.js +126 -0
  95. package/dist/services/runtime.js.map +1 -0
  96. package/dist/services/state.d.ts +43 -0
  97. package/dist/services/state.d.ts.map +1 -0
  98. package/dist/services/state.js +176 -0
  99. package/dist/services/state.js.map +1 -0
  100. package/dist/services/tmux.d.ts +50 -0
  101. package/dist/services/tmux.d.ts.map +1 -0
  102. package/dist/services/tmux.js +157 -0
  103. package/dist/services/tmux.js.map +1 -0
  104. package/dist/swarm/backlog.d.ts +25 -0
  105. package/dist/swarm/backlog.d.ts.map +1 -0
  106. package/dist/swarm/backlog.js +83 -0
  107. package/dist/swarm/backlog.js.map +1 -0
  108. package/dist/swarm/config.d.ts +14 -0
  109. package/dist/swarm/config.d.ts.map +1 -0
  110. package/dist/swarm/config.js +112 -0
  111. package/dist/swarm/config.js.map +1 -0
  112. package/dist/swarm/dependencies.d.ts +36 -0
  113. package/dist/swarm/dependencies.d.ts.map +1 -0
  114. package/dist/swarm/dependencies.js +141 -0
  115. package/dist/swarm/dependencies.js.map +1 -0
  116. package/dist/swarm/director.d.ts +67 -0
  117. package/dist/swarm/director.d.ts.map +1 -0
  118. package/dist/swarm/director.js +358 -0
  119. package/dist/swarm/director.js.map +1 -0
  120. package/dist/swarm/directorPrompt.d.ts +15 -0
  121. package/dist/swarm/directorPrompt.d.ts.map +1 -0
  122. package/dist/swarm/directorPrompt.js +60 -0
  123. package/dist/swarm/directorPrompt.js.map +1 -0
  124. package/dist/swarm/index.d.ts +7 -0
  125. package/dist/swarm/index.d.ts.map +1 -0
  126. package/dist/swarm/index.js +6 -0
  127. package/dist/swarm/index.js.map +1 -0
  128. package/dist/swarm/proposals.d.ts +29 -0
  129. package/dist/swarm/proposals.d.ts.map +1 -0
  130. package/dist/swarm/proposals.js +141 -0
  131. package/dist/swarm/proposals.js.map +1 -0
  132. package/dist/swarm/types.d.ts +65 -0
  133. package/dist/swarm/types.d.ts.map +1 -0
  134. package/dist/swarm/types.js +3 -0
  135. package/dist/swarm/types.js.map +1 -0
  136. package/dist/theme.d.ts +64 -0
  137. package/dist/theme.d.ts.map +1 -0
  138. package/dist/theme.js +161 -0
  139. package/dist/theme.js.map +1 -0
  140. package/dist/triggers/index.d.ts +17 -0
  141. package/dist/triggers/index.d.ts.map +1 -0
  142. package/dist/triggers/index.js +124 -0
  143. package/dist/triggers/index.js.map +1 -0
  144. package/dist/types.d.ts +327 -0
  145. package/dist/types.d.ts.map +1 -0
  146. package/dist/types.js +6 -0
  147. package/dist/types.js.map +1 -0
  148. package/dist/utils/duplicateDetection.d.ts +14 -0
  149. package/dist/utils/duplicateDetection.d.ts.map +1 -0
  150. package/dist/utils/duplicateDetection.js +45 -0
  151. package/dist/utils/duplicateDetection.js.map +1 -0
  152. package/dist/utils/shell.d.ts +46 -0
  153. package/dist/utils/shell.d.ts.map +1 -0
  154. package/dist/utils/shell.js +79 -0
  155. package/dist/utils/shell.js.map +1 -0
  156. package/dist/utils/slug.d.ts +13 -0
  157. package/dist/utils/slug.d.ts.map +1 -0
  158. package/dist/utils/slug.js +32 -0
  159. package/dist/utils/slug.js.map +1 -0
  160. package/dist/version.d.ts +28 -0
  161. package/dist/version.d.ts.map +1 -0
  162. package/dist/version.js +105 -0
  163. package/dist/version.js.map +1 -0
  164. package/examples/run-claude.example.sh +11 -0
  165. package/examples/run-codex.example.sh +11 -0
  166. package/package.json +68 -0
package/dist/app.js ADDED
@@ -0,0 +1,829 @@
1
+ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
2
+ import { Box, Text, useInput, useApp, useStdout } from 'ink';
3
+ import { Header } from './components/Header.js';
4
+ import { Onboarding } from './components/Onboarding.js';
5
+ import { StartupRecoveryView } from './components/StartupRecoveryView.js';
6
+ import { StatusBar } from './components/StatusBar.js';
7
+ import { loadConfigDocument, writeConfigAndLoad } from './config/index.js';
8
+ import { Daemon } from './services/daemon.js';
9
+ import { TmuxService } from './services/tmux.js';
10
+ import { colors, gradient, statusColor, statusIcon, sparkline } from './theme.js';
11
+ import { getDiffStats } from './git/utils.js';
12
+ // Table column widths (fixed columns; title gets remaining space)
13
+ const COL = { num: 7, status: 18, agent: 8, changes: 14, time: 8, activity: 16 };
14
+ function truncate(text, max) {
15
+ if (text.length <= max)
16
+ return text;
17
+ return text.slice(0, max - 1) + '\u2026';
18
+ }
19
+ // Strip ANSI/cursor artifacts so activity tracking ignores tmux UI noise
20
+ function normalizeContent(raw) {
21
+ return raw
22
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '') // CSI sequences
23
+ .replace(/\x1b\][^\x07]*\x07/g, '') // OSC sequences
24
+ .replace(/\x1b[()][0-9A-B]/g, '') // charset sequences
25
+ .split('\n')
26
+ .map(line => line.trimEnd())
27
+ .join('\n')
28
+ .replace(/\n+$/, '');
29
+ }
30
+ // Patterns that indicate the agent is actively working (reused from runner.ts)
31
+ const ACTIVITY_PATTERNS = [
32
+ /thinking/i, /planning/i, /analyzing/i, /building/i, /testing/i,
33
+ /running/i, /searching/i, /reviewing/i, /writing/i, /reading/i,
34
+ /editing/i, /patching/i, /generating/i, /refactoring/i, /fixing/i,
35
+ /checking/i, /staging/i, /committing/i, /finalizing/i,
36
+ /esc to interrupt/i, /\(\d+[smh].*interrupt/i,
37
+ /considering/i, /inspecting/i, /implementing/i, /verifying/i,
38
+ ];
39
+ // OSC 8 terminal hyperlink (clickable in modern terminals/tmux 3.1+)
40
+ function hyperlink(url, text) {
41
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
42
+ }
43
+ const DELETE_MENU_OPTIONS = [
44
+ { action: 'pause', label: 'Pause agent', desc: 'Send interrupt (Ctrl-C) to the agent' },
45
+ { action: 'kill', label: 'Kill agent', desc: 'Kill the tmux pane and remove from list' },
46
+ { action: 'label_kill', label: 'Label no-agents & kill', desc: 'Add ignore label, remove claim label, kill' },
47
+ ];
48
+ function timeAgo(ms) {
49
+ const sec = Math.floor((Date.now() - ms) / 1000);
50
+ if (sec < 5)
51
+ return 'now';
52
+ if (sec < 60)
53
+ return `${sec}s ago`;
54
+ if (sec < 3600)
55
+ return `${Math.floor(sec / 60)}m ago`;
56
+ return `${Math.floor(sec / 3600)}h ago`;
57
+ }
58
+ export const App = ({ settings, sessionName, controlPaneId, configPath }) => {
59
+ const { exit } = useApp();
60
+ const { stdout } = useStdout();
61
+ const termHeight = stdout?.rows || 40;
62
+ const termWidth = stdout?.columns || 80;
63
+ const [activeSettings, setActiveSettings] = useState(settings);
64
+ const [isConfiguring, setIsConfiguring] = useState(false);
65
+ const [configBase, setConfigBase] = useState();
66
+ const [panes, setPanes] = useState([]);
67
+ const [selectedIndex, setSelectedIndex] = useState(0);
68
+ const [statusMessage, setStatusMessage] = useState();
69
+ const [lastPoll, setLastPoll] = useState();
70
+ const [preview, setPreview] = useState('');
71
+ const [daemon, setDaemon] = useState(null);
72
+ const [paneActivitySnapshot, setPaneActivitySnapshot] = useState(new Map());
73
+ const [startupRecovery, setStartupRecovery] = useState({
74
+ loading: true,
75
+ pending: [],
76
+ selectedIds: new Set(),
77
+ cursor: 0,
78
+ recoveredCount: 0,
79
+ applying: false,
80
+ });
81
+ const contentHashesRef = useRef(new Map());
82
+ const paneActivityRef = useRef(new Map());
83
+ const lastActivityTsRef = useRef(new Map());
84
+ const previewCacheRef = useRef(new Map());
85
+ const [lastActivitySnapshot, setLastActivitySnapshot] = useState(new Map());
86
+ const [diffStatsSnapshot, setDiffStatsSnapshot] = useState(new Map());
87
+ const diffStatsRef = useRef(new Map());
88
+ const [deleteMenu, setDeleteMenu] = useState(null);
89
+ const tmux = TmuxService.getInstance();
90
+ const orderedPanes = useMemo(() => [
91
+ ...panes.filter(p => p.status === 'running'),
92
+ ...panes.filter(p => p.status === 'awaiting_review'),
93
+ ...panes.filter(p => p.status === 'needs_attention'),
94
+ ...panes.filter(p => p.status === 'pending'),
95
+ ...panes.filter(p => p.status === 'success' || p.status === 'failed' || p.status === 'timeout'),
96
+ ], [panes]);
97
+ const isStartupRecoveryActive = startupRecovery.loading || startupRecovery.pending.length > 0 || startupRecovery.applying;
98
+ // ── Business logic ────────────────────────────────────────────────
99
+ const upsertPane = useCallback((incoming) => {
100
+ setPanes(prev => {
101
+ const index = prev.findIndex(p => p.id === incoming.id);
102
+ if (index === -1) {
103
+ const pendingMatchIndex = prev.findIndex(p => p.status === 'pending'
104
+ && incoming.status !== 'pending'
105
+ && p.repo
106
+ && incoming.repo
107
+ && p.repo === incoming.repo
108
+ && p.issue_number
109
+ && incoming.issue_number
110
+ && p.issue_number === incoming.issue_number);
111
+ if (pendingMatchIndex !== -1) {
112
+ const next = [...prev];
113
+ next[pendingMatchIndex] = incoming;
114
+ return next;
115
+ }
116
+ return [...prev, incoming];
117
+ }
118
+ const next = [...prev];
119
+ next[index] = { ...next[index], ...incoming };
120
+ return next;
121
+ });
122
+ }, []);
123
+ const applyStartupRecoverySelection = useCallback(async (selectedIds) => {
124
+ if (!daemon)
125
+ return;
126
+ setStartupRecovery(prev => ({ ...prev, applying: true }));
127
+ try {
128
+ await daemon.launchStartupIssues(selectedIds);
129
+ }
130
+ finally {
131
+ daemon.completeStartupRecovery();
132
+ daemon.start();
133
+ setStartupRecovery(prev => ({
134
+ ...prev,
135
+ applying: false,
136
+ loading: false,
137
+ pending: [],
138
+ }));
139
+ }
140
+ }, [daemon]);
141
+ useEffect(() => {
142
+ setActiveSettings(settings);
143
+ }, [settings]);
144
+ const enterConfigureMode = useCallback(() => {
145
+ try {
146
+ setConfigBase(loadConfigDocument(configPath));
147
+ setIsConfiguring(true);
148
+ setStatusMessage('Reconfigure Symphony');
149
+ setTimeout(() => setStatusMessage(undefined), 3000);
150
+ }
151
+ catch (err) {
152
+ setStatusMessage(`Failed to load config: ${err}`);
153
+ setTimeout(() => setStatusMessage(undefined), 5000);
154
+ }
155
+ }, [configPath]);
156
+ const handleConfigureComplete = useCallback((result) => {
157
+ try {
158
+ const nextSettings = writeConfigAndLoad(configPath, result.yaml);
159
+ setStartupRecovery({
160
+ loading: true,
161
+ pending: [],
162
+ selectedIds: new Set(),
163
+ cursor: 0,
164
+ recoveredCount: 0,
165
+ applying: false,
166
+ });
167
+ setConfigBase(loadConfigDocument(configPath));
168
+ setActiveSettings(nextSettings);
169
+ setIsConfiguring(false);
170
+ setStatusMessage('Configuration updated');
171
+ setTimeout(() => setStatusMessage(undefined), 5000);
172
+ }
173
+ catch (err) {
174
+ setStatusMessage(`Failed to write config: ${err}`);
175
+ setTimeout(() => setStatusMessage(undefined), 5000);
176
+ }
177
+ }, [configPath]);
178
+ // ── Daemon initialization ─────────────────────────────────────────
179
+ useEffect(() => {
180
+ const d = new Daemon(activeSettings, sessionName);
181
+ let cancelled = false;
182
+ d.on('poll_start', () => { });
183
+ d.on('poll_end', (result) => {
184
+ setLastPoll(new Date().toISOString());
185
+ if (result.launched > 0) {
186
+ setStatusMessage(`Launched ${result.launched} run(s)`);
187
+ setTimeout(() => setStatusMessage(undefined), 5000);
188
+ }
189
+ });
190
+ d.on('run_started', (pane) => {
191
+ upsertPane(pane);
192
+ setStatusMessage(`Started: #${pane.issue_number} ${pane.issue_title || ''}`);
193
+ setTimeout(() => setStatusMessage(undefined), 5000);
194
+ });
195
+ d.on('run_updated', (pane) => {
196
+ upsertPane(pane);
197
+ if (pane.status === 'awaiting_review') {
198
+ setStatusMessage(`Awaiting review: #${pane.issue_number}`);
199
+ }
200
+ else if (pane.status === 'needs_attention') {
201
+ setStatusMessage(`Needs attention: #${pane.issue_number}`);
202
+ }
203
+ else if (pane.status_detail) {
204
+ setStatusMessage(pane.status_detail);
205
+ }
206
+ setTimeout(() => setStatusMessage(undefined), 5000);
207
+ });
208
+ d.on('run_completed', (pane) => {
209
+ upsertPane(pane);
210
+ setStatusMessage(`Completed: #${pane.issue_number} (${pane.status})`);
211
+ setTimeout(() => setStatusMessage(undefined), 5000);
212
+ });
213
+ d.on('error', (err) => {
214
+ setStatusMessage(`Error: ${err.message}`);
215
+ setTimeout(() => setStatusMessage(undefined), 8000);
216
+ });
217
+ d.on('status', (msg) => {
218
+ setStatusMessage(msg);
219
+ setTimeout(() => setStatusMessage(undefined), 5000);
220
+ });
221
+ setDaemon(d);
222
+ void (async () => {
223
+ try {
224
+ const recovery = await d.prepareStartupRecovery();
225
+ if (cancelled)
226
+ return;
227
+ for (const pane of recovery.recovered) {
228
+ upsertPane(pane);
229
+ }
230
+ if (recovery.pending.length > 0) {
231
+ setStartupRecovery({
232
+ loading: true,
233
+ pending: recovery.pending,
234
+ selectedIds: new Set(recovery.pending.map(item => item.id)),
235
+ cursor: 0,
236
+ recoveredCount: recovery.recovered.length,
237
+ applying: true,
238
+ });
239
+ await d.launchStartupIssues(recovery.pending.map(item => item.id));
240
+ d.completeStartupRecovery();
241
+ setStartupRecovery({
242
+ loading: false,
243
+ pending: [],
244
+ selectedIds: new Set(),
245
+ cursor: 0,
246
+ recoveredCount: recovery.recovered.length,
247
+ applying: false,
248
+ });
249
+ setStatusMessage(recovery.recovered.length > 0
250
+ ? `Recovered ${recovery.recovered.length} run(s); started startup issue processing`
251
+ : `Started startup issue processing`);
252
+ d.start();
253
+ return;
254
+ }
255
+ d.completeStartupRecovery();
256
+ setStartupRecovery({
257
+ loading: false,
258
+ pending: [],
259
+ selectedIds: new Set(),
260
+ cursor: 0,
261
+ recoveredCount: recovery.recovered.length,
262
+ applying: false,
263
+ });
264
+ d.start();
265
+ }
266
+ catch (err) {
267
+ if (cancelled)
268
+ return;
269
+ setStartupRecovery(prev => ({ ...prev, loading: false, applying: false }));
270
+ setStatusMessage(`Startup recovery failed: ${err}`);
271
+ d.start();
272
+ }
273
+ })();
274
+ return () => {
275
+ cancelled = true;
276
+ d.stop();
277
+ };
278
+ }, [activeSettings, sessionName, upsertPane]);
279
+ // ── Periodic pane sync from daemon ────────────────────────────────
280
+ useEffect(() => {
281
+ if (!daemon)
282
+ return;
283
+ const interval = setInterval(() => {
284
+ const activeRuns = daemon.getActiveRuns();
285
+ setPanes(prev => {
286
+ let changed = false;
287
+ const updated = [...prev];
288
+ for (const run of activeRuns) {
289
+ const idx = updated.findIndex(p => p.id === run.id);
290
+ if (idx === -1) {
291
+ updated.push(run);
292
+ changed = true;
293
+ }
294
+ else {
295
+ const existing = updated[idx];
296
+ if (existing.status !== run.status
297
+ || existing.status_detail !== run.status_detail
298
+ || existing.tmux_pane_id !== run.tmux_pane_id
299
+ || existing.branch !== run.branch) {
300
+ updated[idx] = { ...updated[idx], ...run };
301
+ changed = true;
302
+ }
303
+ }
304
+ }
305
+ return changed ? updated : prev;
306
+ });
307
+ }, 3000);
308
+ return () => clearInterval(interval);
309
+ }, [daemon]);
310
+ // Clamp selectedIndex when panes change
311
+ useEffect(() => {
312
+ setSelectedIndex(prev => {
313
+ if (orderedPanes.length === 0)
314
+ return 0;
315
+ return Math.min(prev, orderedPanes.length - 1);
316
+ });
317
+ }, [orderedPanes.length]);
318
+ // ── Preview fetch ─────────────────────────────────────────────────
319
+ const selectedPaneTmuxId = orderedPanes[selectedIndex]?.tmux_pane_id;
320
+ const selectedPaneId = orderedPanes[selectedIndex]?.id;
321
+ useEffect(() => {
322
+ if (isStartupRecoveryActive) {
323
+ setPreview('');
324
+ return;
325
+ }
326
+ if (!selectedPaneTmuxId) {
327
+ // No live tmux pane — show cached preview (for completed/killed panes)
328
+ const cached = selectedPaneId ? previewCacheRef.current.get(selectedPaneId) : '';
329
+ setPreview(cached || '');
330
+ return;
331
+ }
332
+ let cancelled = false;
333
+ const fetchPreview = async () => {
334
+ try {
335
+ const content = await tmux.getPaneContent(selectedPaneTmuxId, 200);
336
+ if (!cancelled) {
337
+ const trimmed = content.trimEnd();
338
+ // Cache every non-empty preview so completed panes retain content
339
+ if (trimmed && selectedPaneId) {
340
+ previewCacheRef.current.set(selectedPaneId, trimmed);
341
+ }
342
+ setPreview(prev => prev === trimmed ? prev : trimmed);
343
+ }
344
+ }
345
+ catch {
346
+ if (!cancelled)
347
+ setPreview('');
348
+ }
349
+ };
350
+ fetchPreview();
351
+ const interval = setInterval(fetchPreview, 3000);
352
+ return () => {
353
+ cancelled = true;
354
+ clearInterval(interval);
355
+ };
356
+ }, [isStartupRecoveryActive, selectedPaneTmuxId, selectedPaneId, tmux]);
357
+ // ── Per-pane activity tracking for sparkline column ────────────────
358
+ useEffect(() => {
359
+ const interval = setInterval(async () => {
360
+ let changed = false;
361
+ for (const pane of orderedPanes) {
362
+ if (!pane.tmux_pane_id)
363
+ continue;
364
+ try {
365
+ const raw = await tmux.getPaneContent(pane.tmux_pane_id, 15);
366
+ const normalized = normalizeContent(raw);
367
+ const prevNormalized = contentHashesRef.current.get(pane.id);
368
+ contentHashesRef.current.set(pane.id, normalized);
369
+ if (prevNormalized !== undefined) {
370
+ const contentChanged = normalized !== prevNormalized;
371
+ // Also check for working-pattern keywords as a secondary signal
372
+ const lines = normalized.split('\n').filter(Boolean);
373
+ const recentText = lines.slice(-8).join('\n');
374
+ const matchesWork = ACTIVITY_PATTERNS.some(p => p.test(recentText));
375
+ const active = (contentChanged || matchesWork) ? 1 : 0;
376
+ const prev = paneActivityRef.current.get(pane.id) || new Array(14).fill(0);
377
+ paneActivityRef.current.set(pane.id, [...prev.slice(-13), active]);
378
+ if (active) {
379
+ lastActivityTsRef.current.set(pane.id, Date.now());
380
+ }
381
+ changed = true;
382
+ }
383
+ else {
384
+ if (!paneActivityRef.current.has(pane.id)) {
385
+ paneActivityRef.current.set(pane.id, new Array(14).fill(0));
386
+ }
387
+ lastActivityTsRef.current.set(pane.id, Date.now());
388
+ }
389
+ }
390
+ catch { /* ignore */ }
391
+ }
392
+ if (changed) {
393
+ setPaneActivitySnapshot(new Map(paneActivityRef.current));
394
+ setLastActivitySnapshot(new Map(lastActivityTsRef.current));
395
+ }
396
+ }, 5000);
397
+ return () => clearInterval(interval);
398
+ }, [orderedPanes, tmux]);
399
+ // ── Diff stats polling ────────────────────────────────────────────
400
+ useEffect(() => {
401
+ const interval = setInterval(async () => {
402
+ let changed = false;
403
+ for (const pane of orderedPanes) {
404
+ if (!pane.worktree_path || !pane.base_sha)
405
+ continue;
406
+ const hasStats = diffStatsRef.current.has(pane.id);
407
+ const isActive = pane.status === 'running' || pane.status === 'awaiting_review' || pane.status === 'needs_attention';
408
+ if (!isActive && hasStats)
409
+ continue; // already have final stats
410
+ try {
411
+ const stats = await getDiffStats(pane.base_sha, pane.worktree_path);
412
+ const prev = diffStatsRef.current.get(pane.id);
413
+ if (!prev || prev.added !== stats.added || prev.removed !== stats.removed) {
414
+ diffStatsRef.current.set(pane.id, stats);
415
+ changed = true;
416
+ }
417
+ }
418
+ catch { /* ignore */ }
419
+ }
420
+ if (changed) {
421
+ setDiffStatsSnapshot(new Map(diffStatsRef.current));
422
+ }
423
+ }, 15000);
424
+ return () => clearInterval(interval);
425
+ }, [orderedPanes]);
426
+ // ── Keyboard input ────────────────────────────────────────────────
427
+ useInput(useCallback((input, key) => {
428
+ if (isConfiguring)
429
+ return;
430
+ // ── Delete action menu ──
431
+ if (deleteMenu) {
432
+ if (key.upArrow) {
433
+ setDeleteMenu(prev => prev ? { ...prev, cursor: Math.max(0, prev.cursor - 1) } : null);
434
+ return;
435
+ }
436
+ if (key.downArrow) {
437
+ setDeleteMenu(prev => prev ? { ...prev, cursor: Math.min(DELETE_MENU_OPTIONS.length - 1, prev.cursor + 1) } : null);
438
+ return;
439
+ }
440
+ if (key.escape) {
441
+ setDeleteMenu(null);
442
+ return;
443
+ }
444
+ if (key.return) {
445
+ const action = DELETE_MENU_OPTIONS[deleteMenu.cursor];
446
+ const pane = deleteMenu.pane;
447
+ setDeleteMenu(null);
448
+ if (action)
449
+ void executeDeleteAction(action.action, pane);
450
+ return;
451
+ }
452
+ return;
453
+ }
454
+ // ── Startup recovery ──
455
+ if (isStartupRecoveryActive) {
456
+ if (startupRecovery.loading || startupRecovery.applying) {
457
+ if (input === 'q') {
458
+ daemon?.stop();
459
+ exit();
460
+ }
461
+ return;
462
+ }
463
+ const item = startupRecovery.pending[startupRecovery.cursor];
464
+ if (key.upArrow && startupRecovery.pending.length > 0) {
465
+ setStartupRecovery(prev => ({ ...prev, cursor: Math.max(0, prev.cursor - 1) }));
466
+ return;
467
+ }
468
+ if (key.downArrow && startupRecovery.pending.length > 0) {
469
+ setStartupRecovery(prev => ({ ...prev, cursor: Math.min(prev.pending.length - 1, prev.cursor + 1) }));
470
+ return;
471
+ }
472
+ if (input === ' ' && item) {
473
+ setStartupRecovery(prev => {
474
+ const next = new Set(prev.selectedIds);
475
+ if (next.has(item.id))
476
+ next.delete(item.id);
477
+ else
478
+ next.add(item.id);
479
+ return { ...prev, selectedIds: next };
480
+ });
481
+ return;
482
+ }
483
+ if (input === 'a') {
484
+ setStartupRecovery(prev => ({ ...prev, selectedIds: new Set(prev.pending.map(e => e.id)) }));
485
+ return;
486
+ }
487
+ if (input === 'n') {
488
+ setStartupRecovery(prev => ({ ...prev, selectedIds: new Set() }));
489
+ return;
490
+ }
491
+ if (key.return) {
492
+ void applyStartupRecoverySelection(Array.from(startupRecovery.selectedIds));
493
+ return;
494
+ }
495
+ if (input === 's') {
496
+ void applyStartupRecoverySelection([]);
497
+ return;
498
+ }
499
+ if (input === 'q') {
500
+ daemon?.stop();
501
+ exit();
502
+ }
503
+ return;
504
+ }
505
+ // ── Normal mode ──
506
+ const paneCount = orderedPanes.length;
507
+ if (key.upArrow && paneCount > 0) {
508
+ setSelectedIndex(prev => Math.max(0, prev - 1));
509
+ return;
510
+ }
511
+ if (key.downArrow && paneCount > 0) {
512
+ setSelectedIndex(prev => Math.min(paneCount - 1, prev + 1));
513
+ return;
514
+ }
515
+ if (key.return) {
516
+ const pane = orderedPanes[selectedIndex];
517
+ if (pane?.tmux_pane_id)
518
+ void jumpToPane(pane);
519
+ return;
520
+ }
521
+ // Delete / d → action menu
522
+ if (key.delete || input === 'd') {
523
+ const pane = orderedPanes[selectedIndex];
524
+ if (pane)
525
+ setDeleteMenu({ pane, cursor: 0 });
526
+ return;
527
+ }
528
+ if (input === 'q') {
529
+ daemon?.stop();
530
+ exit();
531
+ return;
532
+ }
533
+ if (input === 'c') {
534
+ enterConfigureMode();
535
+ return;
536
+ }
537
+ if (input === 'm') {
538
+ const pane = orderedPanes[selectedIndex];
539
+ if (pane && pane.status === 'success')
540
+ handleMerge(pane);
541
+ else {
542
+ setStatusMessage('Select a completed (success) pane to merge');
543
+ setTimeout(() => setStatusMessage(undefined), 3000);
544
+ }
545
+ return;
546
+ }
547
+ if (input === 'f') {
548
+ const pane = orderedPanes[selectedIndex];
549
+ if (pane?.status === 'awaiting_review')
550
+ void handleFinalize(pane);
551
+ else {
552
+ setStatusMessage('Select an awaiting-review pane to finalize');
553
+ setTimeout(() => setStatusMessage(undefined), 3000);
554
+ }
555
+ return;
556
+ }
557
+ if (input === 'r') {
558
+ const pane = orderedPanes[selectedIndex];
559
+ if (pane && (pane.status === 'awaiting_review' || pane.status === 'needs_attention'))
560
+ void handleResume(pane);
561
+ else {
562
+ setStatusMessage('Select a review/attention pane to resume');
563
+ setTimeout(() => setStatusMessage(undefined), 3000);
564
+ }
565
+ return;
566
+ }
567
+ if (input === 'x') {
568
+ const pane = orderedPanes[selectedIndex];
569
+ if (pane)
570
+ handleClose(pane);
571
+ return;
572
+ }
573
+ if (input === 'a') {
574
+ const pane = orderedPanes[selectedIndex];
575
+ if (pane?.tmux_pane_id)
576
+ void jumpToPane(pane);
577
+ return;
578
+ }
579
+ }, [
580
+ applyStartupRecoverySelection,
581
+ daemon,
582
+ deleteMenu,
583
+ enterConfigureMode,
584
+ exit,
585
+ isConfiguring,
586
+ isStartupRecoveryActive,
587
+ orderedPanes,
588
+ selectedIndex,
589
+ startupRecovery,
590
+ ]));
591
+ // ── Action handlers ───────────────────────────────────────────────
592
+ const jumpToPane = async (pane) => {
593
+ if (!pane.tmux_pane_id)
594
+ return;
595
+ try {
596
+ const exists = await tmux.paneExists(pane.tmux_pane_id);
597
+ if (!exists) {
598
+ setPanes(prev => prev.map(p => p.id === pane.id
599
+ ? { ...p, status: p.status === 'success' ? 'success' : 'failed',
600
+ status_detail: 'tmux pane no longer available', tmux_pane_id: undefined }
601
+ : p));
602
+ setStatusMessage(`Pane for #${pane.issue_number || pane.slug} is no longer available`);
603
+ setTimeout(() => setStatusMessage(undefined), 5000);
604
+ return;
605
+ }
606
+ // Switch to the agent's tmux window
607
+ await tmux.selectWindowByPane(pane.tmux_pane_id);
608
+ await tmux.selectPane(pane.tmux_pane_id);
609
+ setStatusMessage(`Opened #${pane.issue_number || pane.slug} \u2014 Ctrl-b 0 to return`);
610
+ setTimeout(() => setStatusMessage(undefined), 5000);
611
+ }
612
+ catch (err) {
613
+ setStatusMessage(`Failed to open: ${err}`);
614
+ setTimeout(() => setStatusMessage(undefined), 5000);
615
+ }
616
+ };
617
+ const handleMerge = async (pane) => {
618
+ setStatusMessage(`Merging ${pane.branch}... (merge logic pending)`);
619
+ setTimeout(() => setStatusMessage(undefined), 5000);
620
+ };
621
+ const handleFinalize = async (pane) => {
622
+ try {
623
+ const ok = pane.run_id ? await daemon?.finalizeRun(pane.run_id) : false;
624
+ if (!ok) {
625
+ setStatusMessage(`Failed to finalize #${pane.issue_number || pane.slug}`);
626
+ setTimeout(() => setStatusMessage(undefined), 5000);
627
+ return;
628
+ }
629
+ setStatusMessage(`Finalizing #${pane.issue_number || pane.slug}`);
630
+ setTimeout(() => setStatusMessage(undefined), 3000);
631
+ }
632
+ catch (err) {
633
+ setStatusMessage(`Finalize failed: ${err}`);
634
+ setTimeout(() => setStatusMessage(undefined), 5000);
635
+ }
636
+ };
637
+ const handleResume = async (pane) => {
638
+ try {
639
+ const ok = pane.run_id ? await daemon?.resumeRun(pane.run_id) : false;
640
+ if (!ok) {
641
+ setStatusMessage(`Failed to resume #${pane.issue_number || pane.slug}`);
642
+ setTimeout(() => setStatusMessage(undefined), 5000);
643
+ return;
644
+ }
645
+ await jumpToPane(pane);
646
+ }
647
+ catch (err) {
648
+ setStatusMessage(`Resume failed: ${err}`);
649
+ setTimeout(() => setStatusMessage(undefined), 5000);
650
+ }
651
+ };
652
+ const handleClose = async (pane) => {
653
+ if (pane.tmux_pane_id) {
654
+ try {
655
+ await tmux.killPane(pane.tmux_pane_id);
656
+ }
657
+ catch { /* ignore */ }
658
+ }
659
+ setPanes(prev => prev.filter(p => p.id !== pane.id));
660
+ setSelectedIndex(prev => Math.max(0, Math.min(prev, panes.length - 2)));
661
+ setStatusMessage(`Closed: #${pane.issue_number || pane.slug}`);
662
+ setTimeout(() => setStatusMessage(undefined), 3000);
663
+ };
664
+ const executeDeleteAction = async (action, pane) => {
665
+ try {
666
+ switch (action) {
667
+ case 'pause':
668
+ if (pane.run_id) {
669
+ await daemon?.pauseRun(pane.run_id);
670
+ setStatusMessage(`Paused #${pane.issue_number || pane.slug}`);
671
+ }
672
+ else if (pane.tmux_pane_id) {
673
+ await tmux.sendKeys(pane.tmux_pane_id, 'C-c');
674
+ setStatusMessage(`Sent interrupt to #${pane.issue_number || pane.slug}`);
675
+ }
676
+ break;
677
+ case 'kill':
678
+ await handleClose(pane);
679
+ return; // handleClose sets its own status message
680
+ case 'label_kill':
681
+ if (pane.run_id && await daemon?.stopAndLabelRun(pane.run_id)) {
682
+ setPanes(prev => prev.filter(p => p.id !== pane.id));
683
+ setSelectedIndex(prev => Math.max(0, Math.min(prev, panes.length - 2)));
684
+ setStatusMessage(`Labeled & removed #${pane.issue_number || pane.slug}`);
685
+ }
686
+ else {
687
+ // Fallback: just close if no run_id
688
+ await handleClose(pane);
689
+ return;
690
+ }
691
+ break;
692
+ }
693
+ setTimeout(() => setStatusMessage(undefined), 5000);
694
+ }
695
+ catch (err) {
696
+ setStatusMessage(`Action failed: ${err}`);
697
+ setTimeout(() => setStatusMessage(undefined), 5000);
698
+ }
699
+ };
700
+ // ── Computed values for render ────────────────────────────────────
701
+ const selectedPane = orderedPanes[selectedIndex] || null;
702
+ const runningCount = panes.filter(p => p.status === 'running' || p.status === 'needs_attention').length;
703
+ const reviewCount = panes.filter(p => p.status === 'awaiting_review').length;
704
+ const completedCount = panes.filter(p => p.status === 'success' || p.status === 'failed' || p.status === 'timeout').length;
705
+ // Table layout
706
+ const titleWidth = Math.max(10, termWidth - COL.num - COL.status - COL.agent - COL.changes - COL.time - COL.activity - 6);
707
+ const maxTableRows = Math.min(orderedPanes.length, Math.max(3, Math.floor(termHeight * 0.35)));
708
+ let tableScrollOffset = 0;
709
+ if (selectedIndex >= maxTableRows) {
710
+ tableScrollOffset = selectedIndex - maxTableRows + 1;
711
+ }
712
+ const visibleRows = orderedPanes.slice(tableScrollOffset, tableScrollOffset + maxTableRows);
713
+ const hasScrollIndicator = orderedPanes.length > maxTableRows;
714
+ // Preview: fill remaining vertical space with explicit height
715
+ const tableHeight = orderedPanes.length === 0
716
+ ? 5 // border(2) + paddingY(2) + text(1)
717
+ : 4 + maxTableRows + (hasScrollIndicator ? 1 : 0); // border(2) + header(1) + sep(1) + rows + scroll
718
+ const previewBoxHeight = Math.max(5, termHeight - 3 - tableHeight - 3);
719
+ // 3=header box, 3=statusbar box
720
+ const previewContentLines = previewBoxHeight - 3; // minus border(2) + header line(1)
721
+ // ── Render ────────────────────────────────────────────────────────
722
+ if (isConfiguring) {
723
+ return (React.createElement(Onboarding, { configPath: configPath, flow: "reconfigure", initialSettings: activeSettings, baseConfig: configBase, onComplete: handleConfigureComplete, onExit: () => setIsConfiguring(false) }));
724
+ }
725
+ return (React.createElement(Box, { flexDirection: "column", height: termHeight },
726
+ React.createElement(Header, { repos: activeSettings.repos.map(r => r.repo), mode: activeSettings.mode, runningCount: runningCount, reviewCount: reviewCount, completedCount: completedCount, lastPoll: lastPoll }),
727
+ isStartupRecoveryActive ? (React.createElement(Box, { flexGrow: 1, borderStyle: "round", borderColor: colors.border, flexDirection: "column" },
728
+ React.createElement(StartupRecoveryView, { loading: startupRecovery.loading || startupRecovery.applying, applying: startupRecovery.applying, recoveredCount: startupRecovery.recoveredCount, pending: startupRecovery.pending, selectedIds: startupRecovery.selectedIds, cursor: startupRecovery.cursor }))) : (React.createElement(React.Fragment, null,
729
+ React.createElement(Box, { borderStyle: "round", borderColor: gradient.blue3, flexDirection: "column", paddingX: 1 }, orderedPanes.length === 0 ? (React.createElement(Box, { paddingY: 1 },
730
+ React.createElement(Text, { color: colors.muted }, "No issues yet. Waiting for GitHub issues..."))) : (React.createElement(React.Fragment, null,
731
+ React.createElement(Box, null,
732
+ React.createElement(Text, { color: gradient.blue5, bold: true },
733
+ ' ',
734
+ '#'.padEnd(COL.num - 2),
735
+ 'Title'.padEnd(titleWidth),
736
+ 'Status'.padEnd(COL.status),
737
+ 'Agent'.padEnd(COL.agent),
738
+ 'Changes'.padEnd(COL.changes),
739
+ 'Update'.padEnd(COL.time),
740
+ 'Activity')),
741
+ React.createElement(Text, { color: colors.border }, '\u2500'.repeat(Math.max(0, termWidth - 4))),
742
+ visibleRows.map((pane, i) => {
743
+ const globalIdx = tableScrollOffset + i;
744
+ const isSelected = globalIdx === selectedIndex;
745
+ const num = pane.issue_number ? `#${pane.issue_number}` : truncate(pane.slug, COL.num - 2);
746
+ const title = truncate(pane.issue_title || pane.slug || '', titleWidth - 1);
747
+ const status = `${statusIcon(pane.status)} ${pane.status}`;
748
+ const agent = pane.agent_name || '-';
749
+ const lastTs = lastActivitySnapshot.get(pane.id);
750
+ const time = lastTs ? timeAgo(lastTs) : (pane.started_at ? timeAgo(new Date(pane.started_at).getTime()) : '-');
751
+ const activity = paneActivitySnapshot.get(pane.id) || new Array(14).fill(0);
752
+ const act = sparkline(activity);
753
+ const bg = isSelected ? colors.selected : undefined;
754
+ return (React.createElement(Box, { key: pane.id },
755
+ React.createElement(Text, { backgroundColor: bg, color: isSelected ? colors.selectedText : colors.primaryLight },
756
+ isSelected ? '\u25b8 ' : ' ',
757
+ pane.issue_url ? hyperlink(pane.issue_url, num) : num,
758
+ ' '.repeat(Math.max(0, COL.num - 2 - num.length))),
759
+ React.createElement(Text, { backgroundColor: bg, color: isSelected ? colors.selectedText : colors.text }, title.padEnd(titleWidth)),
760
+ React.createElement(Text, { backgroundColor: bg, color: isSelected ? colors.selectedText : statusColor(pane.status) }, truncate(status, COL.status - 1).padEnd(COL.status)),
761
+ React.createElement(Text, { backgroundColor: bg, color: isSelected ? colors.selectedText : colors.accent }, truncate(agent, COL.agent - 1).padEnd(COL.agent)),
762
+ (() => {
763
+ const stats = diffStatsSnapshot.get(pane.id);
764
+ if (!stats) {
765
+ return (React.createElement(Text, { backgroundColor: bg, color: isSelected ? colors.selectedText : colors.muted }, (pane.status === 'pending' ? '-' : '\u2026').padEnd(COL.changes)));
766
+ }
767
+ const addStr = `+${stats.added}`;
768
+ const remStr = `-${stats.removed}`;
769
+ const pad = Math.max(0, COL.changes - addStr.length - 1 - remStr.length);
770
+ return (React.createElement(Box, { width: COL.changes },
771
+ React.createElement(Text, { backgroundColor: bg, color: isSelected ? colors.selectedText : colors.success }, addStr),
772
+ React.createElement(Text, { backgroundColor: bg }, ' '),
773
+ React.createElement(Text, { backgroundColor: bg, color: isSelected ? colors.selectedText : colors.error }, remStr),
774
+ React.createElement(Text, { backgroundColor: bg }, ' '.repeat(pad))));
775
+ })(),
776
+ React.createElement(Text, { backgroundColor: bg, color: isSelected ? colors.selectedText : colors.textDim }, time.padEnd(COL.time)),
777
+ React.createElement(Text, { backgroundColor: bg, color: isSelected ? gradient.blue6 : colors.running }, act.padEnd(COL.activity))));
778
+ }),
779
+ hasScrollIndicator && (React.createElement(Box, { justifyContent: "space-between" },
780
+ React.createElement(Text, { color: colors.muted }, tableScrollOffset > 0 ? ' \u2191 more' : ' '),
781
+ React.createElement(Text, { color: colors.muted }, tableScrollOffset + maxTableRows < orderedPanes.length ? '\u2193 more ' : ' ')))))),
782
+ React.createElement(Box, { borderStyle: "round", borderColor: deleteMenu ? colors.warning : colors.border, flexDirection: "column", height: previewBoxHeight, paddingX: 1 }, deleteMenu ? (React.createElement(Box, { flexDirection: "column", paddingY: 1 },
783
+ React.createElement(Text, { bold: true, color: colors.warning },
784
+ ' ',
785
+ "Action: #",
786
+ deleteMenu.pane.issue_number || deleteMenu.pane.slug,
787
+ ' ',
788
+ truncate(deleteMenu.pane.issue_title || '', 40)),
789
+ React.createElement(Text, { color: colors.muted }, ' '),
790
+ DELETE_MENU_OPTIONS.map((opt, i) => {
791
+ const isSel = i === deleteMenu.cursor;
792
+ return (React.createElement(Box, { key: opt.action },
793
+ React.createElement(Text, { color: isSel ? colors.selectedText : colors.text, backgroundColor: isSel ? colors.selected : undefined, bold: isSel },
794
+ isSel ? ' \u25b8 ' : ' ',
795
+ opt.label),
796
+ React.createElement(Text, { color: colors.muted },
797
+ ' ',
798
+ opt.desc)));
799
+ }),
800
+ React.createElement(Text, { color: colors.muted }, ' '),
801
+ React.createElement(Text, { color: colors.muted },
802
+ ' ',
803
+ React.createElement(Text, { color: colors.key }, "\u2191\u2193"),
804
+ " choose",
805
+ ' ',
806
+ React.createElement(Text, { color: colors.key }, "Enter"),
807
+ " confirm",
808
+ ' ',
809
+ React.createElement(Text, { color: colors.key }, "Esc"),
810
+ " cancel"))) : selectedPane ? (React.createElement(React.Fragment, null,
811
+ React.createElement(Box, null,
812
+ React.createElement(Text, { bold: true, color: colors.primaryLight },
813
+ 'Preview: ',
814
+ selectedPane.issue_url
815
+ ? hyperlink(selectedPane.issue_url, `#${selectedPane.issue_number || ''} ${truncate(selectedPane.issue_title || selectedPane.slug || '', Math.max(10, termWidth - 40))}`)
816
+ : `${selectedPane.issue_number ? `#${selectedPane.issue_number} ` : ''}${truncate(selectedPane.issue_title || selectedPane.slug || '', Math.max(10, termWidth - 40))}`),
817
+ selectedPane.agent_name && (React.createElement(Text, { color: colors.accent },
818
+ " [",
819
+ selectedPane.agent_name,
820
+ "]")),
821
+ React.createElement(Text, { color: statusColor(selectedPane.status) },
822
+ ' ',
823
+ statusIcon(selectedPane.status),
824
+ " ",
825
+ selectedPane.status)),
826
+ preview ? (React.createElement(Text, { color: colors.text, wrap: "truncate-end" }, preview.split('\n').slice(-Math.max(3, previewContentLines)).join('\n'))) : (React.createElement(Text, { color: colors.muted }, selectedPane.tmux_pane_id ? 'Loading preview...' : 'No pane available (pending)')))) : (React.createElement(Text, { color: colors.muted }, "No issue selected"))))),
827
+ React.createElement(StatusBar, { message: statusMessage, recoveryMode: isStartupRecoveryActive, deleteMenuActive: deleteMenu !== null })));
828
+ };
829
+ //# sourceMappingURL=app.js.map