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
|
@@ -1,933 +0,0 @@
|
|
|
1
|
-
# NFO Phase 5 — Help Overlay + Permission Notifications Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Ship the two highest-value TUI polish items deferred from Phase 4: a `?`-toggled help overlay that lists every keybinding, and the spec §5.2.1 `notify_on_permission` config flag that triggers a terminal bell plus a platform-native desktop notification whenever a Musician enters `awaiting_permission`.
|
|
6
|
-
|
|
7
|
-
**Architecture:** A new pure presentational `Help.tsx` swaps in for the main layout when `showHelp` is toggled — no z-index gymnastics; the help screen REPLACES the column layout, and `?` toggles it back. The notify side adds an optional `notify_on_permission: boolean` field to `OrchestraState` (defaults to `false`), exposed via a new `--notify-on-permission` flag on `nfo` (launch and restore). A new `src/notify.ts` module owns the bell-write + cross-platform spawn (`notify-send` on Linux, `osascript` on macOS), with a test seam so we can verify the trigger logic without spawning real processes. App.tsx's existing permission-poller `useEffect` is extended: after applying a transition where `newStatus === 'awaiting_permission'`, it calls the notifier (guarded by the state flag) — the transition list already gives us exactly the "new-this-tick" semantics we need, so we don't have to dedup ourselves.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** No new runtime dependencies. Uses `execa@9` (already a dep) for the platform spawns, `os.platform()` from Node stdlib for the OS check, and `process.stdout.write('\x07')` for the bell. Tests stay with vitest, real-tmux harnesses where applicable, and a function-injection test seam for the notifier.
|
|
10
|
-
|
|
11
|
-
**Reference spec:** `docs/specs/2026-05-29-nfo-design.md` §5.2.1 (final paragraph — the optional bell + desktop notification was explicitly deferred from Phase 4) and §8 (TUI design — Phase 3 status bar advertised `[?] help` but the binding was pulled because no handler existed; Phase 5 puts the handler in and re-advertises).
|
|
12
|
-
|
|
13
|
-
**MANDATORY code style (applies to every task):**
|
|
14
|
-
- Control flow uses explicit braced multi-line blocks. NEVER the brace-less single-line form (`if (c) { return x; }`, never `if (c) return x;`). Same for `for`/`while`/`else`/`switch`.
|
|
15
|
-
- Arrow functions use explicit `{ return ... }` bodies — EXCEPT React component definitions returning JSX, which use `(props) => { return (<JSX/>); }`. Array callbacks: `.map((m) => { return <Row .../>; })`, never `.map(m => <Row/>)`.
|
|
16
|
-
- Ternaries (`a ? b : c`) ARE allowed, including inside JSX.
|
|
17
|
-
- Component return type: `import type { ReactElement } from 'react';` then `export function Foo(props: FooProps): ReactElement { ... }`. Never `JSX.Element`.
|
|
18
|
-
- Commit trailer: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`. Local git identity (Javier Furus <javierfurus@gmail.com>) is already configured — do not change it. Use the HEREDOC commit form.
|
|
19
|
-
- Every code task runs `npm run build` (tsc) AND `npm test` before commit — `npm test` already has a `pretest: tsc` hook, but run build explicitly too when iterating.
|
|
20
|
-
|
|
21
|
-
**Explicitly NOT in Phase 5 (defer to Phase 6+):**
|
|
22
|
-
- Token usage hint in the status bar. Parsing claude's status line is version-fragile; deserves its own design pass.
|
|
23
|
-
- Concert Hall orchestra-switching (Tab/Shift-Tab actually attaching a different tmux session from inside Ink). Non-trivial — needs care around session-handoff and detached-attach semantics.
|
|
24
|
-
- Folding the activity poller and permission poller into one pass. Optimization only; the two-poller layout works correctly today.
|
|
25
|
-
- An in-TUI keybinding to toggle `notify_on_permission`. Phase 5 sets it at launch time via the CLI flag; per-session toggling is a future ergonomic ask if the user wants it.
|
|
26
|
-
- Any change to the MCP tool surface or musician primitives.
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## File Structure
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
src/
|
|
34
|
-
├── notify.ts # NEW: notifyAwaitingPermission(opts) — bell + platform notify
|
|
35
|
-
├── state.types.ts # MODIFY: + notify_on_permission?: boolean on OrchestraState; makeInitialState accepts it
|
|
36
|
-
├── cli.ts # MODIFY: --notify-on-permission flag on launch/restore commands
|
|
37
|
-
├── commands/
|
|
38
|
-
│ ├── launch.ts # MODIFY: thread notifyOnPermission into makeInitialState
|
|
39
|
-
│ └── restore.ts # MODIFY: --notify-on-permission overrides the stored value
|
|
40
|
-
├── tui/
|
|
41
|
-
│ ├── Help.tsx # NEW: presentational keybindings list
|
|
42
|
-
│ ├── keymap.ts # MODIFY: 'p' (already), add '?' → toggle-help
|
|
43
|
-
│ ├── StatusBar.tsx # MODIFY: re-advertise [?] help in bottom hint
|
|
44
|
-
│ ├── AppView.tsx # MODIFY: showHelp prop; render <Help/> instead of column when true
|
|
45
|
-
│ └── App.tsx # MODIFY: showHelp state, toggle-help handler, notify on awaiting transition
|
|
46
|
-
tests/
|
|
47
|
-
├── notify.test.ts # NEW
|
|
48
|
-
├── tui/
|
|
49
|
-
│ ├── Help.test.tsx # NEW
|
|
50
|
-
│ ├── keymap.test.ts # MODIFY: '?' cases
|
|
51
|
-
│ ├── StatusBar.test.tsx # MODIFY: [?] hint substring
|
|
52
|
-
│ └── AppView.test.tsx # MODIFY: showHelp prop behavior
|
|
53
|
-
docs/
|
|
54
|
-
└── plans/
|
|
55
|
-
└── 2026-05-29-nfo-phase-5-help-and-notify.md # THIS FILE
|
|
56
|
-
README.md # MODIFY: status section reflects Phase 5
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Task 1: `notify_on_permission` schema field
|
|
62
|
-
|
|
63
|
-
**Files:**
|
|
64
|
-
- Modify: `src/state.types.ts`
|
|
65
|
-
- Modify (tests): None for this task — the schema change is purely a type extension and the existing test for `makeInitialState` covers it once expanded.
|
|
66
|
-
|
|
67
|
-
**Step 1: Read the current schema**
|
|
68
|
-
|
|
69
|
-
- [ ] Open `src/state.types.ts` and confirm the current shape:
|
|
70
|
-
- `OrchestraState` has `version`, `orchestra_id`, `project_path`, `created_at`, `permission_level`, `orchestrator_session_id`, `musicians`, `archived_musicians`.
|
|
71
|
-
- `makeInitialState(args: { orchestraId, projectPath, permissionLevel })` returns the seed state.
|
|
72
|
-
|
|
73
|
-
**Step 2: Add the field**
|
|
74
|
-
|
|
75
|
-
- [ ] Add `notify_on_permission?: boolean` to `OrchestraState` between `permission_level` and `orchestrator_session_id`.
|
|
76
|
-
- [ ] Add `notifyOnPermission?: boolean` to the `makeInitialState` args interface (optional, defaults to `false`).
|
|
77
|
-
- [ ] Set `notify_on_permission: args.notifyOnPermission ?? false` in the returned object.
|
|
78
|
-
|
|
79
|
-
Result (the relevant slice):
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
export interface OrchestraState {
|
|
83
|
-
version: number;
|
|
84
|
-
orchestra_id: string;
|
|
85
|
-
project_path: string;
|
|
86
|
-
created_at: string;
|
|
87
|
-
permission_level: PermissionLevel;
|
|
88
|
-
notify_on_permission?: boolean;
|
|
89
|
-
orchestrator_session_id: string | null;
|
|
90
|
-
musicians: Musician[];
|
|
91
|
-
archived_musicians: ArchivedMusician[];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function makeInitialState(args: {
|
|
95
|
-
orchestraId: string;
|
|
96
|
-
projectPath: string;
|
|
97
|
-
permissionLevel: PermissionLevel;
|
|
98
|
-
notifyOnPermission?: boolean;
|
|
99
|
-
}): OrchestraState {
|
|
100
|
-
const now = new Date().toISOString();
|
|
101
|
-
return {
|
|
102
|
-
version: 1,
|
|
103
|
-
orchestra_id: args.orchestraId,
|
|
104
|
-
project_path: args.projectPath,
|
|
105
|
-
created_at: now,
|
|
106
|
-
permission_level: args.permissionLevel,
|
|
107
|
-
notify_on_permission: args.notifyOnPermission ?? false,
|
|
108
|
-
orchestrator_session_id: null,
|
|
109
|
-
musicians: [],
|
|
110
|
-
archived_musicians: [],
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
**Step 3: Verify build + tests**
|
|
116
|
-
|
|
117
|
-
- [ ] Run `npm run build`. Expected: clean (the field is optional so existing callers compile).
|
|
118
|
-
- [ ] Run `npm test`. Expected: all 107 tests pass (no behavior change yet).
|
|
119
|
-
|
|
120
|
-
**Step 4: Commit**
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
git add src/state.types.ts
|
|
124
|
-
git commit -m "$(cat <<'EOF'
|
|
125
|
-
feat(state): notify_on_permission field on OrchestraState
|
|
126
|
-
|
|
127
|
-
Optional boolean (defaults to false), wired through makeInitialState.
|
|
128
|
-
Phase 5 wires the CLI flag and the App-side notifier in subsequent tasks.
|
|
129
|
-
|
|
130
|
-
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
131
|
-
EOF
|
|
132
|
-
)"
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
|
-
## Task 2: `notify.ts` — bell + cross-platform desktop notification
|
|
138
|
-
|
|
139
|
-
**Files:**
|
|
140
|
-
- Create: `src/notify.ts`
|
|
141
|
-
- Create: `tests/notify.test.ts`
|
|
142
|
-
|
|
143
|
-
This module owns "fire one notification" — terminal bell plus a best-effort desktop notification. Tests use a function-injection seam so we don't spawn real `notify-send` / `osascript` during the suite.
|
|
144
|
-
|
|
145
|
-
**Step 1: Write the failing test (notify.test.ts)**
|
|
146
|
-
|
|
147
|
-
- [ ] Create `tests/notify.test.ts`:
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
151
|
-
import { notifyAwaitingPermission } from '../src/notify.js';
|
|
152
|
-
|
|
153
|
-
describe('notifyAwaitingPermission', () => {
|
|
154
|
-
it('writes a BEL character to the bell sink', async () => {
|
|
155
|
-
const bell = vi.fn();
|
|
156
|
-
const spawn = vi.fn().mockResolvedValue(undefined);
|
|
157
|
-
await notifyAwaitingPermission({
|
|
158
|
-
pendingCount: 1,
|
|
159
|
-
platform: 'linux',
|
|
160
|
-
bell,
|
|
161
|
-
spawn,
|
|
162
|
-
});
|
|
163
|
-
expect(bell).toHaveBeenCalledTimes(1);
|
|
164
|
-
expect(bell).toHaveBeenCalledWith('\x07');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('on linux, spawns notify-send with NFO title and count message', async () => {
|
|
168
|
-
const spawn = vi.fn().mockResolvedValue(undefined);
|
|
169
|
-
await notifyAwaitingPermission({
|
|
170
|
-
pendingCount: 2,
|
|
171
|
-
platform: 'linux',
|
|
172
|
-
bell: vi.fn(),
|
|
173
|
-
spawn,
|
|
174
|
-
});
|
|
175
|
-
expect(spawn).toHaveBeenCalledTimes(1);
|
|
176
|
-
const [bin, args] = spawn.mock.calls[0];
|
|
177
|
-
expect(bin).toBe('notify-send');
|
|
178
|
-
expect(args).toEqual(['NFO', '2 musicians awaiting permission']);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('on darwin, spawns osascript with display notification AppleScript', async () => {
|
|
182
|
-
const spawn = vi.fn().mockResolvedValue(undefined);
|
|
183
|
-
await notifyAwaitingPermission({
|
|
184
|
-
pendingCount: 1,
|
|
185
|
-
platform: 'darwin',
|
|
186
|
-
bell: vi.fn(),
|
|
187
|
-
spawn,
|
|
188
|
-
});
|
|
189
|
-
expect(spawn).toHaveBeenCalledTimes(1);
|
|
190
|
-
const [bin, args] = spawn.mock.calls[0];
|
|
191
|
-
expect(bin).toBe('osascript');
|
|
192
|
-
expect(args.length).toBe(2);
|
|
193
|
-
expect(args[0]).toBe('-e');
|
|
194
|
-
expect(args[1]).toContain('display notification');
|
|
195
|
-
expect(args[1]).toContain('1 musician awaiting permission');
|
|
196
|
-
expect(args[1]).toContain('NFO');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('on unknown platform, fires bell only (no spawn)', async () => {
|
|
200
|
-
const spawn = vi.fn();
|
|
201
|
-
await notifyAwaitingPermission({
|
|
202
|
-
pendingCount: 1,
|
|
203
|
-
platform: 'win32',
|
|
204
|
-
bell: vi.fn(),
|
|
205
|
-
spawn,
|
|
206
|
-
});
|
|
207
|
-
expect(spawn).not.toHaveBeenCalled();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('swallows spawn errors silently', async () => {
|
|
211
|
-
const spawn = vi.fn().mockRejectedValue(new Error('notify-send not installed'));
|
|
212
|
-
await expect(notifyAwaitingPermission({
|
|
213
|
-
pendingCount: 1,
|
|
214
|
-
platform: 'linux',
|
|
215
|
-
bell: vi.fn(),
|
|
216
|
-
spawn,
|
|
217
|
-
})).resolves.toBeUndefined();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('uses singular noun for count=1, plural otherwise', async () => {
|
|
221
|
-
const spawn1 = vi.fn().mockResolvedValue(undefined);
|
|
222
|
-
await notifyAwaitingPermission({ pendingCount: 1, platform: 'linux', bell: vi.fn(), spawn: spawn1 });
|
|
223
|
-
expect(spawn1.mock.calls[0][1]).toEqual(['NFO', '1 musician awaiting permission']);
|
|
224
|
-
|
|
225
|
-
const spawnN = vi.fn().mockResolvedValue(undefined);
|
|
226
|
-
await notifyAwaitingPermission({ pendingCount: 3, platform: 'linux', bell: vi.fn(), spawn: spawnN });
|
|
227
|
-
expect(spawnN.mock.calls[0][1]).toEqual(['NFO', '3 musicians awaiting permission']);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
**Step 2: Run the test, confirm it fails**
|
|
233
|
-
|
|
234
|
-
- [ ] Run `npm test -- tests/notify.test.ts`. Expected: FAIL with "Cannot find module '../src/notify.js'" or similar.
|
|
235
|
-
|
|
236
|
-
**Step 3: Implement `src/notify.ts`**
|
|
237
|
-
|
|
238
|
-
- [ ] Create `src/notify.ts`:
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
import { execa } from 'execa';
|
|
242
|
-
|
|
243
|
-
export interface NotifyOptions {
|
|
244
|
-
pendingCount: number;
|
|
245
|
-
platform?: NodeJS.Platform;
|
|
246
|
-
bell?: (text: string) => void;
|
|
247
|
-
spawn?: (bin: string, args: string[]) => Promise<unknown>;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function defaultBell(text: string): void {
|
|
251
|
-
process.stdout.write(text);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function defaultSpawn(bin: string, args: string[]): Promise<unknown> {
|
|
255
|
-
return execa(bin, args);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function pluralise(count: number): string {
|
|
259
|
-
if (count === 1) {
|
|
260
|
-
return '1 musician awaiting permission';
|
|
261
|
-
}
|
|
262
|
-
return `${count} musicians awaiting permission`;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Fire a single notification: ring the terminal bell and (best-effort) spawn
|
|
267
|
-
* the platform's desktop notifier. All errors are swallowed — a missing
|
|
268
|
-
* notify-send / osascript / etc. must not break the orchestra.
|
|
269
|
-
*/
|
|
270
|
-
export async function notifyAwaitingPermission(opts: NotifyOptions): Promise<void> {
|
|
271
|
-
const bell = opts.bell ?? defaultBell;
|
|
272
|
-
const spawn = opts.spawn ?? defaultSpawn;
|
|
273
|
-
const platform = opts.platform ?? process.platform;
|
|
274
|
-
const message = pluralise(opts.pendingCount);
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
bell('\x07');
|
|
278
|
-
} catch {
|
|
279
|
-
// Swallow — a broken stdout sink should never abort.
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (platform === 'linux') {
|
|
283
|
-
try {
|
|
284
|
-
await spawn('notify-send', ['NFO', message]);
|
|
285
|
-
} catch {
|
|
286
|
-
// notify-send may not be installed — best-effort only.
|
|
287
|
-
}
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (platform === 'darwin') {
|
|
292
|
-
const script = `display notification "${message}" with title "NFO"`;
|
|
293
|
-
try {
|
|
294
|
-
await spawn('osascript', ['-e', script]);
|
|
295
|
-
} catch {
|
|
296
|
-
// osascript should exist on macOS but swallow defensively.
|
|
297
|
-
}
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Unknown platform (win32, freebsd, etc.) — bell-only.
|
|
302
|
-
}
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
**Step 4: Run tests, confirm green**
|
|
306
|
-
|
|
307
|
-
- [ ] Run `npm test -- tests/notify.test.ts`. Expected: 6 passing.
|
|
308
|
-
- [ ] Run `npm test`. Expected: 113/113 passing (107 prior + 6 new).
|
|
309
|
-
- [ ] Run `npm run build`. Expected: clean.
|
|
310
|
-
|
|
311
|
-
**Step 5: Commit**
|
|
312
|
-
|
|
313
|
-
```bash
|
|
314
|
-
git add src/notify.ts tests/notify.test.ts
|
|
315
|
-
git commit -m "$(cat <<'EOF'
|
|
316
|
-
feat(notify): notifyAwaitingPermission — bell + cross-platform desktop notify
|
|
317
|
-
|
|
318
|
-
Single-shot notification fanout: BEL to stdout plus best-effort spawn of
|
|
319
|
-
notify-send (linux) or osascript display notification (darwin). All
|
|
320
|
-
errors swallowed so a missing notifier never aborts. Function-injection
|
|
321
|
-
seam (bell + spawn opts) keeps tests pure — no real spawns in the suite.
|
|
322
|
-
|
|
323
|
-
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
324
|
-
EOF
|
|
325
|
-
)"
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
## Task 3: `--notify-on-permission` CLI flag
|
|
331
|
-
|
|
332
|
-
**Files:**
|
|
333
|
-
- Modify: `src/cli.ts`
|
|
334
|
-
- Modify: `src/commands/launch.ts`
|
|
335
|
-
- Modify: `src/commands/restore.ts`
|
|
336
|
-
|
|
337
|
-
**Step 1: Read `src/cli.ts` to find the launch and restore command registrations**
|
|
338
|
-
|
|
339
|
-
- [ ] Locate the `program` / commander setup that registers `nfo` (default action — launches in cwd) and `nfo <id>` (attaches/restores). Note exactly how `--permission-level` is wired so the new flag mirrors that style.
|
|
340
|
-
|
|
341
|
-
**Step 2: Add the flag to the launch path**
|
|
342
|
-
|
|
343
|
-
- [ ] In the default-action / launch flow in `src/cli.ts`, add:
|
|
344
|
-
|
|
345
|
-
```ts
|
|
346
|
-
.option('--notify-on-permission', 'bell + desktop notify when a musician awaits permission', false)
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
- [ ] Pass the new opt value through `launch` / `decideAction` / `createOrchestra`. Find every call to `createOrchestra(...)` in the codebase (likely only one in `cli.ts`) and add `notifyOnPermission: opts.notifyOnPermission`.
|
|
350
|
-
|
|
351
|
-
**Step 3: Thread it through `src/commands/launch.ts`**
|
|
352
|
-
|
|
353
|
-
- [ ] Add `notifyOnPermission?: boolean` to `CreateOrchestraOptions`.
|
|
354
|
-
- [ ] Pass it into `makeInitialState`:
|
|
355
|
-
|
|
356
|
-
```ts
|
|
357
|
-
const state = makeInitialState({
|
|
358
|
-
orchestraId: opts.orchestraId,
|
|
359
|
-
projectPath: opts.repoRoot,
|
|
360
|
-
permissionLevel: opts.permissionLevel,
|
|
361
|
-
notifyOnPermission: opts.notifyOnPermission,
|
|
362
|
-
});
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
**Step 4: Add the flag to the restore path**
|
|
366
|
-
|
|
367
|
-
- [ ] In `src/cli.ts` for the restore/attach command, add the same `--notify-on-permission` option.
|
|
368
|
-
- [ ] In `src/commands/restore.ts`, accept an optional `notifyOnPermission?: boolean` parameter on `restoreOrchestra`. When provided AND not undefined, override the stored value before reattach:
|
|
369
|
-
|
|
370
|
-
```ts
|
|
371
|
-
if (notifyOnPermission !== undefined) {
|
|
372
|
-
state.notify_on_permission = notifyOnPermission;
|
|
373
|
-
await writeState(orchestraId, state);
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
Place this after `readState` and before `sessionExists` checks. If you have to bring in `writeState`, import it from `../state.js`.
|
|
378
|
-
|
|
379
|
-
**Step 5: Verify**
|
|
380
|
-
|
|
381
|
-
- [ ] Run `npm run build`. Expected: clean (any missed plumbing will surface as type errors).
|
|
382
|
-
- [ ] Run `npm test`. Expected: 113/113 still passing. The existing launch test asserts state shape — confirm `notify_on_permission: false` appears in the seed state.
|
|
383
|
-
- [ ] Run `node dist/cli.js --help`. Expected: shows `--notify-on-permission` in the listed options.
|
|
384
|
-
- [ ] Run `node dist/cli.js <orchestra-id-or-unused> --help` for the restore subcommand. Expected: same flag listed.
|
|
385
|
-
|
|
386
|
-
**Step 6: Commit**
|
|
387
|
-
|
|
388
|
-
```bash
|
|
389
|
-
git add src/cli.ts src/commands/launch.ts src/commands/restore.ts
|
|
390
|
-
git commit -m "$(cat <<'EOF'
|
|
391
|
-
feat(cli): --notify-on-permission flag on launch and restore
|
|
392
|
-
|
|
393
|
-
Persists to state.json via makeInitialState on create; on restore, an
|
|
394
|
-
explicit flag value overrides the stored value (so users can flip it
|
|
395
|
-
on a re-attach without editing state.json by hand).
|
|
396
|
-
|
|
397
|
-
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
398
|
-
EOF
|
|
399
|
-
)"
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
---
|
|
403
|
-
|
|
404
|
-
## Task 4: Fire the notifier from `App.tsx` on awaiting transitions
|
|
405
|
-
|
|
406
|
-
**Files:**
|
|
407
|
-
- Modify: `src/tui/App.tsx`
|
|
408
|
-
|
|
409
|
-
The permission poller already produces `PermissionTransition[]` deltas. We iterate that list anyway — extend the per-transition handling to call the notifier when a Musician *enters* `awaiting_permission` AND the orchestra has `notify_on_permission: true`. We pass the CURRENT total pending count (post-transition) to the notifier so the message text matches what the StatusBar shows.
|
|
410
|
-
|
|
411
|
-
**Step 1: Read the existing permission-poll effect**
|
|
412
|
-
|
|
413
|
-
- [ ] Open `src/tui/App.tsx`. Locate the `useEffect` that calls `pollPermissions` and iterates transitions. It currently looks roughly like:
|
|
414
|
-
|
|
415
|
-
```ts
|
|
416
|
-
const transitions = await pollPermissions(s);
|
|
417
|
-
for (const t of transitions) {
|
|
418
|
-
try {
|
|
419
|
-
await setMusicianStatus(props.orchestraId, t.musicianId, t.newStatus, t.pendingPermission);
|
|
420
|
-
} catch {
|
|
421
|
-
// dismissed-race swallow
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
**Step 2: Add the notifier import**
|
|
427
|
-
|
|
428
|
-
- [ ] At the top of `App.tsx` add:
|
|
429
|
-
|
|
430
|
-
```ts
|
|
431
|
-
import { notifyAwaitingPermission } from '../notify.js';
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
**Step 3: Fire after applying the transitions**
|
|
435
|
-
|
|
436
|
-
- [ ] At the bottom of the same `tick` function, AFTER the `for` loop:
|
|
437
|
-
|
|
438
|
-
```ts
|
|
439
|
-
const newlyAwaiting = transitions.filter((t) => { return t.newStatus === 'awaiting_permission'; });
|
|
440
|
-
if (newlyAwaiting.length > 0 && s.notify_on_permission === true) {
|
|
441
|
-
const fresh = await readState(props.orchestraId);
|
|
442
|
-
if (fresh) {
|
|
443
|
-
const total = fresh.musicians.filter((m) => { return m.status === 'awaiting_permission'; }).length;
|
|
444
|
-
await notifyAwaitingPermission({ pendingCount: total });
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
Notes for the implementer:
|
|
450
|
-
- We re-read state AFTER applying transitions so `total` reflects the post-transition count.
|
|
451
|
-
- We only fire ONCE per tick even if multiple Musicians transition in the same tick (they're effectively concurrent for notification purposes — one bell, one message with the combined count).
|
|
452
|
-
- We use `s.notify_on_permission === true` (strict) so the field's optionality doesn't trip us up.
|
|
453
|
-
|
|
454
|
-
**Step 4: Verify**
|
|
455
|
-
|
|
456
|
-
- [ ] Run `npm run build`. Expected: clean.
|
|
457
|
-
- [ ] Run `npm test`. Expected: 113/113 still passing (App.tsx has no unit test, so this is a behavioral change covered by the manual smoke later).
|
|
458
|
-
|
|
459
|
-
**Step 5: Commit**
|
|
460
|
-
|
|
461
|
-
```bash
|
|
462
|
-
git add src/tui/App.tsx
|
|
463
|
-
git commit -m "$(cat <<'EOF'
|
|
464
|
-
feat(tui): App — fire notifier on new awaiting-permission transitions
|
|
465
|
-
|
|
466
|
-
After applying pollPermissions transitions, if notify_on_permission is
|
|
467
|
-
true on the state, count the post-transition awaiting total and call
|
|
468
|
-
notifyAwaitingPermission. One bell per tick regardless of how many
|
|
469
|
-
musicians transitioned — bell-storm is worse than under-notifying.
|
|
470
|
-
|
|
471
|
-
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
472
|
-
EOF
|
|
473
|
-
)"
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
---
|
|
477
|
-
|
|
478
|
-
## Task 5: `Help.tsx` presentational component
|
|
479
|
-
|
|
480
|
-
**Files:**
|
|
481
|
-
- Create: `src/tui/Help.tsx`
|
|
482
|
-
- Create: `tests/tui/Help.test.tsx`
|
|
483
|
-
|
|
484
|
-
Pure presentational. Lists every keybinding the user might press. Single `?` line at the bottom prompts to close.
|
|
485
|
-
|
|
486
|
-
**Step 1: Write the failing test**
|
|
487
|
-
|
|
488
|
-
- [ ] Create `tests/tui/Help.test.tsx`:
|
|
489
|
-
|
|
490
|
-
```tsx
|
|
491
|
-
import { describe, it, expect } from 'vitest';
|
|
492
|
-
import { render } from 'ink-testing-library';
|
|
493
|
-
import { Help } from '../../src/tui/Help.js';
|
|
494
|
-
|
|
495
|
-
describe('Help', () => {
|
|
496
|
-
it('lists the core keybindings', () => {
|
|
497
|
-
const { lastFrame } = render(<Help />);
|
|
498
|
-
const frame = lastFrame() ?? '';
|
|
499
|
-
expect(frame).toContain('↑');
|
|
500
|
-
expect(frame).toContain('Enter');
|
|
501
|
-
expect(frame).toContain('n');
|
|
502
|
-
expect(frame).toContain('d');
|
|
503
|
-
expect(frame).toContain('p');
|
|
504
|
-
expect(frame).toContain('q');
|
|
505
|
-
expect(frame).toContain('?');
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
it('mentions notes, dismiss, jump-to-pending, focus-orchestrator', () => {
|
|
509
|
-
const { lastFrame } = render(<Help />);
|
|
510
|
-
const frame = (lastFrame() ?? '').toLowerCase();
|
|
511
|
-
expect(frame).toContain('notes');
|
|
512
|
-
expect(frame).toContain('dismiss');
|
|
513
|
-
expect(frame).toContain('awaiting');
|
|
514
|
-
expect(frame).toContain('orchestrator');
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
it('shows a close hint', () => {
|
|
518
|
-
const { lastFrame } = render(<Help />);
|
|
519
|
-
const frame = (lastFrame() ?? '').toLowerCase();
|
|
520
|
-
expect(frame).toContain('close');
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
**Step 2: Run the test, confirm it fails**
|
|
526
|
-
|
|
527
|
-
- [ ] Run `npm test -- tests/tui/Help.test.tsx`. Expected: FAIL with "Cannot find module '../../src/tui/Help.js'".
|
|
528
|
-
|
|
529
|
-
**Step 3: Implement `src/tui/Help.tsx`**
|
|
530
|
-
|
|
531
|
-
- [ ] Create `src/tui/Help.tsx`:
|
|
532
|
-
|
|
533
|
-
```tsx
|
|
534
|
-
import type { ReactElement } from 'react';
|
|
535
|
-
import { Box, Text } from 'ink';
|
|
536
|
-
|
|
537
|
-
interface Row {
|
|
538
|
-
key: string;
|
|
539
|
-
label: string;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const ROWS: Row[] = [
|
|
543
|
-
{ key: '↑ / k', label: 'move selection up' },
|
|
544
|
-
{ key: '↓ / j', label: 'move selection down' },
|
|
545
|
-
{ key: 'Enter', label: 'jump into selected Musician\'s tmux window' },
|
|
546
|
-
{ key: 'n', label: 'open notes for this orchestra' },
|
|
547
|
-
{ key: 'd', label: 'dismiss the selected Musician' },
|
|
548
|
-
{ key: 'p', label: 'jump to next Musician awaiting permission' },
|
|
549
|
-
{ key: 'q', label: 'focus the Orchestrator pane' },
|
|
550
|
-
{ key: '?', label: 'toggle this help / close' },
|
|
551
|
-
];
|
|
552
|
-
|
|
553
|
-
export function Help(): ReactElement {
|
|
554
|
-
return (
|
|
555
|
-
<Box flexDirection="column" paddingX={1}>
|
|
556
|
-
<Text bold={true}>Keybindings</Text>
|
|
557
|
-
{ROWS.map((row) => {
|
|
558
|
-
return (
|
|
559
|
-
<Text key={row.key}>
|
|
560
|
-
<Text color="cyan">{row.key.padEnd(8)}</Text>
|
|
561
|
-
<Text> {row.label}</Text>
|
|
562
|
-
</Text>
|
|
563
|
-
);
|
|
564
|
-
})}
|
|
565
|
-
<Text dimColor={true}>Press ? to close.</Text>
|
|
566
|
-
</Box>
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
**Step 4: Run tests, confirm green**
|
|
572
|
-
|
|
573
|
-
- [ ] Run `npm test -- tests/tui/Help.test.tsx`. Expected: 3 passing.
|
|
574
|
-
- [ ] Run `npm test`. Expected: 116/116 (113 prior + 3 new).
|
|
575
|
-
- [ ] Run `npm run build`. Expected: clean.
|
|
576
|
-
|
|
577
|
-
**Step 5: Commit**
|
|
578
|
-
|
|
579
|
-
```bash
|
|
580
|
-
git add src/tui/Help.tsx tests/tui/Help.test.tsx
|
|
581
|
-
git commit -m "$(cat <<'EOF'
|
|
582
|
-
feat(tui): Help — presentational keybindings overlay
|
|
583
|
-
|
|
584
|
-
Lists every key the user can press in the right pane (arrows, Enter, n,
|
|
585
|
-
d, p, q, ?) with a one-line label. AppView will toggle this in place of
|
|
586
|
-
the main column when showHelp is true (next task).
|
|
587
|
-
|
|
588
|
-
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
589
|
-
EOF
|
|
590
|
-
)"
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
---
|
|
594
|
-
|
|
595
|
-
## Task 6: `?` keybinding — extend `reduceKey` with `toggle-help`
|
|
596
|
-
|
|
597
|
-
**Files:**
|
|
598
|
-
- Modify: `src/tui/keymap.ts`
|
|
599
|
-
- Modify: `tests/tui/keymap.test.ts`
|
|
600
|
-
|
|
601
|
-
**Step 1: Read the current `reduceKey` order**
|
|
602
|
-
|
|
603
|
-
- [ ] Open `src/tui/keymap.ts`. The current order: arrows/j/k, tab, shiftTab, return, n, d, q, p. Add `?` after `p` (or any consistent placement — the order doesn't affect correctness since keys are mutually exclusive).
|
|
604
|
-
|
|
605
|
-
**Step 2: Extend the union and add the branch**
|
|
606
|
-
|
|
607
|
-
- [ ] Add `| { kind: 'toggle-help' }` to `KeyAction`.
|
|
608
|
-
- [ ] After the `p` branch:
|
|
609
|
-
|
|
610
|
-
```ts
|
|
611
|
-
if (key.input === '?') {
|
|
612
|
-
return { ui, action: { kind: 'toggle-help' } };
|
|
613
|
-
}
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
**Step 3: Write the failing test**
|
|
617
|
-
|
|
618
|
-
- [ ] In `tests/tui/keymap.test.ts`, add:
|
|
619
|
-
|
|
620
|
-
```ts
|
|
621
|
-
it("'?' emits toggle-help", () => {
|
|
622
|
-
const result = reduceKey(
|
|
623
|
-
{ selectedIndex: 0, musicianCount: 0 },
|
|
624
|
-
{ input: '?', downArrow: false, upArrow: false, tab: false, shiftTab: false, return: false },
|
|
625
|
-
);
|
|
626
|
-
expect(result.action).toEqual({ kind: 'toggle-help' });
|
|
627
|
-
});
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
**Step 4: Verify**
|
|
631
|
-
|
|
632
|
-
- [ ] Run `npm test`. Expected: 117/117 (116 prior + 1 new).
|
|
633
|
-
- [ ] Run `npm run build`. Expected: clean.
|
|
634
|
-
|
|
635
|
-
**Step 5: Commit**
|
|
636
|
-
|
|
637
|
-
```bash
|
|
638
|
-
git add src/tui/keymap.ts tests/tui/keymap.test.ts
|
|
639
|
-
git commit -m "$(cat <<'EOF'
|
|
640
|
-
feat(tui): keymap — add '?' toggle-help action
|
|
641
|
-
|
|
642
|
-
App.tsx will track showHelp in state and AppView will swap the main
|
|
643
|
-
column for <Help/> when true. The reducer stays pure; ? simply emits
|
|
644
|
-
the action regardless of UI state.
|
|
645
|
-
|
|
646
|
-
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
647
|
-
EOF
|
|
648
|
-
)"
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
---
|
|
652
|
-
|
|
653
|
-
## Task 7: `AppView` + `App` wire the help overlay
|
|
654
|
-
|
|
655
|
-
**Files:**
|
|
656
|
-
- Modify: `src/tui/AppView.tsx`
|
|
657
|
-
- Modify: `src/tui/App.tsx`
|
|
658
|
-
- Modify: `tests/tui/AppView.test.tsx`
|
|
659
|
-
|
|
660
|
-
**Step 1: Extend `AppViewProps`**
|
|
661
|
-
|
|
662
|
-
- [ ] Open `src/tui/AppView.tsx`. Add `showHelp?: boolean` (optional, defaults to false in the function body).
|
|
663
|
-
- [ ] At the top of the function body, BEFORE the existing return, add:
|
|
664
|
-
|
|
665
|
-
```ts
|
|
666
|
-
if (props.showHelp === true) {
|
|
667
|
-
return <Help />;
|
|
668
|
-
}
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
- [ ] Add the import `import { Help } from './Help.js';` at the top of the file.
|
|
672
|
-
|
|
673
|
-
**Step 2: Track `showHelp` in App.tsx and handle the toggle action**
|
|
674
|
-
|
|
675
|
-
- [ ] In `src/tui/App.tsx`:
|
|
676
|
-
|
|
677
|
-
- Add a new state slot near the other `useState` declarations:
|
|
678
|
-
|
|
679
|
-
```ts
|
|
680
|
-
const [showHelp, setShowHelp] = useState(false);
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
- In the `useInput` action-cascade, after the `jump-to-pending` branch, add:
|
|
684
|
-
|
|
685
|
-
```ts
|
|
686
|
-
if (action.kind === 'toggle-help') {
|
|
687
|
-
setShowHelp((prev) => { return !prev; });
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
- In the JSX, thread the prop:
|
|
693
|
-
|
|
694
|
-
```tsx
|
|
695
|
-
<AppView
|
|
696
|
-
orchestras={orchestras}
|
|
697
|
-
currentId={props.orchestraId}
|
|
698
|
-
musicians={musicians}
|
|
699
|
-
activity={activity}
|
|
700
|
-
selectedIndex={selectedIndex}
|
|
701
|
-
permissionLevel={permissionLevel}
|
|
702
|
-
tokenHint="—"
|
|
703
|
-
now={now}
|
|
704
|
-
pendingCount={pendingCount}
|
|
705
|
-
showHelp={showHelp}
|
|
706
|
-
/>
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
**Step 3: Update / add AppView test**
|
|
710
|
-
|
|
711
|
-
- [ ] In `tests/tui/AppView.test.tsx`, add:
|
|
712
|
-
|
|
713
|
-
```tsx
|
|
714
|
-
it('renders the help overlay when showHelp=true', () => {
|
|
715
|
-
const { lastFrame } = render(
|
|
716
|
-
<AppView
|
|
717
|
-
orchestras={[]}
|
|
718
|
-
currentId="abc"
|
|
719
|
-
musicians={[]}
|
|
720
|
-
activity={{}}
|
|
721
|
-
selectedIndex={0}
|
|
722
|
-
permissionLevel="supervised"
|
|
723
|
-
tokenHint="—"
|
|
724
|
-
now={new Date(0).toISOString()}
|
|
725
|
-
pendingCount={0}
|
|
726
|
-
showHelp={true}
|
|
727
|
-
/>,
|
|
728
|
-
);
|
|
729
|
-
const frame = (lastFrame() ?? '').toLowerCase();
|
|
730
|
-
expect(frame).toContain('keybindings');
|
|
731
|
-
expect(frame).not.toContain('auditorium');
|
|
732
|
-
});
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
The "not.toContain('auditorium')" check confirms the help screen REPLACES the main column (no double-rendering).
|
|
736
|
-
|
|
737
|
-
**Step 4: Verify**
|
|
738
|
-
|
|
739
|
-
- [ ] Run `npm test`. Expected: 118/118 (117 prior + 1 new). The existing AppView test still uses `showHelp` defaulted to `undefined` (i.e. falsy) — confirm it still renders the column.
|
|
740
|
-
- [ ] Run `npm run build`. Expected: clean.
|
|
741
|
-
|
|
742
|
-
**Step 5: Commit**
|
|
743
|
-
|
|
744
|
-
```bash
|
|
745
|
-
git add src/tui/App.tsx src/tui/AppView.tsx tests/tui/AppView.test.tsx
|
|
746
|
-
git commit -m "$(cat <<'EOF'
|
|
747
|
-
feat(tui): App/AppView — wire ? help overlay (toggle in/out)
|
|
748
|
-
|
|
749
|
-
showHelp lives in App as useState(false). AppView swaps the main column
|
|
750
|
-
layout for <Help/> when true. Pressing ? toggles; pressing ? again
|
|
751
|
-
closes (no separate close key needed).
|
|
752
|
-
|
|
753
|
-
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
754
|
-
EOF
|
|
755
|
-
)"
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
---
|
|
759
|
-
|
|
760
|
-
## Task 8: Re-advertise `[?] help` in the StatusBar hint
|
|
761
|
-
|
|
762
|
-
**Files:**
|
|
763
|
-
- Modify: `src/tui/StatusBar.tsx`
|
|
764
|
-
- Modify: `tests/tui/StatusBar.test.tsx`
|
|
765
|
-
|
|
766
|
-
Phase 3 pulled `[?] help` from the bottom hint because there was no handler. Now there is. Re-add it.
|
|
767
|
-
|
|
768
|
-
**Step 1: Update the hint text**
|
|
769
|
-
|
|
770
|
-
- [ ] In `src/tui/StatusBar.tsx`, change the bottom-row hint from:
|
|
771
|
-
|
|
772
|
-
```tsx
|
|
773
|
-
<Text dimColor={true}>[↑↓] nav [⏎] enter [n] notes [d] dismiss [q] back</Text>
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
to:
|
|
777
|
-
|
|
778
|
-
```tsx
|
|
779
|
-
<Text dimColor={true}>[↑↓] nav [⏎] enter [n] notes [d] dismiss [q] back [?] help</Text>
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
**Step 2: Update existing test**
|
|
783
|
-
|
|
784
|
-
- [ ] In `tests/tui/StatusBar.test.tsx`, update at least one existing assertion (or add a new one) to require `[?] help` in the rendered frame.
|
|
785
|
-
|
|
786
|
-
```ts
|
|
787
|
-
it('advertises [?] help in the bottom hint', () => {
|
|
788
|
-
const { lastFrame } = render(<StatusBar permissionLevel="supervised" tokenHint="—" pendingCount={0} />);
|
|
789
|
-
const frame = lastFrame() ?? '';
|
|
790
|
-
expect(frame).toContain('[?] help');
|
|
791
|
-
});
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
**Step 3: Verify**
|
|
795
|
-
|
|
796
|
-
- [ ] Run `npm test`. Expected: 119/119 (118 prior + 1 new).
|
|
797
|
-
- [ ] Run `npm run build`. Expected: clean.
|
|
798
|
-
|
|
799
|
-
**Step 4: Commit**
|
|
800
|
-
|
|
801
|
-
```bash
|
|
802
|
-
git add src/tui/StatusBar.tsx tests/tui/StatusBar.test.tsx
|
|
803
|
-
git commit -m "$(cat <<'EOF'
|
|
804
|
-
feat(tui): StatusBar — re-advertise [?] help in bottom hint
|
|
805
|
-
|
|
806
|
-
Now that App.tsx has a real toggle-help handler, [?] is no longer a
|
|
807
|
-
dangling promise. Added to the end of the bottom dim hint line.
|
|
808
|
-
|
|
809
|
-
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
810
|
-
EOF
|
|
811
|
-
)"
|
|
812
|
-
```
|
|
813
|
-
|
|
814
|
-
---
|
|
815
|
-
|
|
816
|
-
## Task 9: Manual smoke test (deferred to user)
|
|
817
|
-
|
|
818
|
-
**Files:** none — runtime exercise.
|
|
819
|
-
|
|
820
|
-
Setup:
|
|
821
|
-
- `npm run build` (no `npm link` needed unless you skipped Phase 4 — the symlink already points at `dist/cli.js`).
|
|
822
|
-
- Kill any existing orchestra for the test repo (`nfo kill <id>`).
|
|
823
|
-
|
|
824
|
-
Help-overlay smoke:
|
|
825
|
-
1. `nfo --notify-on-permission` in a throwaway repo.
|
|
826
|
-
2. Once attached, focus the right pane.
|
|
827
|
-
3. Press `?`. The right pane should swap to the Keybindings screen showing all 8 rows and the "Press ? to close." hint.
|
|
828
|
-
4. Press `?` again. The right pane should return to the Concert Hall / Auditorium / StatusBar column.
|
|
829
|
-
5. The StatusBar's bottom hint should now show `[?] help` at the end.
|
|
830
|
-
|
|
831
|
-
Notify smoke:
|
|
832
|
-
1. From the Orchestrator pane, spawn a Musician with a task that requires permission (same trigger as Phase 4 smoke: `Run \`rm -rf .git/no-such\` and report what happens`).
|
|
833
|
-
2. Within ~2 s of the permission prompt appearing in the Musician's hidden window:
|
|
834
|
-
- You should hear a terminal bell (your terminal must have `audible bell` enabled).
|
|
835
|
-
- On Linux: a `notify-send` desktop notification titled "NFO" with "1 musician awaiting permission".
|
|
836
|
-
- On macOS: an `osascript`-driven notification (banner appears in the upper right).
|
|
837
|
-
3. Spawn a second Musician that also requires permission. When IT transitions, you should get a fresh notification with "2 musicians awaiting permission".
|
|
838
|
-
4. Answer one prompt. No additional notification fires on the awaiting → working transition.
|
|
839
|
-
5. Confirm the StatusBar's yellow banner count updates as expected.
|
|
840
|
-
|
|
841
|
-
Negative smoke (flag off):
|
|
842
|
-
1. Kill the orchestra. Restart with `nfo` (no flag).
|
|
843
|
-
2. Trigger a permission prompt. No bell, no desktop notification — only the in-TUI yellow banner.
|
|
844
|
-
|
|
845
|
-
- [ ] **Step 1: Run all three smokes** as described. Document any deviation.
|
|
846
|
-
|
|
847
|
-
---
|
|
848
|
-
|
|
849
|
-
## Task 10: README update
|
|
850
|
-
|
|
851
|
-
**Files:**
|
|
852
|
-
- Modify: `README.md`
|
|
853
|
-
|
|
854
|
-
**Step 1: Update the status section**
|
|
855
|
-
|
|
856
|
-
- [ ] Open `README.md`. Above the "Phase 4" paragraph, add:
|
|
857
|
-
|
|
858
|
-
```markdown
|
|
859
|
-
Phase 5. Everything from Phase 4, plus a `?`-toggled **help overlay** listing every keybinding in the right pane, and the `--notify-on-permission` flag that arms a **terminal bell + cross-platform desktop notification** (`notify-send` on Linux, `osascript` on macOS) whenever any Musician enters `awaiting_permission`. Notification fires once per tick regardless of how many Musicians transition together. Token usage hint and Concert Hall orchestra-switching ship in a later phase.
|
|
860
|
-
```
|
|
861
|
-
|
|
862
|
-
**Step 2: Update the Use section (if you maintain a flag list)**
|
|
863
|
-
|
|
864
|
-
- [ ] Add `--notify-on-permission` to the list of flags accepted by the launch command:
|
|
865
|
-
|
|
866
|
-
```markdown
|
|
867
|
-
In a git repo: `nfo` (add `--notify-on-permission` for bell + desktop notify on permission prompts)
|
|
868
|
-
```
|
|
869
|
-
|
|
870
|
-
**Step 3: Commit**
|
|
871
|
-
|
|
872
|
-
```bash
|
|
873
|
-
git add README.md
|
|
874
|
-
git commit -m "$(cat <<'EOF'
|
|
875
|
-
docs: README for Phase 5 (help overlay + notify)
|
|
876
|
-
|
|
877
|
-
Adds a Phase 5 paragraph: ? toggle for the help overlay, and the
|
|
878
|
-
--notify-on-permission flag for bell + cross-platform desktop notify
|
|
879
|
-
on awaiting transitions. Calls out one-tick dedup.
|
|
880
|
-
|
|
881
|
-
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
882
|
-
EOF
|
|
883
|
-
)"
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
---
|
|
887
|
-
|
|
888
|
-
## Task 11: Final audit + tag
|
|
889
|
-
|
|
890
|
-
**Files:** none directly.
|
|
891
|
-
|
|
892
|
-
**Step 1: Run the full suite**
|
|
893
|
-
|
|
894
|
-
- [ ] `npm run build && npm test`. Both must be 100% clean.
|
|
895
|
-
|
|
896
|
-
**Step 2: Self-audit checklist**
|
|
897
|
-
|
|
898
|
-
- [ ] No `JSX.Element` in any Phase 5 file. `grep -n "JSX.Element" src/tui/*.tsx`.
|
|
899
|
-
- [ ] No shorthand control flow or implicit-return arrows in NEW Phase 5 code (read `src/notify.ts`, `src/tui/Help.tsx`, the modifications to `keymap.ts`/`AppView.tsx`/`App.tsx`/`StatusBar.tsx`, `src/cli.ts`, `src/commands/launch.ts`, `src/commands/restore.ts`). Ternaries inside JSX are fine.
|
|
900
|
-
- [ ] All commits in `phase-4-complete..HEAD` use the 4.8 trailer.
|
|
901
|
-
- [ ] `notify_on_permission` defaults to `false` everywhere — confirm by reading `makeInitialState`. A user who doesn't pass the flag must get the same UX as Phase 4.
|
|
902
|
-
- [ ] `notifyAwaitingPermission` swallows ALL spawn errors. A user without `notify-send` installed must still see the StatusBar banner and have no orchestrator crash.
|
|
903
|
-
- [ ] One notification per tick — App.tsx must NOT call `notifyAwaitingPermission` inside the per-transition loop; it must call it once at the end of the tick with the total count.
|
|
904
|
-
- [ ] Help overlay REPLACES the main column when shown (no double-rendering). The AppView test asserts `not.toContain('auditorium')` — confirm it passes.
|
|
905
|
-
- [ ] `?` toggles in BOTH directions (a single key for open and close).
|
|
906
|
-
- [ ] StatusBar `[?] help` advertised only when the handler exists (it always does now — fine to unconditionally advertise).
|
|
907
|
-
- [ ] CLI integrity: `node dist/cli.js --help` and `node dist/cli.js <whatever> --help` both render without error and show `--notify-on-permission`.
|
|
908
|
-
- [ ] No Phase 5 file modifies the MCP server, musician primitives, or the existing permission-level logic.
|
|
909
|
-
|
|
910
|
-
**Step 3: Tag**
|
|
911
|
-
|
|
912
|
-
```bash
|
|
913
|
-
git tag phase-5-complete
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
---
|
|
917
|
-
|
|
918
|
-
## Out-of-scope wrap-up (carried to Phase 6+)
|
|
919
|
-
|
|
920
|
-
A future plan should cover:
|
|
921
|
-
|
|
922
|
-
- **Token usage hint** in the StatusBar — parse claude's status line from the captured pane and surface live token / cost info. Needs care around claude version drift.
|
|
923
|
-
- **Concert Hall orchestra-switching** — Tab/Shift-Tab actually attaching a different tmux session from inside Ink. Needs a session-handoff mechanism (probably exec `tmux switch-client` and let Ink unmount cleanly).
|
|
924
|
-
- **Folding the activity poller and permission poller** into one pane-capture pass. Cuts pane I/O in half but the two-poller layout is correct today; only worth doing if the captures show up as a real cost.
|
|
925
|
-
- **Per-session in-TUI toggle for `notify_on_permission`** — a key binding (maybe `N`?) that writes the flag back to state.json. Currently Phase 5 sets it at launch-time only.
|
|
926
|
-
|
|
927
|
-
## Self-review
|
|
928
|
-
|
|
929
|
-
(Run before handing this plan to an implementer — confirms no spec gaps, placeholders, or type drift.)
|
|
930
|
-
|
|
931
|
-
- **Spec coverage:** §5.2.1's "Optional bell" paragraph is covered by Tasks 2–4. §8's `?` help affordance is covered by Tasks 5–8. Token hint and Concert Hall switching are explicitly deferred — listed in the wrap-up section.
|
|
932
|
-
- **No placeholders:** every step shows the exact code, test, command, or commit message. No "TBD" / "add appropriate error handling" / "similar to Task N".
|
|
933
|
-
- **Type consistency:** `notify_on_permission` (state field) and `notifyOnPermission` (input arg) are used consistently between state.types.ts, launch.ts, restore.ts, and App.tsx. `notifyAwaitingPermission` is called with the same `{ pendingCount }` argument in App.tsx as in the test.
|