nfo-cli 0.0.2-c → 0.0.3-a

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 (56) hide show
  1. package/README.md +19 -8
  2. package/dist/cli.js +64 -54
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/restore.js +0 -1
  5. package/dist/commands/restore.js.map +1 -1
  6. package/dist/commands/tui.js +6 -4
  7. package/dist/commands/tui.js.map +1 -1
  8. package/dist/permission.js +8 -8
  9. package/dist/permission.js.map +1 -1
  10. package/dist/prompts/orchestrator-role.js +6 -7
  11. package/dist/prompts/orchestrator-role.js.map +1 -1
  12. package/dist/tui/App.js +5 -5
  13. package/dist/tui/App.js.map +1 -1
  14. package/dist/tui/AppView.js +1 -1
  15. package/dist/tui/AppView.js.map +1 -1
  16. package/dist/tui/SidebarHeader.js +1 -1
  17. package/dist/tui/SidebarHeader.js.map +1 -1
  18. package/dist/tui/StatusBar.js +1 -1
  19. package/dist/tui/StatusBar.js.map +1 -1
  20. package/dist/tui/components/App.js +428 -0
  21. package/dist/tui/components/App.js.map +1 -0
  22. package/dist/tui/components/AppView.js +13 -0
  23. package/dist/tui/components/AppView.js.map +1 -0
  24. package/dist/tui/components/Auditorium.js +17 -0
  25. package/dist/tui/components/Auditorium.js.map +1 -0
  26. package/dist/tui/components/ConcertHall.js +11 -0
  27. package/dist/tui/components/ConcertHall.js.map +1 -0
  28. package/dist/tui/components/Help.js +41 -0
  29. package/dist/tui/components/Help.js.map +1 -0
  30. package/dist/tui/components/OrchestratorPane.js +34 -0
  31. package/dist/tui/components/OrchestratorPane.js.map +1 -0
  32. package/dist/tui/components/SidebarHeader.js +6 -0
  33. package/dist/tui/components/SidebarHeader.js.map +1 -0
  34. package/dist/tui/components/StatusBar.js +6 -0
  35. package/dist/tui/components/StatusBar.js.map +1 -0
  36. package/package.json +2 -2
  37. package/src/cli.ts +119 -86
  38. package/src/commands/tui.tsx +10 -4
  39. package/src/permission.ts +8 -8
  40. package/src/prompts/orchestrator-role.ts +3 -2
  41. package/src/tui/{App.tsx → components/App.tsx} +22 -20
  42. package/src/tui/{AppView.tsx → components/AppView.tsx} +5 -3
  43. package/src/tui/{Auditorium.tsx → components/Auditorium.tsx} +3 -3
  44. package/src/tui/{ConcertHall.tsx → components/ConcertHall.tsx} +1 -1
  45. package/src/tui/{Help.tsx → components/Help.tsx} +0 -9
  46. package/src/tui/{OrchestratorPane.tsx → components/OrchestratorPane.tsx} +1 -1
  47. package/src/tui/{SidebarHeader.tsx → components/SidebarHeader.tsx} +3 -1
  48. package/src/tui/{StatusBar.tsx → components/StatusBar.tsx} +1 -3
  49. package/tests/permission.test.ts +15 -5
  50. package/tests/tui/AppView.test.tsx +2 -2
  51. package/tests/tui/Auditorium.test.tsx +1 -1
  52. package/tests/tui/ConcertHall.test.tsx +1 -1
  53. package/tests/tui/Help.test.tsx +1 -1
  54. package/tests/tui/OrchestratorPane.test.ts +1 -1
  55. package/tests/tui/SidebarHeader.test.tsx +1 -1
  56. package/tests/tui/StatusBar.test.tsx +1 -1
