nfo-cli 0.0.4-improve-prompting → 0.0.6-a89844d-dev

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 (178) 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 +5 -4
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/attach.js +8 -8
  8. package/dist/commands/attach.js.map +1 -1
  9. package/dist/commands/launch.js +3 -6
  10. package/dist/commands/launch.js.map +1 -1
  11. package/dist/commands/restore.js +6 -10
  12. package/dist/commands/restore.js.map +1 -1
  13. package/dist/commands/tui.js +17 -1
  14. package/dist/commands/tui.js.map +1 -1
  15. package/dist/mcp/handlers.js +5 -0
  16. package/dist/mcp/handlers.js.map +1 -1
  17. package/dist/mcp/tool-defs.js +10 -0
  18. package/dist/mcp/tool-defs.js.map +1 -1
  19. package/dist/musicians/dismiss.js +1 -1
  20. package/dist/musicians/dismiss.js.map +1 -1
  21. package/dist/musicians/reconcile.js +27 -0
  22. package/dist/musicians/reconcile.js.map +1 -0
  23. package/dist/musicians/roles.js +15 -0
  24. package/dist/musicians/roles.js.map +1 -0
  25. package/dist/musicians/spawn.js +53 -18
  26. package/dist/musicians/spawn.js.map +1 -1
  27. package/dist/permission.js +6 -0
  28. package/dist/permission.js.map +1 -1
  29. package/dist/prompts/musician-role.js +2 -1
  30. package/dist/prompts/musician-role.js.map +1 -1
  31. package/dist/prompts/orchestrator-role.js +18 -6
  32. package/dist/prompts/orchestrator-role.js.map +1 -1
  33. package/dist/prompts/tool-discipline.js +7 -3
  34. package/dist/prompts/tool-discipline.js.map +1 -1
  35. package/dist/tmux.js +23 -0
  36. package/dist/tmux.js.map +1 -1
  37. package/dist/tui/components/App.js +8 -12
  38. package/dist/tui/components/App.js.map +1 -1
  39. package/dist/tui/components/Help.js +1 -1
  40. package/dist/tui/components/Help.js.map +1 -1
  41. package/dist/tui/keymap.js +1 -1
  42. package/dist/tui/keymap.js.map +1 -1
  43. package/package.json +18 -8
  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 +0 -428
  48. package/dist/tui/App.js.map +0 -1
  49. package/dist/tui/AppView.js +0 -13
  50. package/dist/tui/AppView.js.map +0 -1
  51. package/dist/tui/Auditorium.js +0 -17
  52. package/dist/tui/Auditorium.js.map +0 -1
  53. package/dist/tui/ConcertHall.js +0 -11
  54. package/dist/tui/ConcertHall.js.map +0 -1
  55. package/dist/tui/Help.js +0 -49
  56. package/dist/tui/Help.js.map +0 -1
  57. package/dist/tui/OrchestratorPane.js +0 -34
  58. package/dist/tui/OrchestratorPane.js.map +0 -1
  59. package/dist/tui/SidebarHeader.js +0 -6
  60. package/dist/tui/SidebarHeader.js.map +0 -1
  61. package/dist/tui/StatusBar.js +0 -6
  62. package/dist/tui/StatusBar.js.map +0 -1
  63. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  64. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  65. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  66. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  67. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  68. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  69. package/plan-explorer-musician-hardening.md +0 -56
  70. package/src/claude-command.ts +0 -35
  71. package/src/claude-detect.ts +0 -42
  72. package/src/cli.ts +0 -197
  73. package/src/commands/attach.ts +0 -24
  74. package/src/commands/dashboard-window.ts +0 -33
  75. package/src/commands/kill.ts +0 -50
  76. package/src/commands/launch.ts +0 -134
  77. package/src/commands/list.ts +0 -43
  78. package/src/commands/mcp-server.ts +0 -18
  79. package/src/commands/notes.ts +0 -18
  80. package/src/commands/restore.ts +0 -153
  81. package/src/commands/tui.tsx +0 -22
  82. package/src/config.ts +0 -44
  83. package/src/dashboard.ts +0 -1
  84. package/src/mcp/config.ts +0 -39
  85. package/src/mcp/handlers.ts +0 -141
  86. package/src/mcp/server.ts +0 -50
  87. package/src/mcp/tool-defs.ts +0 -151
  88. package/src/musicians/dismiss.ts +0 -60
  89. package/src/musicians/ids.ts +0 -21
  90. package/src/musicians/lookup.ts +0 -13
  91. package/src/musicians/message-log.ts +0 -152
  92. package/src/musicians/message.ts +0 -99
  93. package/src/musicians/query.ts +0 -19
  94. package/src/musicians/spawn.ts +0 -139
  95. package/src/notes.ts +0 -39
  96. package/src/notify.ts +0 -62
  97. package/src/orchestrator/report-back.ts +0 -33
  98. package/src/permission.ts +0 -30
  99. package/src/project-key.ts +0 -12
  100. package/src/prompts/musician-role.ts +0 -22
  101. package/src/prompts/orchestrator-role.ts +0 -84
  102. package/src/prompts/tool-discipline.ts +0 -41
  103. package/src/repo.ts +0 -14
  104. package/src/shell-quote.ts +0 -7
  105. package/src/state-updaters.ts +0 -132
  106. package/src/state.ts +0 -49
  107. package/src/state.types.ts +0 -67
  108. package/src/tmux.ts +0 -226
  109. package/src/tui/activity-line.ts +0 -16
  110. package/src/tui/components/App.tsx +0 -534
  111. package/src/tui/components/AppView.tsx +0 -98
  112. package/src/tui/components/Auditorium.tsx +0 -56
  113. package/src/tui/components/ConcertHall.tsx +0 -31
  114. package/src/tui/components/Help.tsx +0 -63
  115. package/src/tui/components/OrchestratorPane.tsx +0 -98
  116. package/src/tui/components/SidebarHeader.tsx +0 -34
  117. package/src/tui/components/StatusBar.tsx +0 -42
  118. package/src/tui/detect-permission.ts +0 -93
  119. package/src/tui/embedded-session-lifecycle.ts +0 -44
  120. package/src/tui/embedded-terminal.ts +0 -325
  121. package/src/tui/format-time.ts +0 -25
  122. package/src/tui/keymap.ts +0 -104
  123. package/src/tui/poll-activity.ts +0 -25
  124. package/src/tui/poll-idle.ts +0 -149
  125. package/src/tui/poll-permission.ts +0 -50
  126. package/src/tui/status-icon.ts +0 -35
  127. package/src/tui/terminal-input.ts +0 -136
  128. package/src/tui/watch-state.ts +0 -43
  129. package/src/worktree.ts +0 -41
  130. package/tests/claude-command.test.ts +0 -30
  131. package/tests/claude-detect.test.ts +0 -14
  132. package/tests/commands/attach.test.ts +0 -60
  133. package/tests/commands/kill.test.ts +0 -66
  134. package/tests/commands/launch.test.ts +0 -75
  135. package/tests/commands/list.test.ts +0 -47
  136. package/tests/commands/notes.test.ts +0 -53
  137. package/tests/commands/restore.test.ts +0 -126
  138. package/tests/helpers/tmp-config.ts +0 -16
  139. package/tests/helpers/tmp-repo.ts +0 -29
  140. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  141. package/tests/mcp/handlers.test.ts +0 -163
  142. package/tests/mcp/tool-defs.test.ts +0 -35
  143. package/tests/musicians/dismiss.test.ts +0 -102
  144. package/tests/musicians/message.test.ts +0 -159
  145. package/tests/musicians/query.test.ts +0 -65
  146. package/tests/musicians/spawn.test.ts +0 -125
  147. package/tests/notes.test.ts +0 -56
  148. package/tests/notify.test.ts +0 -80
  149. package/tests/orchestrator/report-back.test.ts +0 -18
  150. package/tests/permission.test.ts +0 -39
  151. package/tests/project-key.test.ts +0 -33
  152. package/tests/prompts/tool-discipline.test.ts +0 -25
  153. package/tests/repo.test.ts +0 -38
  154. package/tests/state-updaters.test.ts +0 -126
  155. package/tests/state.test.ts +0 -85
  156. package/tests/tmux.test.ts +0 -126
  157. package/tests/tui/AppView.test.tsx +0 -92
  158. package/tests/tui/Auditorium.test.tsx +0 -67
  159. package/tests/tui/ConcertHall.test.tsx +0 -22
  160. package/tests/tui/Help.test.tsx +0 -38
  161. package/tests/tui/OrchestratorPane.test.ts +0 -30
  162. package/tests/tui/SidebarHeader.test.tsx +0 -20
  163. package/tests/tui/StatusBar.test.tsx +0 -51
  164. package/tests/tui/activity-line.test.ts +0 -21
  165. package/tests/tui/detect-permission.test.ts +0 -92
  166. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  167. package/tests/tui/embedded-terminal.test.ts +0 -80
  168. package/tests/tui/format-time.test.ts +0 -25
  169. package/tests/tui/keymap.test.ts +0 -93
  170. package/tests/tui/poll-activity.test.ts +0 -81
  171. package/tests/tui/poll-idle.test.ts +0 -159
  172. package/tests/tui/poll-permission.test.ts +0 -222
  173. package/tests/tui/status-icon.test.ts +0 -27
  174. package/tests/tui/terminal-input.test.ts +0 -113
  175. package/tests/tui/watch-state.test.ts +0 -54
  176. package/tests/worktree.test.ts +0 -73
  177. package/tsconfig.json +0 -19
  178. 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
- }