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