@@ -0,0 +1,6 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ export function StatusBar(props) {
4
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderTop: true, paddingX: 1, children: [props.pendingCount > 0 ? (_jsxs(Text, { color: "yellow", children: ["\u26A0 ", props.pendingCount, " awaiting permission \u00B7 [p] jump to next"] })) : null, props.dismissConfirmation ? (_jsx(Text, { color: "red", children: props.dismissConfirmation })) : null, _jsxs(Text, { children: [props.permissionLevel, " \u00B7 ", props.tokenHint] }), props.orchestratorFocused ? (_jsx(Text, { dimColor: true, children: "[type] active terminal [Ctrl+g] sidebar" })) : (_jsx(Text, { dimColor: true, children: "[\u2191\u2193] nav [\u23CE] open left pane [d] dismiss [p] pending [n] notes [Ctrl+g] terminal" })), _jsx(Text, { dimColor: true, children: "[q] detach [?] help" })] }));
5
+ }
6
+ //# sourceMappingURL=StatusBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StatusBar.js","sourceRoot":"","sources":["../../../src/tui/components/StatusBar.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAUhC,MAAM,UAAU,SAAS,CAAC,KAAqB;IAC7C,OAAO,CACL,MAAC,GAAG,IACF,aAAa,EAAC,QAAQ,EACtB,WAAW,EAAC,QAAQ,EACpB,SAAS,EAAE,IAAI,EACf,QAAQ,EAAE,CAAC,aAEV,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CACxB,MAAC,IAAI,IAAC,KAAK,EAAC,QAAQ,wBACf,KAAK,CAAC,YAAY,oDAChB,CACR,CAAC,CAAC,CAAC,IAAI,EACP,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAC3B,KAAC,IAAI,IAAC,KAAK,EAAC,KAAK,YAAE,KAAK,CAAC,mBAAmB,GAAQ,CACrD,CAAC,CAAC,CAAC,IAAI,EACR,MAAC,IAAI,eACF,KAAK,CAAC,eAAe,cAAK,KAAK,CAAC,SAAS,IACrC,EACN,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAC3B,KAAC,IAAI,IAAC,QAAQ,EAAE,IAAI,wDAAgD,CACrE,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,QAAQ,EAAE,IAAI,+GAGb,CACR,EACD,KAAC,IAAI,IAAC,QAAQ,EAAE,IAAI,oCAA4B,IAC5C,CACP,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nfo-cli",
3
- "version": "0.0.2-c",
4
- "description": "NoFluffOrchestraTUI multi-agent orchestrator for existing repos",
3
+ "version": "0.0.3-a",
4
+ "description": "No Fluff Orchestra a simple, fluff-free CLI multi-agent orchestrator",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "nfo": "./dist/cli.js"
package/src/cli.ts CHANGED
@@ -1,99 +1,117 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { decideAction, createOrchestra } from './commands/launch.js';
4
- import { attachOrRestore } from './commands/attach.js';
5
- import { listOrchestras, formatOrchestraList, type OrchestraSummary } from './commands/list.js';
6
- import { isPermissionLevel, AUTO_CONFIRM_PHRASE, AUTO_WARNING, type PermissionLevel } from './permission.js';
7
- import { detectClaude } from './claude-detect.js';
8
- import { createInterface } from 'node:readline/promises';
2
+ import { Command } from "commander";
3
+ import { decideAction, createOrchestra } from "./commands/launch.js";
4
+ import { attachOrRestore } from "./commands/attach.js";
5
+ import {
6
+ listOrchestras,
7
+ formatOrchestraList,
8
+ type OrchestraSummary,
9
+ } from "./commands/list.js";
10
+ import {
11
+ isPermissionLevel,
12
+ DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE,
13
+ DANGEROUSLY_SKIP_PERMISSIONS_WARNING,
14
+ type PermissionLevel,
15
+ } from "./permission.js";
16
+ import { detectClaude } from "./claude-detect.js";
17
+ import { createInterface } from "node:readline/promises";
18
+ import packageJson from "../package.json" with { type: "json" };
9
19
 
10
20
  const program = new Command();
11
21
  program
12
- .name('nfo')
13
- .description('NoFluffOrchestra — TUI multi-agent orchestrator')
14
- .version('0.0.0');
22
+ .name("nfo")
23
+ .description("NoFluffOrchestra — TUI multi-agent orchestrator")
24
+ .version(packageJson.version);
15
25
 
16
26
  program
