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.
- package/dist/claude-command.js +6 -1
- package/dist/claude-command.js.map +1 -1
- package/dist/claude-trust.js +46 -0
- package/dist/claude-trust.js.map +1 -0
- package/dist/cli.js +64 -54
- package/dist/cli.js.map +1 -1
- package/dist/commands/restore.js +0 -1
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/tui.js +6 -4
- package/dist/commands/tui.js.map +1 -1
- package/dist/mcp/handlers.js +5 -0
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/mcp/tool-defs.js +10 -0
- package/dist/mcp/tool-defs.js.map +1 -1
- package/dist/musicians/dismiss.js +1 -1
- package/dist/musicians/dismiss.js.map +1 -1
- package/dist/musicians/roles.js +15 -0
- package/dist/musicians/roles.js.map +1 -0
- package/dist/musicians/spawn.js +53 -18
- package/dist/musicians/spawn.js.map +1 -1
- package/dist/permission.js +14 -8
- package/dist/permission.js.map +1 -1
- package/dist/prompts/musician-role.js +2 -1
- package/dist/prompts/musician-role.js.map +1 -1
- package/dist/prompts/orchestrator-role.js +42 -8
- package/dist/prompts/orchestrator-role.js.map +1 -1
- package/dist/prompts/tool-discipline.js +10 -0
- package/dist/prompts/tool-discipline.js.map +1 -1
- package/dist/tui/{App.js → components/App.js} +20 -20
- package/dist/tui/components/App.js.map +1 -0
- package/dist/tui/components/AppView.js +13 -0
- package/dist/tui/components/AppView.js.map +1 -0
- package/dist/tui/{Auditorium.js → components/Auditorium.js} +2 -2
- package/dist/tui/components/Auditorium.js.map +1 -0
- package/dist/tui/components/ConcertHall.js.map +1 -0
- package/dist/tui/{Help.js → components/Help.js} +0 -8
- package/dist/tui/components/Help.js.map +1 -0
- package/dist/tui/components/OrchestratorPane.js.map +1 -0
- package/dist/tui/components/SidebarHeader.js +6 -0
- package/dist/tui/components/SidebarHeader.js.map +1 -0
- package/dist/tui/{StatusBar.js → components/StatusBar.js} +1 -1
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/package.json +8 -1
- package/assets/agent-screen.png +0 -0
- package/assets/main-screen.png +0 -0
- package/assets/orche-clawd.png +0 -0
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/AppView.js +0 -13
- package/dist/tui/AppView.js.map +0 -1
- package/dist/tui/Auditorium.js.map +0 -1
- package/dist/tui/ConcertHall.js.map +0 -1
- package/dist/tui/Help.js.map +0 -1
- package/dist/tui/OrchestratorPane.js.map +0 -1
- package/dist/tui/SidebarHeader.js +0 -6
- package/dist/tui/SidebarHeader.js.map +0 -1
- package/dist/tui/StatusBar.js.map +0 -1
- package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
- package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
- package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
- package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
- package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
- package/docs/specs/2026-05-29-nfo-design.md +0 -468
- package/src/claude-command.ts +0 -35
- package/src/claude-detect.ts +0 -42
- package/src/cli.ts +0 -164
- package/src/commands/attach.ts +0 -24
- package/src/commands/dashboard-window.ts +0 -33
- package/src/commands/kill.ts +0 -50
- package/src/commands/launch.ts +0 -134
- package/src/commands/list.ts +0 -43
- package/src/commands/mcp-server.ts +0 -18
- package/src/commands/notes.ts +0 -18
- package/src/commands/restore.ts +0 -153
- package/src/commands/tui.tsx +0 -16
- package/src/config.ts +0 -44
- package/src/dashboard.ts +0 -1
- package/src/mcp/config.ts +0 -39
- package/src/mcp/handlers.ts +0 -141
- package/src/mcp/server.ts +0 -50
- package/src/mcp/tool-defs.ts +0 -151
- package/src/musicians/dismiss.ts +0 -60
- package/src/musicians/ids.ts +0 -21
- package/src/musicians/lookup.ts +0 -13
- package/src/musicians/message-log.ts +0 -152
- package/src/musicians/message.ts +0 -99
- package/src/musicians/query.ts +0 -19
- package/src/musicians/spawn.ts +0 -139
- package/src/notes.ts +0 -39
- package/src/notify.ts +0 -62
- package/src/orchestrator/report-back.ts +0 -33
- package/src/permission.ts +0 -30
- package/src/project-key.ts +0 -12
- package/src/prompts/musician-role.ts +0 -22
- package/src/prompts/orchestrator-role.ts +0 -60
- package/src/prompts/tool-discipline.ts +0 -35
- package/src/repo.ts +0 -14
- package/src/shell-quote.ts +0 -7
- package/src/state-updaters.ts +0 -132
- package/src/state.ts +0 -49
- package/src/state.types.ts +0 -67
- package/src/tmux.ts +0 -226
- package/src/tui/App.tsx +0 -532
- package/src/tui/AppView.tsx +0 -96
- package/src/tui/Auditorium.tsx +0 -56
- package/src/tui/ConcertHall.tsx +0 -31
- package/src/tui/Help.tsx +0 -72
- package/src/tui/OrchestratorPane.tsx +0 -98
- package/src/tui/SidebarHeader.tsx +0 -32
- package/src/tui/StatusBar.tsx +0 -44
- package/src/tui/activity-line.ts +0 -16
- package/src/tui/detect-permission.ts +0 -93
- package/src/tui/embedded-session-lifecycle.ts +0 -44
- package/src/tui/embedded-terminal.ts +0 -325
- package/src/tui/format-time.ts +0 -25
- package/src/tui/keymap.ts +0 -104
- package/src/tui/poll-activity.ts +0 -25
- package/src/tui/poll-idle.ts +0 -149
- package/src/tui/poll-permission.ts +0 -50
- package/src/tui/status-icon.ts +0 -35
- package/src/tui/terminal-input.ts +0 -136
- package/src/tui/watch-state.ts +0 -43
- package/src/worktree.ts +0 -41
- package/tests/claude-command.test.ts +0 -30
- package/tests/claude-detect.test.ts +0 -14
- package/tests/commands/attach.test.ts +0 -60
- package/tests/commands/kill.test.ts +0 -66
- package/tests/commands/launch.test.ts +0 -75
- package/tests/commands/list.test.ts +0 -47
- package/tests/commands/notes.test.ts +0 -53
- package/tests/commands/restore.test.ts +0 -126
- package/tests/helpers/tmp-config.ts +0 -16
- package/tests/helpers/tmp-repo.ts +0 -29
- package/tests/integration/orchestrator-spawn.test.ts +0 -108
- package/tests/mcp/handlers.test.ts +0 -163
- package/tests/mcp/tool-defs.test.ts +0 -35
- package/tests/musicians/dismiss.test.ts +0 -102
- package/tests/musicians/message.test.ts +0 -159
- package/tests/musicians/query.test.ts +0 -65
- package/tests/musicians/spawn.test.ts +0 -125
- package/tests/notes.test.ts +0 -56
- package/tests/notify.test.ts +0 -80
- package/tests/orchestrator/report-back.test.ts +0 -18
- package/tests/permission.test.ts +0 -29
- package/tests/project-key.test.ts +0 -33
- package/tests/prompts/tool-discipline.test.ts +0 -25
- package/tests/repo.test.ts +0 -38
- package/tests/state-updaters.test.ts +0 -126
- package/tests/state.test.ts +0 -85
- package/tests/tmux.test.ts +0 -126
- package/tests/tui/AppView.test.tsx +0 -92
- package/tests/tui/Auditorium.test.tsx +0 -67
- package/tests/tui/ConcertHall.test.tsx +0 -22
- package/tests/tui/Help.test.tsx +0 -38
- package/tests/tui/OrchestratorPane.test.ts +0 -30
- package/tests/tui/SidebarHeader.test.tsx +0 -20
- package/tests/tui/StatusBar.test.tsx +0 -51
- package/tests/tui/activity-line.test.ts +0 -21
- package/tests/tui/detect-permission.test.ts +0 -92
- package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
- package/tests/tui/embedded-terminal.test.ts +0 -80
- package/tests/tui/format-time.test.ts +0 -25
- package/tests/tui/keymap.test.ts +0 -93
- package/tests/tui/poll-activity.test.ts +0 -81
- package/tests/tui/poll-idle.test.ts +0 -159
- package/tests/tui/poll-permission.test.ts +0 -222
- package/tests/tui/status-icon.test.ts +0 -27
- package/tests/tui/terminal-input.test.ts +0 -113
- package/tests/tui/watch-state.test.ts +0 -54
- package/tests/worktree.test.ts +0 -73
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -12
- /package/dist/tui/{ConcertHall.js → components/ConcertHall.js} +0 -0
- /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
|
-
}
|
package/src/tui/AppView.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/tui/Auditorium.tsx
DELETED
|
@@ -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
|
-
}
|