opendoor-plugin 0.4.0 → 0.5.0
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/README.md +210 -363
- package/dist/browser/pi-web-plugin.js +203 -24
- package/dist/package.json +2 -2
- package/dist/server-plugin.js +35 -13
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,132 +1,72 @@
|
|
|
1
1
|
# opendoor — a Doorstop requirements-management plugin for PI WEB
|
|
2
2
|
|
|
3
|
-
`opendoor` is a
|
|
4
|
-
[Doorstop](https://github.com/doorstop-dev/doorstop) requirements stored in
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
falls back to the workspace terminal. The paired backend also recovers the
|
|
13
|
-
reviewed version of an item file from git history, so the item detail pane
|
|
14
|
-
can show a **Changes since review** diff — and Review can optionally record
|
|
15
|
-
a pathspec-limited review commit (`commitAfterReview`).
|
|
3
|
+
`opendoor` is a PI WEB plugin that surfaces
|
|
4
|
+
[Doorstop](https://github.com/doorstop-dev/doorstop) requirements stored in the
|
|
5
|
+
workspace in a **Requirements** workspace tab with
|
|
6
|
+
the document tree, per-item state (reviewed/unreviewed changes, suspect links),
|
|
7
|
+
validation findings and `doorstop` actions, plus agent prompts
|
|
8
|
+
that insert requirement context into the session.
|
|
9
|
+
The paired backend also recovers an item file's reviewed version from git for
|
|
10
|
+
the **Changes since review** diff, and Review can opt in to a review commit
|
|
11
|
+
(`commitAfterReview`).
|
|
16
12
|
|
|
17
13
|
Full feature spec: [`docs/feature-doorstop-plugin.md`](docs/feature-doorstop-plugin.md)
|
|
18
14
|
(§5 architecture, §6 requirements state model, §7–§8 UI/actions).
|
|
19
15
|
|
|
20
16
|
## Status
|
|
21
17
|
|
|
22
|
-
Requires **pi-web 1.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
collapsible **Changes since review** diff), `item-action-palette`
|
|
53
|
-
(Review / Clear suspect links / Edit / Link / Unlink / Ask-agent menu,
|
|
54
|
-
sized to the host prompt footer), and `status-bar` (status badge,
|
|
55
|
-
duration, Dismiss button, and the auto-expanding stdout/stderr of the
|
|
56
|
-
latest CLI run, plus the review-commit outcome line when the review
|
|
57
|
-
requested a commit);
|
|
58
|
-
- **actions** `view.doorstop` (mod+7) and `workspace.refresh-doorstop`
|
|
59
|
-
(mod+shift+d);
|
|
60
|
-
- **label** `doorstop-status` — the informational requirement-health counts
|
|
61
|
-
(async-cache idiom, feature spec §7.4);
|
|
62
|
-
- **session-row label** `doorstop.review` — a compact indicator on session
|
|
63
|
-
rows carrying public `opendoor` metadata; clicking it reopens the
|
|
64
|
-
Requirements tab on the row's machine/workspace (works for unselected and
|
|
65
|
-
archived rows, no live Pi connection needed). See **Session metadata and
|
|
66
|
-
session-list row labels** below.
|
|
67
|
-
|
|
68
|
-
- **Server entry** (`src/server-plugin.ts`, apiVersion 3, default export `PiWebServerPlugin`)
|
|
69
|
-
contributes a workspace provider that claims only projects whose root
|
|
70
|
-
contains a `.doorstop.yml` (cheap `fs.access` probe — everything else stays
|
|
71
|
-
with the bundled Git provider) and serves a six-operation backend:
|
|
72
|
-
`doorstop.run` runs one Doorstop CLI invocation — a review request carrying
|
|
73
|
-
the opt-in `commit: true` flag is followed by a pathspec-limited git commit
|
|
74
|
-
of the item file (`doorstop: review <uid>`) — the read-only
|
|
75
|
-
`doorstop.item-baseline` recovers the reviewed version of an item file from
|
|
76
|
-
git history, and the four git actions `doorstop.git-status`,
|
|
77
|
-
`doorstop.git-stage`, `doorstop.git-unstage`, and `doorstop.git-commit`
|
|
78
|
-
power the panel's git strip and stage/unstage/commit controls (see **Git
|
|
79
|
-
actions** below). `src/doorstop-backend.ts` validates each request, builds argv
|
|
80
|
-
server-side (the browser never sends shell strings), and runs through the
|
|
81
|
-
host's `execFile()` helper with `cwd` = the workspace path, timeouts
|
|
82
|
-
clamped to 8.5 s (the review+git pipeline shares one deadline budget under
|
|
83
|
-
the host's 10 s callback bound), and per-workspace run serialization. The
|
|
84
|
-
server entry permanently declares no `requires` and no `start()`: the
|
|
85
|
-
capability-driven `doorstop.ask-agent` dispatch it once carried was
|
|
86
|
-
unreachable by construction on a workspace-provider-contributing entry
|
|
87
|
-
(PI WEB rejects the provider-dependency capability cycle) and has been
|
|
88
|
-
retired. The review dispatch now runs in the Pi extension below.
|
|
18
|
+
Requires **pi-web 1.202611.1 or newer** (browser and server plugin API v5).
|
|
19
|
+
|
|
20
|
+
One package (`opendoor-plugin`) ships the PI WEB plugin (both entries below)
|
|
21
|
+
and the Pi extension (`/opendoor-review`).
|
|
22
|
+
|
|
23
|
+
- **Browser entry** (`src/browser/pi-web-plugin.ts`, apiVersion 5, name
|
|
24
|
+
`Opendoor`) delegates to `createOpendoorBrowserContributions`
|
|
25
|
+
(`src/browser/doorstop-contributions.ts`): the **panel** `workspace.doorstop`
|
|
26
|
+
(title **Requirements**, order 5 — below the Files panel's order 10 so doorstop
|
|
27
|
+
item links reach its `fileOpenQuery` first, at the cost of the Requirements tab
|
|
28
|
+
preceding Files in the tab order; controller registry in
|
|
29
|
+
`src/browser/doorstop-panel.ts`, sections in **Using the panel**),
|
|
30
|
+
the **actions** `view.doorstop` (mod+7) and `workspace.refresh-doorstop`
|
|
31
|
+
(mod+shift+d), the **label** `doorstop-status` (requirement-health counts,
|
|
32
|
+
feature spec §7.4), and the **session-row label** `doorstop.review` (reopens the
|
|
33
|
+
Requirements tab from a row carrying public `opendoor` metadata, archived rows
|
|
34
|
+
included — feature spec, **Session metadata and session-list row labels**).
|
|
35
|
+
- **Server entry** (`src/server-plugin.ts`, apiVersion 5, default export
|
|
36
|
+
`PiWebServerPlugin`) contributes a workspace provider for projects whose root
|
|
37
|
+
has a `.doorstop.yml` (cheap `fs.access` probe; everything else stays with the
|
|
38
|
+
bundled Git provider) and serves six operations: `doorstop.run` (one CLI
|
|
39
|
+
invocation; a review carrying the opt-in `commit: true` flag is followed by a
|
|
40
|
+
pathspec-limited `doorstop: review <uid>` commit), the read-only
|
|
41
|
+
`doorstop.item-baseline` (the reviewed version, from git history), and
|
|
42
|
+
`doorstop.git-status` / `-stage` / `-unstage` / `-commit` (see **Git
|
|
43
|
+
actions**). `src/doorstop-backend.ts` validates each request, builds argv
|
|
44
|
+
server-side (never a browser shell string) and runs it via `execFile()`; see
|
|
45
|
+
its header for timeouts and per-workspace serialization. The entry declares no
|
|
46
|
+
`requires` and no `start()`: the retired `doorstop.ask-agent` dispatch could
|
|
47
|
+
not live there (provider-cycle rule) — see `src/server-plugin.ts`.
|
|
89
48
|
- **Pi extension** (`src/pi-extension/opendoor-review.ts`, bundled to
|
|
90
|
-
`dist/pi-extension/index.js
|
|
91
|
-
`/opendoor-review <UID> [instruction…]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
request via `context.peer.request("doorstop.run", …)` (the review-commit
|
|
104
|
-
flag rides along when the setting is enabled; the "Changes since review"
|
|
105
|
-
section fetches via `context.peer.request("doorstop.item-baseline", …)`)
|
|
106
|
-
and the captured output appears in the panel's status bar (auto-expanded);
|
|
107
|
-
the panel auto-rescans on completion. When the backend is absent (unpaired
|
|
108
|
-
install, or a workspace the provider does not own), the action falls back to
|
|
109
|
-
`terminal.runCommand()` with the same shell command, so output stays visible
|
|
110
|
-
and auditable in the terminal.
|
|
111
|
-
|
|
112
|
-
The shared types + function-signature contracts live in
|
|
113
|
-
`src/doorstop-contract.ts`; the two backend operation contracts
|
|
114
|
-
(`doorstop.run` and `doorstop.item-baseline`) shared by both halves, plus
|
|
115
|
-
the four git-action contracts (`doorstop.git-status`, `doorstop.git-stage`,
|
|
116
|
-
`doorstop.git-unstage`, `doorstop.git-commit`), live in
|
|
117
|
-
`src/doorstop-backend-contract.ts`. Later modules import these and must not
|
|
118
|
-
redefine them.
|
|
49
|
+
`dist/pi-extension/index.js`, declared by `pi.extensions`) registers
|
|
50
|
+
`/opendoor-review <UID> [instruction…]`: it publishes the public `opendoor`
|
|
51
|
+
metadata (`{ schema: 1, op: "review", uids }`) and asks PI WEB's per-session
|
|
52
|
+
dispatch bridge to start an independent conversation in the same project (the
|
|
53
|
+
`spawn_session` rules, model/thinking inherited), reporting every outcome in
|
|
54
|
+
the initiating conversation — the pathway **Start review session** drives.
|
|
55
|
+
|
|
56
|
+
Dispatch (`src/browser/doorstop-panel-element.ts`) uses
|
|
57
|
+
`context.peer.request("doorstop.run", …)` (the review-commit flag rides along
|
|
58
|
+
when enabled; **Changes since review** uses `doorstop.item-baseline`) with the
|
|
59
|
+
output in the auto-expanded status bar, or falls back to `terminal.runCommand()`
|
|
60
|
+
with the same shell command when unpaired. Shared types and operation contracts
|
|
61
|
+
live in `src/doorstop-contract.ts` and `src/doorstop-backend-contract.ts`.
|
|
119
62
|
|
|
120
63
|
## Workspace settings (`.pi-web/opendoor.json`)
|
|
121
64
|
|
|
122
|
-
Optional per-workspace configuration, read
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
bundled workspace-tasks plugin. The *server* entry's host-scoped knobs
|
|
128
|
-
(CLI binary path, exec timeout) are configured through
|
|
129
|
-
`plugins.opendoor.settings` instead — see **Host settings** below:
|
|
65
|
+
Optional per-workspace configuration, read on every plugin load (missing file →
|
|
66
|
+
defaults; malformed or invalid values → warning diagnostics + defaults). pi-web's
|
|
67
|
+
`plugins.<id>.settings` is host-side and reaches only server entries, so this
|
|
68
|
+
file plays the role `.pi-web/tasks.json` plays for the bundled workspace-tasks
|
|
69
|
+
plugin; server-entry knobs are in **Host settings** below:
|
|
130
70
|
|
|
131
71
|
```json
|
|
132
72
|
{
|
|
@@ -138,228 +78,178 @@ bundled workspace-tasks plugin. The *server* entry's host-scoped knobs
|
|
|
138
78
|
}
|
|
139
79
|
```
|
|
140
80
|
|
|
141
|
-
- `publish.target` — path passed to `doorstop publish all
|
|
81
|
+
- `publish.target` — path passed to `doorstop publish all`; must be a safe
|
|
142
82
|
workspace-relative path (no `..`, no absolute, no drive prefix, no leading
|
|
143
|
-
`-`).
|
|
144
|
-
|
|
145
|
-
|
|
83
|
+
`-`). Shell-quoted at the terminal-command boundary when it contains characters
|
|
84
|
+
outside the inert `\w./-` alphabet; on the backend path it is an argv element,
|
|
85
|
+
never a shell string.
|
|
146
86
|
- `discovery.excludedDirectories` — additional directory names the
|
|
147
87
|
`.doorstop.yml` walk never enters (merged with the built-in
|
|
148
88
|
`.git`/`node_modules` skips, max 16 names).
|
|
149
|
-
- `showAdditionalAttribute` —
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
attribute
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
with
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
Committing user content is strictly opt-in, and the status bar
|
|
167
|
-
narrates every commit outcome (`commit: <sha>` / `clean` (already
|
|
168
|
-
committed) / `skipped` (not a git repository | item file not found |
|
|
169
|
-
review failed | deadline) / `failed` — `<stderr>`). Under the default,
|
|
170
|
-
Review behaves exactly as before.
|
|
89
|
+
- `showAdditionalAttribute` — attribute NAMES rendered in every item-list row as
|
|
90
|
+
`key: value` (values jsonish-quoted, joined with ` · `, in configured order);
|
|
91
|
+
any extended attribute key works, an attribute an item lacks is skipped, and no
|
|
92
|
+
unconfigured attribute ever appears. The modeled `text` is special-cased: the
|
|
93
|
+
body renders raw and unquoted on an indented second line below the row, clamped
|
|
94
|
+
to two lines with an ellipsis (the full body lives in the detail pane), skipped
|
|
95
|
+
when empty. Default `[]` leaves today's rows unchanged. Invalid entries are
|
|
96
|
+
dropped with a warning each, duplicates deduplicated, capped at 16 names;
|
|
97
|
+
unknown keys simply match nothing.
|
|
98
|
+
- `commitAfterReview` — opt-in boolean (default `false`): on a paired workspace
|
|
99
|
+
the **Review** action's `doorstop review <uid>` is followed by a
|
|
100
|
+
pathspec-limited commit of the item file with the conforming message
|
|
101
|
+
`doorstop: review <uid>` (git `add` then `commit -m … -- <path>`; other
|
|
102
|
+
staged/unstaged work is untouched). Committing user content is strictly opt-in;
|
|
103
|
+
the status bar narrates every outcome (`commit: <sha>` / `clean` (already
|
|
104
|
+
committed) / `skipped` (not a git repository | item file not found | review
|
|
105
|
+
failed | deadline) / `failed` — `<stderr>`); Review is otherwise unchanged.
|
|
171
106
|
|
|
172
107
|
## Host settings (`plugins.opendoor.settings`)
|
|
173
108
|
|
|
174
|
-
Optional, server-side only (
|
|
175
|
-
|
|
109
|
+
Optional, server-side only (needs the paired install and a sessiond restart);
|
|
110
|
+
wrong types fall back to defaults:
|
|
176
111
|
|
|
177
112
|
```json
|
|
178
113
|
{ "doorstopPath": "/opt/venv/bin/doorstop", "gitPath": "git", "timeoutMs": 8000 }
|
|
179
114
|
```
|
|
180
115
|
|
|
181
|
-
- `doorstopPath` — the Doorstop CLI binary
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
- `gitPath` — the git binary
|
|
185
|
-
`doorstop.item-baseline` fetch
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
bounds every git exec of the review→commit pipeline, the baseline fetch,
|
|
193
|
-
and the git actions, which share one deadline budget per callback.
|
|
116
|
+
- `doorstopPath` — the Doorstop CLI binary, default `"doorstop"`, resolved on the
|
|
117
|
+
**sessiond host PATH** (the daemon environment, not your login shell — set it
|
|
118
|
+
when the CLI lives in a venv or is off the service PATH).
|
|
119
|
+
- `gitPath` — the git binary for the review→commit pipeline, the
|
|
120
|
+
`doorstop.item-baseline` fetch and the four git actions. Default `"git"` on the
|
|
121
|
+
**sessiond host PATH** — the same escape hatch as `doorstopPath`.
|
|
122
|
+
- `timeoutMs` — per-run exec timeout in ms. May SHORTEN the 8 500 ms default but
|
|
123
|
+
never exceed it (the host bounds every provider callback at 10 s, so a longer run
|
|
124
|
+
would be killed by pi-web, not by the plugin). It also bounds every git exec of
|
|
125
|
+
the review→commit pipeline, the `doorstop.item-baseline` fetch and the git
|
|
126
|
+
actions: one deadline budget per callback.
|
|
194
127
|
|
|
195
128
|
## Using the panel
|
|
196
129
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
- **
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
- **Git
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
`DU`, `AU`, `UD`, `UA`) are excluded — a `git reset` on an unmerged path
|
|
295
|
-
would silently resolve the conflict in the index. On unpaired installs no
|
|
296
|
-
git control is rendered at all; while a run is in flight no minus appears
|
|
297
|
-
and the palette buttons are disabled (the per-row affordance is also
|
|
298
|
-
suppressed when the status file list is truncated, so stale path data is
|
|
299
|
-
never acted on).
|
|
300
|
-
- **Git commit** — a single-line message input plus a **Commit** button,
|
|
301
|
-
served by the `doorstop.git-commit` operation. The commit records **the
|
|
302
|
-
staged index** (`git commit -m <message>` with no pathspec and no
|
|
303
|
-
add) — exactly like the git CLI. Consequences, all intended: unstaged
|
|
304
|
-
WIP is never swept into a commit; files you staged yourself (via the
|
|
305
|
-
git CLI or any other tool) are committed too; the strip's `staged`
|
|
306
|
-
count tells you what a commit will record before you click. Empty or
|
|
307
|
-
whitespace-only messages are rejected client-side (git with an empty
|
|
308
|
-
`-m` would open an editor and hang the exec). Committing with nothing
|
|
309
|
-
staged reports `clean — nothing staged`. Outcomes are narrated in the
|
|
310
|
-
status bar (`staged <n> paths` / `committed <sha>` / `clean — nothing
|
|
311
|
-
to stage` / `clean — nothing staged` / `skipped` / `failed — <stderr
|
|
312
|
-
excerpt>`); a failing pre-commit hook surfaces its stderr in the
|
|
313
|
-
expanded status bar body.
|
|
314
|
-
- **Ask the agent** menu: inserts ready-made prompts (explain, fix suspect
|
|
315
|
-
links, draft child requirement, review readiness) into the session prompt
|
|
316
|
-
editor using the item's paths and state (feature spec §7.3). **Start
|
|
317
|
-
review session** composes `/opendoor-review <UID>` plus the item context
|
|
318
|
-
into that same editor and needs no paired backend: sending it lets the
|
|
319
|
-
opendoor Pi extension start an independent review conversation whose list
|
|
320
|
-
row carries a **`firstUid +n` label** that reopens the Requirements tab on
|
|
321
|
-
the row's workspace — including for unselected and archived rows and with
|
|
322
|
-
no live Pi connection — landing on the item detail pane. After the insert
|
|
323
|
-
the panel's status bar shows a quiet `ok` **review dispatch** run row.
|
|
324
|
-
Without the extension installed the inserted text is still a
|
|
325
|
-
self-sufficient review instruction, so the review happens in the current
|
|
326
|
-
conversation; a refused dispatch is reported in the chat by the extension.
|
|
327
|
-
See **Session metadata and session-list row labels** in the feature spec.
|
|
328
|
-
- Fingerprints and the requirement state are recomputed in the browser from
|
|
329
|
-
the YAML files; everything mutating goes through the visible `doorstop`
|
|
330
|
-
CLI — paired, headlessly via the server entry (output captured in the
|
|
331
|
-
panel), or in a workspace terminal when unpaired — plus, opt-in, the
|
|
332
|
-
narrated pathspec-limited review commit (`commitAfterReview`) — so all
|
|
333
|
-
effects stay auditable in git.
|
|
130
|
+
Five vertical sections, top to bottom: `project-actions` (project heading — root
|
|
131
|
+
path, basename shown, full path on hover — the Items/Findings toggle, Refresh, Run
|
|
132
|
+
validation, Publish HTML, and on paired installs the Git strip, Stage all and
|
|
133
|
+
Commit), `item-list` (filters row above rows showing level, UID, excerpt,
|
|
134
|
+
`showAdditionalAttribute` values and state chips), `item-details` (Markdown item
|
|
135
|
+
text, links in/out, references, extended attributes, the Changes diff),
|
|
136
|
+
`item-action-palette` (Review, Clear suspect links, Link, Ask agent — spanning the
|
|
137
|
+
panel at chat-prompt-footer height, with the muted "Select an item…" placeholder,
|
|
138
|
+
hidden in the Findings view) and `status-bar` (see below).
|
|
139
|
+
|
|
140
|
+
- **Items / Findings toggle**: Findings lists plugin-computed findings and
|
|
141
|
+
diagnostics (error > warning > info), and a finding's UID navigates back to the
|
|
142
|
+
item. They are informational — **Run validation** gives the authoritative
|
|
143
|
+
`doorstop` output (paired: status bar; unpaired: terminal).
|
|
144
|
+
- **Status bar** (sized like the center-panel one): after a paired action it shows
|
|
145
|
+
status (ok / failed / killed / error) and duration, and the panel re-scans
|
|
146
|
+
automatically. Captured stdout/stderr *auto-expands* whenever there is something
|
|
147
|
+
to show (output, an error, a review-commit line); a run with nothing to display
|
|
148
|
+
stays collapsed. **Dismiss** clears the run and collapses the bar; the next run
|
|
149
|
+
with output re-expands it. Killed runs keep partial output (prefer the terminal
|
|
150
|
+
for long publishes). With `commitAfterReview` on, the bar adds the `commit: …`
|
|
151
|
+
line from **Workspace settings**; a failed or skipped commit never changes the
|
|
152
|
+
run's ok/failed badge.
|
|
153
|
+
- **State chips** per item (feature spec §6): inactive, non-normative, reviewed /
|
|
154
|
+
unreviewed changes, suspect links, no child links, no links, unknown link,
|
|
155
|
+
missing reference.
|
|
156
|
+
- **Changes since review** (item detail pane, paired installs only): an item with
|
|
157
|
+
unreviewed changes and a stored `reviewed` fingerprint gets a collapsed section;
|
|
158
|
+
expanding it fetches the reviewed item file from git history via
|
|
159
|
+
`doorstop.item-baseline`, stamp-matches it against the fingerprint and renders a
|
|
160
|
+
line diff plus chips for `ref`/`references`/link-UID/extended-attribute changes.
|
|
161
|
+
With no recoverable baseline it says so ("no git history", "could not locate the
|
|
162
|
+
reviewed version — history may have been rewritten"). The `doorstop: review
|
|
163
|
+
<uid>` message is the historical anchor feature spec §12's deferred browser-side
|
|
164
|
+
git parsing could not give; recovery runs server-side.
|
|
165
|
+
- **Item actions** (item-action-palette): Review (`doorstop review UID`, plus the
|
|
166
|
+
review commit when `commitAfterReview` is on), Clear suspect links (`doorstop
|
|
167
|
+
clear UID [parents…]`), Edit (`doorstop edit UID`), Link/Unlink via inline UID
|
|
168
|
+
input (Doorstop UID grammar enforced; shell metacharacters rejected), and on
|
|
169
|
+
paired installs Stage / Unstage for the item's own file (`doorstop.git-stage` /
|
|
170
|
+
`doorstop.git-unstage`; see **Git actions**). Each disables independently: Stage
|
|
171
|
+
without unstaged (`Y`) changes, Unstage with nothing staged (`X`), both mid-run.
|
|
172
|
+
- **Git actions** (paired only; otherwise every git control is hidden and you
|
|
173
|
+
commit manually):
|
|
174
|
+
- **Git status strip** — compact readout above the item list:
|
|
175
|
+
`⎇ <branch> · <n> staged · <n> dirty · ↑<ahead> ↓<behind>` (zero/absent parts
|
|
176
|
+
omitted, `no git` outside a repository, a retry link on fetch errors).
|
|
177
|
+
Refreshes on panel load, after every run (a stage, unstage or commit
|
|
178
|
+
invalidates the cache) and on click; outside a repository the other git
|
|
179
|
+
controls report `skipped`.
|
|
180
|
+
- **Stage all** — stages every Doorstop-managed file (root `.doorstop.yml`, each
|
|
181
|
+
document config, each item file) via `doorstop.git-stage`. Idempotent: a
|
|
182
|
+
second click over an already-staged set reports `clean — nothing to stage`
|
|
183
|
+
instead of a git error (staged deletions are skipped, not fatal). Disabled
|
|
184
|
+
during a run or with no documents in the loaded index, and only as fresh as
|
|
185
|
+
that index — files created after the last **Refresh** are missed until you
|
|
186
|
+
refresh and stage again.
|
|
187
|
+
- **Unstage (per item)** — a staged item file swaps its row's plus for a
|
|
188
|
+
**minus** and enables **Unstage** in the palette, both via
|
|
189
|
+
`doorstop.git-unstage` (`git reset -q -- :(literal)<path>` server-side). A file
|
|
190
|
+
both staged and further modified (`MM`) still shows the minus; the palette then
|
|
191
|
+
offers Stage and Unstage as two independently-disabled buttons. Unstaging
|
|
192
|
+
restores the index entry but never touches the working tree (a staged addition
|
|
193
|
+
returns to `??`, a staged deletion to an unstaged deletion); it is idempotent
|
|
194
|
+
(`clean — nothing to unstage`) and excludes unmerged paths (`UU`, `AA`, `DD`,
|
|
195
|
+
`DU`, `AU`, `UD`, `UA`), where `git reset` would silently resolve the conflict.
|
|
196
|
+
During a run no minus appears and the palette buttons are disabled; the per-row
|
|
197
|
+
affordance is also suppressed when the status file list is truncated, so stale
|
|
198
|
+
path data is never acted on.
|
|
199
|
+
- **Git commit** — a message input plus **Commit**, served by
|
|
200
|
+
`doorstop.git-commit`. It records **the staged index** (`git commit -m
|
|
201
|
+
<message>`, no pathspec, no add), exactly like the git CLI: unstaged WIP is
|
|
202
|
+
never swept in, files you staged yourself are included, and the strip's
|
|
203
|
+
`staged` count shows what a commit will record. Empty or whitespace-only
|
|
204
|
+
messages are rejected client-side (git would open an editor and hang the exec);
|
|
205
|
+
nothing staged reports `clean — nothing staged`. Outcomes are narrated in the
|
|
206
|
+
status bar (`staged <n> paths` / `committed <sha>` / `clean — nothing to stage`
|
|
207
|
+
/ `clean — nothing staged` / `skipped` / `failed — <stderr excerpt>`), including
|
|
208
|
+
a failing pre-commit hook's stderr.
|
|
209
|
+
- **Ask the agent** menu: inserts ready-made prompts (explain, fix suspect links,
|
|
210
|
+
draft child requirement, review readiness) into the session prompt editor from
|
|
211
|
+
the item's paths and state (feature spec §7.3). **Start review session**
|
|
212
|
+
composes `/opendoor-review <UID>` plus the item context there and needs no
|
|
213
|
+
paired backend: the extension starts an independent review conversation whose
|
|
214
|
+
row carries a **`firstUid +n` label** reopening the Requirements tab on that
|
|
215
|
+
workspace (unselected and archived rows included, no live Pi connection needed),
|
|
216
|
+
landing on the item detail pane. After the insert the status bar shows a quiet
|
|
217
|
+
`ok` **review dispatch** run row. Without the extension the inserted text is
|
|
218
|
+
still a self-sufficient review instruction, so the review happens in the current
|
|
219
|
+
conversation; a refused dispatch is reported in the chat by the extension (see
|
|
220
|
+
**Session metadata and session-list row labels** in the feature spec).
|
|
221
|
+
- Fingerprints and requirement state are recomputed in the browser from the YAML
|
|
222
|
+
files; everything mutating goes through the visible `doorstop` CLI — paired,
|
|
223
|
+
headlessly via the server entry, or in a workspace terminal when unpaired —
|
|
224
|
+
plus, opt-in, the narrated review commit (`commitAfterReview`) — so all effects
|
|
225
|
+
stay auditable in git.
|
|
226
|
+
|
|
334
227
|
|
|
335
228
|
## Commands
|
|
336
229
|
|
|
337
230
|
```sh
|
|
338
|
-
npm install
|
|
339
|
-
npm test
|
|
231
|
+
npm install # install dependencies
|
|
232
|
+
npm test # vitest (unit tests under src/**/*.test.ts)
|
|
340
233
|
npm run test:coverage # v8 coverage report
|
|
341
|
-
npm run typecheck
|
|
342
|
-
npm run build
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
npm run dev # watch mode build
|
|
234
|
+
npm run typecheck # tsc --noEmit (strict)
|
|
235
|
+
npm run build # esbuild → dist/browser, dist/server, dist/pi-extension
|
|
236
|
+
# + the dist-root manifest (see the build script header)
|
|
237
|
+
npm run dev # watch build
|
|
346
238
|
```
|
|
347
239
|
|
|
348
240
|
## Installing the plugin locally (id `opendoor`)
|
|
349
241
|
|
|
350
242
|
The package carries two independently enabled halves: the **PI WEB plugin**
|
|
351
|
-
(browser + server entries) and the **Pi extension** (`/opendoor-review`).
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
available for plugin-only development.
|
|
243
|
+
(browser + server entries) and the **Pi extension** (`/opendoor-review`). The
|
|
244
|
+
repository-root `package.json` is the npm-package-root manifest, so both halves
|
|
245
|
+
come from ONE install through **Settings → Pi packages** (npm name, git/URL or a
|
|
246
|
+
local source); the `ln -s dist` route below remains for plugin-only development.
|
|
356
247
|
|
|
357
248
|
### PI WEB plugin (browser + server entries)
|
|
358
249
|
|
|
359
250
|
Install the package once — **Settings → Pi packages** with source
|
|
360
251
|
`opendoor-plugin` (or this repository's path / git URL) — then enable
|
|
361
|
-
**Opendoor** under **Settings → PI WEB plugins**.
|
|
362
|
-
build the symlink route still works:
|
|
252
|
+
**Opendoor** under **Settings → PI WEB plugins**. Plugin-only local build:
|
|
363
253
|
|
|
364
254
|
```sh
|
|
365
255
|
npm run build
|
|
@@ -367,66 +257,23 @@ ln -s "$(pwd)/dist" ~/.pi-web/plugins/opendoor
|
|
|
367
257
|
```
|
|
368
258
|
|
|
369
259
|
Then **restart the session daemon** (server entries load at sessiond startup
|
|
370
|
-
only; the browser bundle hot-reloads on page reload) and reload the
|
|
371
|
-
page:
|
|
260
|
+
only; the browser bundle hot-reloads on page reload) and reload the page:
|
|
372
261
|
|
|
373
262
|
```sh
|
|
374
263
|
systemctl --user restart pi-web-sessiond.service
|
|
375
264
|
curl -s http://127.0.0.1:8504/pi-web-plugins/manifest.json
|
|
376
265
|
```
|
|
377
266
|
|
|
378
|
-
The manifest should list an `opendoor`
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
plugin still works fully via the terminal fallback.
|
|
267
|
+
The manifest should list an `opendoor` entry **with a `backendRevision`** (signing
|
|
268
|
+
the paired server module); if it is missing, the server module failed to load
|
|
269
|
+
(check the sessiond log) and the plugin works fully via the terminal fallback.
|
|
382
270
|
|
|
383
271
|
### Review dispatch (Pi extension)
|
|
384
272
|
|
|
385
|
-
Installing the package
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
session
|
|
391
|
-
|
|
392
|
-
**Start review session** entry needs no paired backend — it only composes the
|
|
393
|
-
command into the prompt editor.
|
|
394
|
-
|
|
395
|
-
## Rollout (pi-web 1.202609.1)
|
|
396
|
-
|
|
397
|
-
pi-web 1.202609.1 is a **hard break** for plugins — there is no
|
|
398
|
-
compatibility shim in either direction: a v4/v3 host rejects v2/v1 plugin
|
|
399
|
-
modules, and a v2/v1 host rejects the migrated v4/v3 modules. Upgrade the
|
|
400
|
-
opendoor package and pi-web **together**, then restart
|
|
401
|
-
`pi-web-sessiond.service` and reload the PI WEB page. During the transition
|
|
402
|
-
the opendoor panel is **entirely absent** (both plugin entries rejected), not
|
|
403
|
-
degraded. Federated gateways/targets must be upgraded in lockstep.
|
|
404
|
-
|
|
405
|
-
### pi-web 1.202610.3 (and later 1.202610.x)
|
|
406
|
-
|
|
407
|
-
Unlike 1.202609.1, release **1.202610.3** is **additive** for opendoor: the
|
|
408
|
-
untouched v4/v3 plugin keeps loading on a v1.202610.3 host, with a
|
|
409
|
-
once-per-plugin deprecation diagnostic (no removal date). No source changes
|
|
410
|
-
were needed — the only mindful adaptation was a no-op `navigate` stub in the
|
|
411
|
-
test fixtures (`src/test-fixtures.ts`, `src/browser/pi-web-plugin.test.ts`),
|
|
412
|
-
because 1.202610.x made `navigate` a required member of
|
|
413
|
-
`PluginRuntimeContext` / `WorkspacePanelContext`.
|
|
414
|
-
|
|
415
|
-
Host upgrade order for 1.202610.x: refresh the pi-web checkout, then
|
|
416
|
-
**manually restart `pi-web-sessiond.service`** — a UI/API autoreload alone
|
|
417
|
-
is **not sufficient** (the release touched the session-serving paths
|
|
418
|
-
heavily). The restart interrupts active sessions and terminals; reload the
|
|
419
|
-
PI WEB page afterwards and confirm the manifest shows the opendoor entry
|
|
420
|
-
with a `backendRevision`.
|
|
421
|
-
|
|
422
|
-
#### Review dispatch and session-row labels (1.202610.x floor)
|
|
423
|
-
|
|
424
|
-
The **Start review session** dispatch and the session-list row labels require
|
|
425
|
-
a **1.202610.x** session daemon: that release introduced the public session
|
|
426
|
-
metadata, the session-list label contribution, and the per-session dispatch
|
|
427
|
-
bridge the extension emits on. The opendoor **server** entry declares no
|
|
428
|
-
capabilities and loads across the whole supported range. `opendoor-plugin`
|
|
429
|
-
**0.4.0** is the first release carrying the extension. Restart
|
|
430
|
-
`pi-web-sessiond.service` once to load the server-side change, then run
|
|
431
|
-
`/reload` in each idle conversation to load the extension's
|
|
432
|
-
`/opendoor-review` command; the browser side only needs a page reload.
|
|
273
|
+
Installing the package loads the extension; enabling the PI WEB plugin is a
|
|
274
|
+
separate decision. After installing or updating the package, run `/reload` in
|
|
275
|
+
each idle conversation so `/opendoor-review` is registered there (an ordinary
|
|
276
|
+
Pi-resource change — no provider restart). The extension needs PI WEB's
|
|
277
|
+
session-dispatch bridge (1.202610.x); on an older host it reports "requires PI
|
|
278
|
+
WEB's session dispatch bridge" and starts nothing.
|
|
279
|
+
|
|
@@ -6562,7 +6562,8 @@ var DoorstopWorkspaceRegistry = class {
|
|
|
6562
6562
|
/** Panel invalidation: re-run discovery + load unconditionally for the
|
|
6563
6563
|
* connected workspace (paired or unpaired: the load job is browser-side, so there is no owned-workspace gate).
|
|
6564
6564
|
*
|
|
6565
|
-
* Plan Phase C step C1 — the
|
|
6565
|
+
* Plan Phase C step C1 — the typed invalidation (browser API v4,
|
|
6566
|
+
* unchanged on v5): `invalidation` carries
|
|
6566
6567
|
* the reason (`"manual"` Refresh action / `"mutation"` a workspace file
|
|
6567
6568
|
* changed / `"agent-activity"` agent work finished) and the affected
|
|
6568
6569
|
* resources. The rescan stays REASON-AGNOSTIC deliberately: every reason
|
|
@@ -6819,6 +6820,10 @@ function jsonishText(value) {
|
|
|
6819
6820
|
const json2 = JSON.stringify(value);
|
|
6820
6821
|
return json2 === void 0 ? String(value) : json2;
|
|
6821
6822
|
}
|
|
6823
|
+
var DOORSTOP_MARKDOWN_ATTRIBUTES = Object.freeze(["rationale"]);
|
|
6824
|
+
function isMarkdownAttribute(key, value) {
|
|
6825
|
+
return typeof value === "string" && DOORSTOP_MARKDOWN_ATTRIBUTES.includes(key.toLowerCase());
|
|
6826
|
+
}
|
|
6822
6827
|
function diffValueText(value) {
|
|
6823
6828
|
return value === void 0 ? "\u2014" : jsonishText(value);
|
|
6824
6829
|
}
|
|
@@ -7977,6 +7982,107 @@ var detailPaneStyles = i`
|
|
|
7977
7982
|
font-style: italic;
|
|
7978
7983
|
}
|
|
7979
7984
|
|
|
7985
|
+
/* Plan §2.1: the item body is host-rendered Markdown. The host renderer
|
|
7986
|
+
* mounts its nodes in THIS shadow root (the host's
|
|
7987
|
+
* pi-web-content-rendering-boundary element deliberately has no shadow
|
|
7988
|
+
* root of its own — createRenderRoot returns the element), so the prose
|
|
7989
|
+
* rules below reach them. The plain-text pre-wrap on .doorstop-text is
|
|
7990
|
+
* not appropriate inside Markdown blocks; the boundary's own source
|
|
7991
|
+
* fallback sets its own pre-wrap inline.
|
|
7992
|
+
*
|
|
7993
|
+
* This element does NOT inherit host typography: the boundary only
|
|
7994
|
+
* avoids adding a shadow root, it does not import host theme CSS into
|
|
7995
|
+
* ours, so every rule the document needs is restated here. */
|
|
7996
|
+
.doorstop-text-markdown {
|
|
7997
|
+
white-space: normal;
|
|
7998
|
+
}
|
|
7999
|
+
|
|
8000
|
+
/* Edge trimming must reach the BLOCKS, not the wrappers: the boundary
|
|
8001
|
+
* element (display: contents) and its .content-rendering-body wrapper
|
|
8002
|
+
* sit between this element and the rendered blocks, so a direct-child
|
|
8003
|
+
* (> :first-child) selector only ever matched the boundary itself. No
|
|
8004
|
+
* matching :last-child rule: the body keeps the source-fallback span
|
|
8005
|
+
* AFTER the content wrapper, so the last block is never a last-child;
|
|
8006
|
+
* its trailing 8px margin is invisible inside the bordered box. */
|
|
8007
|
+
.doorstop-text-markdown :is(p, ul, ol, pre, blockquote, .table-scroll, h1, h2, h3, h4, h5, h6):first-child {
|
|
8008
|
+
margin-top: 0;
|
|
8009
|
+
}
|
|
8010
|
+
|
|
8011
|
+
.doorstop-text-markdown :is(p, ul, ol, blockquote, .table-scroll) {
|
|
8012
|
+
margin: 0 0 8px;
|
|
8013
|
+
}
|
|
8014
|
+
|
|
8015
|
+
.doorstop-text-markdown :is(ul, ol) {
|
|
8016
|
+
padding-left: 20px;
|
|
8017
|
+
}
|
|
8018
|
+
|
|
8019
|
+
.doorstop-text-markdown :is(h1, h2, h3, h4, h5, h6) {
|
|
8020
|
+
margin: 12px 0 6px;
|
|
8021
|
+
font-size: 14px;
|
|
8022
|
+
font-weight: 600;
|
|
8023
|
+
}
|
|
8024
|
+
|
|
8025
|
+
.doorstop-text-markdown code {
|
|
8026
|
+
border: 1px solid var(--pi-border-muted);
|
|
8027
|
+
border-radius: 4px;
|
|
8028
|
+
background: var(--pi-bg);
|
|
8029
|
+
padding: 0 3px;
|
|
8030
|
+
font-size: 12px;
|
|
8031
|
+
}
|
|
8032
|
+
|
|
8033
|
+
.doorstop-text-markdown pre {
|
|
8034
|
+
border: 1px solid var(--pi-border-muted);
|
|
8035
|
+
border-radius: 6px;
|
|
8036
|
+
padding: 8px;
|
|
8037
|
+
overflow-x: auto;
|
|
8038
|
+
}
|
|
8039
|
+
|
|
8040
|
+
.doorstop-text-markdown pre code {
|
|
8041
|
+
border: 0;
|
|
8042
|
+
background: transparent;
|
|
8043
|
+
padding: 0;
|
|
8044
|
+
}
|
|
8045
|
+
|
|
8046
|
+
.doorstop-text-markdown blockquote {
|
|
8047
|
+
border-left: 3px solid var(--pi-border-muted);
|
|
8048
|
+
padding-left: 8px;
|
|
8049
|
+
color: var(--pi-muted);
|
|
8050
|
+
}
|
|
8051
|
+
|
|
8052
|
+
/* The host wraps every table in a div.table-scroll role=region
|
|
8053
|
+
* tabindex=0 and styles it ONLY from host theme CSS
|
|
8054
|
+
* (pi-web base/formattedText.css), which does not reach this
|
|
8055
|
+
* unregistered shadow root. Without these two rules a wide table is
|
|
8056
|
+
* clipped by the pane and its focusable region is unusable, so the
|
|
8057
|
+
* panel restates the host's rules (pi-web 1.202611.1).
|
|
8058
|
+
*
|
|
8059
|
+
* overflow-wrap: normal undoes the anywhere inherited from
|
|
8060
|
+
* .doorstop-text: harmless (host-identical) in prose, but inside table
|
|
8061
|
+
* cells it would let every column collapse to min-content instead of
|
|
8062
|
+
* making the table overflow into its scroll region. */
|
|
8063
|
+
.doorstop-text-markdown .table-scroll {
|
|
8064
|
+
max-width: 100%;
|
|
8065
|
+
overflow-x: auto;
|
|
8066
|
+
overflow-y: hidden;
|
|
8067
|
+
overflow-wrap: normal;
|
|
8068
|
+
}
|
|
8069
|
+
|
|
8070
|
+
.doorstop-text-markdown .table-scroll:focus-visible {
|
|
8071
|
+
outline: 1px solid var(--pi-accent);
|
|
8072
|
+
outline-offset: 2px;
|
|
8073
|
+
}
|
|
8074
|
+
|
|
8075
|
+
.doorstop-text-markdown table {
|
|
8076
|
+
border-collapse: collapse;
|
|
8077
|
+
margin: 0;
|
|
8078
|
+
}
|
|
8079
|
+
|
|
8080
|
+
.doorstop-text-markdown :is(th, td) {
|
|
8081
|
+
border: 1px solid var(--pi-border-muted);
|
|
8082
|
+
padding: 2px 6px;
|
|
8083
|
+
text-align: left;
|
|
8084
|
+
}
|
|
8085
|
+
|
|
7980
8086
|
.doorstop-links {
|
|
7981
8087
|
display: grid;
|
|
7982
8088
|
gap: 4px;
|
|
@@ -8051,6 +8157,28 @@ var detailPaneStyles = i`
|
|
|
8051
8157
|
overflow-wrap: anywhere;
|
|
8052
8158
|
}
|
|
8053
8159
|
|
|
8160
|
+
/* Plan plan-attribute-markdown D5: an allowlisted prose attribute
|
|
8161
|
+
* (initially "rationale") is host-rendered Markdown, so its row uses a
|
|
8162
|
+
* STACKED variant — the label above a full-width block — instead of
|
|
8163
|
+
* the two-column key|value grid sized for single-line <code> values.
|
|
8164
|
+
* The dt keeps the existing small muted label rule above.
|
|
8165
|
+
*
|
|
8166
|
+
* Nothing here restates the §2.1 prose rules: the rendered container
|
|
8167
|
+
* carries the shared .doorstop-text-markdown class, so block
|
|
8168
|
+
* margins, code/pre, blockquote and the 9bc9cc9 .table-scroll fixes
|
|
8169
|
+
* apply automatically. */
|
|
8170
|
+
.doorstop-attribute.doorstop-attribute-markdown {
|
|
8171
|
+
grid-template-columns: minmax(0, 1fr);
|
|
8172
|
+
gap: 3px;
|
|
8173
|
+
}
|
|
8174
|
+
|
|
8175
|
+
/* The escaped-text fallback (no host renderer) is prose too: keep its
|
|
8176
|
+
* line breaks. This only reaches the fallback — the markdown container
|
|
8177
|
+
* inside resets white-space to normal (.doorstop-text-markdown). */
|
|
8178
|
+
.doorstop-attribute-markdown dd {
|
|
8179
|
+
white-space: pre-wrap;
|
|
8180
|
+
}
|
|
8181
|
+
|
|
8054
8182
|
.doorstop-findings {
|
|
8055
8183
|
display: grid;
|
|
8056
8184
|
gap: 4px;
|
|
@@ -8685,7 +8813,7 @@ function renderDetail(host, result) {
|
|
|
8685
8813
|
${item.stateKeys.map((key) => host.renderStateChip(key))}
|
|
8686
8814
|
</div>
|
|
8687
8815
|
<h4 class="doorstop-section-title">Text</h4>
|
|
8688
|
-
|
|
8816
|
+
${renderItemText(host, item)}
|
|
8689
8817
|
${renderChangesSinceReview(host, item)}
|
|
8690
8818
|
<h4 class="doorstop-section-title">Parent links</h4>
|
|
8691
8819
|
${renderLinksOut(host, item, result.index)}
|
|
@@ -8696,9 +8824,7 @@ function renderDetail(host, result) {
|
|
|
8696
8824
|
${Object.entries(item.attributes).length === 0 ? A : b2`
|
|
8697
8825
|
<h4 class="doorstop-section-title">Extended attributes</h4>
|
|
8698
8826
|
<dl class="doorstop-attributes">
|
|
8699
|
-
${Object.entries(item.attributes).map(
|
|
8700
|
-
([key, value]) => b2`<div class="doorstop-attribute"><dt>${key}</dt><dd><code>${jsonishText(value)}</code></dd></div>`
|
|
8701
|
-
)}
|
|
8827
|
+
${Object.entries(item.attributes).map(([key, value]) => renderAttribute(host, key, value))}
|
|
8702
8828
|
</dl>
|
|
8703
8829
|
`}
|
|
8704
8830
|
<h4 class="doorstop-section-title">Findings</h4>
|
|
@@ -8711,6 +8837,31 @@ function renderDetail(host, result) {
|
|
|
8711
8837
|
</section>
|
|
8712
8838
|
`;
|
|
8713
8839
|
}
|
|
8840
|
+
function renderItemText(host, item) {
|
|
8841
|
+
if (item.text === "") {
|
|
8842
|
+
return b2`<p class="doorstop-text doorstop-text-empty">empty</p>`;
|
|
8843
|
+
}
|
|
8844
|
+
const renderer = host.markdownRenderer;
|
|
8845
|
+
const machineId = host.context?.machine.id;
|
|
8846
|
+
if (renderer === void 0 || machineId === void 0) {
|
|
8847
|
+
return b2`<p class="doorstop-text">${item.text}</p>`;
|
|
8848
|
+
}
|
|
8849
|
+
return b2`<div class="doorstop-text doorstop-text-markdown">${renderer.render({
|
|
8850
|
+
machineId,
|
|
8851
|
+
text: item.text,
|
|
8852
|
+
allowManualPreview: true
|
|
8853
|
+
})}</div>`;
|
|
8854
|
+
}
|
|
8855
|
+
function renderAttribute(host, key, value) {
|
|
8856
|
+
if (!isMarkdownAttribute(key, value)) {
|
|
8857
|
+
return b2`<div class="doorstop-attribute"><dt>${key}</dt><dd><code>${jsonishText(value)}</code></dd></div>`;
|
|
8858
|
+
}
|
|
8859
|
+
const text = value.trim();
|
|
8860
|
+
const renderer = host.markdownRenderer;
|
|
8861
|
+
const machineId = host.context?.machine.id;
|
|
8862
|
+
const body = text === "" ? b2`<p class="doorstop-text doorstop-text-empty">empty</p>` : renderer === void 0 || machineId === void 0 ? text : b2`<div class="doorstop-text-markdown">${renderer.render({ machineId, text, allowManualPreview: true })}</div>`;
|
|
8863
|
+
return b2`<div class="doorstop-attribute doorstop-attribute-markdown"><dt>${key}</dt><dd>${body}</dd></div>`;
|
|
8864
|
+
}
|
|
8714
8865
|
function renderFlags(host, item) {
|
|
8715
8866
|
return b2`
|
|
8716
8867
|
<div class="doorstop-flags" aria-label="Item flags">
|
|
@@ -9502,14 +9653,15 @@ function defineDoorstopPanelBodyElement() {
|
|
|
9502
9653
|
`;
|
|
9503
9654
|
}
|
|
9504
9655
|
/** section-API: the baseline fetch and the review→commit pipeline live
|
|
9505
|
-
* server-side, so this gates the sections that need them. The
|
|
9506
|
-
* is the exact revision-paired path to this
|
|
9507
|
-
* entry
|
|
9508
|
-
* `
|
|
9509
|
-
*
|
|
9510
|
-
*
|
|
9511
|
-
* ownership
|
|
9512
|
-
* Phase C revisits
|
|
9656
|
+
* server-side, so this gates the sections that need them. The peer
|
|
9657
|
+
* (`context.peer.request`) is the exact revision-paired path to this
|
|
9658
|
+
* package's own active server entry — unchanged by the v5 migration
|
|
9659
|
+
* (the v5 scoped `backend` is not consulted here), so `peer.request`
|
|
9660
|
+
* presence stays the paired-backend gate (a legal openChannel-only
|
|
9661
|
+
* peer is NOT paired for these request-shaped sections). The opendoor
|
|
9662
|
+
* provider ownership check is kept for the ownership-based
|
|
9663
|
+
* information display (behavior-preserving Phase A; Phase C revisits
|
|
9664
|
+
* it). */
|
|
9513
9665
|
backendActive() {
|
|
9514
9666
|
const context = this.context;
|
|
9515
9667
|
return context?.peer?.request !== void 0 && context.workspace.provider?.pluginId === OPENDOOR_PLUGIN_ID;
|
|
@@ -9667,6 +9819,9 @@ function defineDoorstopPanelBodyElement() {
|
|
|
9667
9819
|
__decorateClass([
|
|
9668
9820
|
n6({ attribute: false })
|
|
9669
9821
|
], DoorstopPanelBodyElement.prototype, "context", 2);
|
|
9822
|
+
__decorateClass([
|
|
9823
|
+
n6({ attribute: false })
|
|
9824
|
+
], DoorstopPanelBodyElement.prototype, "markdownRenderer", 2);
|
|
9670
9825
|
__decorateClass([
|
|
9671
9826
|
n6({ attribute: false })
|
|
9672
9827
|
], DoorstopPanelBodyElement.prototype, "result", 2);
|
|
@@ -9742,14 +9897,14 @@ function defineCustomElementOnce(tag, define) {
|
|
|
9742
9897
|
// src/browser/doorstop-contributions.ts
|
|
9743
9898
|
var DOORSTOP_PANEL_LOCAL_ID2 = "workspace.doorstop";
|
|
9744
9899
|
var DOORSTOP_LABEL_STATE_LIMIT = 8;
|
|
9745
|
-
function createOpendoorBrowserContributions(runtimePluginId, _html, _svg) {
|
|
9900
|
+
function createOpendoorBrowserContributions(runtimePluginId, _html, _svg, markdown) {
|
|
9746
9901
|
const panelId = `${runtimePluginId}:${DOORSTOP_PANEL_LOCAL_ID2}`;
|
|
9747
9902
|
const registry = new DoorstopWorkspaceRegistry();
|
|
9748
9903
|
const labelCache = new DoorstopLabelCache();
|
|
9749
9904
|
defineDoorstopPanelElements();
|
|
9750
9905
|
return {
|
|
9751
9906
|
actions: createDoorstopActions(panelId),
|
|
9752
|
-
workspacePanels: [createDoorstopPanel(registry)],
|
|
9907
|
+
workspacePanels: [createDoorstopPanel(registry, markdown)],
|
|
9753
9908
|
workspaceLabels: [createDoorstopLabel(labelCache)],
|
|
9754
9909
|
sessionLabels: [createDoorstopSessionLabel(runtimePluginId)]
|
|
9755
9910
|
};
|
|
@@ -9789,7 +9944,7 @@ function createDoorstopActions(panelId) {
|
|
|
9789
9944
|
}
|
|
9790
9945
|
];
|
|
9791
9946
|
}
|
|
9792
|
-
function createDoorstopPanel(registry) {
|
|
9947
|
+
function createDoorstopPanel(registry, markdown) {
|
|
9793
9948
|
return {
|
|
9794
9949
|
id: DOORSTOP_PANEL_LOCAL_ID2,
|
|
9795
9950
|
title: "Requirements",
|
|
@@ -9817,8 +9972,8 @@ function createDoorstopPanel(registry) {
|
|
|
9817
9972
|
// terminal fallback keeps every action working; the discovery-based
|
|
9818
9973
|
// empty state carries the "no documents" story (feature spec §7.1).
|
|
9819
9974
|
visible: () => true,
|
|
9820
|
-
// Plan Phase C step C1 — typed invalidation (browser API v4
|
|
9821
|
-
// fixed workspace resources whose automatic invalidations this panel
|
|
9975
|
+
// Plan Phase C step C1 — typed invalidation (browser API v4, unchanged
|
|
9976
|
+
// on v5): declare the fixed workspace resources whose automatic invalidations this panel
|
|
9822
9977
|
// receives. With `workspace.files` declared, the host calls onInvalidate
|
|
9823
9978
|
// not only for the manual Refresh action (refreshWorkspacePanels) but
|
|
9824
9979
|
// also when workspace files change (`reason: "mutation"` — e.g. the
|
|
@@ -9839,10 +9994,10 @@ function createDoorstopPanel(registry) {
|
|
|
9839
9994
|
// (never loads, never renders); unknown paths and pre-first-load panels
|
|
9840
9995
|
// return undefined (the host asks the next panel / serves Files).
|
|
9841
9996
|
fileOpenQuery: (context, path) => registry.fileOpenQuery(context, path),
|
|
9842
|
-
render: (context) => renderDoorstopPanel(registry, context)
|
|
9997
|
+
render: (context) => renderDoorstopPanel(registry, context, markdown)
|
|
9843
9998
|
};
|
|
9844
9999
|
}
|
|
9845
|
-
function renderDoorstopPanel(registry, context) {
|
|
10000
|
+
function renderDoorstopPanel(registry, context, markdown) {
|
|
9846
10001
|
const controller = registry.for(context);
|
|
9847
10002
|
if (context.navigation !== void 0) {
|
|
9848
10003
|
controller.applyNavigationQuery(readPanelNavigationQuery(context.navigation));
|
|
@@ -9854,6 +10009,7 @@ function renderDoorstopPanel(registry, context) {
|
|
|
9854
10009
|
<pi-web-opendoor-panel-body
|
|
9855
10010
|
.controller=${controller}
|
|
9856
10011
|
.context=${context}
|
|
10012
|
+
.markdownRenderer=${markdown.renderer}
|
|
9857
10013
|
.result=${controller.result}
|
|
9858
10014
|
.loading=${controller.loading}
|
|
9859
10015
|
.stale=${controller.stale}
|
|
@@ -9948,13 +10104,36 @@ function doorstopLabelKey(context) {
|
|
|
9948
10104
|
return JSON.stringify([context.machine.id, context.workspace.projectId, context.workspace.id]);
|
|
9949
10105
|
}
|
|
9950
10106
|
|
|
10107
|
+
// src/browser/doorstop-markdown.ts
|
|
10108
|
+
var DOORSTOP_MARKDOWN_CAPABILITY = {
|
|
10109
|
+
pluginId: "pi-web",
|
|
10110
|
+
id: "markdown-rendering",
|
|
10111
|
+
version: 1,
|
|
10112
|
+
parse(value) {
|
|
10113
|
+
if (typeof value !== "object" || value === null || !("render" in value) || typeof value.render !== "function") {
|
|
10114
|
+
throw new Error("Markdown rendering capability v1 is unavailable");
|
|
10115
|
+
}
|
|
10116
|
+
return value;
|
|
10117
|
+
}
|
|
10118
|
+
};
|
|
10119
|
+
function createDoorstopMarkdownHolder() {
|
|
10120
|
+
return { renderer: void 0 };
|
|
10121
|
+
}
|
|
10122
|
+
|
|
9951
10123
|
// src/browser/pi-web-plugin.ts
|
|
9952
10124
|
var plugin = {
|
|
9953
|
-
apiVersion:
|
|
10125
|
+
apiVersion: 5,
|
|
9954
10126
|
name: "Opendoor",
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
10127
|
+
requires: [DOORSTOP_MARKDOWN_CAPABILITY],
|
|
10128
|
+
activate: ({ runtimePluginId, html, svg }) => {
|
|
10129
|
+
const markdown = createDoorstopMarkdownHolder();
|
|
10130
|
+
return {
|
|
10131
|
+
contributions: createOpendoorBrowserContributions(runtimePluginId, html, svg, markdown),
|
|
10132
|
+
start: ({ capabilities }) => {
|
|
10133
|
+
markdown.renderer = capabilities.resolve(DOORSTOP_MARKDOWN_CAPABILITY);
|
|
10134
|
+
}
|
|
10135
|
+
};
|
|
10136
|
+
}
|
|
9958
10137
|
};
|
|
9959
10138
|
var pi_web_plugin_default = plugin;
|
|
9960
10139
|
export {
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opendoor-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Opendoor — a pi-web browser plugin that surfaces Doorstop requirements stored in the workspace: the document tree, per-item state (reviewed/unreviewed changes, suspect links), validation findings, and one-click doorstop CLI actions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
]
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@jmfederico/pi-web": ">=1.
|
|
29
|
+
"@jmfederico/pi-web": ">=1.202611.1"
|
|
30
30
|
}
|
|
31
31
|
}
|
package/dist/server-plugin.js
CHANGED
|
@@ -993,16 +993,20 @@ async function probeContainsDoorstopMarker(root, signal) {
|
|
|
993
993
|
return walk(root, 0);
|
|
994
994
|
}
|
|
995
995
|
var plugin = {
|
|
996
|
-
apiVersion:
|
|
996
|
+
apiVersion: 5,
|
|
997
997
|
name: "Opendoor",
|
|
998
998
|
// Permanently no `requires`/`start()` — see the module header: the review
|
|
999
999
|
// dispatch moved to the Pi extension, so this provider-contributing entry
|
|
1000
|
-
// never approaches the host's provider-cycle rule
|
|
1000
|
+
// never approaches the host's provider-cycle rule (and never declares the
|
|
1001
|
+
// host catalog capability, which is prohibited for provider-contributing
|
|
1002
|
+
// entries).
|
|
1001
1003
|
activate(context) {
|
|
1002
1004
|
return {
|
|
1003
1005
|
workspaceProvider: createDoorstopWorkspaceProvider(context),
|
|
1004
|
-
// v3: the six-operation dispatch lives at the
|
|
1005
|
-
// the provider (`WorkspaceProvider.request`
|
|
1006
|
+
// v3 onward (unchanged on v5): the six-operation dispatch lives at the
|
|
1007
|
+
// activation level, not on the provider (`WorkspaceProvider.request`
|
|
1008
|
+
// was removed). The v5 context carries a `scope` union; the dispatch
|
|
1009
|
+
// accepts only the workspace scope and rejects the rest.
|
|
1006
1010
|
peer: {
|
|
1007
1011
|
request: (request) => dispatchDoorstopOperation(context, request)
|
|
1008
1012
|
}
|
|
@@ -1034,25 +1038,43 @@ function createDoorstopWorkspaceProvider(context) {
|
|
|
1034
1038
|
});
|
|
1035
1039
|
}
|
|
1036
1040
|
function dispatchDoorstopOperation(context, request) {
|
|
1037
|
-
|
|
1041
|
+
const { scope, operation, input, signal } = request;
|
|
1042
|
+
if (scope.kind !== "workspace") {
|
|
1043
|
+
return Promise.reject(unsupportedPeerScopeError(operation, scope.kind));
|
|
1044
|
+
}
|
|
1045
|
+
const handlerRequest = {
|
|
1046
|
+
project: scope.project,
|
|
1047
|
+
workspace: scope.workspace,
|
|
1048
|
+
operation,
|
|
1049
|
+
input,
|
|
1050
|
+
signal
|
|
1051
|
+
};
|
|
1052
|
+
switch (operation) {
|
|
1038
1053
|
case DOORSTOP_RUN_OPERATION:
|
|
1039
|
-
return requestDoorstopBackend(context,
|
|
1054
|
+
return requestDoorstopBackend(context, handlerRequest);
|
|
1040
1055
|
case DOORSTOP_BASELINE_OPERATION:
|
|
1041
|
-
return requestDoorstopBaseline(context,
|
|
1056
|
+
return requestDoorstopBaseline(context, handlerRequest);
|
|
1042
1057
|
case DOORSTOP_GIT_STATUS_OPERATION:
|
|
1043
|
-
return requestDoorstopGitStatus(context,
|
|
1058
|
+
return requestDoorstopGitStatus(context, handlerRequest);
|
|
1044
1059
|
case DOORSTOP_GIT_STAGE_OPERATION:
|
|
1045
|
-
return requestDoorstopGitStage(context,
|
|
1060
|
+
return requestDoorstopGitStage(context, handlerRequest);
|
|
1046
1061
|
case DOORSTOP_GIT_UNSTAGE_OPERATION:
|
|
1047
|
-
return requestDoorstopGitUnstage(context,
|
|
1062
|
+
return requestDoorstopGitUnstage(context, handlerRequest);
|
|
1048
1063
|
case DOORSTOP_GIT_COMMIT_OPERATION:
|
|
1049
|
-
return requestDoorstopGitCommit(context,
|
|
1064
|
+
return requestDoorstopGitCommit(context, handlerRequest);
|
|
1050
1065
|
default:
|
|
1051
|
-
return Promise.reject(unsupportedBackendOperationError(
|
|
1066
|
+
return Promise.reject(unsupportedBackendOperationError(operation));
|
|
1052
1067
|
}
|
|
1053
1068
|
}
|
|
1069
|
+
var peerScopeKinds = /* @__PURE__ */ new Set(["machine", "project", "workspace"]);
|
|
1070
|
+
function unsupportedPeerScopeError(operation, scopeKind) {
|
|
1071
|
+
const op = /^[A-Za-z0-9.-]+$/.test(operation) ? operation : "unknown";
|
|
1072
|
+
const kind = peerScopeKinds.has(scopeKind) ? scopeKind : "unknown";
|
|
1073
|
+
return new Error(`opendoor: ${op} peer requests require workspace scope (got ${kind})`);
|
|
1074
|
+
}
|
|
1054
1075
|
export {
|
|
1055
1076
|
createDoorstopWorkspaceProvider,
|
|
1056
1077
|
server_plugin_default as default,
|
|
1057
|
-
dispatchDoorstopOperation
|
|
1078
|
+
dispatchDoorstopOperation,
|
|
1079
|
+
unsupportedPeerScopeError
|
|
1058
1080
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opendoor-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Opendoor — a pi-web browser plugin that surfaces Doorstop requirements stored in the workspace: the document tree, per-item state (reviewed/unreviewed changes, suspect links), validation findings, and one-click doorstop CLI actions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@earendil-works/pi-coding-agent": "*",
|
|
49
|
-
"@jmfederico/pi-web": ">=1.
|
|
49
|
+
"@jmfederico/pi-web": ">=1.202611.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@jmfederico/pi-web": "1.
|
|
52
|
+
"@jmfederico/pi-web": "^1.202611.1",
|
|
53
53
|
"@types/js-yaml": "^4.0.9",
|
|
54
54
|
"@types/node": "^24.19.2",
|
|
55
55
|
"@vitest/coverage-v8": "^4.1.12",
|