nfo-cli 0.0.4-improve-prompting → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/claude-command.js +6 -1
  2. package/dist/claude-command.js.map +1 -1
  3. package/dist/claude-trust.js +46 -0
  4. package/dist/claude-trust.js.map +1 -0
  5. package/dist/cli.js +0 -0
  6. package/dist/mcp/handlers.js +5 -0
  7. package/dist/mcp/handlers.js.map +1 -1
  8. package/dist/mcp/tool-defs.js +10 -0
  9. package/dist/mcp/tool-defs.js.map +1 -1
  10. package/dist/musicians/dismiss.js +1 -1
  11. package/dist/musicians/dismiss.js.map +1 -1
  12. package/dist/musicians/roles.js +15 -0
  13. package/dist/musicians/roles.js.map +1 -0
  14. package/dist/musicians/spawn.js +53 -18
  15. package/dist/musicians/spawn.js.map +1 -1
  16. package/dist/permission.js +6 -0
  17. package/dist/permission.js.map +1 -1
  18. package/dist/prompts/musician-role.js +2 -1
  19. package/dist/prompts/musician-role.js.map +1 -1
  20. package/dist/prompts/orchestrator-role.js +18 -6
  21. package/dist/prompts/orchestrator-role.js.map +1 -1
  22. package/dist/prompts/tool-discipline.js +7 -3
  23. package/dist/prompts/tool-discipline.js.map +1 -1
  24. package/package.json +8 -1
  25. package/assets/agent-screen.png +0 -0
  26. package/assets/main-screen.png +0 -0
  27. package/assets/orche-clawd.png +0 -0
  28. package/dist/tui/App.js +0 -428
  29. package/dist/tui/App.js.map +0 -1
  30. package/dist/tui/AppView.js +0 -13
  31. package/dist/tui/AppView.js.map +0 -1
  32. package/dist/tui/Auditorium.js +0 -17
  33. package/dist/tui/Auditorium.js.map +0 -1
  34. package/dist/tui/ConcertHall.js +0 -11
  35. package/dist/tui/ConcertHall.js.map +0 -1
  36. package/dist/tui/Help.js +0 -49
  37. package/dist/tui/Help.js.map +0 -1
  38. package/dist/tui/OrchestratorPane.js +0 -34
  39. package/dist/tui/OrchestratorPane.js.map +0 -1
  40. package/dist/tui/SidebarHeader.js +0 -6
  41. package/dist/tui/SidebarHeader.js.map +0 -1
  42. package/dist/tui/StatusBar.js +0 -6
  43. package/dist/tui/StatusBar.js.map +0 -1
  44. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  45. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  46. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  47. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  48. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  49. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  50. package/plan-explorer-musician-hardening.md +0 -56
  51. package/src/claude-command.ts +0 -35
  52. package/src/claude-detect.ts +0 -42
  53. package/src/cli.ts +0 -197
  54. package/src/commands/attach.ts +0 -24
  55. package/src/commands/dashboard-window.ts +0 -33
  56. package/src/commands/kill.ts +0 -50
  57. package/src/commands/launch.ts +0 -134
  58. package/src/commands/list.ts +0 -43
  59. package/src/commands/mcp-server.ts +0 -18
  60. package/src/commands/notes.ts +0 -18
  61. package/src/commands/restore.ts +0 -153
  62. package/src/commands/tui.tsx +0 -22
  63. package/src/config.ts +0 -44
  64. package/src/dashboard.ts +0 -1
  65. package/src/mcp/config.ts +0 -39
  66. package/src/mcp/handlers.ts +0 -141
  67. package/src/mcp/server.ts +0 -50
  68. package/src/mcp/tool-defs.ts +0 -151
  69. package/src/musicians/dismiss.ts +0 -60
  70. package/src/musicians/ids.ts +0 -21
  71. package/src/musicians/lookup.ts +0 -13
  72. package/src/musicians/message-log.ts +0 -152
  73. package/src/musicians/message.ts +0 -99
  74. package/src/musicians/query.ts +0 -19
  75. package/src/musicians/spawn.ts +0 -139
  76. package/src/notes.ts +0 -39
  77. package/src/notify.ts +0 -62
  78. package/src/orchestrator/report-back.ts +0 -33
  79. package/src/permission.ts +0 -30
  80. package/src/project-key.ts +0 -12
  81. package/src/prompts/musician-role.ts +0 -22
  82. package/src/prompts/orchestrator-role.ts +0 -84
  83. package/src/prompts/tool-discipline.ts +0 -41
  84. package/src/repo.ts +0 -14
  85. package/src/shell-quote.ts +0 -7
  86. package/src/state-updaters.ts +0 -132
  87. package/src/state.ts +0 -49
  88. package/src/state.types.ts +0 -67
  89. package/src/tmux.ts +0 -226
  90. package/src/tui/activity-line.ts +0 -16
  91. package/src/tui/components/App.tsx +0 -534
  92. package/src/tui/components/AppView.tsx +0 -98
  93. package/src/tui/components/Auditorium.tsx +0 -56
  94. package/src/tui/components/ConcertHall.tsx +0 -31
  95. package/src/tui/components/Help.tsx +0 -63
  96. package/src/tui/components/OrchestratorPane.tsx +0 -98
  97. package/src/tui/components/SidebarHeader.tsx +0 -34
  98. package/src/tui/components/StatusBar.tsx +0 -42
  99. package/src/tui/detect-permission.ts +0 -93
  100. package/src/tui/embedded-session-lifecycle.ts +0 -44
  101. package/src/tui/embedded-terminal.ts +0 -325
  102. package/src/tui/format-time.ts +0 -25
  103. package/src/tui/keymap.ts +0 -104
  104. package/src/tui/poll-activity.ts +0 -25
  105. package/src/tui/poll-idle.ts +0 -149
  106. package/src/tui/poll-permission.ts +0 -50
  107. package/src/tui/status-icon.ts +0 -35
  108. package/src/tui/terminal-input.ts +0 -136
  109. package/src/tui/watch-state.ts +0 -43
  110. package/src/worktree.ts +0 -41
  111. package/tests/claude-command.test.ts +0 -30
  112. package/tests/claude-detect.test.ts +0 -14
  113. package/tests/commands/attach.test.ts +0 -60
  114. package/tests/commands/kill.test.ts +0 -66
  115. package/tests/commands/launch.test.ts +0 -75
  116. package/tests/commands/list.test.ts +0 -47
  117. package/tests/commands/notes.test.ts +0 -53
  118. package/tests/commands/restore.test.ts +0 -126
  119. package/tests/helpers/tmp-config.ts +0 -16
  120. package/tests/helpers/tmp-repo.ts +0 -29
  121. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  122. package/tests/mcp/handlers.test.ts +0 -163
  123. package/tests/mcp/tool-defs.test.ts +0 -35
  124. package/tests/musicians/dismiss.test.ts +0 -102
  125. package/tests/musicians/message.test.ts +0 -159
  126. package/tests/musicians/query.test.ts +0 -65
  127. package/tests/musicians/spawn.test.ts +0 -125
  128. package/tests/notes.test.ts +0 -56
  129. package/tests/notify.test.ts +0 -80
  130. package/tests/orchestrator/report-back.test.ts +0 -18
  131. package/tests/permission.test.ts +0 -39
  132. package/tests/project-key.test.ts +0 -33
  133. package/tests/prompts/tool-discipline.test.ts +0 -25
  134. package/tests/repo.test.ts +0 -38
  135. package/tests/state-updaters.test.ts +0 -126
  136. package/tests/state.test.ts +0 -85
  137. package/tests/tmux.test.ts +0 -126
  138. package/tests/tui/AppView.test.tsx +0 -92
  139. package/tests/tui/Auditorium.test.tsx +0 -67
  140. package/tests/tui/ConcertHall.test.tsx +0 -22
  141. package/tests/tui/Help.test.tsx +0 -38
  142. package/tests/tui/OrchestratorPane.test.ts +0 -30
  143. package/tests/tui/SidebarHeader.test.tsx +0 -20
  144. package/tests/tui/StatusBar.test.tsx +0 -51
  145. package/tests/tui/activity-line.test.ts +0 -21
  146. package/tests/tui/detect-permission.test.ts +0 -92
  147. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  148. package/tests/tui/embedded-terminal.test.ts +0 -80
  149. package/tests/tui/format-time.test.ts +0 -25
  150. package/tests/tui/keymap.test.ts +0 -93
  151. package/tests/tui/poll-activity.test.ts +0 -81
  152. package/tests/tui/poll-idle.test.ts +0 -159
  153. package/tests/tui/poll-permission.test.ts +0 -222
  154. package/tests/tui/status-icon.test.ts +0 -27
  155. package/tests/tui/terminal-input.test.ts +0 -113
  156. package/tests/tui/watch-state.test.ts +0 -54
  157. package/tests/worktree.test.ts +0 -73
  158. package/tsconfig.json +0 -19
  159. package/vitest.config.ts +0 -12
