nfo-cli 0.0.3 → 0.0.5

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 (173) hide show
  1. package/dist/claude-command.js +6 -1
  2. package/dist/claude-command.js.map +1 -1
  3. package/dist/claude-trust.js +46 -0
  4. package/dist/claude-trust.js.map +1 -0
  5. package/dist/cli.js +64 -54
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/restore.js +0 -1
  8. package/dist/commands/restore.js.map +1 -1
  9. package/dist/commands/tui.js +6 -4
  10. package/dist/commands/tui.js.map +1 -1
  11. package/dist/mcp/handlers.js +5 -0
  12. package/dist/mcp/handlers.js.map +1 -1
  13. package/dist/mcp/tool-defs.js +10 -0
  14. package/dist/mcp/tool-defs.js.map +1 -1
  15. package/dist/musicians/dismiss.js +1 -1
  16. package/dist/musicians/dismiss.js.map +1 -1
  17. package/dist/musicians/roles.js +15 -0
  18. package/dist/musicians/roles.js.map +1 -0
  19. package/dist/musicians/spawn.js +53 -18
  20. package/dist/musicians/spawn.js.map +1 -1
  21. package/dist/permission.js +14 -8
  22. package/dist/permission.js.map +1 -1
  23. package/dist/prompts/musician-role.js +2 -1
  24. package/dist/prompts/musician-role.js.map +1 -1
  25. package/dist/prompts/orchestrator-role.js +42 -8
  26. package/dist/prompts/orchestrator-role.js.map +1 -1
  27. package/dist/prompts/tool-discipline.js +10 -0
  28. package/dist/prompts/tool-discipline.js.map +1 -1
  29. package/dist/tui/{App.js → components/App.js} +20 -20
  30. package/dist/tui/components/App.js.map +1 -0
  31. package/dist/tui/components/AppView.js +13 -0
  32. package/dist/tui/components/AppView.js.map +1 -0
  33. package/dist/tui/{Auditorium.js → components/Auditorium.js} +2 -2
  34. package/dist/tui/components/Auditorium.js.map +1 -0
  35. package/dist/tui/components/ConcertHall.js.map +1 -0
  36. package/dist/tui/{Help.js → components/Help.js} +0 -8
  37. package/dist/tui/components/Help.js.map +1 -0
  38. package/dist/tui/components/OrchestratorPane.js.map +1 -0
  39. package/dist/tui/components/SidebarHeader.js +6 -0
  40. package/dist/tui/components/SidebarHeader.js.map +1 -0
  41. package/dist/tui/{StatusBar.js → components/StatusBar.js} +1 -1
  42. package/dist/tui/components/StatusBar.js.map +1 -0
  43. package/package.json +8 -1
  44. package/assets/agent-screen.png +0 -0
  45. package/assets/main-screen.png +0 -0
  46. package/assets/orche-clawd.png +0 -0
  47. package/dist/tui/App.js.map +0 -1
  48. package/dist/tui/AppView.js +0 -13
  49. package/dist/tui/AppView.js.map +0 -1
  50. package/dist/tui/Auditorium.js.map +0 -1
  51. package/dist/tui/ConcertHall.js.map +0 -1
  52. package/dist/tui/Help.js.map +0 -1
  53. package/dist/tui/OrchestratorPane.js.map +0 -1
  54. package/dist/tui/SidebarHeader.js +0 -6
  55. package/dist/tui/SidebarHeader.js.map +0 -1
  56. package/dist/tui/StatusBar.js.map +0 -1
  57. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  58. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  59. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  60. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  61. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  62. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  63. package/src/claude-command.ts +0 -35
  64. package/src/claude-detect.ts +0 -42
  65. package/src/cli.ts +0 -164
  66. package/src/commands/attach.ts +0 -24
  67. package/src/commands/dashboard-window.ts +0 -33
  68. package/src/commands/kill.ts +0 -50
  69. package/src/commands/launch.ts +0 -134
  70. package/src/commands/list.ts +0 -43
  71. package/src/commands/mcp-server.ts +0 -18
  72. package/src/commands/notes.ts +0 -18
  73. package/src/commands/restore.ts +0 -153
  74. package/src/commands/tui.tsx +0 -16
  75. package/src/config.ts +0 -44
  76. package/src/dashboard.ts +0 -1
  77. package/src/mcp/config.ts +0 -39
  78. package/src/mcp/handlers.ts +0 -141
  79. package/src/mcp/server.ts +0 -50
  80. package/src/mcp/tool-defs.ts +0 -151
  81. package/src/musicians/dismiss.ts +0 -60
  82. package/src/musicians/ids.ts +0 -21
  83. package/src/musicians/lookup.ts +0 -13
  84. package/src/musicians/message-log.ts +0 -152
  85. package/src/musicians/message.ts +0 -99
  86. package/src/musicians/query.ts +0 -19
  87. package/src/musicians/spawn.ts +0 -139
  88. package/src/notes.ts +0 -39
  89. package/src/notify.ts +0 -62
  90. package/src/orchestrator/report-back.ts +0 -33
  91. package/src/permission.ts +0 -30
  92. package/src/project-key.ts +0 -12
  93. package/src/prompts/musician-role.ts +0 -22
  94. package/src/prompts/orchestrator-role.ts +0 -60
  95. package/src/prompts/tool-discipline.ts +0 -35
  96. package/src/repo.ts +0 -14
  97. package/src/shell-quote.ts +0 -7
  98. package/src/state-updaters.ts +0 -132
  99. package/src/state.ts +0 -49
  100. package/src/state.types.ts +0 -67
  101. package/src/tmux.ts +0 -226
  102. package/src/tui/App.tsx +0 -532
  103. package/src/tui/AppView.tsx +0 -96
  104. package/src/tui/Auditorium.tsx +0 -56
  105. package/src/tui/ConcertHall.tsx +0 -31
  106. package/src/tui/Help.tsx +0 -72
  107. package/src/tui/OrchestratorPane.tsx +0 -98
  108. package/src/tui/SidebarHeader.tsx +0 -32
  109. package/src/tui/StatusBar.tsx +0 -44
  110. package/src/tui/activity-line.ts +0 -16
  111. package/src/tui/detect-permission.ts +0 -93
  112. package/src/tui/embedded-session-lifecycle.ts +0 -44
  113. package/src/tui/embedded-terminal.ts +0 -325
  114. package/src/tui/format-time.ts +0 -25
  115. package/src/tui/keymap.ts +0 -104
  116. package/src/tui/poll-activity.ts +0 -25
  117. package/src/tui/poll-idle.ts +0 -149
  118. package/src/tui/poll-permission.ts +0 -50
  119. package/src/tui/status-icon.ts +0 -35
  120. package/src/tui/terminal-input.ts +0 -136
  121. package/src/tui/watch-state.ts +0 -43
  122. package/src/worktree.ts +0 -41
  123. package/tests/claude-command.test.ts +0 -30
  124. package/tests/claude-detect.test.ts +0 -14
  125. package/tests/commands/attach.test.ts +0 -60
  126. package/tests/commands/kill.test.ts +0 -66
  127. package/tests/commands/launch.test.ts +0 -75
  128. package/tests/commands/list.test.ts +0 -47
  129. package/tests/commands/notes.test.ts +0 -53
  130. package/tests/commands/restore.test.ts +0 -126
  131. package/tests/helpers/tmp-config.ts +0 -16
  132. package/tests/helpers/tmp-repo.ts +0 -29
  133. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  134. package/tests/mcp/handlers.test.ts +0 -163
  135. package/tests/mcp/tool-defs.test.ts +0 -35
  136. package/tests/musicians/dismiss.test.ts +0 -102
  137. package/tests/musicians/message.test.ts +0 -159
  138. package/tests/musicians/query.test.ts +0 -65
  139. package/tests/musicians/spawn.test.ts +0 -125
  140. package/tests/notes.test.ts +0 -56
  141. package/tests/notify.test.ts +0 -80
  142. package/tests/orchestrator/report-back.test.ts +0 -18
  143. package/tests/permission.test.ts +0 -29
  144. package/tests/project-key.test.ts +0 -33
  145. package/tests/prompts/tool-discipline.test.ts +0 -25
  146. package/tests/repo.test.ts +0 -38
  147. package/tests/state-updaters.test.ts +0 -126
  148. package/tests/state.test.ts +0 -85
  149. package/tests/tmux.test.ts +0 -126
  150. package/tests/tui/AppView.test.tsx +0 -92
  151. package/tests/tui/Auditorium.test.tsx +0 -67
  152. package/tests/tui/ConcertHall.test.tsx +0 -22
  153. package/tests/tui/Help.test.tsx +0 -38
  154. package/tests/tui/OrchestratorPane.test.ts +0 -30
  155. package/tests/tui/SidebarHeader.test.tsx +0 -20
  156. package/tests/tui/StatusBar.test.tsx +0 -51
  157. package/tests/tui/activity-line.test.ts +0 -21
  158. package/tests/tui/detect-permission.test.ts +0 -92
  159. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  160. package/tests/tui/embedded-terminal.test.ts +0 -80
  161. package/tests/tui/format-time.test.ts +0 -25
  162. package/tests/tui/keymap.test.ts +0 -93
  163. package/tests/tui/poll-activity.test.ts +0 -81
  164. package/tests/tui/poll-idle.test.ts +0 -159
  165. package/tests/tui/poll-permission.test.ts +0 -222
  166. package/tests/tui/status-icon.test.ts +0 -27
  167. package/tests/tui/terminal-input.test.ts +0 -113
  168. package/tests/tui/watch-state.test.ts +0 -54
  169. package/tests/worktree.test.ts +0 -73
  170. package/tsconfig.json +0 -19
  171. package/vitest.config.ts +0 -12
  172. /package/dist/tui/{ConcertHall.js → components/ConcertHall.js} +0 -0
  173. /package/dist/tui/{OrchestratorPane.js → components/OrchestratorPane.js} +0 -0
