Skip to content

Commit 1a440e7

Browse files
authored
Add relative Markdown links and per-Surface Back/Forward navigation (#1172)
2 parents 72ce7a8 + 9513216 commit 1a440e7

52 files changed

Lines changed: 1007 additions & 87 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/specs/dor-browser.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ Source of truth: `lib/src/components/wall/agent-browser-surface-controller.ts` (
200200

201201
- `launch` opens an http(s) `url` in a new GUI session, or for a named session navigates its browser when up in the mode asked for, else relaunches it headed or headless; it resolves when the browser is up, not the page. **Never stop a browser a named launch finds up in its mode**: a Tool re-announced (rationale).
202202
- **Never start a browser from `attach` or `measure`**, except that `attach` relaunches a gone one at the page its caller names, answering `relaunched`.
203-
- **Never expose arbitrary CLI arguments, JavaScript or CDP methods through the webview channel**: each operation is one fixed argv or client call, and `edit` runs fixed host-owned JS plus an OS clipboard write. The trusted `dor` process keeps native passthrough.
203+
- **Never expose arbitrary CLI arguments, JavaScript or CDP methods through the webview channel**: each operation is one fixed argv or client call, and `edit` runs fixed host-owned JS plus an OS clipboard write. Tool reading-state operations follow `docs/specs/dor-tool.md` → Document navigation. The trusted `dor` process keeps native passthrough.
204204

205205
**Must answer a launch inside `BROWSER_REQUEST_TIMEOUT_MS`**, queueing included — the wait every transport gives any reply, past agent-browser's own 25 s action timeout, so the webview never re-asks while the host still works.
206206

‎docs/specs/dor-tool.md‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Source of truth: `useToolRunEnd` in `lib/src/components/wall/use-tool-run-end.ts
131131

132132
A reap stops an idle Tool's Session, shell included; rehydrate starts it again in the same Surface, which keeps its leaf, Tool metadata, scrollback, rename, and alert, and reads as an exited Session meanwhile.
133133

134-
**Must reap only a Tool whose running designated command declared itself safe to stop** with `dehydrate: true` in the run's latest `serve` ([OSC 367](#osc-367)): its args alone restart it, and a payload only adds fidelity. Clean or unreported state never qualifies (rationale). **Never reap a Tool** that reports unsaved changes, announced `persist: "never"`, is a marked preview slot or popped out, awaits approval, has an auxiliary helper, is a pending kill (`docs/specs/reopen.md`), or belongs to a closing or transferring Workspace (rationale).
134+
**Must reap only a Tool whose running designated command declared itself safe to stop** with `dehydrate: true` in the run's latest `serve` ([OSC 367](#osc-367)): its args alone restart it, and a payload only adds fidelity. Clean or unreported state never qualifies (rationale). **Never reap a Tool** that reports unsaved changes, announced `persist: "never"`, is a marked preview slot or popped out, awaits approval or document navigation, has an auxiliary helper, is a pending kill (`docs/specs/reopen.md`), or belongs to a closing or transferring Workspace (rationale).
135135

136136
**Must reap only after 30 minutes continuously out of sight and without PTY output** — Doored, in an inactive Workspace, or in a hidden webview (rationale): never on the minimize or switch itself, and never for memory pressure. The stop:
137137

@@ -233,6 +233,18 @@ Source of truth: `resolveOpenTool` in `lib/src/host/tool-open.ts`.
233233

234234
Source of truth: `runFilePicker` in `dor/src/commands/open-picker.ts`; `listOpenHandlers` in `lib/src/host/tool-open.ts`.
235235

236+
## Document navigation
237+
238+
- **Must resolve a Tool's document-link request through ordinary open dispatch and trust checks**, relative to the current canonical document's directory. The request chooses the current Surface or another Surface; it never requests a preview or a fresh instance. Existing matching Tools are focused through ordinary reuse without changing either Surface's history.
239+
- **Must keep Back/Forward history on the navigating Surface**, covering every file/folder handler, including user Tools. Traversal reopens the recorded canonical target with its selected handler and lookup directory in that Surface even when another Surface shows the target; a new in-place visit after Back drops the forward branch. Pin a preview on a committed in-place file visit.
240+
- **Must obtain Save / Discard / Cancel before replacing a dirty document**, using [Closing unsaved Tools](#closing-unsaved-tools). A same-document anchor or reuse of another Surface leaves the draft alone. Never cache drafts or undo state in history.
241+
- **Must validate the destination before interrupting the current Tool**, and keep the current location/history on lookup failure or cancelled consent. Refuse overlapping navigation from one Surface and stale requests whose source run changed. When the interrupt or the new run fails, keep or restore the source's run, never typing over user input. Report navigation failures on the source, without opening the preview error viewer.
242+
- **Must retain history through minimize, renderer changes, and live Workspace transfer, but never in durable saves**. Only the current document restarts after cold restore. Ending designation, Break, or closing discards history. Refuse Workspace transfer during navigation or pending reading-position restoration.
243+
- **Must release pending reading-position restoration on timeout**, even when the destination never serves a page or never answers; renderer rebinding must not renew the deadline.
244+
- **Must capture and restore Markdown reading position during history traversal**, including source/rich-text mode; an anchor visit selects rich text. Capture is best effort: a page that cannot answer still navigates. Reading-state replies are matched as save replies are, and never acknowledge a save or update dirty state. Automation renderers use a fixed host-owned read/apply operation with validated reading-state data, never page-supplied code.
245+
246+
Source of truth: `surface.tool` in `lib/src/components/wall/use-dor-control.ts`; `toolView` in `lib/src/components/wall/tool-navigation.ts`; `ToolPanel` in `lib/src/components/wall/ToolPanel.tsx`; `dor-tools-lib/src/protocol.ts`.
247+
236248
## Preview slot
237249

238250
**Must keep at most one preview slot per Workspace**: a Tool Surface whose params carry `toolPreview`, marked in its Pane header and Door (`docs/specs/layout.md` → Pane header) and tagged `[preview]` by `dor list`. Creating a slot pins every other marked Surface, a closing one included, so a refused close never leaves two. **Must resolve a preview through the user's ordered `open` list**, as `dor open` does, never through a faster built-in-only renderer (rationale). A Tool browsing files selects with `dor open --preview <file>` and activates with `dor open <file>`, or sends either as an OSC 367 `open` ([OSC 367](#osc-367)). Same Tool means same scope, name and run; a run a superseded retarget interrupted is not running.
@@ -342,9 +354,9 @@ Source of truth: `toolTakesOverCaller` in `lib/src/components/wall/tool-takeover
342354
- **Must reconstruct announcements, state, and resets from raw replay without emitting replies or acting on an `open`**, preserving transferred announcements when since-mark replay has no command start. Ordinary terminal announcements stay inert.
343355
- **Must read a `serve` carrying only `dehydrate` or `persist` as an announcement**, which selects no port. `dehydrate: true` and `persist: "never"` govern [Reaping](#reaping) alone; `persist: "respawn"` is the default, and neither changes cold restore.
344356
- Reserved: **Must retain `name` as an inert parsed field**, serving the announced-name item under [Future](#future).
345-
- **Must act on `open` only from live output of a Tool Session's launched run**, as `dor open` from that Session (`--preview` when `preview` is true), resolved from the Session's local CWD, else the target's directory. The path is absolute, with no control character; the Tool gets no answer.
357+
- **Must act on `open` only from live output of a Tool Session's launched run**, using [Document navigation](#document-navigation) when a disposition is present, otherwise as `dor open` from that Session (`--preview` when `preview` is true), resolved from the Session's local CWD, else the target's directory. The path is absolute, with no control character; the Tool gets no answer.
346358
- **Must drop every `open` at the PTY owner's parse outside the launched run**: from the first command start after a write the host marked `launch`, if the shell reports that write's line, to the next start, finish, or prompt. Any start ends the launch. Output never begins a run; a reported line only narrows, never vouches (rationale). **Must mark `launch` only command lines the host types to run**, never user input, `dor send`, or a remote Client's write.
347-
- **Must show a failed `open` in the preview slot**: a preview whose lookup fails runs the built-in error viewer there instead (`docs/specs/dor-tools-builtin.md` → Error viewer), and a failed activate is sent again as a preview unless a newer `open` from that Session followed it.
359+
- **Must show a failed legacy `open` without a disposition in the preview slot**: a preview whose lookup fails runs the built-in error viewer there instead (`docs/specs/dor-tools-builtin.md` → Error viewer), and a failed activate is sent again as a preview unless a newer `open` from that Session followed it.
348360
- **Never assign an OSC 367 verb beyond `serve`, `state`, `open`, and `dehydrate`**; existing title/progress protocols keep those roles.
349361

350362
Source of truth: `dor-tools-lib/src/osc.ts`; `TerminalProtocolParser` in `lib/src/lib/terminal-protocol.ts`; `ToolLaunchLatch` in `lib/src/lib/tool-launch-latch.ts`; `applyLiveToolEvents` in `lib/src/lib/tool-events.ts`.

‎docs/specs/dor-tools-builtin.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ Source of truth: `saveText` / `StorePath` in `dor-tools-builtin/src/editor-route
5555

5656
Source of truth: `dor-tools-builtin/viewer/markdown.tsx`; `safeCreateDOM` in `dor-tools-builtin/viewer/markdown-safety.ts`; `editorRoutes` in `dor-tools-builtin/src/editor-routes.ts`; `dor-tools-builtin/src/markdown-images.ts`. Tests: `dor-tools-builtin/test/markdown-editor.test.mjs`.
5757

58+
### Relative links
59+
60+
- **Must follow a relative link on plain click**, and offer Open in another surface and Edit link on its context menu. Cmd/Ctrl-click chooses another Surface. Dispatch follows `docs/specs/dor-tool.md` → Document navigation. (rationale)
61+
- **Must treat only a scheme-less relative reference as a document link**, resolved from the opened canonical file's directory with parent references allowed and its query ignored; HTTP(S)/mail links keep their existing handling.
62+
- **Must send document opens to the Tool process**, which emits OSC 367 (`docs/specs/security-local.md` → Local-file viewer); **never navigate relative to the capability URL**. The page receives only sent/error, with host failures shown by the Surface.
63+
- **Must resolve a fragment to the rendered heading whose case-folded, punctuation-stripped, duplicate-suffixed id it names**, never writing ids into saved Markdown. Same-document fragments scroll without reloading or prompting; a missing heading reports an error while retaining the document. Back/Forward reading-state semantics belong to `docs/specs/dor-tool.md` → Document navigation.
64+
65+
Source of truth: `dor-tools-builtin/viewer/markdown.tsx`; `markdownLink` in `dor-tools-builtin/src/markdown-links.ts`; `runFileViewer` in `dor-tools-builtin/src/file-viewer.ts`.
66+
5867
## Image editor
5968

6069
`builtin:file` edits PNG and JPEG, alone or over a [Markdown document](#markdown-editor).

‎docs/specs/dor-tools-builtin.rationale.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,8 @@ Windows tests in 2026-10 (Node 22.22.3) showed an atomic Node rename failing wit
3939
Microsoft documents ReplaceFile failures that have already moved the original or replacement file (1176/1177), as well as a native commit preceding a lost helper reply. Deleting the draft or backup after an unconfirmed replacement can therefore delete the only surviving bytes. Confirmed saves and failures before replacement still delete both.
4040

4141
The Windows draft inherits its directory's ACL, so a protected document in a more broadly readable directory exposes its unsaved draft to that directory's readers until replacement merges the document's ACL. An owner-only stage was tried and dropped (2026-10): creating it needed a PowerShell `Add-Type` P/Invoke to `CreateDirectoryW`, which compiled C# and spawned PowerShell a second time on every save, failed under Constrained Language Mode, and protected draft bytes only in that rare configuration.
42+
43+
44+
## Relative links
45+
46+
Lexical 0.48 formats a bare Markdown filename such as `b.md` as `https://b.md` in the live anchor, while retaining the original URL in its node. The viewer preserves validated document references in that rendering step so ordinary clicks and the link dialog agree (innerdogfood, 2026-10).

‎docs/specs/dor-tools-lib.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Dor Tools Library
22

33
> - See `docs/specs/glossary.md` for Session / Pane vocabulary.
4-
> - Owns `dor-tools-lib`, the MIT package that Tools and their hosts share for the Tool integration protocol: its license and dependency boundary, entry points, and consumers. The wire rules it implements belong to `docs/specs/dor-tool.md` → OSC 367, Unsaved changes, and Closing unsaved Tools.
4+
> - Owns `dor-tools-lib`, the MIT package that Tools and their hosts share for the Tool integration protocol: its license and dependency boundary, entry points, and consumers. The wire rules it implements belong to `docs/specs/dor-tool.md` → OSC 367, Unsaved changes, Closing unsaved Tools, and Document navigation.
55
66
## Package
77

@@ -13,7 +13,7 @@
1313
|---|---|---|
1414
| `osc` | an encoder per verb, throwing on a value the host would ignore; a reader for its own dehydrate payload | a parser per verb |
1515
| `protocol` | reads host messages | reads frame messages |
16-
| `frame` | the save-channel client | — |
16+
| `frame` | the save/reading-state channel client | — |
1717

1818
- **The host (`lib`) must reach the package through source aliases**, beside every `dor-tools-builtin/*` alias, since no host build compiles the package first.
1919
- `dor-tools-builtin` speaks the protocol through it (`docs/specs/dor-tools-builtin.md` → Packaging).

‎docs/specs/layout.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Pane drag scopes: `docs/specs/tiling-engine.md` → "Hierarchical drag and drop"
2929

3030
**Must hold the slot's header while a switch holds its ghost** (`docs/specs/dor-tool.md` → Switching the slot): the ghost's face and Display glyph stay until the new view is ready. A terminal face's label keeps its name until the retarget, then shows the Tool's name (`docs/specs/dor-tool.md` → Naming); derivation resumes once ready. A serving Tool's name, read from its params, needs no hold (rationale).
3131

32-
**Never give a Tool navigation, an address, or a dev-server chip** (rationale).
32+
**Must offer document Back/Forward on both Tool faces when the Surface has history**, in Terminal Context at narrow widths (`docs/specs/dor-tool.md` → Document navigation). **Never give a Tool an address or a dev-server chip** (rationale).
3333

3434
Every Pane header ends with the pane-action group: zoom/unzoom, minimize, kill.
3535

‎docs/specs/layout.rationale.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
The keep is judged by the burst's first press, so a double-click inside a rename opened earlier still selects a word. A preview's label opens no rename because the double-click's first click would open the field under the second.
88

9-
Tools carry no navigation, address, or dev-server chip (Ned, 2026-09-29): a Tool is named for what it is, and a page it serves is not somewhere to navigate from, so that chrome stays with plain browser Surfaces.
9+
Tools remain named for what they are, without exposing a capability URL as an address. Document Back/Forward extends their file workflow; it is separate from a browser renderer's own URL history (2026-10).
1010

1111
A serving preview's header changed size on every switch while its name was the dev-server chip (standalone, 2026-09-29): the chip names a pane only once the Window's port scan resolves the page's loopback port (600 ms of debounce, then a scan at idle), which landed after the switch's hold had ended, so the address widened to the whole `localhost:<port>/<path>` and shrank back. A name from params changes once, with the retarget, so nothing needs holding.
1212

0 commit comments

Comments
 (0)