@gavin-hu/my-pi-agent 0.20.0 → 0.21.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 +3 -2
- package/README.zh-CN.md +3 -2
- package/docs/extensions/README.md +1 -0
- package/docs/extensions/board.md +57 -42
- package/docs/extensions/bytefly.md +85 -89
- package/docs/extensions/file-browse.md +36 -41
- package/docs/extensions/plan.md +31 -14
- package/docs/extensions/serve.md +129 -0
- package/docs/extensions/session-bus.md +1 -1
- package/docs/extensions/team.md +11 -11
- package/extensions/board/commands.ts +27 -14
- package/extensions/board/index.ts +14 -2
- package/extensions/board/join.ts +31 -10
- package/extensions/board/page.ts +53 -83
- package/extensions/board/result.ts +1 -24
- package/extensions/board/rows.ts +1 -1
- package/extensions/board/schema.ts +9 -3
- package/extensions/board/snapshot.ts +7 -6
- package/extensions/board/tools.ts +27 -15
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +0 -10
- package/extensions/board/view/client.ts +3 -2
- package/extensions/board/view/live.ts +1 -7
- package/extensions/board/view/page.ts +16 -5
- package/extensions/board/view/render.ts +22 -17
- package/extensions/board/view/styles.ts +7 -1
- package/extensions/bytefly/footer/config.ts +3 -6
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +4 -4
- package/extensions/bytefly/footer/render/segments.ts +10 -34
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/check/render.ts +10 -1
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +27 -48
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/tools.ts +19 -7
- package/extensions/job/tui.ts +2 -28
- package/extensions/plan/commands.ts +2 -2
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/index.ts +7 -1
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/runtime.ts +12 -2
- package/extensions/plan/tools.ts +2 -2
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +99 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-markdown.ts +1 -1
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +77 -130
- package/extensions/serve/config.ts +37 -0
- package/extensions/serve/index.ts +71 -0
- package/extensions/serve/registry.ts +251 -0
- package/extensions/session-bus/join-request.ts +16 -9
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/team/commands.ts +3 -1
- package/extensions/todo/tui.ts +21 -3
- package/lib/README.md +5 -4
- package/lib/board/page-pointer.ts +29 -4
- package/lib/board/session.ts +84 -18
- package/lib/board/store.ts +34 -8
- package/lib/board/view.ts +20 -14
- package/lib/bus-request.ts +83 -0
- package/lib/http/index.ts +11 -7
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +25 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/plan-entry.ts +9 -13
- package/lib/requirements.ts +2 -2
- package/lib/session-label.ts +23 -7
- package/lib/session-tool.ts +37 -21
- package/lib/tui/README.md +6 -3
- package/lib/tui/glyphs.ts +2 -4
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tool.ts +30 -1
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/markup.ts +18 -1
- package/package.json +3 -2
- package/extensions/board/config.ts +0 -36
|
@@ -18,13 +18,13 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
|
|
|
18
18
|
|---|---|---|---|
|
|
19
19
|
| One-command browsing | `/browse` | The server starts at the effective working directory, prints its URL, and opens the browser. | Shipped |
|
|
20
20
|
| Read-only serving | `/browse` | Nothing is edited or written, and the server binds `127.0.0.1` only. | Shipped |
|
|
21
|
-
| Two-pane page |
|
|
21
|
+
| Two-pane page | `/files/`, `/files/browse[/<rel>]` | A collapsible tree beside the current folder's listing, with the tree, filter, and quick-open trigger pinned in their own pane. | Shipped |
|
|
22
22
|
| File previews | `/view/<rel>` | Text, rendered Markdown, image thumbnails, and download cards for binary or over-size files, each with a per-language icon. | Shipped |
|
|
23
|
-
| Quick open |
|
|
23
|
+
| Quick open | `/files/`, `Ctrl/Cmd+K`, `/files/api/files` | A fuzzy path finder over the tree opens a file without walking the tree. | Shipped |
|
|
24
24
|
| Git diff view | `/diff`, `/diff/<rel>` | A change overview and one file's unified diff, scoped to `head`, `staged`, or `worktree`. | Shipped |
|
|
25
25
|
| Live status | `/events`, the `◉ live` chip | An open page keeps its branch, change markers, and footer up to date without a reload. | Shipped |
|
|
26
26
|
| Reload hint | `/api/dir-rev`, `⟳ reload` | A page whose folder gained or lost a file offers a reload control instead of changing its rows. | Shipped |
|
|
27
|
-
|
|
|
27
|
+
| The session's page port | `serve`'s `◉ <port>` chip | The footer names the one port every page of the session answers on, this one included. | Shipped |
|
|
28
28
|
| Listing controls | the `Name`, `Size`, and `Modified` headers | The current folder sorts by name, size, or modified time, directories first, and the choice is remembered per browser. | Shipped |
|
|
29
29
|
| Keyboard navigation | the tree's keymap, `?` | The tree is one tab stop with arrow, fold, and open keys; a shortcut sheet lists the page's own keys. | Shipped |
|
|
30
30
|
| Diff hunk navigation | `n`/`p` in `/diff` | Jump between changed hunks without scrolling the whole file. | Shipped |
|
|
@@ -35,7 +35,7 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
|
|
|
35
35
|
| Markdown preview | `/view/<rel>`, `/view/<rel>?raw=1` | A README renders as headings, lists, tables, code, and links, with a one-click raw view. | Shipped |
|
|
36
36
|
| Copy and wrap | the page toolbar, `y`, `u` | Copy a file's path or contents or a `path:12-30` line reference, and toggle line wrapping for text and raw views. | Shipped |
|
|
37
37
|
| Theme toggle | the header button, `/theme.js` | Dark and light palettes, following the system until an explicit choice is remembered per browser. | Shipped |
|
|
38
|
-
| Idle stop and
|
|
38
|
+
| Idle stop and prefix reuse | `/browse stop`, the idle grace | The page gives its prefix back when nobody is reading it, so a closed tab never holds the session's server. | Shipped |
|
|
39
39
|
|
|
40
40
|
## Quickstart
|
|
41
41
|
|
|
@@ -66,33 +66,32 @@ selects the root.
|
|
|
66
66
|
|
|
67
67
|
- The root is the effective cwd, so an active `worktree` session serves the
|
|
68
68
|
worktree. `<path>` must resolve inside it.
|
|
69
|
-
- The port is
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
port; a busy port is reported, not silently changed.
|
|
69
|
+
- The port is the session's: `serve` picks one in `4780`–`5779` (and `serve.port` pins
|
|
70
|
+
it), so the origin stays stable across roots and restarts. A busy pinned port is
|
|
71
|
+
reported, not silently changed.
|
|
73
72
|
- Starting while already running with the same root reprints the URL without
|
|
74
|
-
restarting; a different root
|
|
73
|
+
restarting; a different root restarts the page.
|
|
75
74
|
- The server is closed when its last page has been gone for ten seconds, so
|
|
76
75
|
closing the browser window ends the session's browse server and `/browse`
|
|
77
|
-
starts it again on the
|
|
76
|
+
starts it again on the session's port. A reload or a new tab inside that grace keeps
|
|
78
77
|
it. `session_shutdown` closes it too, and `/browse stop` closes it at once.
|
|
79
78
|
|
|
80
79
|
## Routes
|
|
81
80
|
|
|
82
81
|
| Route | Result |
|
|
83
82
|
|---|---|
|
|
84
|
-
|
|
|
85
|
-
| `/view/<rel>` | tree + file page (text, Markdown, image, binary, or over-size) |
|
|
86
|
-
| `/view/<rel>?raw=1` | raw Markdown/text source instead of the rendered view |
|
|
87
|
-
| `/raw/<rel>` | raw bytes, streamed; downloads and image sources |
|
|
88
|
-
| `/diff[?mode=head\|staged\|worktree]` | changed-file overview with `--stat` |
|
|
89
|
-
| `/diff/<rel>[?mode=…]` | one file's unified diff |
|
|
90
|
-
| `/api/tree?path=<rel>` | JSON children for lazy tree expansion |
|
|
91
|
-
| `/api/files?q=<query>` | JSON fuzzy path matches for the quick-open palette |
|
|
92
|
-
| `/api/dir-rev?path=<rel>` | JSON listing revision for the reload check |
|
|
93
|
-
| `/events` | Server-Sent Events stream of `status` frames for the open page |
|
|
94
|
-
| `/app.js` | the first-party client script |
|
|
95
|
-
| `/theme.js` | the pre-paint theme bootstrap |
|
|
83
|
+
| `/files/`, `/files/browse[/<rel>]` | tree + directory listing |
|
|
84
|
+
| `/files/view/<rel>` | tree + file page (text, Markdown, image, binary, or over-size) |
|
|
85
|
+
| `/files/view/<rel>?raw=1` | raw Markdown/text source instead of the rendered view |
|
|
86
|
+
| `/files/raw/<rel>` | raw bytes, streamed; downloads and image sources |
|
|
87
|
+
| `/files/diff[?mode=head\|staged\|worktree]` | changed-file overview with `--stat` |
|
|
88
|
+
| `/files/diff/<rel>[?mode=…]` | one file's unified diff |
|
|
89
|
+
| `/files/api/tree?path=<rel>` | JSON children for lazy tree expansion |
|
|
90
|
+
| `/files/api/files?q=<query>` | JSON fuzzy path matches for the quick-open palette |
|
|
91
|
+
| `/files/api/dir-rev?path=<rel>` | JSON listing revision for the reload check |
|
|
92
|
+
| `/files/events` | Server-Sent Events stream of `status` frames for the open page |
|
|
93
|
+
| `/files/app.js` | the first-party client script |
|
|
94
|
+
| `/files/theme.js` | the pre-paint theme bootstrap |
|
|
96
95
|
|
|
97
96
|
## Configuration
|
|
98
97
|
|
|
@@ -102,7 +101,6 @@ global document then project document, project wins — are in
|
|
|
102
101
|
|
|
103
102
|
| Key | Default | Meaning |
|
|
104
103
|
|---|---|---|
|
|
105
|
-
| `port` | `0` | `0` picks one random port fixed for the session; a positive value pins it. |
|
|
106
104
|
| `autoOpen` | `true` | Open the browser on start; `false` for headless. |
|
|
107
105
|
| `maxFileBytes` | `1048576` | Larger files show a download card only. |
|
|
108
106
|
| `maxThumbBytes` | `5242880` | Larger images get an icon, not a thumbnail. |
|
|
@@ -342,12 +340,11 @@ old and new sides for `lib/highlight`.
|
|
|
342
340
|
|
|
343
341
|
## Live status
|
|
344
342
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
[`bytefly`](./bytefly.md)
|
|
348
|
-
`⎇ branch` and `⑂ worktree
|
|
349
|
-
|
|
350
|
-
reflects only this session's server, with no cross-session sharing.
|
|
343
|
+
The page answers at `/files` on the session's one server, so the footer's one
|
|
344
|
+
`◉ <port>` chip is the session's, not this page's: [`serve`](./serve.md) publishes it
|
|
345
|
+
while anything is mounted, and [`bytefly`](./bytefly.md) routes it to line 1's right
|
|
346
|
+
zone, after `⎇ branch` and `⑂ worktree`. It is cleared when the session's server closes,
|
|
347
|
+
so a session with no page open shows nothing at all.
|
|
351
348
|
|
|
352
349
|
The page has its own live status, which is what a left-open tab reads. A `◉ live`
|
|
353
350
|
chip beside `read-only` says whether status is flowing (green), still
|
|
@@ -452,22 +449,20 @@ stops at the first difference because the control, not the checker, is the fix.
|
|
|
452
449
|
| Integration point | How |
|
|
453
450
|
|---|---|
|
|
454
451
|
| Command | `/browse` registers a single command from the factory; no tools are exposed. |
|
|
455
|
-
| State | The running
|
|
452
|
+
| State | The running mount, its root, and the session id live in a module-local closure; nothing is persisted and no session entries are written. The port is `serve`'s, not this extension's. |
|
|
456
453
|
| Lifecycle | The factory opens no socket; the server is created on `/browse` and closed idempotently by `/browse stop`, by `session_shutdown`, and once its last page has been gone for the idle grace, which also ends every attached status stream. |
|
|
457
454
|
|
|
458
455
|
## Design notes
|
|
459
456
|
|
|
460
457
|
- **Lifecycle.** `/browse` resolves `resolveEffectiveCwd(ctx.cwd)`, loads config,
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
`IDLE_STOP_MS` and the command closes the server and clears the chip — the
|
|
470
|
-
cached port survives, so the next `/browse` binds the same one.
|
|
458
|
+
parses args, and mounts the page through `requestMount`: `realpath` the root, claim
|
|
459
|
+
`/files` on the session's server, return `{ url, port, root, close }`. The
|
|
460
|
+
port belongs to `serve` — `serve.port` pins the session's origin — so nothing here
|
|
461
|
+
picks or retries one. `/browse stop` and `session_shutdown` await the idempotent
|
|
462
|
+
`close()`, which gives the prefix back. The page is the mount's only reader, so the
|
|
463
|
+
channel reports the last page's departure after `IDLE_STOP_MS` and the command
|
|
464
|
+
releases the prefix — and the session's port is `serve`'s, remembered per session,
|
|
465
|
+
so the next `/browse` answers on the same origin.
|
|
471
466
|
- **Pure routing.** `handleRequest(context, method, rawUrl, headers)` validates
|
|
472
467
|
method and `Host`, parses the URL, matches a route, and runs every path
|
|
473
468
|
through `paths.ts`. It returns a plain `ServeResponse`, so routing is tested
|
|
@@ -529,7 +524,7 @@ stops at the first difference because the control, not the checker, is the fix.
|
|
|
529
524
|
| `view/diff-html.ts` | The unified-diff body: the gutter, the sign, the hunk sections, and per-hunk highlighting |
|
|
530
525
|
| `transport/router.ts` | Pure request handler: routes, the `/raw` sandbox policy, and the error page |
|
|
531
526
|
| `transport/server.ts` | This extension's routes over `lib/http`'s loopback server: the request count, the stream cap, and the idle policy |
|
|
532
|
-
| [`../../lib/http/index.ts`](../../lib/http/index.ts) | Loopback HTTP layer shared with `plan`: one server entry (`
|
|
527
|
+
| [`../../lib/http/index.ts`](../../lib/http/index.ts) | Loopback HTTP layer shared with `plan`: one server entry (`serveHttp`) and the mount contract a page claims a prefix with (`requestMount`), page/script/JSON/event-stream headers, `ServeResponse`, the request-envelope helpers, the event-stream sink and its idle grace |
|
|
533
528
|
| [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform browser opener (shared with `plan`) |
|
|
534
529
|
|
|
535
530
|
## Testing
|
package/docs/extensions/plan.md
CHANGED
|
@@ -41,6 +41,8 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
|
|
|
41
41
|
| Inline comments | the page's `+ Comment`, **Save feedback** | Comments are saved into the plan file as `> [!REVIEW]` lines and returned to the model. | Shipped |
|
|
42
42
|
| Live page | the page's `events` stream, the plan-file poll | An edit made elsewhere repaints every open page without a reload. | Shipped |
|
|
43
43
|
| Changed sections | the `exit_plan_mode` review screen, the `/plan review` outline | After a refinement, only the sections that changed are marked. | Shipped |
|
|
44
|
+
| Block-level change marks | the `/plan review` page's **Changed** view | After a refinement, each block that is new or edited is marked — an inserted step marks itself and leaves the rest of its section alone — and a section that lost blocks says how much shorter it became. **All** / **Changed** / **Comments** switch what the page shows, `v` cycles them, `n`/`N` walk the blocks that carry a comment, and a printed review is always the whole plan. | Unreleased |
|
|
45
|
+
| Settled comments | the page's `Resolve` / `Reopen` on a note you wrote | The note keeps its place, its quote and its history and is written `> [!REVIEW resolved]`, where a later review and the model read it as closed; the panel counts `2 open · 1 resolved`, and every card names who wrote it. | Unreleased |
|
|
44
46
|
| Section navigation | the review screen, `n`/`p` | The screen names the section you are in and jumps between headings. | Shipped |
|
|
45
47
|
| Remote review | `exit_plan_mode` on a WeChat turn | The plan file is sent, then one compact prompt to approve, keep planning, or exit. | Shipped |
|
|
46
48
|
|
|
@@ -562,6 +564,12 @@ reveals a **+ Comment** action on hover; select
|
|
|
562
564
|
text first to quote it. **Save feedback** writes the comments into the plan
|
|
563
565
|
file and returns them to the model.
|
|
564
566
|
|
|
567
|
+
- **A comment can be settled.** **Resolve** (on your own note) keeps it — its place, its
|
|
568
|
+
quote, its history — and marks it `> [!REVIEW resolved]` in the file, where a later review
|
|
569
|
+
and the model both read it as closed; **Reopen** puts it back. The panel counts
|
|
570
|
+
`2 open · 1 resolved`. A note you wrote is labelled **You** on your page and **The owner**
|
|
571
|
+
on somebody else's, a reviewer's is `@name`, and one sentence beside the facts says what
|
|
572
|
+
**Approve & run** and **Save feedback** each do.
|
|
565
573
|
- **Shared theme.** The page uses the same Bytefly tokens, base rules, and
|
|
566
574
|
syntax-token colours as `file-browse` (`lib/web/`), follows the system light/dark
|
|
567
575
|
preference until the header toggle sets an explicit choice, and remembers that
|
|
@@ -654,14 +662,15 @@ file and returns them to the model.
|
|
|
654
662
|
save that carries no comments asks for nothing, so the review screen comes back
|
|
655
663
|
instead. `/plan review` sends them as a user message (a plan-mode revision or a
|
|
656
664
|
plain update, depending on the mode).
|
|
657
|
-
- **Loopback first, tokenized.** The
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
saved theme
|
|
665
|
+
- **Loopback first, tokenized.** The review mounts `/plan/<id>` on the session's one
|
|
666
|
+
server, which binds `127.0.0.1`; the URL carries a random per-session token, a `Host`
|
|
667
|
+
header the server was not bound on is rejected, a cross-origin POST is refused, only
|
|
668
|
+
`GET`, `HEAD`, and `POST` are served, and the request body is capped. Sharing asks that
|
|
669
|
+
mount for one LAN listener (below); it never widens the loopback socket. A save or
|
|
670
|
+
cancel resolves the session only after its HTTP response is delivered, so the browser
|
|
671
|
+
never sees a false "Failed to fetch". The prefix is the review's, on the session's one
|
|
672
|
+
origin, so the browser's saved theme survives a review. It is closed on the owner's
|
|
673
|
+
save or cancel, on the owner's
|
|
665
674
|
page going away, or on `session_shutdown`; a reload reconnects inside that grace,
|
|
666
675
|
so refreshing or reloading a conflicted page does not end the review. A
|
|
667
676
|
reviewer's tab opening or closing is not the review's lifetime.
|
|
@@ -766,6 +775,10 @@ both.
|
|
|
766
775
|
refuses a file it did not render (`409`). It is a poll rather than a file
|
|
767
776
|
watcher, so a change on a filesystem that reports nothing at all is still seen
|
|
768
777
|
at the next request, because every page read re-compares the file.
|
|
778
|
+
- **A settled note is a flag in the marker.** `> [!REVIEW resolved]` (and
|
|
779
|
+
`moved resolved`) is read as open when absent; a build from before this one does not know
|
|
780
|
+
the flag and shows the marker line as body text, so a file reviewed on both sides of the
|
|
781
|
+
upgrade reads differently there.
|
|
769
782
|
- A shared review holds at most eight links. Each is a live bearer credential, so
|
|
770
783
|
a ninth is refused with the cap named rather than minted.
|
|
771
784
|
|
|
@@ -946,14 +959,16 @@ both.
|
|
|
946
959
|
| `plan-index.ts` | `/plan list`: the notice text and the docked index screen (store size and oldest plan, title, step progress, age, focused path; Enter reviews). |
|
|
947
960
|
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks), and `contentBlockAt`: the block with content a comment beside blank space belongs to. |
|
|
948
961
|
| `feedback.ts` | Comment codec: authored markers, `stripFeedback`, `readFeedback`/`parseFeedback` (body, blocks, and comments out of one parse), `formatFeedback` (writer-scoped), `displayFeedback`, `carryFeedback`, `feedbackSummary`, and payload validation. |
|
|
949
|
-
| `web.ts` | `createPlanWebReview`: the review session (per-token roles, the change stream, the plan-file poll, the body cap, settle-after-flush, abort) over `lib/http`'s one loopback server. |
|
|
962
|
+
| `web.ts` | `createPlanWebReview`: the review session (per-token roles, the change stream, the plan-file poll, the body cap, settle-after-flush, abort) over `lib/http`'s one loopback server, and the router wiring — the island is `web-payload.ts` and the writes are `web-write.ts`. |
|
|
950
963
|
| `web-tokens.ts` | The token table: the role each token names, the collision rule that keeps two roles off one token, and the names the other live links have bound. |
|
|
951
964
|
| `web-streams.ts` | The event streams: who is listening, what a frame is addressed to, and the closed-tab grace that ends the review when the owner's page goes away. |
|
|
952
965
|
| `web-router.ts` | Pure request handler: token role, `Host` allowlist, origin, and method checks through one POST-and-role guard, then the page, comment, owner-save, approve, cancel, and the four share routes. |
|
|
953
|
-
| `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet, approve confirmation, share and name dialogs)
|
|
966
|
+
| `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet, approve confirmation, share and name dialogs) and inline CSS. |
|
|
967
|
+
| `web-payload.ts` | The page's data island: `ReviewPageData` and `reviewPageData`, which builds every fact the browser renders — the blocks, the stored comments, the outline and its change marks, the step progress, the highlighted code, the caps, and who is asking — from one read of the plan, with no server or socket in sight. |
|
|
968
|
+
| `web-write.ts` | The review's two write paths over the plan store: `saveOwnerAnnotations` (the owner's save, which returns the plan it wrote) and `postReviewerAnnotations` (a reviewer's post, signed and scoped to its link), sharing one 404/409 check so a comment can never land beside text nobody read. |
|
|
954
969
|
| `web-share.ts` | Pure sharing rules: the reachable LAN addresses from an injected snapshot, one tokenized URL, the link table (mint, find, revoke, list, state) with its cap, and the reasons a share is unavailable. |
|
|
955
970
|
| `web-share-session.ts` | The shared listener: its bound address, the link table over the session's tokens, `ShareState`, and invite/revoke/stop/close, injected with the request handler, role map, and frame hooks. |
|
|
956
|
-
| `web-keys.ts` | The client's keyboard as typed source (`WEB_KEYS_JS`): `
|
|
971
|
+
| `web-keys.ts` | The client's keyboard as typed source (`WEB_KEYS_JS`), and the one table the page's keys come from: `webKeys()` gives each binding its keys, the shortcut sheet's sentence, the sidebar hint's words and whether the page offers it — a function rather than a constant, because the bundle carries a body and `String()` of a table would drop the closures — read by `handleKey` (Esc peels the composer before closing, a field and the sheet own their keys, `s` is the write the page offers) and by the sheet and hint line the page renders; plus `rememberSelection`, which keeps the text a reader selected as the next comment's quote, and the `keyRows`/`surfaceOf` the handler and the page's own sheet both read, spliced with them for the same reason. |
|
|
957
972
|
| `web-settle.ts` | The client's settlement as typed source (`WEB_SETTLE_JS`): `submitAnnotations` — one POST shape whose 409 disables the writes, offers a reload and says the server's own sentence — and the four ways a review ends, of which only the owner's save, approve and close settle it. |
|
|
958
973
|
| `web-plan.ts` | The client's frame as typed source (`WEB_PLAN_JS`, `renderPlanFrame`): it draws every block and its comment rail, then puts the reader back — focus only when it was inside the plan, the active block in place rather than by scrolling. Reads arrive as getters, mutations leave as callbacks, so the bundle keeps every variable. |
|
|
959
974
|
| `web-composer.ts` | The client's composer lifecycle as typed source (`WEB_COMPOSER_JS`): `closeComposer` (over one `ComposerDeps` the client builds where the state is), `reopenComposer` for the draft a reload interrupted, and `commentOnActive`, which returns the index so the client keeps its one `activeIndex`. |
|
|
@@ -995,9 +1010,11 @@ Twelve co-located suites, declared in `test/suite-manifest.ts`:
|
|
|
995
1010
|
`todo`, the session goal, and the call/result renderings.
|
|
996
1011
|
- `review-prompt.test.ts` — the compact remote prompt and its reply parser
|
|
997
1012
|
against one action table.
|
|
998
|
-
- `web
|
|
999
|
-
|
|
1000
|
-
|
|
1013
|
+
- `web-*.test.ts` — the browser review, one capability per suite: the comment
|
|
1014
|
+
codec and the router (`web-router`), the page roles and its shell (`web-page`),
|
|
1015
|
+
the client script's rules (`web-client`, `web-outline`, `web-composer`,
|
|
1016
|
+
`web-settlement`), the stream and the plan-file poll (`web-stream`), the
|
|
1017
|
+
loopback session (`web-session`), and the share rules (`web-shared-review`).
|
|
1001
1018
|
- `web-behaviour.test.ts` — the served review, driven in a real browser through
|
|
1002
1019
|
the router: the blocks and the plan's name, a block's comment control opening the
|
|
1003
1020
|
composer with Escape peeling it before the review, a stored comment in its block's
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# serve — the session's one loopback server
|
|
2
|
+
|
|
3
|
+
## Capabilities
|
|
4
|
+
|
|
5
|
+
| Capability | How | What you get |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| One port for every page of a session | automatic: the first page that opens binds it | the file browser at `/files/`, the board at `/board/`, and a review at `/plan/<id>/` answer on one origin, so a bookmark, a firewall rule, and a port to remember are one thing |
|
|
8
|
+
| One chip for that port | `◉ <port>` in the status bar | the session's port while anything is served on it, and nothing when no page is open |
|
|
9
|
+
| A port that survives a restart | `serve.port` in `bytefly.json` | the same address after `/resume`, so a saved URL still opens the page it names |
|
|
10
|
+
| A second session on its own port | automatic: servers are keyed by Pi session | a WeChat peer conversation never answers on this session's port |
|
|
11
|
+
| One page reachable on the LAN, on request | the asking page calls `mount.share()` | a teammate's link names exactly one address, and only that page answers there |
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
There is nothing to run: `serve` is infrastructure, and the pages that use it are
|
|
16
|
+
`/board`, `/browse`, and `/plan review`.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
/board # http://127.0.0.1:4780/board/?token=…
|
|
20
|
+
/browse # http://127.0.0.1:4780/files/
|
|
21
|
+
/plan review # http://127.0.0.1:4780/plan/3f9c21aa/?token=…
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
All three answer on `:4780`; the footer shows `◉ 4780` while any of them is open
|
|
25
|
+
and drops the chip when the last one closes.
|
|
26
|
+
|
|
27
|
+
## Routes
|
|
28
|
+
|
|
29
|
+
The server holds prefixes, not pages. Each one is claimed by the extension that
|
|
30
|
+
serves it, and a request reaches that extension with its prefix stripped:
|
|
31
|
+
|
|
32
|
+
| Prefix | Served by | Note |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `/files/` | `file-browse` | the file browser: its tree, its file pages, its own script, and the links it rewrites out of Markdown, all built from this prefix; `/files/browse/<path>` is one directory |
|
|
35
|
+
| `/board/` | `board` | the read-only board page, with its token in the query |
|
|
36
|
+
| `/plan/<id>/` | `plan` | one review; the id is the review's, so two reviews never share a namespace |
|
|
37
|
+
|
|
38
|
+
Anything else is a `404` with a plain-text body, and a prefix named without its
|
|
39
|
+
trailing slash (`/board`) is redirected to the slashed form, because a page's own
|
|
40
|
+
links are relative to its directory. A prefix another page already holds is
|
|
41
|
+
refused by name rather than served twice.
|
|
42
|
+
|
|
43
|
+
## Behaviour by mode
|
|
44
|
+
|
|
45
|
+
The server runs in every mode, but the chip needs a UI: RPC and JSON sessions serve
|
|
46
|
+
their pages and skip it. Nothing here reads or writes the transcript.
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
The `serve` section of the shared `bytefly.json` (global, then project, project wins:
|
|
51
|
+
|
|
52
|
+
| Key | Default | What it does |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `port` | `0` | `0` takes one free port in `4780`–`5779` for the session and remembers it; a positive value pins the session's server so a bookmarked address survives a restart |
|
|
55
|
+
|
|
56
|
+
The pin belongs to the first session in a process that binds: a second session — a
|
|
57
|
+
WeChat peer conversation loads every extension except the bridge itself — takes an
|
|
58
|
+
OS-chosen port instead of failing on a port the host already holds. A second
|
|
59
|
+
*process* that finds the pinned port taken is told so rather than moved silently off
|
|
60
|
+
the address a bookmark names.
|
|
61
|
+
|
|
62
|
+
## Security
|
|
63
|
+
|
|
64
|
+
The server binds `127.0.0.1`. The unit of exposure is the mount: a prefix answers
|
|
65
|
+
only what its page routes, and each page keeps its own `Host` allowlist, its own
|
|
66
|
+
token, and its own method list in `lib/http`.
|
|
67
|
+
|
|
68
|
+
A shared review is the one exception, and it is the reviewing page's own: it asks
|
|
69
|
+
for a listener on a single LAN address, and that listener carries only the prefixes
|
|
70
|
+
that asked for it. `file-browse` and `board` are never reachable that way, and a page
|
|
71
|
+
that stops sharing leaves the listener at once — the socket closes with the last page
|
|
72
|
+
that was still sharing it.
|
|
73
|
+
|
|
74
|
+
## Limitations
|
|
75
|
+
|
|
76
|
+
- A page needs `serve` loaded. Without it every page refuses with a sentence naming
|
|
77
|
+
the extension, instead of binding a rival socket.
|
|
78
|
+
- One pin is one port for the whole session: two repositories running pages on this
|
|
79
|
+
machine at once need a project-level `serve.port` each, or neither pinned.
|
|
80
|
+
- The port moves to an OS-chosen one when the pinned port is taken, so an old
|
|
81
|
+
bookmark keeps failing until the setting or the other process is dealt with.
|
|
82
|
+
|
|
83
|
+
## Non-goals
|
|
84
|
+
|
|
85
|
+
- It serves no page of its own, and registers no tool and no command.
|
|
86
|
+
- It is not a general web server: loopback, a page's own routes, one optional LAN
|
|
87
|
+
address per shared page, and nothing else.
|
|
88
|
+
- It does not proxy or rewrite; a page's paths are its own, under its prefix.
|
|
89
|
+
|
|
90
|
+
## Pi integration
|
|
91
|
+
|
|
92
|
+
- Registers `pi.events.on(HTTP_MOUNT_CHANNEL, …)` in the factory — registration only.
|
|
93
|
+
Binding happens on the first mount, never at load and never on `session_start`.
|
|
94
|
+
- `session_start` records the session's working directory and its context, so the
|
|
95
|
+
pin is read from that session's config and the chip is drawn in its UI.
|
|
96
|
+
- `session_shutdown` closes the session's socket, its LAN listener, and its chip.
|
|
97
|
+
It is idempotent.
|
|
98
|
+
|
|
99
|
+
## Design notes
|
|
100
|
+
|
|
101
|
+
A session has one port, so the socket cannot belong to a page. The page asks for a
|
|
102
|
+
prefix over `pi.events` — the mechanism the repository already uses for a
|
|
103
|
+
cross-extension request that must be answered — and `lib/http` owns the socket, the
|
|
104
|
+
dispatch, the body cap per mount, and the idle grace per mount.
|
|
105
|
+
|
|
106
|
+
The handshake is *claimed*, not awaited: `server.listen` cannot answer synchronously,
|
|
107
|
+
so the owner responds with the pending bind and only a request nobody claimed is
|
|
108
|
+
refused. Without that distinction a bind that is still starting looks exactly like a
|
|
109
|
+
session with no server.
|
|
110
|
+
|
|
111
|
+
## Files
|
|
112
|
+
|
|
113
|
+
| Path | Responsibility |
|
|
114
|
+
|---|---|
|
|
115
|
+
| [`../../extensions/serve/index.ts`](../../extensions/serve/index.ts) | The Pi wiring: the bus listener, the per-session directory and context, the port chip, and the shutdown |
|
|
116
|
+
| [`../../extensions/serve/registry.ts`](../../extensions/serve/registry.ts) | The session's servers: binding on first mount, the pin's owner, the remembered port, prefix refusals, the LAN listener a shared page borrows, and closing with the last mount |
|
|
117
|
+
| [`../../extensions/serve/config.ts`](../../extensions/serve/config.ts) | The `serve` section: `port`, its clamp, and the two documents it resolves over |
|
|
118
|
+
|
|
119
|
+
## Testing
|
|
120
|
+
|
|
121
|
+
`config.test.ts`, `registry.test.ts`, and `extension.test.ts`, declared under
|
|
122
|
+
`serve` in `test/suite-manifest.ts`.
|
|
123
|
+
|
|
124
|
+
The registry cases bind real sockets on `127.0.0.1`: one port for two pages of a
|
|
125
|
+
session, a second session on its own port, a refused duplicate prefix, the socket
|
|
126
|
+
closing with its last page and reopening where it was, the pin belonging to the
|
|
127
|
+
first session that binds, and a LAN share reaching one page only. The extension
|
|
128
|
+
cases drive the shipped bus path through `requestMount`, including the chip and the
|
|
129
|
+
shutdown.
|
|
@@ -16,7 +16,7 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
|
|
|
16
16
|
|
|
17
17
|
| Capability | How | What you get | Status |
|
|
18
18
|
|---|---|---|---|
|
|
19
|
-
| Membership | `session join [label]`, `/team join [<label>]`, `/board`,
|
|
19
|
+
| Membership | `session join [label]`, `/team join [<label>]`, `/board`, the first `board` claim, the board's `w` | The join: this session is on the bus under a stable label peers address, and listens for mail unless told not to. A label given to `session join` or `/team join` is also Pi's session name, so the footer and `/resume` agree with the record; `/board` joins as the page's seat (`team-lead`) or, on a page it is only watching, under its own `team-agent-<id>` name; the first `board` claim joins under that same name, and renames nothing; `w` joins under the name the bus derives and writes nothing to Pi. | Shipped |
|
|
20
20
|
| Peer discovery | `session status` | Every active session with its unread count and claims, plus this session's identity, in one read — and a session whose process is gone says so, its paths free from that read on. | Unreleased |
|
|
21
21
|
| Live board | `/sessions` | Sessions, this session's unread mail, and every claim held on the repository, on one screen. | Shipped |
|
|
22
22
|
| Board actions | `/sessions` (`w`, `x`) | Start or stop listening from the board, or release an own claim behind a confirmation, without leaving it. | Shipped |
|
package/docs/extensions/team.md
CHANGED
|
@@ -20,7 +20,7 @@ owner whose label nothing answers to, and a teammate that is not listening.*
|
|
|
20
20
|
|
|
21
21
|
| Capability | How | What you get |
|
|
22
22
|
|---|---|---|
|
|
23
|
-
| Join the team | `/board` as the lead, `/team join <label>` as a teammate, or
|
|
23
|
+
| Join the team | `/board` as the lead, `/team join <label>` as a teammate, or the first card you claim | The session is on the team under a name peers address, listening, and `/team` and the Team tab list it |
|
|
24
24
|
| Hand a feature to a running session | `team` action `assign` | The card is `doing`, owned by that session, and a handoff is in its inbox |
|
|
25
25
|
| Tell the peer what to do first | `team` action `assign` with `note` | The handoff carries the card, its paths, its plan, and that line |
|
|
26
26
|
| Hand a feature to whoever is free | `team` action `offer` | Every other live session gets an event naming the card; the card stays unowned until one claims it, and it records when the offer was made, so a lead can tell an offer nobody took from a card nobody has looked at |
|
|
@@ -58,15 +58,15 @@ its surface is the point: `board` and `team` join this session for you.
|
|
|
58
58
|
| You are | Action | What happens |
|
|
59
59
|
|---|---|---|
|
|
60
60
|
| The lead | `/board` | The session joins as `team-lead` — the page's own seat name — and listens, so a handoff or an offer is reported at its next turn |
|
|
61
|
-
| A teammate |
|
|
62
|
-
|
|
|
63
|
-
| A viewer | `/board` on a session that is not serving | The page is somebody else's to serve, so this session joins under its own
|
|
61
|
+
| A teammate | the first card it claims | The session joins as its own `team-agent-<id>` name and listens, and Pi's footer keeps the name it already had |
|
|
62
|
+
| A teammate who wants a name | `/team join billing-core` | The session joins as `billing-core`, and Pi's footer and `/resume` show that name — the one join that renames |
|
|
63
|
+
| A viewer | `/board` on a session that is not serving | The page is somebody else's to serve, so this session joins under its own name instead of the seat |
|
|
64
64
|
|
|
65
65
|
Joining is listening: mail is reported at that session's next turn, and **no turn is
|
|
66
66
|
raised** for it, so being on the team never spends a turn. `/team` says who is live and
|
|
67
|
-
who is live but not listening. `/board status`
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
who is live but not listening. `/board status` reports the team this session is on — which
|
|
68
|
+
label, and whether mail reaches it — whichever door put it there, and prints no team clause
|
|
69
|
+
only when this session never joined at all.
|
|
70
70
|
|
|
71
71
|
**The seat is not a session.** The page is a view: it has one name, `team-lead`, and an id
|
|
72
72
|
of its own derived from it, and it writes nothing, so it holds no card for a lead to take
|
|
@@ -159,7 +159,7 @@ decision the plain unassigned row never offered: re-offer, `assign` it, or take
|
|
|
159
159
|
|
|
160
160
|
- **Nothing here can start a peer's turn.** A message is reported at the recipient's next
|
|
161
161
|
turn; a session that is not listening at all reads it only when it reads its inbox.
|
|
162
|
-
- **`to` must be on the team.** A session that never opened the board, joined, or
|
|
162
|
+
- **`to` must be on the team.** A session that never opened the board, joined, or claimed a
|
|
163
163
|
card cannot be handed a card, and an offer to nobody says so.
|
|
164
164
|
- **`team-lead` is a label, not a person.** The seat is not a session, so its actor id
|
|
165
165
|
`board:team-lead` answers no message; the label is the one the session serving the page
|
|
@@ -207,9 +207,9 @@ decision the plain unassigned row never offered: re-offer, `assign` it, or take
|
|
|
207
207
|
and must work with the bus disabled; `team` is the lead's round and is the only thing
|
|
208
208
|
that needs the bus. If the round ever stops needing it, `team` folds back in.
|
|
209
209
|
- **Joining is the bus's, wearing `board`'s clothes.** A lead should not have to learn a
|
|
210
|
-
second tool to be on a team, so `/board` joins,
|
|
211
|
-
the one naming action. `wake` stays a bus-level choice and is never
|
|
212
|
-
behalf, because it spends turns.
|
|
210
|
+
second tool to be on a team, so `/board` joins, the first claimed card joins, and
|
|
211
|
+
`/team join` is the one naming action. `wake` stays a bus-level choice and is never
|
|
212
|
+
armed on a user's behalf, because it spends turns.
|
|
213
213
|
- **Push and pull are two verbs.** `assign` writes the owner and sends one message;
|
|
214
214
|
`offer` broadcasts and writes nothing. One verb with an optional `to` would have two
|
|
215
215
|
outcomes and one details shape, and a reader could not tell which happened.
|
|
@@ -9,21 +9,25 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { plural } from "../../lib/board/round-text.ts";
|
|
12
|
+
import type { BoardTeam } from "../../lib/board/session.ts";
|
|
12
13
|
import type { JoinResult } from "../../lib/session-tool.ts";
|
|
13
14
|
import { createTeamJoin, type TeamJoin } from "../../lib/session-tool.ts";
|
|
14
|
-
import {
|
|
15
|
+
import { teamLine } from "./join.ts";
|
|
15
16
|
import type { BoardPage, BoardPageStatus } from "./page.ts";
|
|
16
17
|
|
|
17
18
|
/** The command's name, which is also the word a user types. */
|
|
18
19
|
export const BOARD_COMMAND = "board";
|
|
19
20
|
|
|
20
21
|
/** The words the command accepts, in the order a user is likely to want them. */
|
|
21
|
-
|
|
22
|
+
const BOARD_WORDS = ["status", "stop", "team"] as const;
|
|
22
23
|
|
|
23
|
-
export function registerCommands(
|
|
24
|
+
export function registerCommands(
|
|
25
|
+
pi: ExtensionAPI,
|
|
26
|
+
session: BoardTeam,
|
|
27
|
+
page: BoardPage,
|
|
28
|
+
deps: { team?: TeamJoin } = {},
|
|
29
|
+
): void {
|
|
24
30
|
const team = deps.team ?? createTeamJoin(pi);
|
|
25
|
-
/** What the last `/board` did about the team, for the status line. */
|
|
26
|
-
let joined: JoinResult | undefined;
|
|
27
31
|
pi.registerCommand(BOARD_COMMAND, {
|
|
28
32
|
description: "Open the shared board in a local browser: /board [team | status | stop]",
|
|
29
33
|
getArgumentCompletions: (prefix: string) => {
|
|
@@ -35,7 +39,7 @@ export function registerCommands(pi: ExtensionAPI, page: BoardPage, deps: { team
|
|
|
35
39
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
36
40
|
const word = args.trim().toLowerCase();
|
|
37
41
|
if (word === "status") {
|
|
38
|
-
ctx.ui.notify(statusLine(await page.status(),
|
|
42
|
+
ctx.ui.notify(statusLine(await page.status(), session.team.last()), "info");
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
if (word === "stop") {
|
|
@@ -60,7 +64,11 @@ export function registerCommands(pi: ExtensionAPI, page: BoardPage, deps: { team
|
|
|
60
64
|
);
|
|
61
65
|
return;
|
|
62
66
|
}
|
|
63
|
-
|
|
67
|
+
const stopped = await page.stop();
|
|
68
|
+
// The word is this session saying it serves nothing now: the page clears the seat too
|
|
69
|
+
// when it closes itself (the idle grace), and both writes are the same fact.
|
|
70
|
+
session.serve(undefined);
|
|
71
|
+
ctx.ui.notify(stopped ? "board: stopped" : "board: not running", "info");
|
|
64
72
|
return;
|
|
65
73
|
}
|
|
66
74
|
if (word !== "" && word !== "team") {
|
|
@@ -71,18 +79,21 @@ export function registerCommands(pi: ExtensionAPI, page: BoardPage, deps: { team
|
|
|
71
79
|
return;
|
|
72
80
|
}
|
|
73
81
|
const started = await page.start(ctx, word === "team" ? "team" : "board");
|
|
74
|
-
// A failed start has already said why. Otherwise the
|
|
75
|
-
//
|
|
76
|
-
//
|
|
82
|
+
// A failed start has already said why. Otherwise the seat is set from the page's own
|
|
83
|
+
// answer — the page a session serves is a role, and every name this session writes and
|
|
84
|
+
// is addressed by follows from it — and the join happens here, because opening the page
|
|
85
|
+
// is the lead's one action.
|
|
77
86
|
if (started.kind === "failed") return;
|
|
78
|
-
|
|
87
|
+
session.serve(started.kind === "served" ? started.seat : undefined);
|
|
88
|
+
const joined = await team.join(ctx, { label: session.who(ctx, ctx.cwd).label });
|
|
89
|
+
session.team.record(joined);
|
|
79
90
|
// The address is on the line because the browser is not the only way in: a machine
|
|
80
91
|
// with no default browser prints it here, and a tab closed by mistake is one
|
|
81
92
|
// command from being opened again.
|
|
82
93
|
ctx.ui.notify(
|
|
83
94
|
started.kind === "attached"
|
|
84
95
|
? `board: ${started.seat} is serving this repository on :${started.port} — viewing · open ${started.url}${teamLine(joined)}`
|
|
85
|
-
: `board: ${started.seat} serving ${started.
|
|
96
|
+
: `board: ${started.seat} serving ${started.repo} on :${started.port} · open ${started.url}${teamLine(joined)}`,
|
|
86
97
|
"info",
|
|
87
98
|
);
|
|
88
99
|
},
|
|
@@ -93,7 +104,7 @@ export function registerCommands(pi: ExtensionAPI, page: BoardPage, deps: { team
|
|
|
93
104
|
export function statusLine(status: BoardPageStatus, team?: JoinResult): string {
|
|
94
105
|
if (status.running) {
|
|
95
106
|
const watching = plural(status.watchers ?? 0, "tab");
|
|
96
|
-
return `board: ${status.seat} serving on :${status.port} · ${watching} watching · ${status.where}${teamLine(team)}`;
|
|
107
|
+
return `board: ${status.seat} serving ${status.repo} on :${status.port} · ${watching} watching · ${status.where}${teamLine(team)}`;
|
|
97
108
|
}
|
|
98
109
|
if (status.stale === true) {
|
|
99
110
|
return `board: watching ${status.seat} on :${status.port} — that note answers nobody; /board stop drops it${teamLine(team)}`;
|
|
@@ -101,5 +112,7 @@ export function statusLine(status: BoardPageStatus, team?: JoinResult): string {
|
|
|
101
112
|
if (status.attached === true) {
|
|
102
113
|
return `board: watching ${status.seat} on :${status.port} — nothing to stop here${teamLine(team)}`;
|
|
103
114
|
}
|
|
104
|
-
|
|
115
|
+
// The team outlives the page: a session that claimed a card is on it with no page open, so
|
|
116
|
+
// the line says which team this session is on whichever door put it there.
|
|
117
|
+
return `board: not running — /board opens it${teamLine(team)}`;
|
|
105
118
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
18
|
+
import { requestMount } from "../../lib/http/index.ts";
|
|
18
19
|
import { registerCommands } from "./commands.ts";
|
|
19
20
|
import { type BoardPage, createBoardPage } from "./page.ts";
|
|
20
21
|
import { type BoardSession, openBoardSession } from "../../lib/board/session.ts";
|
|
@@ -30,9 +31,20 @@ export interface BoardDeps {
|
|
|
30
31
|
export default function board(pi: ExtensionAPI, deps: BoardDeps = {}): void {
|
|
31
32
|
if (!isExtensionEnabled("board")) return;
|
|
32
33
|
const session = deps.session ?? openBoardSession(pi);
|
|
33
|
-
|
|
34
|
+
/** The session the page serves as; the mount lands on this session's one server. */
|
|
35
|
+
let sessionId = "";
|
|
36
|
+
const page =
|
|
37
|
+
deps.page ??
|
|
38
|
+
createBoardPage(session, {
|
|
39
|
+
mount: (spec) => requestMount(pi, spec),
|
|
40
|
+
session: () => sessionId,
|
|
41
|
+
});
|
|
34
42
|
registerTools(pi, session);
|
|
35
|
-
registerCommands(pi, page);
|
|
43
|
+
registerCommands(pi, session, page);
|
|
44
|
+
|
|
45
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
46
|
+
sessionId = ctx.sessionManager.getSessionId();
|
|
47
|
+
});
|
|
36
48
|
|
|
37
49
|
pi.on("session_shutdown", async () => {
|
|
38
50
|
await page.closeAll();
|