17
- .argument('[id]', 'Orchestra id to attach (optional)')
18
- .option('--notify-on-permission', 'bell + desktop notify when a musician awaits permission')
19
- .action(async (id: string | undefined, opts: { notifyOnPermission?: boolean }) => {
20
- await detectClaude();
21
- try {
22
- if (id) {
23
- await attachOrRestore(id);
24
- return;
25
- }
26
- const decision = await decideAction(process.cwd());
27
- switch (decision.kind) {
28
- case 'create': {
29
- const level = await promptPermissionLevel();
30
- await createOrchestra({
31
- repoRoot: decision.repoRoot,
32
- orchestraId: decision.orchestraId,
33
- permissionLevel: level,
34
- notifyOnPermission: opts.notifyOnPermission,
35
- });
27
+ .argument("[id]", "Orchestra id to attach (optional)")
28
+ .option(
29
+ "--notify-on-permission",
30
+ "bell + desktop notify when a musician awaits permission",
31
+ )
32
+ .action(
33
+ async (id: string | undefined, opts: { notifyOnPermission?: boolean }) => {
34
+ await detectClaude();
35
+ try {
36
+ if (id) {
37
+ await attachOrRestore(id);
36
38
  return;
37
39
  }
38
- case 'attach_existing':
39
- await attachOrRestore(decision.orchestraId);
40
- return;
41
- case 'pick': {
42
- const picked = await promptOrchestraPicker(decision.summaries);
43
- await attachOrRestore(picked);
44
- return;
40
+ const decision = await decideAction(process.cwd());
41
+ switch (decision.kind) {
42
+ case "create": {
43
+ const level = await promptPermissionLevel();
44
+ await createOrchestra({
45
+ repoRoot: decision.repoRoot,
46
+ orchestraId: decision.orchestraId,
47
+ permissionLevel: level,
48
+ notifyOnPermission: opts.notifyOnPermission,
49
+ });
50
+ return;
51
+ }
52
+ case "attach_existing":
53
+ await attachOrRestore(decision.orchestraId);
54
+ return;
55
+ case "pick": {
56
+ const picked = await promptOrchestraPicker(decision.summaries);
57
+ await attachOrRestore(picked);
58
+ return;
59
+ }
60
+ case "error":
61
+ console.error(decision.message);
62
+ process.exit(1);
45
63
  }
46
- case 'error':
47
- console.error(decision.message);
48
- process.exit(1);
64
+ } catch (err) {
65
+ console.error(err instanceof Error ? err.message : String(err));
66
+ process.exit(1);
49
67
  }
50
- } catch (err) {
51
- console.error(err instanceof Error ? err.message : String(err));
52
- process.exit(1);
53
- }
54
- });
68
+ },
69
+ );
55
70
 
56
71
  program
57
- .command('list')
58
- .description('List all known orchestras')
72
+ .command("list")
73
+ .description("List all known orchestras")
59
74
  .action(async () => {
60
75
  const summaries = await listOrchestras();
61
76
  console.log(formatOrchestraList(summaries));
62
77
  });
63
78
 
64
79
  program
65
- .command('restore <id>')
66
- .description('Force-restore a stopped orchestra')
67
- .option('--notify-on-permission', 'bell + desktop notify when a musician awaits permission')
80
+ .command("restore <id>")
81
+ .description("Force-restore a stopped orchestra")
82
+ .option(
83
+ "--notify-on-permission",
84
+ "bell + desktop notify when a musician awaits permission",
85
+ )
68
86
  .action(async (id: string, opts: { notifyOnPermission?: boolean }) => {
69
- const { restoreOrchestra } = await import('./commands/restore.js');
87
+ const { restoreOrchestra } = await import("./commands/restore.js");
70
88
  await restoreOrchestra(id, undefined, opts.notifyOnPermission);
71
89
  });
72
90
 
73
91
  program
74
- .command('kill <id>')
75
- .description('Tear down an orchestra (state archived, notes preserved)')
76
- .option('-y, --yes', 'Skip confirmation prompt')
92
+ .command("kill <id>")
93
+ .description("Tear down an orchestra (state archived, notes preserved)")
94
+ .option("-y, --yes", "Skip confirmation prompt")
77
95
  .action(async (id: string, opts: { yes?: boolean }) => {
78
- const { killOrchestra } = await import('./commands/kill.js');
96
+ const { killOrchestra } = await import("./commands/kill.js");
79
97
  await killOrchestra(id, opts);
80
98
  });
81
99
 
82
100
  program
83
- .command('notes <id>')
84
- .description('Open the orchestra\'s notes/ directory in $EDITOR')
101
+ .command("notes <id>")
102
+ .description("Open the orchestra's notes/ directory in $EDITOR")
85
103
  .action(async (id: string) => {
86
- const { openNotes } = await import('./commands/notes.js');
104
+ const { openNotes } = await import("./commands/notes.js");
87
105
  await openNotes(id);
88
106
  });
89
107
 
90
108
  program
91
- .command('mcp-server', { hidden: true })
92
- .description('(internal) Run the NFO MCP server attached to an orchestra')
93
- .requiredOption('--orchestra-id <id>', 'Orchestra id')
94
- .option('--caller-musician-id <id>', 'When the server is hosting a Musician')
109
+ .command("mcp-server", { hidden: true })
110
+ .description("(internal) Run the NFO MCP server attached to an orchestra")
111
+ .requiredOption("--orchestra-id <id>", "Orchestra id")
112
+ .option("--caller-musician-id <id>", "When the server is hosting a Musician")
95
113
  .action(async (opts: { orchestraId: string; callerMusicianId?: string }) => {
96
- const { runMcpServerCli } = await import('./commands/mcp-server.js');
114
+ const { runMcpServerCli } = await import("./commands/mcp-server.js");
97
115
  await runMcpServerCli({
98
116
  orchestraId: opts.orchestraId,
99
117
  callerMusicianId: opts.callerMusicianId,
@@ -101,12 +119,15 @@ program
101
119
  });
102
120
 
103
121
  program
104
- .command('tui', { hidden: true })
105
- .description('(internal) Run the NFO Ink TUI for an orchestra')
106
- .requiredOption('--orchestra-id <id>', 'Orchestra id')
122
+ .command("tui", { hidden: true })
123
+ .description("(internal) Run the NFO Ink TUI for an orchestra")
124
+ .requiredOption("--orchestra-id <id>", "Orchestra id")
107
125
  .action(async (opts: { orchestraId: string }) => {
108
- const { runTui } = await import('./commands/tui.js');
109
- await runTui({ orchestraId: opts.orchestraId });
126
+ const { runTui } = await import("./commands/tui.js");
127
+ await runTui({
128
+ orchestraId: opts.orchestraId,
129
+ version: packageJson.version,
130
+ });
110
131
  });
111
132
 
112
133
  program.parseAsync(process.argv);
@@ -114,28 +135,36 @@ program.parseAsync(process.argv);
114
135
  async function promptPermissionLevel(): Promise<PermissionLevel> {
115
136
  const rl = createInterface({ input: process.stdin, output: process.stdout });
116
137
  try {
117
- const ans = (await rl.question(
118
- `Permission level for this orchestra:
119
- 1) auto RISKY: bypasses all permission checks
120
- 2) autonomous auto-accept edits, prompt on risky tools
121
- 3) supervised claude's default prompt-on-risky behavior
122
- 4) strict read-only / plan mode
123
- Choose [1-4] (default 3): `,
124
- )).trim();
138
+ const ans = (
139
+ await rl.question(
140
+ `Permission level for this orchestra:
141
+ 1) Dangerously skip permissions RISKY: bypasses all permission checks
142
+ 2) auto Claude auto mode (prompts only on risky actions)
143
+ 3) edits auto-accept edits, prompt on shell/tools
144
+ 4) supervised — claude's default prompt-on-risky behavior
145
+ 5) strict — read-only / plan mode
146
+ Choose [1-5] (default 4): `,
147
+ )
148
+ ).trim();
125
149
 
126
150
  const map: Record<string, PermissionLevel> = {
127
- '1': 'auto', '2': 'autonomous', '3': 'supervised', '4': 'strict', '': 'supervised',
151
+ "1": "dangerouslySkipPermissions",
152
+ "2": "auto",
153
+ "3": "acceptEdits",
154
+ "4": "supervised",
155
+ "5": "strict",
156
+ "": "supervised",
128
157
  };
129
158
  const level = map[ans];
130
159
  if (!level || !isPermissionLevel(level)) {
131
160
  throw new Error(`Invalid choice: ${ans}`);
132
161
  }
133
162
 
134
- if (level === 'auto') {
135
- console.log('\n' + AUTO_WARNING + '\n');
136
- const confirm = (await rl.question('> ')).trim();
137
- if (confirm !== AUTO_CONFIRM_PHRASE) {
138
- throw new Error('Auto mode not confirmed. Aborting.');
163
+ if (level === "dangerouslySkipPermissions") {
164
+ console.log("\n" + DANGEROUSLY_SKIP_PERMISSIONS_WARNING + "\n");
165
+ const confirm = (await rl.question("> ")).trim();
166
+ if (confirm !== DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE) {
167
+ throw new Error("Auto mode not confirmed. Aborting.");
139
168
  }
140
169
  }
141
170
 
@@ -145,17 +174,21 @@ Choose [1-4] (default 3): `,
145
174
  }
146
175
  }
147
176
 
148
- async function promptOrchestraPicker(summaries: OrchestraSummary[]): Promise<string> {
177
+ async function promptOrchestraPicker(
178
+ summaries: OrchestraSummary[],
179
+ ): Promise<string> {
149
180
  const rl = createInterface({ input: process.stdin, output: process.stdout });
150
181
  try {
151
- console.log('Multiple orchestras found:');
182
+ console.log("Multiple orchestras found:");
152
183
  summaries.forEach((s, i) => {
153
- console.log(` ${i + 1}) ${s.running ? '●' : '○'} ${s.id} (${s.project_path})`);
184
+ console.log(
185
+ ` ${i + 1}) ${s.running ? "●" : "○"} ${s.id} (${s.project_path})`,
186
+ );
154
187
  });
155
- const choice = (await rl.question('Pick one [1-N]: ')).trim();
188
+ const choice = (await rl.question("Pick one [1-N]: ")).trim();
156
189
  const idx = Number(choice) - 1;
157
190
  if (Number.isNaN(idx) || idx < 0 || idx >= summaries.length) {
158
- throw new Error('Invalid choice');
191
+ throw new Error("Invalid choice");
159
192
  }
160
193
  return summaries[idx].id;
161
194
  } finally {
@@ -1,9 +1,10 @@
1
- import { render } from 'ink';
2
- import { App } from '../tui/App.js';
3
- import { readState } from '../state.js';
1
+ import { render } from "ink";
2
+ import { App } from "../tui/components/App.js";
3
+ import { readState } from "../state.js";
4
4
 
5
5
  export interface RunTuiOptions {
6
6
  orchestraId: string;
7
+ version: string;
7
8
  }
8
9
 
9
10
  export async function runTui(opts: RunTuiOptions): Promise<void> {
@@ -11,6 +12,11 @@ export async function runTui(opts: RunTuiOptions): Promise<void> {
11
12
  if (!state) {
12
13
  throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
13
14
  }
14
- const instance = render(<App orchestraId={opts.orchestraId} />, { exitOnCtrlC: false });
15
+ const instance = render(
16
+ <App orchestraId={opts.orchestraId} version={opts.version} />,
17
+ {
18
+ exitOnCtrlC: false,
19
+ },
20
+ );
15
21
  await instance.waitUntilExit();
16
22
  }
package/src/permission.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const PERMISSION_LEVELS = ['auto', 'autonomous', 'supervised', 'strict'] as const;
1
+ export const PERMISSION_LEVELS = ['dangerouslySkipPermissions', 'auto', 'acceptEdits', 'supervised', 'strict'] as const;
2
2
  export type PermissionLevel = (typeof PERMISSION_LEVELS)[number];
3
3
 
4
4
  export function isPermissionLevel(s: string): s is PermissionLevel {
@@ -7,11 +7,11 @@ export function isPermissionLevel(s: string): s is PermissionLevel {
7
7
 
8
8
  export function claudeFlagsForLevel(level: PermissionLevel): string[] {
9
9
  switch (level) {
10
- case 'auto':
11
- // Spec §5.2 + §12.2 open question: exact bypass flag is `--dangerously-skip-permissions`
12
- // in current Claude Code releases. If a future release renames it, update here.
10
+ case 'dangerouslySkipPermissions':
13
11
  return ['--dangerously-skip-permissions'];
14
- case 'autonomous':
12
+ case 'auto':
13
+ return ['--permission-mode', 'auto'];
14
+ case 'acceptEdits':
15
15
  return ['--permission-mode', 'acceptEdits'];
16
16
  case 'supervised':
17
17
  return ['--permission-mode', 'default'];
@@ -20,11 +20,11 @@ export function claudeFlagsForLevel(level: PermissionLevel): string[] {
20
20
  }
21
21
  }
22
22
 
23
- export const AUTO_CONFIRM_PHRASE = 'I understand';
23
+ export const DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE = 'I understand';
24
24
 
25
- export const AUTO_WARNING = `⚠ AUTO mode disables all permission checks.
25
+ export const DANGEROUSLY_SKIP_PERMISSIONS_WARNING = `⚠ "Dangerously skip permissions" mode disables all permission checks.
26
26
  Musicians can execute arbitrary shell commands, modify files anywhere on
27
27
  this system, and access the network without asking. Worktrees limit but
28
28
  do not contain risky operations. Use this only in trusted sandboxes or
29
29
  when you accept these risks.
30
- Type "${AUTO_CONFIRM_PHRASE}" to continue.`;
30
+ Type "${DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE}" to continue.`;
@@ -9,8 +9,9 @@ Available NFO tools (in addition to your normal Claude Code tools):
9
9
 
10
10
  spawn_musician({ name, task, worktree?, branch_from?, model? })
11
11
  Create a Musician with the given task. By default the Musician runs in a
12
- fresh git worktree off HEAD. Pass worktree=false for trivially isolated
13
- work (e.g., docs-only) that doesn't need an isolated branch. Returns the
12
+ fresh git worktree off HEAD. Deploy research musicians that investigate the codebase for the given task. They are only allowed
13
+ to research and report back findings, without modifying the codebase.
14
+ Pass worktree=false for trivially isolated and research work (e.g., docs-only) that doesn't need an isolated branch. Returns the
14
15
  musician_id. Provide a model to be used by the Musician, otherwise it defaults to sonnet.
15
16
  For trivial tasks Haiku is a good choice; for complex coding work, Sonnet is better.
16
17
 
@@ -2,30 +2,30 @@ import type { ReactElement } from "react";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { useInput, useStdout, useWindowSize } from "ink";
4
4
  import { AppView } from "./AppView.js";
5
- import { reduceKey } from "./keymap.js";
6
- import { pollActivity } from "./poll-activity.js";
5
+ import { reduceKey } from "../keymap.js";
6
+ import { pollActivity } from "../poll-activity.js";
7
7
  import {
8
8
  syncMusicianIdleState,
9
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";
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
15
  import {
16
16
  EmbeddedTerminal,
17
17
  type EmbeddedTerminalSnapshot,
18
- } from "./embedded-terminal.js";
18
+ } from "../embedded-terminal.js";
19
19
  import {
20
20
  claimEmbeddedSessionLease,
21
21
  embeddedSessionLeaseIsCurrent,
22
22
  runEmbeddedSessionOperation,
23
- } from "./embedded-session-lifecycle.js";
23
+ } from "../embedded-session-lifecycle.js";
24
24
  import {
25
25
  toTerminalMouseScroll,
26
26
  toTerminalInput,
27
27
  toTerminalViewportCommand,
28
- } from "./terminal-input.js";
28
+ } from "../terminal-input.js";
29
29
  import {
30
30
  detachCurrentClient,
31
31
  embeddedSessionName,
@@ -33,15 +33,16 @@ import {
33
33
  killSession,
34
34
  selectWindow,
35
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";
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
42
 
43
43
  export interface AppProps {
44
44
  orchestraId: string;
45
+ version: string;
45
46
  }
46
47
 
47
48
  function textLines(...lines: string[]): EmbeddedTerminalSnapshot["lines"] {
@@ -358,10 +359,10 @@ export function App(props: AppProps): ReactElement {
358
359
  const mouseScroll = toTerminalMouseScroll(input);
359
360
  if (mouseScroll) {
360
361
  const insideTerminalViewport =
361
- mouseScroll.column >= terminalScreenLeft
362
- && mouseScroll.column <= terminalScreenRight
363
- && mouseScroll.row >= terminalScreenTop
364
- && mouseScroll.row <= terminalScreenBottom;
362
+ mouseScroll.column >= terminalScreenLeft &&
363
+ mouseScroll.column <= terminalScreenRight &&
364
+ mouseScroll.row >= terminalScreenTop &&
365
+ mouseScroll.row <= terminalScreenBottom;
365
366
  if (insideTerminalViewport) {
366
367
  const translatedColumn = mouseScroll.column - terminalScreenLeft + 1;
367
368
  const translatedRow = mouseScroll.row - terminalScreenTop + 1;
@@ -527,6 +528,7 @@ export function App(props: AppProps): ReactElement {
527
528
  orchestratorConnected={orchestratorSnapshot.connected}
528
529
  activeMusicianId={activePaneMusician?.id ?? null}
529
530
  orchestratorActive={activePaneMusician === null}
531
+ version={props.version}
530
532
  />
531
533
  );
532
534
  }
@@ -1,8 +1,8 @@
1
1
  import type { ReactElement } from "react";
2
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";
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
6
  import { ConcertHall } from "./ConcertHall.js";
7
7
  import { Auditorium } from "./Auditorium.js";
8
8
  import { StatusBar } from "./StatusBar.js";
@@ -28,6 +28,7 @@ export interface AppViewProps {
28
28
  orchestratorConnected: boolean;
29
29
  activeMusicianId?: string | null;
30
30
  orchestratorActive?: boolean;
31
+ version: string;
31
32
  }
32
33
 
33
34
  export function AppView(props: AppViewProps): ReactElement {
@@ -46,6 +47,7 @@ export function AppView(props: AppViewProps): ReactElement {
46
47
  orchestraId={props.currentId}
47
48
  musicianCount={props.musicians.length}
48
49
  pendingCount={pendingCount}
50
+ version={props.version}
49
51
  />
50
52
  <ConcertHall
51
53
  orchestras={props.orchestras}
@@ -1,8 +1,8 @@
1
1
  import type { ReactElement } from "react";
2
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";
3
+ import type { Musician } from "../../state.types.js";
4
+ import { statusIcon, statusColor } from "../status-icon.js";
5
+ import { formatRelativeTime } from "../format-time.js";
6
6
 
7
7
  export interface AuditoriumProps {
8
8
  musicians: Musician[];
@@ -1,6 +1,6 @@
1
1
  import type { ReactElement } from "react";
2
2
  import { Box, Text } from "ink";
3
- import type { OrchestraSummary } from "../commands/list.js";
3
+ import type { OrchestraSummary } from "../../commands/list.js";
4
4
 
5
5
  export interface ConcertHallProps {
6
6
  orchestras: OrchestraSummary[];
@@ -42,15 +42,6 @@ const ROWS: Row[] = [
42
42
  label:
43
43
  "scroll the left terminal through local scrollback when the pointer is over that pane",
44
44
  },
45
- {
46
- key: "F6",
47
- label: "tmux global key: jump to the dashboard window from any NFO window",
48
- },
49
- {
50
- key: "F7",
51
- label:
52
- "tmux global key: jump to the Orchestrator window from any NFO window",
53
- },
54
45
  { key: "?", label: "toggle this help / close" },
55
46
  ];
56
47
 
@@ -3,7 +3,7 @@ import { Box, Text } from "ink";
3
3
  import type {
4
4
  EmbeddedTerminalLine,
5
5
  EmbeddedTerminalSpan,
6
- } from "./embedded-terminal.js";
6
+ } from "../embedded-terminal.js";
7
7
 
8
8
  export interface OrchestratorPaneProps {
9
9
  title: string;
@@ -5,17 +5,19 @@ export interface SidebarHeaderProps {
5
5
  orchestraId: string;
6
6
  musicianCount: number;
7
7
  pendingCount: number;
8
+ version: string;
8
9
  }
9
10
 
10
11
  export function SidebarHeader(props: SidebarHeaderProps): ReactElement {
11
12
  return (
12
13
  <Box
13
14
  flexDirection="column"
14
- borderStyle="single"
15
+ borderStyle="round"
15
16
  borderBottom={true}
16
17
  paddingX={1}
17
18
  >
18
19
  <Text bold={true}>No Fluff Orchestra · {props.orchestraId}</Text>
20
+ <Text bold={true}>v.{props.version}</Text>
19
21
  {props.pendingCount > 0 ? (
20
22
  <Text color="yellow">
21
23
  {props.musicianCount} musicians · {props.pendingCount} awaiting
@@ -36,9 +36,7 @@ export function StatusBar(props: StatusBarProps): ReactElement {
36
36
  terminal
37
37
  </Text>
38
38
  )}
39
- <Text dimColor={true}>
40
- [q] detach [F6] dashboard [F7] orchestrator [?] help
41
- </Text>
39
+ <Text dimColor={true}>[q] detach [?] help</Text>
42
40
  </Box>
43
41
  );
44
42
  }
@@ -7,13 +7,20 @@ import {
7
7
  } from '../src/permission.js';
8
8
 
9
9
  describe('permission levels', () => {
10
- it('lists all four levels in order from most to least permissive', () => {
11
- expect(PERMISSION_LEVELS).toEqual(['auto', 'autonomous', 'supervised', 'strict']);
10
+ it('lists all five levels in order from most to least permissive', () => {
11
+ expect(PERMISSION_LEVELS).toEqual([
12
+ 'dangerouslySkipPermissions',
13
+ 'auto',
14
+ 'acceptEdits',
15
+ 'supervised',
16
+ 'strict',
17
+ ]);
12
18
  });
13
19
 
14
20
  it('isPermissionLevel rejects unknown strings', () => {
21
+ expect(isPermissionLevel('dangerouslySkipPermissions')).toBe(true);
15
22
  expect(isPermissionLevel('auto')).toBe(true);
16
- expect(isPermissionLevel('autonomous')).toBe(true);
23
+ expect(isPermissionLevel('acceptEdits')).toBe(true);
17
24
  expect(isPermissionLevel('supervised')).toBe(true);
18
25
  expect(isPermissionLevel('strict')).toBe(true);
19
26
  expect(isPermissionLevel('YOLO')).toBe(false);
@@ -21,8 +28,11 @@ describe('permission levels', () => {
21
28
  });
22
29
 
23
30
  it('claudeFlagsForLevel returns the right flag list per level', () => {
24
- expect(claudeFlagsForLevel('auto')).toEqual(['--dangerously-skip-permissions']);
25
- expect(claudeFlagsForLevel('autonomous')).toEqual(['--permission-mode', 'acceptEdits']);
31
+ expect(claudeFlagsForLevel('dangerouslySkipPermissions')).toEqual([
32
+ '--dangerously-skip-permissions',
33
+ ]);
34
+ expect(claudeFlagsForLevel('auto')).toEqual(['--permission-mode', 'auto']);
35
+ expect(claudeFlagsForLevel('acceptEdits')).toEqual(['--permission-mode', 'acceptEdits']);
26
36
  expect(claudeFlagsForLevel('supervised')).toEqual(['--permission-mode', 'default']);
27
37
  expect(claudeFlagsForLevel('strict')).toEqual(['--permission-mode', 'plan']);
28
38
  });
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { render } from 'ink-testing-library';
3
- import { AppView } from '../../src/tui/AppView.js';
4
- import { OrchestratorPane } from '../../src/tui/OrchestratorPane.js';
3
+ import { AppView } from '../../src/tui/components/AppView.js';
4
+ import { OrchestratorPane } from '../../src/tui/components/OrchestratorPane.js';
5
5
  import type { Musician } from '../../src/state.types.js';
6
6
  import type { OrchestraSummary } from '../../src/commands/list.js';
7
7
 
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { render } from 'ink-testing-library';
3
- import { Auditorium } from '../../src/tui/Auditorium.js';
3
+ import { Auditorium } from '../../src/tui/components/Auditorium.js';
4
4
  import type { Musician } from '../../src/state.types.js';
5
5
 
6
6
  function mus(over: Partial<Musician>): Musician {