package/src/tui/App.tsx DELETED
@@ -1,532 +0,0 @@
1
- import type { ReactElement } from "react";
2
- import { useEffect, useRef, useState } from "react";
3
- import { useInput, useStdout, useWindowSize } from "ink";
4
- import { AppView } from "./AppView.js";
5
- import { reduceKey } from "./keymap.js";
6
- import { pollActivity } from "./poll-activity.js";
7
- import {
8
- syncMusicianIdleState,
9
- type MusicianIdleTracker,
10
- } from "./poll-idle.js";
11
- import { pollPermissions } from "./poll-permission.js";
12
- import { setMusicianStatus } from "../state-updaters.js";
13
- import { watchOrchestraState, type StopWatching } from "./watch-state.js";
14
- import { listOrchestras, type OrchestraSummary } from "../commands/list.js";
15
- import {
16
- EmbeddedTerminal,
17
- type EmbeddedTerminalSnapshot,
18
- } from "./embedded-terminal.js";
19
- import {
20
- claimEmbeddedSessionLease,
21
- embeddedSessionLeaseIsCurrent,
22
- runEmbeddedSessionOperation,
23
- } from "./embedded-session-lifecycle.js";
24
- import {
25
- toTerminalMouseScroll,
26
- toTerminalInput,
27
- toTerminalViewportCommand,
28
- } from "./terminal-input.js";
29
- import {
30
- detachCurrentClient,
31
- embeddedSessionName,
32
- ensureEmbeddedSession,
33
- killSession,
34
- selectWindow,
35
- sessionName,
36
- } from "../tmux.js";
37
- import { openNotes } from "../commands/notes.js";
38
- import { dismissMusician } from "../musicians/dismiss.js";
39
- import { readState } from "../state.js";
40
- import { notifyAwaitingPermission } from "../notify.js";
41
- import type { Musician, OrchestraState } from "../state.types.js";
42
-
43
- export interface AppProps {
44
- orchestraId: string;
45
- }
46
-
47
- function textLines(...lines: string[]): EmbeddedTerminalSnapshot["lines"] {
48
- return lines.map((line) => {
49
- return { spans: [{ text: line }] };
50
- });
51
- }
52
-
53
- export function App(props: AppProps): ReactElement {
54
- const windowSize = useWindowSize();
55
- const { stdout } = useStdout();
56
- const [state, setState] = useState<OrchestraState | null>(null);
57
- const [orchestras, setOrchestras] = useState<OrchestraSummary[]>([]);
58
- const [activity, setActivity] = useState<Record<string, string>>({});
59
- const [selectedIndex, setSelectedIndex] = useState(0);
60
- const [pendingDismissIndex, setPendingDismissIndex] = useState<number | null>(
61
- null,
62
- );
63
- const [now, setNow] = useState(new Date().toISOString());
64
- const [showHelp, setShowHelp] = useState(false);
65
- const [orchestratorSnapshot, setOrchestratorSnapshot] =
66
- useState<EmbeddedTerminalSnapshot>({
67
- title: "Claude",
68
- lines: textLines("Starting embedded Claude terminal…"),
69
- connected: true,
70
- });
71
- const [activePaneMusicianId, setActivePaneMusicianId] = useState<
72
- string | null
73
- >(null);
74
- const [orchestratorFocused, setOrchestratorFocused] = useState(false);
75
- const terminalRef = useRef<EmbeddedTerminal | null>(null);
76
- const idleTrackerRef = useRef<MusicianIdleTracker>({});
77
-
78
- // Watch state.json.
79
- useEffect(() => {
80
- let stop: StopWatching | undefined;
81
- void watchOrchestraState(props.orchestraId, (s) => {
82
- setState(s);
83
- }).then((fn) => {
84
- stop = fn;
85
- });
86
- return () => {
87
- if (stop) {
88
- void stop();
89
- }
90
- };
91
- }, [props.orchestraId]);
92
-
93
- // Detect musicians that are visibly idle at the Claude prompt and flush queued follow-ups.
94
- useEffect(() => {
95
- const tick = async (): Promise<void> => {
96
- idleTrackerRef.current = await syncMusicianIdleState(
97
- props.orchestraId,
98
- idleTrackerRef.current,
99
- );
100
- };
101
- void tick();
102
- const timer = setInterval(() => {
103
- void tick();
104
- }, 2000);
105
- return () => {
106
- clearInterval(timer);
107
- };
108
- }, [props.orchestraId]);
109
-
110
- // Poll activity + clock every 2s.
111
- useEffect(() => {
112
- const tick = async (): Promise<void> => {
113
- setNow(new Date().toISOString());
114
- const s = await readState(props.orchestraId);
115
- if (s) {
116
- const a = await pollActivity(s);
117
- setActivity(a);
118
- }
119
- };
120
- void tick();
121
- const timer = setInterval(() => {
122
- void tick();
123
- }, 2000);
124
- return () => {
125
- clearInterval(timer);
126
- };
127
- }, [props.orchestraId]);
128
-
129
- // Poll permission-prompt state every 2s and apply transitions.
130
- useEffect(() => {
131
- const tick = async (): Promise<void> => {
132
- const s = await readState(props.orchestraId);
133
- if (!s) {
134
- return;
135
- }
136
- const transitions = await pollPermissions(s);
137
- for (const t of transitions) {
138
- try {
139
- await setMusicianStatus(
140
- props.orchestraId,
141
- t.musicianId,
142
- t.newStatus,
143
- t.pendingPermission,
144
- );
145
- } catch {
146
- // Musician may have been dismissed between poll and write; safe to swallow.
147
- }
148
- }
149
- const newlyAwaiting = transitions.filter((t) => {
150
- return t.newStatus === "awaiting_permission";
151
- });
152
- if (newlyAwaiting.length > 0 && s.notify_on_permission === true) {
153
- const fresh = await readState(props.orchestraId);
154
- if (fresh) {
155
- const total = fresh.musicians.filter((m) => {
156
- return m.status === "awaiting_permission";
157
- }).length;
158
- await notifyAwaitingPermission({ pendingCount: total });
159
- }
160
- }
161
- };
162
- void tick();
163
- const timer = setInterval(() => {
164
- void tick();
165
- }, 2000);
166
- return () => {
167
- clearInterval(timer);
168
- };
169
- }, [props.orchestraId]);
170
-
171
- // Refresh the orchestra list every 3s.
172
- useEffect(() => {
173
- const tick = async (): Promise<void> => {
174
- const list = await listOrchestras();
175
- setOrchestras(list);
176
- };
177
- void tick();
178
- const timer = setInterval(() => {
179
- void tick();
180
- }, 3000);
181
- return () => {
182
- clearInterval(timer);
183
- };
184
- }, []);
185
-
186
- const musicians: Musician[] = state ? state.musicians : [];
187
- const session = sessionName(props.orchestraId);
188
- const embedSession = embeddedSessionName(props.orchestraId);
189
- const projectPath = state?.project_path;
190
- const activePaneMusician = activePaneMusicianId
191
- ? (musicians.find((musician) => {
192
- return musician.id === activePaneMusicianId;
193
- }) ?? null)
194
- : null;
195
- const paneTitle = activePaneMusician
196
- ? `Musician · ${activePaneMusician.name}`
197
- : `Orchestrator · ${orchestratorSnapshot.title}`;
198
- const terminalCols = Math.max(40, windowSize.columns - 53);
199
- const terminalRows = Math.max(12, windowSize.rows - 4);
200
- const terminalScreenLeft = 3;
201
- const terminalScreenTop = 3;
202
- const terminalScreenRight = terminalScreenLeft + terminalCols - 1;
203
- const terminalScreenBottom = terminalScreenTop + terminalRows - 1;
204
-
205
- const showTerminalError = (message: string): void => {
206
- setOrchestratorSnapshot((current) => {
207
- return {
208
- title: current.title,
209
- lines: textLines(message),
210
- connected: false,
211
- };
212
- });
213
- };
214
-
215
- const openSelectedTarget = async (
216
- nextSelectedIndex: number,
217
- ): Promise<void> => {
218
- const musician =
219
- nextSelectedIndex === 0 ? null : musicians[nextSelectedIndex - 1];
220
- if (nextSelectedIndex > 0 && !musician) {
221
- return;
222
- }
223
- const windowTarget = musician ? musician.tmux_window_id : "0";
224
- try {
225
- await selectWindow(embedSession, windowTarget);
226
- setActivePaneMusicianId(musician?.id ?? null);
227
- } catch (error) {
228
- const message = error instanceof Error ? error.message : String(error);
229
- showTerminalError(
230
- `Unable to open the selected target in the left pane: ${message}`,
231
- );
232
- }
233
- };
234
-
235
- useEffect(() => {
236
- if (!projectPath) {
237
- return;
238
- }
239
-
240
- const embeddedSessionLease = claimEmbeddedSessionLease(embedSession);
241
- let disposed = false;
242
- let unsubscribe: (() => void) | undefined;
243
-
244
- const start = async (): Promise<void> => {
245
- try {
246
- setOrchestratorSnapshot({
247
- title: "Claude",
248
- lines: textLines("Starting embedded Claude terminal…"),
249
- connected: true,
250
- });
251
- await runEmbeddedSessionOperation(embedSession, async () => {
252
- await killSession(embedSession);
253
- await ensureEmbeddedSession(session, embedSession, projectPath);
254
-
255
- if (!embeddedSessionLeaseIsCurrent(embeddedSessionLease)) {
256
- await killSession(embedSession);
257
- }
258
- });
259
- if (disposed || !embeddedSessionLeaseIsCurrent(embeddedSessionLease)) {
260
- return;
261
- }
262
- const terminal = new EmbeddedTerminal({
263
- sessionName: embedSession,
264
- cwd: projectPath,
265
- cols: terminalCols,
266
- rows: terminalRows,
267
- });
268
- terminalRef.current = terminal;
269
- unsubscribe = terminal.onChange((snapshot) => {
270
- if (!disposed) {
271
- setOrchestratorSnapshot(snapshot);
272
- }
273
- });
274
- } catch (error) {
275
- const message =
276
- error instanceof Error ? error.message : "unknown error";
277
- setOrchestratorSnapshot({
278
- title: "Claude",
279
- lines: textLines(
280
- `Unable to start embedded Claude terminal: ${message}`,
281
- ),
282
- connected: false,
283
- });
284
- }
285
- };
286
-
287
- void start();
288
-
289
- return () => {
290
- disposed = true;
291
- unsubscribe?.();
292
- terminalRef.current?.dispose();
293
- terminalRef.current = null;
294
- void runEmbeddedSessionOperation(embedSession, async () => {
295
- if (!embeddedSessionLeaseIsCurrent(embeddedSessionLease)) {
296
- return;
297
- }
298
-
299
- await killSession(embedSession);
300
- });
301
- };
302
- }, [embedSession, projectPath, props.orchestraId, session]);
303
-
304
- useEffect(() => {
305
- const terminal = terminalRef.current;
306
- if (!terminal) {
307
- return;
308
- }
309
- try {
310
- terminal.resize(terminalCols, terminalRows);
311
- } catch {
312
- showTerminalError("Embedded Claude terminal resize failed.");
313
- }
314
- }, [terminalCols, terminalRows]);
315
-
316
- useEffect(() => {
317
- if (!stdout.isTTY) {
318
- return;
319
- }
320
-
321
- stdout.write("\u001b[?1000h\u001b[?1006h");
322
- return () => {
323
- stdout.write("\u001b[?1000l\u001b[?1006l");
324
- };
325
- }, [stdout]);
326
-
327
- useEffect(() => {
328
- if (activePaneMusicianId === null) {
329
- return;
330
- }
331
- if (
332
- musicians.some((musician) => {
333
- return musician.id === activePaneMusicianId;
334
- })
335
- ) {
336
- return;
337
- }
338
- setActivePaneMusicianId(null);
339
- if (!projectPath) {
340
- return;
341
- }
342
- void selectWindow(embedSession, "0").catch((error: unknown) => {
343
- const message = error instanceof Error ? error.message : String(error);
344
- showTerminalError(
345
- `Unable to return the left pane to the orchestrator: ${message}`,
346
- );
347
- });
348
- }, [activePaneMusicianId, embedSession, musicians, projectPath]);
349
-
350
- useInput((input, key) => {
351
- if (showHelp) {
352
- if (input === "?" || key.escape) {
353
- setShowHelp(false);
354
- }
355
- return;
356
- }
357
-
358
- const mouseScroll = toTerminalMouseScroll(input);
359
- if (mouseScroll) {
360
- const insideTerminalViewport =
361
- mouseScroll.column >= terminalScreenLeft
362
- && mouseScroll.column <= terminalScreenRight
363
- && mouseScroll.row >= terminalScreenTop
364
- && mouseScroll.row <= terminalScreenBottom;
365
- if (insideTerminalViewport) {
366
- const translatedColumn = mouseScroll.column - terminalScreenLeft + 1;
367
- const translatedRow = mouseScroll.row - terminalScreenTop + 1;
368
- terminalRef.current?.write(
369
- `\u001b[<${mouseScroll.button};${translatedColumn};${translatedRow}M`,
370
- );
371
- }
372
- return;
373
- }
374
-
375
- if (orchestratorFocused) {
376
- if (key.ctrl && input.toLowerCase() === "g") {
377
- setOrchestratorFocused(false);
378
- return;
379
- }
380
- const viewportCommand = toTerminalViewportCommand(key);
381
- if (viewportCommand) {
382
- if (viewportCommand.kind === "scroll-pages") {
383
- terminalRef.current?.scrollPages(viewportCommand.pageCount);
384
- return;
385
- }
386
- if (viewportCommand.kind === "scroll-top") {
387
- terminalRef.current?.scrollToTop();
388
- return;
389
- }
390
- terminalRef.current?.scrollToBottom();
391
- return;
392
- }
393
- const terminalInput = toTerminalInput(input, key);
394
- if (terminalInput) {
395
- terminalRef.current?.write(terminalInput);
396
- }
397
- return;
398
- }
399
-
400
- if (key.ctrl && input.toLowerCase() === "c") {
401
- return;
402
- }
403
- if (key.ctrl && input.toLowerCase() === "g") {
404
- setOrchestratorFocused(true);
405
- return;
406
- }
407
-
408
- // Ink reports key.tab=true for BOTH Tab and Shift-Tab (with key.shift set on
409
- // the latter). Disambiguate so the reducer's `tab`-before-`shiftTab` order is
410
- // correct: plain Tab only when shift is NOT held.
411
- const isTab = key.tab && !key.shift;
412
- const isShiftTab = key.tab && key.shift;
413
- const result = reduceKey(
414
- { selectedIndex, musicianCount: musicians.length, pendingDismissIndex },
415
- {
416
- input,
417
- downArrow: key.downArrow,
418
- upArrow: key.upArrow,
419
- tab: isTab,
420
- shiftTab: isShiftTab,
421
- return: key.return,
422
- escape: key.escape,
423
- },
424
- );
425
- setSelectedIndex(result.ui.selectedIndex);
426
- setPendingDismissIndex(result.ui.pendingDismissIndex);
427
- if (!result.action) {
428
- return;
429
- }
430
- const action = result.action;
431
- if (action.kind === "open-target") {
432
- void openSelectedTarget(action.selectedIndex);
433
- return;
434
- }
435
- if (action.kind === "detach-session") {
436
- void detachCurrentClient().catch((error: unknown) => {
437
- const message = error instanceof Error ? error.message : String(error);
438
- setOrchestratorSnapshot({
439
- title: "Claude",
440
- lines: textLines(
441
- `Unable to detach the current tmux client: ${message}`,
442
- ),
443
- connected: false,
444
- });
445
- });
446
- return;
447
- }
448
- if (action.kind === "open-notes") {
449
- void openNotes(props.orchestraId);
450
- return;
451
- }
452
- if (action.kind === "dismiss-musician") {
453
- const m = musicians[action.index];
454
- if (m) {
455
- void dismissMusician({
456
- orchestraId: props.orchestraId,
457
- musicianId: m.id,
458
- });
459
- }
460
- return;
461
- }
462
- if (action.kind === "request-dismiss-musician") {
463
- return;
464
- }
465
- if (action.kind === "jump-to-pending") {
466
- const pendingIndex = musicians.findIndex((m) => {
467
- return m.status === "awaiting_permission";
468
- });
469
- if (pendingIndex >= 0) {
470
- setSelectedIndex(pendingIndex + 1);
471
- void openSelectedTarget(pendingIndex + 1);
472
- }
473
- return;
474
- }
475
- if (action.kind === "toggle-help") {
476
- setShowHelp((prev) => {
477
- return !prev;
478
- });
479
- return;
480
- }
481
- // next-orchestra / prev-orchestra: Phase 3 leaves session-switching to a
482
- // later iteration (attaching a different tmux session from inside Ink
483
- // needs care). Intentionally a no-op here.
484
- });
485
-
486
- useEffect(() => {
487
- setSelectedIndex((prev) => {
488
- return Math.min(prev, musicians.length);
489
- });
490
- setPendingDismissIndex((prev) => {
491
- if (prev === null) {
492
- return prev;
493
- }
494
- if (prev >= musicians.length) {
495
- return null;
496
- }
497
- return prev;
498
- });
499
- }, [musicians.length]);
500
-
501
- const permissionLevel = state ? state.permission_level : "…";
502
- const pendingCount = musicians.filter((m) => {
503
- return m.status === "awaiting_permission";
504
- }).length;
505
- const dismissTarget =
506
- pendingDismissIndex !== null ? musicians[pendingDismissIndex] : null;
507
- const dismissConfirmation = dismissTarget
508
- ? `Confirm dismiss ${dismissTarget.name} · [y]/[Enter] confirm · [n]/[Esc] cancel`
509
- : null;
510
-
511
- return (
512
- <AppView
513
- orchestras={orchestras}
514
- currentId={props.orchestraId}
515
- musicians={musicians}
516
- activity={activity}
517
- selectedIndex={selectedIndex}
518
- permissionLevel={permissionLevel}
519
- tokenHint="—"
520
- now={now}
521
- pendingCount={pendingCount}
522
- dismissConfirmation={dismissConfirmation}
523
- showHelp={showHelp}
524
- orchestratorTitle={paneTitle}
525
- orchestratorLines={orchestratorSnapshot.lines}
526
- orchestratorFocused={orchestratorFocused}
527
- orchestratorConnected={orchestratorSnapshot.connected}
528
- activeMusicianId={activePaneMusician?.id ?? null}
529
- orchestratorActive={activePaneMusician === null}
530
- />
531
- );
532
- }
@@ -1,96 +0,0 @@
1
- import type { ReactElement } from "react";
2
- import { Box } from "ink";
3
- import type { Musician } from "../state.types.js";
4
- import type { OrchestraSummary } from "../commands/list.js";
5
- import type { EmbeddedTerminalLine } from "./embedded-terminal.js";
6
- import { ConcertHall } from "./ConcertHall.js";
7
- import { Auditorium } from "./Auditorium.js";
8
- import { StatusBar } from "./StatusBar.js";
9
- import { Help } from "./Help.js";
10
- import { SidebarHeader } from "./SidebarHeader.js";
11
- import { OrchestratorPane } from "./OrchestratorPane.js";
12
-
13
- export interface AppViewProps {
14
- orchestras: OrchestraSummary[];
15
- currentId: string;
16
- musicians: Musician[];
17
- activity: Record<string, string>;
18
- selectedIndex: number;
19
- permissionLevel: string;
20
- tokenHint: string;
21
- pendingCount?: number;
22
- dismissConfirmation?: string | null;
23
- now: string;
24
- showHelp?: boolean;
25
- orchestratorTitle: string;
26
- orchestratorLines: EmbeddedTerminalLine[];
27
- orchestratorFocused: boolean;
28
- orchestratorConnected: boolean;
29
- activeMusicianId?: string | null;
30
- orchestratorActive?: boolean;
31
- }
32
-
33
- export function AppView(props: AppViewProps): ReactElement {
34
- const pendingCount = props.pendingCount ?? 0;
35
- return (
36
- <Box width="100%" height="100%">
37
- <Box flexDirection="row" width="100%" height="100%">
38
- <OrchestratorPane
39
- title={props.orchestratorTitle}
40
- lines={props.orchestratorLines}
41
- focused={props.orchestratorFocused}
42
- connected={props.orchestratorConnected}
43
- />
44
- <Box width={48} flexDirection="column">
45
- <SidebarHeader
46
- orchestraId={props.currentId}
47
- musicianCount={props.musicians.length}
48
- pendingCount={pendingCount}
49
- />
50
- <ConcertHall
51
- orchestras={props.orchestras}
52
- currentId={props.currentId}
53
- />
54
- <Auditorium
55
- musicians={props.musicians}
56
- activity={props.activity}
57
- selectedIndex={props.selectedIndex}
58
- now={props.now}
59
- orchestratorActive={props.orchestratorActive ?? false}
60
- activeMusicianId={props.activeMusicianId ?? null}
61
- />
62
- <StatusBar
63
- permissionLevel={props.permissionLevel}
64
- tokenHint={props.tokenHint}
65
- pendingCount={pendingCount}
66
- dismissConfirmation={props.dismissConfirmation}
67
- orchestratorFocused={props.orchestratorFocused}
68
- />
69
- </Box>
70
- {props.showHelp && (
71
- <Box
72
- position="absolute"
73
- top={0}
74
- left={0}
75
- width="100%"
76
- height="100%"
77
- justifyContent="center"
78
- alignItems="center"
79
- >
80
- <Box
81
- borderStyle="round"
82
- paddingX={1}
83
- paddingY={1}
84
- width={64}
85
- flexDirection="column"
86
- borderBackgroundColor={"black"}
87
- backgroundColor="black"
88
- >
89
- <Help />
90
- </Box>
91
- </Box>
92
- )}
93
- </Box>
94
- </Box>
95
- );
96
- }
@@ -1,56 +0,0 @@
1
- import type { ReactElement } from "react";
2
- import { Box, Text } from "ink";
3
- import type { Musician } from "../state.types.js";
4
- import { statusIcon, statusColor } from "./status-icon.js";
5
- import { formatRelativeTime } from "./format-time.js";
6
-
7
- export interface AuditoriumProps {
8
- musicians: Musician[];
9
- activity: Record<string, string>;
10
- selectedIndex: number;
11
- now: string;
12
- activeMusicianId?: string | null;
13
- orchestratorActive?: boolean;
14
- }
15
-
16
- export function Auditorium(props: AuditoriumProps): ReactElement {
17
- return (
18
- <Box flexDirection="column" paddingX={1}>
19
- <Text bold={true}>Auditorium</Text>
20
- <Box flexDirection="column">
21
- <Text color={props.orchestratorActive ? "cyan" : undefined}>
22
- {props.selectedIndex === 0 ? "▸" : " "} ◉ orchestrator
23
- {props.orchestratorActive ? " [open]" : ""}
24
- </Text>
25
- <Text dimColor={true}> Claude / tmux controller</Text>
26
- </Box>
27
- {props.musicians.length === 0 ? (
28
- <Text dimColor={true}>No musicians yet.</Text>
29
- ) : null}
30
- {props.musicians.map((m, i) => {
31
- const selected = i + 1 === props.selectedIndex;
32
- const marker = selected ? "▸" : " ";
33
- const since = formatRelativeTime(m.last_activity, props.now);
34
- const line =
35
- m.status === "awaiting_permission"
36
- ? `awaiting: ${m.pending_permission ?? "tool"}`
37
- : (props.activity[m.id] ?? "");
38
- const active = props.activeMusicianId === m.id;
39
- return (
40
- <Box key={m.id} flexDirection="column">
41
- <Text color={active ? "cyan" : undefined}>
42
- {marker}{" "}
43
- <Text color={statusColor(m.status)}>{statusIcon(m.status)}</Text>{" "}
44
- {m.id} {m.name}
45
- {active ? " [open]" : ""}
46
- </Text>
47
- <Text dimColor={true}>
48
- {" "}
49
- {since} · {line}
50
- </Text>
51
- </Box>
52
- );
53
- })}
54
- </Box>
55
- );
56
- }