@@ -1,126 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import {
3
- addMusician,
4
- setMusicianStatus,
5
- archiveMusician,
6
- setOrchestratorSessionId,
7
- setMusicianClaudeSessionId,
8
- setMusicianTmuxWindowId,
9
- touchMusicianActivity,
10
- } from '../src/state-updaters.js';
11
- import { ensureOrchestraDir, writeState, readState } from '../src/state.js';
12
- import { makeInitialState } from '../src/state.types.js';
13
- import { makeTmpConfig } from './helpers/tmp-config.js';
14
-
15
- describe('state updaters', () => {
16
- const cleanups: Array<() => Promise<void>> = [];
17
-
18
- beforeEach(() => { process.env.NFO_HOME = ''; });
19
- afterEach(async () => {
20
- for (const c of cleanups) await c();
21
- cleanups.length = 0;
22
- delete process.env.NFO_HOME;
23
- });
24
-
25
- async function freshState(id: string) {
26
- const tmp = await makeTmpConfig();
27
- cleanups.push(tmp.cleanup);
28
- process.env.NFO_HOME = tmp.path;
29
- await ensureOrchestraDir(id);
30
- await writeState(id, makeInitialState({
31
- orchestraId: id, projectPath: '/tmp/x', permissionLevel: 'supervised',
32
- }));
33
- }
34
-
35
- it('addMusician appends a working musician', async () => {
36
- await freshState('orch-a');
37
- await addMusician('orch-a', {
38
- id: 'mus-001',
39
- name: 'tester',
40
- task_summary: 'run tests',
41
- status: 'working',
42
- tmux_window_id: '@1',
43
- claude_session_id: null,
44
- worktree_path: '/tmp/w',
45
- branch: 'nfo/mus-001',
46
- spawned_at: '2026-05-29T10:00:00Z',
47
- last_activity: '2026-05-29T10:00:00Z',
48
- });
49
- const state = await readState('orch-a');
50
- expect(state!.musicians).toHaveLength(1);
51
- expect(state!.musicians[0].id).toBe('mus-001');
52
- });
53
-
54
- it('setMusicianStatus updates only that musician', async () => {
55
- await freshState('orch-b');
56
- await addMusician('orch-b', baseMus('mus-001'));
57
- await addMusician('orch-b', baseMus('mus-002'));
58
-
59
- await setMusicianStatus('orch-b', 'mus-001', 'idle');
60
-
61
- const state = await readState('orch-b');
62
- expect(state!.musicians.find(m => m.id === 'mus-001')!.status).toBe('idle');
63
- expect(state!.musicians.find(m => m.id === 'mus-002')!.status).toBe('working');
64
- });
65
-
66
- it('archiveMusician moves the musician to archived_musicians with summary + timestamp', async () => {
67
- await freshState('orch-c');
68
- await addMusician('orch-c', baseMus('mus-001'));
69
-
70
- await archiveMusician('orch-c', 'mus-001', { summary: 'done', dismissedAt: '2026-05-29T11:00:00Z' });
71
-
72
- const state = await readState('orch-c');
73
- expect(state!.musicians).toHaveLength(0);
74
- expect(state!.archived_musicians).toHaveLength(1);
75
- expect(state!.archived_musicians[0].id).toBe('mus-001');
76
- expect(state!.archived_musicians[0].dismissed_at).toBe('2026-05-29T11:00:00Z');
77
- expect(state!.archived_musicians[0].summary).toBe('done');
78
- expect(state!.archived_musicians[0].status).toBe('stopped');
79
- });
80
-
81
- it('setOrchestratorSessionId records the session id', async () => {
82
- await freshState('orch-d');
83
- await setOrchestratorSessionId('orch-d', 'sess-abc');
84
- const state = await readState('orch-d');
85
- expect(state!.orchestrator_session_id).toBe('sess-abc');
86
- });
87
-
88
- it('setMusicianClaudeSessionId records the session id', async () => {
89
- await freshState('orch-e');
90
- await addMusician('orch-e', baseMus('mus-001'));
91
- await setMusicianClaudeSessionId('orch-e', 'mus-001', 'sess-xyz');
92
- const state = await readState('orch-e');
93
- expect(state!.musicians[0].claude_session_id).toBe('sess-xyz');
94
- });
95
-
96
- it('touchMusicianActivity updates last_activity', async () => {
97
- await freshState('orch-f');
98
- await addMusician('orch-f', baseMus('mus-001'));
99
- await touchMusicianActivity('orch-f', 'mus-001', '2026-05-29T12:00:00Z');
100
- const state = await readState('orch-f');
101
- expect(state!.musicians[0].last_activity).toBe('2026-05-29T12:00:00Z');
102
- });
103
-
104
- it('setMusicianTmuxWindowId records the window id', async () => {
105
- await freshState('orch-g');
106
- await addMusician('orch-g', baseMus('mus-001'));
107
- await setMusicianTmuxWindowId('orch-g', 'mus-001', '@42');
108
- const state = await readState('orch-g');
109
- expect(state!.musicians[0].tmux_window_id).toBe('@42');
110
- });
111
- });
112
-
113
- function baseMus(id: string) {
114
- return {
115
- id,
116
- name: 'm',
117
- task_summary: 't',
118
- status: 'working' as const,
119
- tmux_window_id: '@0',
120
- claude_session_id: null,
121
- worktree_path: null,
122
- branch: null,
123
- spawned_at: '2026-05-29T10:00:00Z',
124
- last_activity: '2026-05-29T10:00:00Z',
125
- };
126
- }
@@ -1,85 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { readState, writeState, ensureOrchestraDir } from '../src/state.js';
3
- import { makeInitialState } from '../src/state.types.js';
4
- import { makeTmpConfig } from './helpers/tmp-config.js';
5
- import { existsSync } from 'node:fs';
6
- import { join } from 'node:path';
7
-
8
- describe('state read/write', () => {
9
- const cleanups: Array<() => Promise<void>> = [];
10
-
11
- beforeEach(() => {
12
- process.env.NFO_HOME = '';
13
- });
14
-
15
- afterEach(async () => {
16
- for (const c of cleanups) await c();
17
- cleanups.length = 0;
18
- delete process.env.NFO_HOME;
19
- });
20
-
21
- it('writes and reads back the orchestra state round-trip', async () => {
22
- const tmp = await makeTmpConfig();
23
- cleanups.push(tmp.cleanup);
24
- process.env.NFO_HOME = tmp.path;
25
-
26
- const state = makeInitialState({
27
- orchestraId: 'abc123-test',
28
- projectPath: '/tmp/example',
29
- permissionLevel: 'supervised',
30
- });
31
-
32
- await ensureOrchestraDir('abc123-test');
33
- await writeState('abc123-test', state);
34
-
35
- const loaded = await readState('abc123-test');
36
- expect(loaded).not.toBeNull();
37
- expect(loaded!.orchestra_id).toBe('abc123-test');
38
- expect(loaded!.permission_level).toBe('supervised');
39
- expect(loaded!.musicians).toEqual([]);
40
- });
41
-
42
- it('returns null when no state exists for the given key', async () => {
43
- const tmp = await makeTmpConfig();
44
- cleanups.push(tmp.cleanup);
45
- process.env.NFO_HOME = tmp.path;
46
-
47
- const loaded = await readState('does-not-exist');
48
- expect(loaded).toBeNull();
49
- });
50
-
51
- it('ensureOrchestraDir creates the standard subdirectory layout', async () => {
52
- const tmp = await makeTmpConfig();
53
- cleanups.push(tmp.cleanup);
54
- process.env.NFO_HOME = tmp.path;
55
-
56
- await ensureOrchestraDir('abc123-test');
57
- const base = join(tmp.path, 'projects', 'abc123-test');
58
- expect(existsSync(base)).toBe(true);
59
- expect(existsSync(join(base, 'notes'))).toBe(true);
60
- expect(existsSync(join(base, 'logs'))).toBe(true);
61
- expect(existsSync(join(base, 'worktrees'))).toBe(true);
62
- expect(existsSync(join(base, 'archive'))).toBe(true);
63
- });
64
-
65
- it('serial writes leave a complete file (atomic rename)', async () => {
66
- const tmp = await makeTmpConfig();
67
- cleanups.push(tmp.cleanup);
68
- process.env.NFO_HOME = tmp.path;
69
-
70
- const state = makeInitialState({
71
- orchestraId: 'serial-test',
72
- projectPath: '/tmp/example',
73
- permissionLevel: 'autonomous',
74
- });
75
- await ensureOrchestraDir('serial-test');
76
-
77
- // Hammer writes serially; each must produce a valid file on disk.
78
- for (let i = 0; i < 20; i++) {
79
- state.orchestrator_session_id = `session-${i}`;
80
- await writeState('serial-test', state);
81
- const loaded = await readState('serial-test');
82
- expect(loaded!.orchestrator_session_id).toBe(`session-${i}`);
83
- }
84
- });
85
- });
@@ -1,126 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import {
3
- sessionExists,
4
- createDetachedSession,
5
- killSession,
6
- capturePane,
7
- respawnPane,
8
- sendKeys,
9
- sessionName,
10
- embeddedSessionName,
11
- selectWindow,
12
- selectPane,
13
- setPaneOption,
14
- ensureNfoSessionUi,
15
- } from '../src/tmux.js';
16
-
17
- describe('tmux wrapper', () => {
18
- const sessionsToKill: string[] = [];
19
- afterEach(async () => {
20
- for (const s of sessionsToKill) {
21
- try { await killSession(s); } catch { /* ignore */ }
22
- }
23
- sessionsToKill.length = 0;
24
- });
25
-
26
- it('sessionName composes from project key', () => {
27
- expect(sessionName('abcd1234ef-myrepo')).toBe('nfo-abcd1234ef-myrepo');
28
- });
29
-
30
- it('embeddedSessionName composes from project key', () => {
31
- expect(embeddedSessionName('abcd1234ef-myrepo')).toBe('nfo-abcd1234ef-myrepo-embed');
32
- });
33
-
34
- it('sessionExists returns false when no such session', async () => {
35
- expect(await sessionExists('nfo-does-not-exist-zzz')).toBe(false);
36
- });
37
-
38
- it('creates a detached session and detects it exists', async () => {
39
- const name = `nfo-test-${Date.now()}`;
40
- sessionsToKill.push(name);
41
- await createDetachedSession(name, '/tmp');
42
- expect(await sessionExists(name)).toBe(true);
43
- });
44
-
45
- it('killSession removes a running session', async () => {
46
- const name = `nfo-test-kill-${Date.now()}`;
47
- await createDetachedSession(name, '/tmp');
48
- await killSession(name);
49
- expect(await sessionExists(name)).toBe(false);
50
- });
51
-
52
- it('capturePane returns the visible pane content after sendKeys', async () => {
53
- const name = `nfo-test-cap-${Date.now()}`;
54
- sessionsToKill.push(name);
55
- await createDetachedSession(name, '/tmp');
56
- await sendKeys(`${name}:0`, 'echo hello-from-test', true);
57
- // Allow shell to render output.
58
- await new Promise(r => setTimeout(r, 250));
59
- const out = await capturePane(`${name}:0`, 20);
60
- expect(out).toContain('hello-from-test');
61
- });
62
-
63
- it('respawnPane runs a direct command without typing it into the shell', async () => {
64
- const name = `nfo-test-respawn-${Date.now()}`;
65
- sessionsToKill.push(name);
66
- await createDetachedSession(name, '/tmp');
67
- await setPaneOption(`${name}:0`, 'remain-on-exit', 'on');
68
- await respawnPane(`${name}:0`, "printf 'hello-from-respawn\\n'");
69
- await new Promise(r => setTimeout(r, 250));
70
- const out = await capturePane(`${name}:0`, 20);
71
- expect(out).toContain('hello-from-respawn');
72
- });
73
-
74
- it('selectWindow makes a window active', async () => {
75
- const name = `nfo-test-selwin-${Date.now()}`;
76
- sessionsToKill.push(name);
77
- await createDetachedSession(name, '/tmp');
78
- const { execa } = await import('execa');
79
- const { stdout: winId } = await execa('tmux', [
80
- 'new-window', '-t', name, '-n', 'second', '-c', '/tmp', '-d',
81
- '-P', '-F', '#{window_id}',
82
- ]);
83
- await selectWindow(name, winId.trim());
84
- const { stdout: active } = await execa('tmux', [
85
- 'display-message', '-p', '-t', name, '#{window_id}',
86
- ]);
87
- expect(active.trim()).toBe(winId.trim());
88
- });
89
-
90
- it('selectPane makes a pane active', async () => {
91
- const name = `nfo-test-selpane-${Date.now()}`;
92
- sessionsToKill.push(name);
93
- await createDetachedSession(name, '/tmp');
94
- const { execa } = await import('execa');
95
- await execa('tmux', ['split-window', '-h', '-t', `${name}:0`, '-c', '/tmp']);
96
- await selectPane(`${name}:0.0`);
97
- const { stdout: active } = await execa('tmux', [
98
- 'display-message', '-p', '-t', name, '#{pane_index}',
99
- ]);
100
- expect(active.trim()).toBe('0');
101
- });
102
-
103
- it('ensureNfoSessionUi enables extkeys for modified enter passthrough', async () => {
104
- const name = `nfo-test-extkeys-${Date.now()}`;
105
- sessionsToKill.push(name);
106
- await createDetachedSession(name, '/tmp');
107
-
108
- await ensureNfoSessionUi(name);
109
- await ensureNfoSessionUi(name);
110
-
111
- const { execa } = await import('execa');
112
- const { stdout: extendedKeys } = await execa('tmux', [
113
- 'show-options', '-t', name, 'extended-keys',
114
- ]);
115
- expect(extendedKeys.trim()).toBe('extended-keys on');
116
-
117
- const { stdout: terminalFeatures } = await execa('tmux', [
118
- 'show-options', '-t', name, 'terminal-features',
119
- ]);
120
- const lines = terminalFeatures.trim().split('\n');
121
-
122
- expect(lines.filter((line) => { return line.endsWith('xterm*:extkeys'); })).toHaveLength(1);
123
- expect(lines.filter((line) => { return line.endsWith('screen*:extkeys'); })).toHaveLength(1);
124
- expect(lines.filter((line) => { return line.endsWith('tmux*:extkeys'); })).toHaveLength(1);
125
- });
126
- });
@@ -1,92 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render } from 'ink-testing-library';
3
- import { AppView } from '../../src/tui/components/AppView.js';
4
- import { OrchestratorPane } from '../../src/tui/components/OrchestratorPane.js';
5
- import type { Musician } from '../../src/state.types.js';
6
- import type { OrchestraSummary } from '../../src/commands/list.js';
7
-
8
- const musicians: Musician[] = [{
9
- id: 'mus-001', name: 'alpha', task_summary: 't', status: 'working',
10
- tmux_window_id: '@1', claude_session_id: null, worktree_path: null, branch: null,
11
- spawned_at: '2026-05-29T10:00:00Z', last_activity: '2026-05-29T10:00:00Z',
12
- }];
13
- const orchestras: OrchestraSummary[] = [{
14
- id: 'aaa-one', project_path: '/tmp/one', permission_level: 'supervised',
15
- created_at: '2026-05-29T10:00:00Z', running: true, musician_count: 1,
16
- }];
17
-
18
- describe('AppView', () => {
19
- it('renders concert hall, auditorium, and status bar together', () => {
20
- const { lastFrame } = render(
21
- <AppView
22
- orchestras={orchestras}
23
- currentId="aaa-one"
24
- musicians={musicians}
25
- activity={{ 'mus-001': 'building' }}
26
- selectedIndex={0}
27
- permissionLevel="supervised"
28
- tokenHint="—"
29
- pendingCount={1}
30
- now="2026-05-29T10:01:00Z"
31
- orchestratorTitle="Claude / tmux"
32
- orchestratorLines={[{ spans: [{ text: 'claude output' }] }]}
33
- orchestratorFocused={false}
34
- orchestratorConnected={true}
35
- />,
36
- );
37
- const frame = lastFrame() ?? '';
38
- expect(frame).toContain('Claude');
39
- expect(frame).toContain('claude output');
40
- expect(frame).toContain('No Fluff Orchestra');
41
- expect(frame).toContain('Concert Hall');
42
- expect(frame).toContain('Auditorium');
43
- expect(frame).toContain('alpha');
44
- expect(frame).toContain('building');
45
- expect(frame).toContain('supervised');
46
- expect(frame).toContain('awaiting permission');
47
- });
48
-
49
- it('renders the help overlay when showHelp=true', () => {
50
- const { lastFrame } = render(
51
- <AppView
52
- orchestras={[]}
53
- currentId="abc"
54
- musicians={[]}
55
- activity={{}}
56
- selectedIndex={0}
57
- permissionLevel="supervised"
58
- tokenHint="—"
59
- now={new Date(0).toISOString()}
60
- pendingCount={0}
61
- showHelp={true}
62
- orchestratorTitle="Claude"
63
- orchestratorLines={[]}
64
- orchestratorFocused={false}
65
- orchestratorConnected={true}
66
- />,
67
- );
68
- const frame = (lastFrame() ?? '').toLowerCase();
69
- expect(frame).toContain('cancel pending dismiss');
70
- expect(frame).toContain('mouse wheel');
71
- expect(frame).toContain('claude');
72
- });
73
- });
74
-
75
- describe('OrchestratorPane', () => {
76
- it('renders lines in a plain box without scroll props', () => {
77
- const { lastFrame } = render(
78
- <OrchestratorPane
79
- title="Orchestrator"
80
- lines={[
81
- { spans: [{ text: 'line one' }] },
82
- { spans: [{ text: 'line two' }] },
83
- ]}
84
- focused={false}
85
- connected={true}
86
- />,
87
- );
88
- const frame = lastFrame() ?? '';
89
- expect(frame).toContain('line one');
90
- expect(frame).toContain('line two');
91
- });
92
- });
@@ -1,67 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render } from 'ink-testing-library';
3
- import { Auditorium } from '../../src/tui/components/Auditorium.js';
4
- import type { Musician } from '../../src/state.types.js';
5
-
6
- function mus(over: Partial<Musician>): Musician {
7
- return {
8
- id: 'mus-001', name: 'tester', task_summary: 't', status: 'working',
9
- tmux_window_id: '@1', claude_session_id: null, worktree_path: null, branch: null,
10
- spawned_at: '2026-05-29T10:00:00Z', last_activity: '2026-05-29T10:00:00Z',
11
- ...over,
12
- };
13
- }
14
-
15
- describe('Auditorium', () => {
16
- it('renders one row per musician with name and activity', () => {
17
- const musicians = [
18
- mus({ id: 'mus-001', name: 'alpha' }),
19
- mus({ id: 'mus-002', name: 'beta', status: 'idle' }),
20
- ];
21
- const activity = { 'mus-001': 'Running tests', 'mus-002': 'done' };
22
- const { lastFrame } = render(
23
- <Auditorium musicians={musicians} activity={activity} selectedIndex={0} now="2026-05-29T10:02:00Z" />,
24
- );
25
- const frame = lastFrame() ?? '';
26
- expect(frame).toContain('orchestrator');
27
- expect(frame).toContain('alpha');
28
- expect(frame).toContain('beta');
29
- expect(frame).toContain('Running tests');
30
- });
31
- it('marks the selected target row', () => {
32
- const musicians = [mus({ id: 'mus-001', name: 'alpha' })];
33
- const { lastFrame } = render(
34
- <Auditorium musicians={musicians} activity={{}} selectedIndex={1} now="2026-05-29T10:02:00Z" />,
35
- );
36
- expect(lastFrame() ?? '').toContain('▸');
37
- });
38
- it('shows the orchestrator row and an empty-state message when there are no musicians', () => {
39
- const { lastFrame } = render(
40
- <Auditorium musicians={[]} activity={{}} selectedIndex={0} now="2026-05-29T10:02:00Z" />,
41
- );
42
- const frame = lastFrame() ?? '';
43
- expect(frame).toContain('orchestrator');
44
- expect(frame).toContain('No musicians');
45
- });
46
- it('renders awaiting: <tool> and ⚠ when status is awaiting_permission with a pending_permission', () => {
47
- const musicians = [
48
- mus({ id: 'mus-001', name: 'alpha', status: 'awaiting_permission', pending_permission: 'Bash: `rm -rf foo`', last_activity: '2026-05-29T10:00:00Z' }),
49
- ];
50
- const { lastFrame } = render(
51
- <Auditorium musicians={musicians} activity={{}} selectedIndex={1} now="2026-05-29T10:02:00Z" />,
52
- );
53
- const frame = lastFrame() ?? '';
54
- expect(frame).toContain('awaiting: Bash:');
55
- expect(frame).toContain('⚠');
56
- });
57
- it('renders awaiting: tool when status is awaiting_permission with null pending_permission', () => {
58
- const musicians = [
59
- mus({ id: 'mus-001', name: 'alpha', status: 'awaiting_permission', pending_permission: null, last_activity: '2026-05-29T10:00:00Z' }),
60
- ];
61
- const { lastFrame } = render(
62
- <Auditorium musicians={musicians} activity={{}} selectedIndex={1} now="2026-05-29T10:02:00Z" />,
63
- );
64
- const frame = lastFrame() ?? '';
65
- expect(frame).toContain('awaiting: tool');
66
- });
67
- });
@@ -1,22 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render } from 'ink-testing-library';
3
- import { ConcertHall } from '../../src/tui/components/ConcertHall.js';
4
- import type { OrchestraSummary } from '../../src/commands/list.js';
5
-
6
- function orch(over: Partial<OrchestraSummary>): OrchestraSummary {
7
- return {
8
- id: 'aaa-one', project_path: '/tmp/one', permission_level: 'supervised',
9
- created_at: '2026-05-29T10:00:00Z', running: true, musician_count: 2, ...over,
10
- };
11
- }
12
-
13
- describe('ConcertHall', () => {
14
- it('lists orchestras and marks the current one', () => {
15
- const list = [orch({ id: 'aaa-one' }), orch({ id: 'bbb-two', running: false })];
16
- const { lastFrame } = render(<ConcertHall orchestras={list} currentId="aaa-one" />);
17
- const frame = lastFrame() ?? '';
18
- expect(frame).toContain('aaa-one');
19
- expect(frame).toContain('bbb-two');
20
- expect(frame).toContain('▸');
21
- });
22
- });
@@ -1,38 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render } from 'ink-testing-library';
3
- import { Help } from '../../src/tui/components/Help.js';
4
-
5
- describe('Help', () => {
6
- it('lists the core keybindings', () => {
7
- const { lastFrame } = render(<Help />);
8
- const frame = lastFrame() ?? '';
9
- expect(frame).toContain('↑');
10
- expect(frame).toContain('Enter');
11
- expect(frame).toContain('Alt+Enter');
12
- expect(frame).toContain('Shift+Enter');
13
- expect(frame).toContain('n');
14
- expect(frame).toContain('d');
15
- expect(frame).toContain('p');
16
- expect(frame).toContain('q');
17
- expect(frame).toContain('?');
18
- });
19
-
20
- it('mentions notes, dismiss, jump-to-pending, and Claude compose', () => {
21
- const { lastFrame } = render(<Help />);
22
- const frame = (lastFrame() ?? '').toLowerCase();
23
- expect(frame).toContain('notes');
24
- expect(frame).toContain('dismiss');
25
- expect(frame).toContain('awaiting');
26
- expect(frame).toContain('claude');
27
- expect(frame).toContain('ctrl+g');
28
- expect(frame).toContain('ctrl+j');
29
- expect(frame).toContain('scroll');
30
- expect(frame).toContain('without killing');
31
- });
32
-
33
- it('shows a close hint', () => {
34
- const { lastFrame } = render(<Help />);
35
- const frame = (lastFrame() ?? '').toLowerCase();
36
- expect(frame).toContain('close');
37
- });
38
- });
@@ -1,30 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { resolveSpanStyle } from '../../src/tui/components/OrchestratorPane.js';
3
-
4
- describe('resolveSpanStyle', () => {
5
- it('renders a visible block cursor when the terminal is focused', () => {
6
- expect(resolveSpanStyle({ text: 'x', color: 'red', cursor: true }, true)).toEqual({
7
- color: 'black',
8
- backgroundColor: 'white',
9
- dimColor: undefined,
10
- bold: undefined,
11
- italic: undefined,
12
- underline: undefined,
13
- strikethrough: undefined,
14
- inverse: false,
15
- });
16
- });
17
-
18
- it('leaves non-focused cursor spans unchanged', () => {
19
- expect(resolveSpanStyle({ text: 'x', color: 'red', cursor: true }, false)).toEqual({
20
- color: 'red',
21
- backgroundColor: undefined,
22
- dimColor: undefined,
23
- bold: undefined,
24
- italic: undefined,
25
- underline: undefined,
26
- strikethrough: undefined,
27
- inverse: undefined,
28
- });
29
- });
30
- });
@@ -1,20 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render } from 'ink-testing-library';
3
- import { SidebarHeader } from '../../src/tui/components/SidebarHeader.js';
4
-
5
- describe('SidebarHeader', () => {
6
- it('renders orchestra id and musician counts', () => {
7
- const { lastFrame } = render(<SidebarHeader orchestraId="aaa-one" musicianCount={3} pendingCount={0} />);
8
- const frame = lastFrame() ?? '';
9
- expect(frame).toContain('No Fluff Orchestra');
10
- expect(frame).toContain('aaa-one');
11
- expect(frame).toContain('3 musicians');
12
- expect(frame).toContain('0 awaiting permission');
13
- });
14
-
15
- it('shows pending count when musicians await permission', () => {
16
- const { lastFrame } = render(<SidebarHeader orchestraId="aaa-one" musicianCount={3} pendingCount={2} />);
17
- const frame = lastFrame() ?? '';
18
- expect(frame).toContain('2 awaiting permission');
19
- });
20
- });
@@ -1,51 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render } from 'ink-testing-library';
3
- import { StatusBar } from '../../src/tui/components/StatusBar.js';
4
-
5
- describe('StatusBar', () => {
6
- it('shows permission level and the token placeholder', () => {
7
- const { lastFrame } = render(<StatusBar permissionLevel="supervised" tokenHint="—" pendingCount={0} dismissConfirmation={null} orchestratorFocused={false} />);
8
- const frame = lastFrame() ?? '';
9
- expect(frame).toContain('supervised');
10
- expect(frame).toContain('—');
11
- expect(frame).not.toContain('awaiting permission');
12
- });
13
- it('shows key hints', () => {
14
- const { lastFrame } = render(<StatusBar permissionLevel="auto" tokenHint="—" pendingCount={0} dismissConfirmation={null} orchestratorFocused={false} />);
15
- const frame = lastFrame() ?? '';
16
- expect(frame).toContain('nav');
17
- expect(frame).toContain('Ctrl+g');
18
- expect(frame).toContain('[q] detach');
19
- });
20
- it('shows the pending-permission banner when pendingCount > 0', () => {
21
- const { lastFrame } = render(<StatusBar permissionLevel="supervised" tokenHint="—" pendingCount={2} dismissConfirmation={null} orchestratorFocused={false} />);
22
- const frame = lastFrame() ?? '';
23
- expect(frame).toContain('2 awaiting permission');
24
- expect(frame).toContain('[p] jump to next');
25
- });
26
- it('advertises [?] help in the bottom hint', () => {
27
- const { lastFrame } = render(<StatusBar permissionLevel="supervised" tokenHint="—" pendingCount={0} dismissConfirmation={null} orchestratorFocused={false} />);
28
- const frame = lastFrame() ?? '';
29
- expect(frame).toContain('[?] help');
30
- });
31
- it('shows compose hints while the left pane is focused', () => {
32
- const { lastFrame } = render(<StatusBar permissionLevel="supervised" tokenHint="—" pendingCount={0} dismissConfirmation={null} orchestratorFocused={true} />);
33
- const frame = lastFrame() ?? '';
34
- expect(frame).toContain('active terminal');
35
- expect(frame).toContain('Ctrl+g');
36
- });
37
- it('shows dismiss confirmation guidance when present', () => {
38
- const { lastFrame } = render(
39
- <StatusBar
40
- permissionLevel="supervised"
41
- tokenHint="—"
42
- pendingCount={0}
43
- dismissConfirmation="Confirm dismiss alpha · [y]/[Enter] confirm · [n]/[Esc] cancel"
44
- orchestratorFocused={false}
45
- />,
46
- );
47
- const frame = lastFrame() ?? '';
48
- expect(frame).toContain('Confirm dismiss alpha');
49
- expect(frame).toContain('[y]/[Enter]');
50
- });
51
- });