record D-81: right-pane reader load via a retained ReaderNav

Documents why the markdown/decisions readers load from a retained
per-reader nav-history (grab-current-on-mount + single 'load' path)
rather than per-view state (dies with the widget — the T-196 bug) or
MessageBus retention (wrong layer). The user chose the nav-history
helper over a bus fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 12:45:24 +02:00
co-authored by Claude Opus 4.8
parent 0bb89a2e58
commit 1db65f8481
4 changed files with 16 additions and 0 deletions
@@ -1,2 +1,4 @@
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-194', 'status', 'backlog', 'done', NULL, '2026-06-01 06:49:47', '2026-06-01 06:49:47', '2026-06-01 06:49:47', NULL, '09bfe6f27513d7d16f7139543961b4f8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-195', 'status', 'backlog', 'done', NULL, '2026-06-01 07:08:16', '2026-06-01 07:08:16', '2026-06-01 07:08:16', NULL, 'c2fb7c6ede9e970a8186b5932141f472', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-197', 'status', 'backlog', 'done', NULL, '2026-06-01 10:45:14', '2026-06-01 10:45:14', '2026-06-01 10:45:14', NULL, '7d9a5a6040ccaff63f95c106c088f7b5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-196', 'status', 'backlog', 'done', NULL, '2026-06-01 10:45:14', '2026-06-01 10:45:14', '2026-06-01 10:45:14', NULL, 'a6afc45a7b24dc8dc65a0a6cda319f7d', 1) ON CONFLICT(hash) DO NOTHING;
+2
View File
@@ -1,2 +1,4 @@
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-194', 'bug', 'T-132', 'Markdown reader 404s on absolute paths handed to files.read', 'The sidebar markdown reader (and any files.read caller) was handed an absolute path (e.g. the Claude Config tab publishing a skill''s absolute SKILL.md manifestPath). resolveUnderRoot joined the absolute path onto the workspace root (/repo + /repo/x = /repo/repo/x), which does not exist, so the reader showed ''file not found'' for a file that exists. Fix: resolveUnderRoot normalizes an absolute input as-is and relies on the existing containment check (so absolute-under-root resolves, absolute-outside still throws PathOutsideRoot). Found live opening .claude/skills/frame0-wireframe/SKILL.md.', 'done', 'high', NULL, NULL, NULL, '2026-06-01 06:49:25', '2026-06-01 06:49:47', NULL, '01a3a891c9786f7afb7a1b95e8ab0b2f', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-195', 'story', 'T-132', 'Reader can''t open user-scope Claude config files (~/.claude skills/agents/commands)', 'Clicking a user-scope skill in the Claude Config tab (e.g. /home/<user>/.claude/skills/peon-ping-toggle/SKILL.md) shows ''path outside workspace''. The markdown reader loads via files.read, which is workspace-confined for security (T-102); but user-scope Claude config files legitimately live under ~/.claude, outside the repo. Per D-76, Claude''s config surface (skills/agents/commands under ~/.claude AND the repo .claude) is clide-managed and safe to surface. Proposed: a scoped trusted-read — a new IPC verb (e.g. claude.read-config-file) that permits reads ONLY under the resolved Claude config roots (~/.claude + <repo>/.claude), still rejecting arbitrary paths. Route the Config tab''s skill/agent/command markdown clicks through it, or have the markdown reader fall back to it when the path is under a Claude config root but outside the workspace. Alternative: the Config tab already reads SKILL.md during scan (claude_config.dart) and could publish content directly via a load-by-content channel rather than a path (the reader currently only reads by path). Repo-scope skills already work after T-194; this covers the user-scope half.', 'done', 'medium', NULL, NULL, NULL, '2026-06-01 06:56:05', '2026-06-01 07:08:16', NULL, '7b46d0491c01bcc056c430a7af48f734', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-197', 'bug', NULL, 'Editor pane never reveals on editor.open (edit button, file-tree open)', 'Opening a file in the editor (the reader''s edit pencil, a non-.md file-tree click, a decision''s edit) calls editor.open and the daemon opens the buffer, but the editor pane never appears in the workspace slot above the Claude pane. Root cause: EditorExtension contributes a workspace tab (editor.active, priority 80) but has no activate() that reveals/activates it. editor.opened is emitted on the DaemonBus, but the EditorController that handles it only exists once EditorView is mounted — and nothing ever activates the editor tab to mount it. Fix: add EditorExtension.activate() subscribing to the editor.opened / editor.active-changed DaemonEvents and calling panels.activateTab(Slots.workspace, ''editor.active''). EditorView.hydrate() already pulls the active buffer on mount, so reveal-then-hydrate avoids any publish/subscribe race.', 'done', 'high', NULL, NULL, NULL, '2026-06-01 08:58:19', '2026-06-01 10:45:14', NULL, '05ae54e31d0c9befdbfef2fe096737c0', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-196', 'bug', 'T-186', 'Decision reader: first click switches tab but doesn''t load (lost on tab mount)', 'Clicking a D-record activates the decisions.detail context-panel tab but does not load the record; only the SECOND click (any record) works. Root cause: DecisionDetailView subscribes to the ''selection'' MessageBus channel in didChangeDependencies, which runs only AFTER the extension''s activateTab mounts the view. The bus is broadcast (no replay), so the ''selection'' message that triggered the reveal is already gone — the view subscribes too late and misses it. The markdown extension already solved this: activateTab, then addPostFrameCallback -> re-publish on a separate ''load'' channel the viewer subscribes to, so the load fires after the viewer mounts+subscribes. Fix: mirror that — DecisionDetailView subscribes to ''load''; DecisionsExtension forwards ''selection'' -> post-frame ''load'' after activateTab.', 'done', 'high', NULL, NULL, NULL, '2026-06-01 08:58:10', '2026-06-01 10:45:14', NULL, 'c87a8726edd727831291bd1055e39ae1', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+1
View File
@@ -122,6 +122,7 @@ You might also want, project-permitting:
- [D-78: Claude permission/prompt transport is the stdio control channel](decisions/architecture.md#d-78-claude-permissionprompt-transport-is-the-stdio-control-channel) — _architecture_
- [D-79: Workspace content search is a pure-Dart in-process engine, outside pql](decisions/architecture.md#d-79-workspace-content-search-is-a-pure-dart-in-process-engine-outside-pql) — _architecture_
- [D-80: `files.read` allows trusted Claude config roots beyond the workspace](decisions/architecture.md#d-80-filesread-allows-trusted-claude-config-roots-beyond-the-workspace) — _architecture_
- [D-81: Right-pane reader load is driven by a retained `ReaderNav`, not per-view state or bus retention](decisions/architecture.md#d-81-right-pane-reader-load-is-driven-by-a-retained-readernav-not-per-view-state-or-bus-retention) — _architecture_
## Open questions
+11
View File
@@ -368,3 +368,14 @@ Core, rendering, IPC, kernel, panel manager.
- **Raised by:** 2026-06-01 — after T-194 fixed repo-scope skill reads, the user hit `path outside workspace` opening a *user-scope* skill (`~/.claude/skills/peon-ping-toggle/SKILL.md`) and said the `.claude` dir should be in the workspace. Chose extending the read allow-list over a separate trusted-read verb.
---
### D-81: Right-pane reader load is driven by a retained `ReaderNav`, not per-view state or bus retention
- **Date:** 2026-06-01
- **Status:** accepted
- **Decision:** The right-pane readers (markdown, decisions) load content from a **retained per-reader `ReaderNav`** (a kernel `ChangeNotifier` held in a `ReaderNavRegistry`), not from per-view `State` and not from a "retained/replay-latest" MessageBus channel. `ReaderNav` owns the back/forward history + pin, subscribes to its reader's `selection` channel to record entries, and **(re-)emits `load`** — the single channel a reader displays from. A reader **grabs `nav.current` on mount** (so a selection that revealed its tab before it subscribed isn't lost), and loads from `load` while mounted. Back/forward/pin navigate the nav and re-emit `load`, so every load flows through one path.
- **Rationale:** The bug (T-196): a reader subscribes in `didChangeDependencies`, which runs only *after* the tab is revealed, so the broadcast `selection` that triggered the reveal is already gone — first click did nothing. A post-frame re-publish "fixed" it but is timing-fragile (hard to test deterministically). Two clean options remained: make the MessageBus retain-and-replay the last value to new subscribers, or hold the retained state in the nav-history helper. **The nav-history is the better home** — the reader's "what am I showing" *is* its current history entry, the helper already exists for back/forward, and it keeps the MessageBus a dumb pipe (no per-channel retention semantics, no unbounded memory question). Grab-on-mount is a pull; the bus stays push-only.
- **Cost / alternatives:** Rejected **bus retention** (BehaviorSubject-style replay) as the wrong layer — it would bake stateful replay into every channel and surprise non-reader subscribers. Rejected **per-view `ReaderHistory`** (the old `ReaderHistoryMixin`, now deleted) because it dies with the widget, which is the whole bug. The cost is a small kernel service (`ReaderNavRegistry`) on `KernelServices` + the extension context.
- **Cross-reference:** fixes T-196; the readers reveal their tab on `selection` (extension) then pull `nav.current`. Relates to [D-78](#d-78-claude-permissionprompt-transport-is-the-stdio-control-channel)-era reader work (T-187/188/189). Implemented in `lib/kernel/src/reader_nav.dart` + the markdown/decisions readers.
- **Raised by:** 2026-06-01 — the user reported decisions opening only on the second click, diagnosed the lost-on-mount race, and explicitly chose a "right-pane nav history helper" over a MessageBus fix ("leaving them in the messagebus is the wrong shape").
---