@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- 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 +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- 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/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -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-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- 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-router.ts +18 -9
- 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 +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
package/extensions/board/join.ts
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The board's side of the team: how a join reads on a `/board` line.
|
|
3
3
|
*
|
|
4
4
|
* The user's surface is `/board` and `team`; the session bus is the transport under
|
|
5
|
-
* them. So the board joins on the user's behalf — when the page opens, and
|
|
6
|
-
*
|
|
5
|
+
* them. So the board joins on the user's behalf — when the page opens, and the first
|
|
6
|
+
* time this session claims a card — and never asks anybody to run a bus action.
|
|
7
7
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
* request through `lib/session-tool.ts` (a command context cannot call a tool).
|
|
8
|
+
* The join itself is `lib/board/join.ts`'s `joinOnce`, because a claim joins from either
|
|
9
|
+
* tool; this module holds what is the board's own: what a `/board` line says about it.
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
12
|
import type { JoinResult } from "../../lib/session-tool.ts";
|
|
14
|
-
import { DEFAULT_SEAT } from "./seat.ts";
|
|
15
13
|
|
|
16
|
-
/**
|
|
17
|
-
* The join itself is `lib/session-tool.ts`'s, because `team` asks for it too; this
|
|
18
|
-
* module only holds what is the board's — the label it lands on, and how the outcome
|
|
19
|
-
* reads on a `/board` line.
|
|
20
|
-
*/
|
|
21
14
|
export type { TeamJoin } from "../../lib/session-tool.ts";
|
|
22
15
|
|
|
23
|
-
/**
|
|
24
|
-
* The label `/board` lands on: the page's seat when this session serves it, and
|
|
25
|
-
* nothing when it opened somebody else's page, so a session keeps its own name.
|
|
26
|
-
*/
|
|
27
|
-
export function joinLabel(served: boolean): string | undefined {
|
|
28
|
-
return served ? DEFAULT_SEAT : undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
16
|
/**
|
|
32
17
|
* What the join did, as a clause for a `/board` line.
|
|
33
18
|
*
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board's page, loaded when `/board` first needs it.
|
|
3
|
+
*
|
|
4
|
+
* The page module owns the HTTP surface, the snapshots, and the view; none of it
|
|
5
|
+
* is needed to register the command or the tool. The factory therefore returns a
|
|
6
|
+
* proxy over `lib/lazy`: every command path loads the real page once, and
|
|
7
|
+
* `closeAll` on shutdown only closes a page that was already opened, so a
|
|
8
|
+
* session that never touched the board imports none of it.
|
|
9
|
+
*
|
|
10
|
+
* The loader is a seam: a test drives the proxy without a socket.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { BoardSession } from "../../lib/board/session.ts";
|
|
14
|
+
import { lazy } from "../../lib/lazy.ts";
|
|
15
|
+
import type { BoardPage, BoardPageDeps } from "./page.ts";
|
|
16
|
+
|
|
17
|
+
/** The page module, as the proxy loads it. */
|
|
18
|
+
export type BoardPageModule = typeof import("./page.ts");
|
|
19
|
+
|
|
20
|
+
/** How the proxy reaches the page module; production uses the dynamic import. */
|
|
21
|
+
export type BoardPageLoader = () => Promise<BoardPageModule>;
|
|
22
|
+
|
|
23
|
+
export function createLazyBoardPage(
|
|
24
|
+
session: BoardSession,
|
|
25
|
+
deps: BoardPageDeps,
|
|
26
|
+
load: BoardPageLoader = () => import("./page.ts"),
|
|
27
|
+
): BoardPage {
|
|
28
|
+
const page = lazy(async () => (await load()).createBoardPage(session, deps));
|
|
29
|
+
return {
|
|
30
|
+
start: async (ctx, view) => (await page.get()).start(ctx, view),
|
|
31
|
+
stop: async () => (await page.get()).stop(),
|
|
32
|
+
status: async () => (await page.get()).status(),
|
|
33
|
+
dropStale: async (ctx) => (await page.get()).dropStale(ctx),
|
|
34
|
+
// Shutdown must not start a load: a page that never opened has nothing to close.
|
|
35
|
+
closeAll: async () => {
|
|
36
|
+
const opened = page.peek();
|
|
37
|
+
if (opened !== undefined) await opened.closeAll();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
package/extensions/board/page.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The page's lifecycle: one
|
|
2
|
+
* The page's lifecycle: one mount per session, started by `/board` and given back by
|
|
3
3
|
* `/board stop`, by the idle grace once the last tab has been gone for a minute, or by
|
|
4
4
|
* `session_shutdown`.
|
|
5
5
|
*
|
|
6
|
-
* Only the lifecycle lives here — the state machine, the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* nothing of the page's markup, and
|
|
6
|
+
* Only the lifecycle lives here — the state machine, the prefix it holds, the note that
|
|
7
|
+
* names it, and the three closes. The session's port belongs to `serve`, so nothing here
|
|
8
|
+
* picks one. What the page *reads* is `snapshot.ts`, and it writes nothing: how a request
|
|
9
|
+
* reaches the snapshot is `transport/`, the router knows nothing of the page's markup, and
|
|
10
|
+
* this is still the one place they meet.
|
|
10
11
|
*
|
|
11
12
|
* All state lives in this closure, not at module scope: Pi loads each extension with an
|
|
12
13
|
* isolated module cache, so a module variable would be one copy per extension.
|
|
@@ -15,11 +16,10 @@
|
|
|
15
16
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
17
|
import type { BoardStore } from "../../lib/board/store.ts";
|
|
17
18
|
import { openInBrowser } from "../../lib/browser.ts";
|
|
18
|
-
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
19
|
-
import { paint } from "../../lib/tui/tokens.ts";
|
|
20
19
|
import type { RepeatTimer } from "../../lib/timer.ts";
|
|
21
20
|
import type { BoardSession } from "../../lib/board/session.ts";
|
|
22
21
|
import {
|
|
22
|
+
BOARD_BASE,
|
|
23
23
|
clearPointer,
|
|
24
24
|
PAGE_FILE,
|
|
25
25
|
type PagePointer,
|
|
@@ -29,25 +29,17 @@ import {
|
|
|
29
29
|
settlePointer,
|
|
30
30
|
urlOf,
|
|
31
31
|
} from "../../lib/board/page-pointer.ts";
|
|
32
|
-
import { mintToken } from "../../lib/http/index.ts";
|
|
32
|
+
import { mintToken, type MountSource } from "../../lib/http/index.ts";
|
|
33
|
+
import { messageOf } from "../../lib/errors.ts";
|
|
33
34
|
import type { IdleReason } from "../../lib/http/sink.ts";
|
|
34
35
|
import { join } from "node:path";
|
|
35
|
-
import
|
|
36
|
-
import { pageActor } from "
|
|
36
|
+
import type { Actor } from "../../lib/board/rules.ts";
|
|
37
|
+
import { pageActor } from "../../lib/board/seat.ts";
|
|
37
38
|
import { viewOf } from "./snapshot.ts";
|
|
38
39
|
import { type BoardServer, createBoardServer } from "./transport/server.ts";
|
|
39
40
|
import { BOARD_CLIENT_JS } from "./view/client.ts";
|
|
40
41
|
import { type PageView, renderBoardPage } from "./view/page.ts";
|
|
41
42
|
|
|
42
|
-
/**
|
|
43
|
-
* A free port in the range the other loopback pages use, so a session's pages cluster.
|
|
44
|
-
* Only the unpinned page draws one: `board.port` in `bytefly.json` states the port a
|
|
45
|
-
* bookmarked URL keeps.
|
|
46
|
-
*/
|
|
47
|
-
export function randomPort(): number {
|
|
48
|
-
return 4780 + Math.floor(Math.random() * 1000);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
43
|
/**
|
|
52
44
|
* How long the page waits for a tab after the last one leaves.
|
|
53
45
|
*
|
|
@@ -81,6 +73,8 @@ export interface BoardPageStatus {
|
|
|
81
73
|
seat?: string;
|
|
82
74
|
port?: number;
|
|
83
75
|
url?: string;
|
|
76
|
+
/** The repository's name, so `status` says which board the page is. */
|
|
77
|
+
repo?: string;
|
|
84
78
|
where?: string;
|
|
85
79
|
/** How many tabs are attached, while this session serves the page. */
|
|
86
80
|
watchers?: number;
|
|
@@ -94,14 +88,17 @@ export interface BoardPageStatus {
|
|
|
94
88
|
* page* is a third answer a `running: false` could not express.
|
|
95
89
|
*/
|
|
96
90
|
export type PageStart =
|
|
97
|
-
| { kind: "served"; seat: string; port: number; url: string; where: string }
|
|
98
|
-
| { kind: "attached"; seat: string; port: number; url: string; where: string }
|
|
91
|
+
| { kind: "served"; seat: string; port: number; url: string; repo: string; where: string }
|
|
92
|
+
| { kind: "attached"; seat: string; port: number; url: string; repo: string; where: string }
|
|
99
93
|
| { kind: "failed"; reason: string };
|
|
100
94
|
|
|
101
95
|
/** Test seams and the one place a caller may redirect the browser. */
|
|
102
96
|
export interface BoardPageDeps {
|
|
103
97
|
open?: (url: string) => void;
|
|
104
|
-
|
|
98
|
+
/** Where the prefix comes from: the session's server, or a suite's own. */
|
|
99
|
+
mount: MountSource;
|
|
100
|
+
/** The Pi session this page serves as, so the prefix lands on its socket. */
|
|
101
|
+
session: () => string;
|
|
105
102
|
interval?: RepeatTimer;
|
|
106
103
|
/** Grace after the last tab leaves; defaults to {@link IDLE_STOP_MS}. */
|
|
107
104
|
idleMs?: number;
|
|
@@ -109,14 +106,13 @@ export interface BoardPageDeps {
|
|
|
109
106
|
firstIdleMs?: number;
|
|
110
107
|
/** Whether a page another session left a note about is answering; a test drives it. */
|
|
111
108
|
probe?: ProbeFetch;
|
|
112
|
-
/** How the `board` section answers; a test drives it without a config file. */
|
|
113
|
-
config?: (cwd: string) => BoardConfig;
|
|
114
109
|
}
|
|
115
110
|
|
|
116
111
|
/** The page as `/board` and `session_shutdown` drive it. */
|
|
117
112
|
export interface BoardPage {
|
|
118
113
|
start: (ctx: ExtensionContext, view?: PageView) => Promise<PageStart>;
|
|
119
|
-
|
|
114
|
+
/** Give the prefix back; the session's server closes with its last mount. */
|
|
115
|
+
stop: () => Promise<boolean>;
|
|
120
116
|
/** What `/board status` prints: served here, watched, watched and dead, or absent. */
|
|
121
117
|
status: () => Promise<BoardPageStatus>;
|
|
122
118
|
/** Drop the note of a page that has stopped answering. False when it still answers. */
|
|
@@ -124,36 +120,34 @@ export interface BoardPage {
|
|
|
124
120
|
closeAll: () => Promise<void>;
|
|
125
121
|
}
|
|
126
122
|
|
|
127
|
-
export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps
|
|
128
|
-
let running: { server: BoardServer; store: BoardStore; token: string } | undefined;
|
|
123
|
+
export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps): BoardPage {
|
|
124
|
+
let running: { server: BoardServer; store: BoardStore; token: string; seat: Actor } | undefined;
|
|
129
125
|
/** The page another session serves, when this session opened that one instead. */
|
|
130
126
|
let attachedTo: PagePointer | undefined;
|
|
131
|
-
let sessionPort: number | undefined;
|
|
132
|
-
// The page is a seat, not a session: one name and one id for its whole life, so a card
|
|
133
|
-
// a browser claimed is still the seat's after the session that served it ends.
|
|
134
|
-
const seat = pageActor();
|
|
135
|
-
|
|
136
127
|
const storeOf = async (ctx: ExtensionContext): Promise<BoardStore> => runtime.store(ctx.cwd);
|
|
137
128
|
|
|
138
129
|
/**
|
|
139
130
|
* Close the page this session serves, and take its note with it.
|
|
140
131
|
*
|
|
141
|
-
* One close for both callers: `/board stop` has a context and clears the
|
|
142
|
-
* clean `session_shutdown` has none. The note must leave either way — a note nobody
|
|
132
|
+
* One close for both callers: `/board stop` has a context and clears the status line, and
|
|
133
|
+
* a clean `session_shutdown` has none. The note must leave either way — a note nobody
|
|
143
134
|
* answers makes every open tab reconnect forever and costs the next `/board` a probe
|
|
144
135
|
* and a takeover before it serves.
|
|
145
136
|
*/
|
|
146
|
-
const closeRunning = async (
|
|
137
|
+
const closeRunning = async (): Promise<boolean> => {
|
|
147
138
|
if (running === undefined) return false;
|
|
148
139
|
const { server, store, token } = running;
|
|
149
140
|
running = undefined;
|
|
150
141
|
await server.close();
|
|
151
142
|
await clearPointer(store.dir, token);
|
|
152
|
-
|
|
143
|
+
// A page that closed is no seat: the session's next card is written as itself. This is
|
|
144
|
+
// the one place all three closes pass — `/board stop`, the idle grace, and shutdown — so
|
|
145
|
+
// a page nobody is looking at cannot leave the session serving in name only.
|
|
146
|
+
runtime.serve(undefined);
|
|
153
147
|
return true;
|
|
154
148
|
};
|
|
155
149
|
|
|
156
|
-
const stop = (
|
|
150
|
+
const stop = (): Promise<boolean> => closeRunning();
|
|
157
151
|
|
|
158
152
|
const start = async (ctx: ExtensionContext, view: PageView = "board"): Promise<PageStart> => {
|
|
159
153
|
// `/board` is how the page is summoned, and a page whose tab was closed is one
|
|
@@ -162,11 +156,9 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps = {})
|
|
|
162
156
|
(deps.open ?? openInBrowser)(view === "board" ? running.server.url : `${running.server.url}&view=team`);
|
|
163
157
|
return describe(running);
|
|
164
158
|
}
|
|
165
|
-
const config = (deps.config ?? loadBoardConfig)(ctx.cwd);
|
|
166
|
-
const pinned = config.port > 0 ? config.port : undefined;
|
|
167
159
|
/** One refusal for every way a start can fail, named by what failed. */
|
|
168
160
|
const failed = (error: unknown): PageStart => {
|
|
169
|
-
const reason =
|
|
161
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
170
162
|
ctx.ui.notify(`board: the page did not start (${reason})`, "error");
|
|
171
163
|
return { kind: "failed", reason };
|
|
172
164
|
};
|
|
@@ -179,6 +171,11 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps = {})
|
|
|
179
171
|
} catch (error) {
|
|
180
172
|
return failed(error);
|
|
181
173
|
}
|
|
174
|
+
// The page is a seat, not a session: one name and one id for its whole life, so a card a
|
|
175
|
+
// browser claimed is still the seat's after the session that served it ends. That name is
|
|
176
|
+
// the board's lead, so serving the page is what makes a session the lead — a repository
|
|
177
|
+
// that names one gets a page served under that name, and the gate accepts it.
|
|
178
|
+
const seat = pageActor(store.config().lead);
|
|
182
179
|
const open = (url: string) => (deps.open ?? openInBrowser)(view === "board" ? url : `${url}&view=team`);
|
|
183
180
|
/** Watch the page another session serves: its note is the URL, its seat the name. */
|
|
184
181
|
const attach = (pointer: PagePointer): PageStart => {
|
|
@@ -189,6 +186,7 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps = {})
|
|
|
189
186
|
seat: pointer.seat.label,
|
|
190
187
|
port: pointer.port,
|
|
191
188
|
url: urlOf(pointer),
|
|
189
|
+
repo: store.name(),
|
|
192
190
|
where: store.where(),
|
|
193
191
|
};
|
|
194
192
|
};
|
|
@@ -199,34 +197,44 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps = {})
|
|
|
199
197
|
attachedTo = undefined;
|
|
200
198
|
const actor = () => seat;
|
|
201
199
|
const now = () => runtime.now();
|
|
202
|
-
// Read above, so a pinned port names itself in a refusal; the port itself is
|
|
203
|
-
// remembered for the session so a closed and reopened page lands where it was.
|
|
204
|
-
const preferred = pinned ?? sessionPort ?? (deps.pickPort ?? randomPort)();
|
|
205
200
|
// Minted here rather than by the server: the note this session leaves has to carry it,
|
|
206
201
|
// and that is what a second session reads to attach instead of serving its own page.
|
|
207
202
|
const token = mintToken();
|
|
208
|
-
|
|
203
|
+
/** Whether this page has already said it cannot read the board. */
|
|
204
|
+
let blind = false;
|
|
205
|
+
// A frame the page cannot build is a dropped frame, not a crash: the ticker asks again
|
|
206
|
+
// in a second. Said once, because a failure that persists would otherwise become one
|
|
207
|
+
// notice per tick — and unsaid, a page that stopped following the board looks frozen.
|
|
208
|
+
const warnBlind = (error: unknown): void => {
|
|
209
|
+
if (blind) return;
|
|
210
|
+
blind = true;
|
|
211
|
+
ctx.ui.notify(`board: the page cannot read the board (${messageOf(error)}) — it will keep trying`, "warning");
|
|
212
|
+
};
|
|
209
213
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
ports: pinned === undefined ? [preferred, 0] : [pinned],
|
|
214
|
+
const started = await createBoardServer({
|
|
215
|
+
mount: deps.mount,
|
|
216
|
+
session: deps.session(),
|
|
214
217
|
token,
|
|
215
218
|
...(deps.interval === undefined ? {} : { interval: deps.interval }),
|
|
219
|
+
onError: warnBlind,
|
|
216
220
|
idleMs: deps.idleMs ?? IDLE_STOP_MS,
|
|
217
221
|
firstIdleMs: deps.firstIdleMs ?? IDLE_FIRST_MS,
|
|
218
222
|
onIdle: (reason) => void stopIdle(ctx, reason),
|
|
219
|
-
page: async (view) => renderBoardPage({ view }),
|
|
223
|
+
page: async (view) => renderBoardPage({ view, base: BOARD_BASE, repo: store.name(), root: store.root() }),
|
|
220
224
|
script: BOARD_CLIENT_JS,
|
|
221
225
|
view: () =>
|
|
222
226
|
viewOf({
|
|
223
227
|
store,
|
|
224
228
|
actor,
|
|
225
|
-
session: () => runtime.who(ctx, ctx.cwd),
|
|
226
229
|
now,
|
|
227
230
|
busDir: () => runtime.busDir(ctx.cwd),
|
|
228
231
|
}),
|
|
229
232
|
});
|
|
233
|
+
if (started.kind === "refused") {
|
|
234
|
+
ctx.ui.notify(`board: the page did not start (${started.reason})`, "error");
|
|
235
|
+
return { kind: "failed", reason: started.reason };
|
|
236
|
+
}
|
|
237
|
+
const server = started.server;
|
|
230
238
|
// The note and the seat: written last, on the port the server actually bound, and only if no
|
|
231
239
|
// other session got there first: the loser of that race attaches to the page that
|
|
232
240
|
// won rather than serving a second page nobody can find. A note that answers
|
|
@@ -263,22 +271,17 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps = {})
|
|
|
263
271
|
ctx.ui.notify(`board: the page did not start (${reason})`, "error");
|
|
264
272
|
return { kind: "failed", reason };
|
|
265
273
|
}
|
|
266
|
-
|
|
267
|
-
running = { server, store, token };
|
|
268
|
-
setChip(ctx, server.port);
|
|
274
|
+
running = { server, store, token, seat };
|
|
269
275
|
open(server.url);
|
|
270
276
|
return describe(running);
|
|
271
277
|
} catch (error) {
|
|
272
|
-
// A bind that failed has no server to close; a seat that could not be written does,
|
|
273
|
-
// and leaving it bound would hold a port no note names — invisible until shutdown.
|
|
274
|
-
await server?.close();
|
|
275
278
|
return failed(error);
|
|
276
279
|
}
|
|
277
280
|
};
|
|
278
281
|
|
|
279
282
|
/** The idle grace closes the page and says which grace it was, once. */
|
|
280
283
|
const stopIdle = async (ctx: ExtensionContext, reason: IdleReason): Promise<void> => {
|
|
281
|
-
if (!(await stop(
|
|
284
|
+
if (!(await stop())) return;
|
|
282
285
|
ctx.ui.notify(
|
|
283
286
|
reason === "first"
|
|
284
287
|
? "board: stopped — no tab ever opened; /board starts it again"
|
|
@@ -287,11 +290,12 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps = {})
|
|
|
287
290
|
);
|
|
288
291
|
};
|
|
289
292
|
|
|
290
|
-
const describe = (state: { server: BoardServer; store: BoardStore }): PageStart => ({
|
|
293
|
+
const describe = (state: { server: BoardServer; store: BoardStore; seat: Actor }): PageStart => ({
|
|
291
294
|
kind: "served",
|
|
292
|
-
seat: seat.label,
|
|
295
|
+
seat: state.seat.label,
|
|
293
296
|
port: state.server.port,
|
|
294
297
|
url: state.server.url,
|
|
298
|
+
repo: state.store.name(),
|
|
295
299
|
where: state.store.where(),
|
|
296
300
|
});
|
|
297
301
|
|
|
@@ -307,9 +311,10 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps = {})
|
|
|
307
311
|
if (running !== undefined) {
|
|
308
312
|
return {
|
|
309
313
|
running: true,
|
|
310
|
-
seat: seat.label,
|
|
314
|
+
seat: running.seat.label,
|
|
311
315
|
port: running.server.port,
|
|
312
316
|
url: running.server.url,
|
|
317
|
+
repo: running.store.name(),
|
|
313
318
|
where: running.store.where(),
|
|
314
319
|
watchers: running.server.watchers(),
|
|
315
320
|
};
|
|
@@ -353,36 +358,3 @@ export function createBoardPage(runtime: BoardSession, deps: BoardPageDeps = {})
|
|
|
353
358
|
},
|
|
354
359
|
};
|
|
355
360
|
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Why a start failed, naming the setting when the pinned port was the reason: a
|
|
359
|
-
* bind error's own text says the address, not the key that chose it.
|
|
360
|
-
*/
|
|
361
|
-
function failureReason(error: unknown, pinned: number | undefined): string {
|
|
362
|
-
// The bind path rejects with the `listen` error, so `code` is there to read.
|
|
363
|
-
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
364
|
-
if (pinned !== undefined && code === "EADDRINUSE") {
|
|
365
|
-
return `port ${pinned} is in use — change "port" in the bytefly.json board section, or stop what answers on it`;
|
|
366
|
-
}
|
|
367
|
-
return error instanceof Error ? error.message : String(error);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** The chip naming the port, cleared with an empty text. */
|
|
371
|
-
function setChip(ctx: ExtensionContext, port: number): void {
|
|
372
|
-
try {
|
|
373
|
-
ctx.ui.setStatus(
|
|
374
|
-
STATUS_KEYS.board,
|
|
375
|
-
`${paint(ctx.ui.theme, "ok", GLYPHS.board)} ${paint(ctx.ui.theme, "accent", String(port))}`,
|
|
376
|
-
);
|
|
377
|
-
} catch {
|
|
378
|
-
// A host without a status line is not a reason to keep the page from starting.
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function clearChip(ctx: ExtensionContext): void {
|
|
383
|
-
try {
|
|
384
|
-
ctx.ui.setStatus(STATUS_KEYS.board, undefined);
|
|
385
|
-
} catch {
|
|
386
|
-
// Same as above: nothing to clear.
|
|
387
|
-
}
|
|
388
|
-
}
|
|
@@ -8,19 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
12
11
|
import { type Card, cardRef } from "../../lib/board/card.ts";
|
|
12
|
+
import { LANE_RULES } from "../../lib/board/lanes.ts";
|
|
13
13
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
14
14
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
15
|
-
import {
|
|
15
|
+
import type { TailResultInput, TailSection, TailRowLine } from "../../lib/tui/tool.ts";
|
|
16
16
|
import { plural } from "../../lib/board/round-text.ts";
|
|
17
17
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { boardRowLineFor } from "./rows.ts";
|
|
19
|
+
import { TONE_ROLE } from "./tone.ts";
|
|
19
20
|
import type { BoardDetails } from "./types.ts";
|
|
20
21
|
|
|
21
|
-
/** One section of the expanded block. */
|
|
22
|
-
function section(label: string | undefined, cards: readonly Card[]): TailSection<Card> {
|
|
23
|
-
return { label, rows: cards, more: 0, rowLine
|
|
22
|
+
/** One section of the expanded block, drawn as the session that asked for it. */
|
|
23
|
+
function section(label: string | undefined, cards: readonly Card[], rowLine: TailRowLine<Card>): TailSection<Card> {
|
|
24
|
+
return { label, rows: cards, more: 0, rowLine };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -33,19 +34,25 @@ function section(label: string | undefined, cards: readonly Card[]): TailSection
|
|
|
33
34
|
*/
|
|
34
35
|
export function resultInput(details: BoardDetails): TailResultInput<Card> | undefined {
|
|
35
36
|
if (details.action !== "list") return undefined;
|
|
36
|
-
|
|
37
|
+
// Only a read narrowed to this session's own cards marks them, and it marks them with
|
|
38
|
+
// the rule the filter used: the rows and the filter agree about what "mine" means.
|
|
39
|
+
const rowLine = boardRowLineFor(details.mine === true ? details.me : undefined);
|
|
40
|
+
if (details.card) return { kind: "list", rows: [details.card], more: 0, rowLine };
|
|
37
41
|
const round = details.round;
|
|
38
42
|
if (!round) {
|
|
39
43
|
const cards = details.cards ?? [];
|
|
40
|
-
return cards.length === 0 ? undefined : { kind: "list", rows: cards, more: 0, rowLine
|
|
44
|
+
return cards.length === 0 ? undefined : { kind: "list", rows: cards, more: 0, rowLine };
|
|
41
45
|
}
|
|
42
46
|
const sections: TailSection<Card>[] = [];
|
|
43
|
-
for (const group of round.owners) sections.push(section(group.label, group.cards));
|
|
44
|
-
if (round.unassigned.length > 0) sections.push(section("Unassigned", round.unassigned));
|
|
45
|
-
if (round.unplanned.length > 0) sections.push(section("Unplanned", round.unplanned));
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (round.
|
|
47
|
+
for (const group of round.owners) sections.push(section(group.label, group.cards, rowLine));
|
|
48
|
+
if (round.unassigned.length > 0) sections.push(section("Unassigned", round.unassigned, rowLine));
|
|
49
|
+
if (round.unplanned.length > 0) sections.push(section("Unplanned", round.unplanned, rowLine));
|
|
50
|
+
// The lead's queue: a contract nobody has signed is a card the flow stops on, so it is
|
|
51
|
+
// drawn in the order `formatRound` prints it rather than left to the text above.
|
|
52
|
+
if (round.unapproved.length > 0) sections.push(section("Awaiting approval", round.unapproved, rowLine));
|
|
53
|
+
if (round.blocked.length > 0) sections.push(section("Blocked", round.blocked, rowLine));
|
|
54
|
+
if (round.idle.length > 0) sections.push(section("Idle", round.idle, rowLine));
|
|
55
|
+
if (round.done.length > 0) sections.push(section("Done", round.done, rowLine));
|
|
49
56
|
return sections.length === 0 ? undefined : { kind: "sections", sections };
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -78,31 +85,15 @@ export function collapsedLine(theme: Theme, details: BoardDetails): string {
|
|
|
78
85
|
return `${paint(theme, "label", plural(count, "card"))} ${SEPARATORS.item} ${plural(owners, "owner")} ${SEPARATORS.item} ${me}`;
|
|
79
86
|
}
|
|
80
87
|
|
|
81
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* `#4 execute · Billing webhook accepts orders`, clipped by the caller's row helper.
|
|
90
|
+
*
|
|
91
|
+
* The lane wears the colour the page gives it — `LANE_RULES`' role, through the tone
|
|
92
|
+
* bridge — rather than one accent for all five.
|
|
93
|
+
*/
|
|
82
94
|
function cardLine(theme: Theme, card: Card): string {
|
|
83
95
|
const id = paint(theme, "meta", cardRef(card.id));
|
|
84
|
-
const lane = paint(theme,
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The expanded block, as a component the transcript can reuse across renders. */
|
|
89
|
-
export class BoardResult implements Component {
|
|
90
|
-
private readonly view: TailResult<Card>;
|
|
91
|
-
|
|
92
|
-
constructor(input: TailResultInput<Card>, theme: Theme) {
|
|
93
|
-
this.view = new TailResult(input, theme);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Update in place so the transcript reuses this component. */
|
|
97
|
-
setInput(input: TailResultInput<Card>, theme: Theme): void {
|
|
98
|
-
this.view.setInput(input, theme);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
invalidate(): void {
|
|
102
|
-
this.view.invalidate();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
render(width: number): string[] {
|
|
106
|
-
return this.view.render(width);
|
|
107
|
-
}
|
|
96
|
+
const lane = paint(theme, TONE_ROLE[LANE_RULES[card.lane].role], card.lane);
|
|
97
|
+
const title = paint(theme, "label", sanitizeLine(card.title));
|
|
98
|
+
return `${id} ${lane} ${SEPARATORS.item} ${title}`;
|
|
108
99
|
}
|
package/extensions/board/rows.ts
CHANGED
|
@@ -4,45 +4,82 @@
|
|
|
4
4
|
* Every field comes from a card file, which the model wrote and a person may have
|
|
5
5
|
* edited, so it is sanitized here at the boundary that reaches the terminal, and
|
|
6
6
|
* every row is clipped to the width it was given by the shared row helpers.
|
|
7
|
+
*
|
|
8
|
+
* The colours come from `lib/board/tone.ts` — the table the page reads too — through
|
|
9
|
+
* `TONE_ROLE`, so a lane's word in the transcript is the lane's colour on the page. Each
|
|
10
|
+
* word is painted on its own, which means the separator between two words is painted as
|
|
11
|
+
* well: a bare ` · ` would fall back to the terminal's default foreground between two
|
|
12
|
+
* coloured words.
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
15
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { type Card, cardRef, DEFAULT_PRIORITY, priorityOf } from "../../lib/board/card.ts";
|
|
16
|
+
import { type Card, cardRef, DEFAULT_PRIORITY, holds, priorityOf } from "../../lib/board/card.ts";
|
|
17
|
+
import { LANE_RULES } from "../../lib/board/lanes.ts";
|
|
18
|
+
import type { Actor } from "../../lib/board/rules.ts";
|
|
19
|
+
import { type BoardTone, REASON_TONE } from "../../lib/board/tone.ts";
|
|
11
20
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
21
|
+
import { clip, splitRow } from "../../lib/tui/columns.ts";
|
|
12
22
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
13
|
-
import {
|
|
23
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
14
24
|
import type { TailRowLine } from "../../lib/tui/tool.ts";
|
|
25
|
+
import { TONE_ROLE } from "./tone.ts";
|
|
15
26
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
/** One fact the row states about a card, and the tone it wears. */
|
|
28
|
+
interface Fact {
|
|
29
|
+
text: string;
|
|
30
|
+
tone: BoardTone;
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
34
|
+
* Who holds a card, the lane it sits in, and — for work in flight — whether it was planned
|
|
35
|
+
* first, with the priority when it is not the ordinary one.
|
|
25
36
|
*
|
|
26
37
|
* A row carries the lane because a flat listing has no section heading to say it, and
|
|
27
|
-
* "planned" because a
|
|
28
|
-
*
|
|
29
|
-
*
|
|
38
|
+
* "planned" because a card nobody planned is the one a lead looks at. The priority rides
|
|
39
|
+
* along for the same reason: inside a lane the order is the priority, and an order nobody
|
|
40
|
+
* can see reads as an accident.
|
|
30
41
|
*/
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
function factsOf(card: Card): Fact[] {
|
|
43
|
+
const facts: Fact[] = [
|
|
44
|
+
{
|
|
45
|
+
text: card.owner === undefined ? "unassigned" : sanitizeLine(card.owner.label),
|
|
46
|
+
tone: "muted",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
const priority = priorityOf(card);
|
|
50
|
+
if (priority !== DEFAULT_PRIORITY) facts.push({ text: priority, tone: "muted" });
|
|
51
|
+
facts.push({ text: card.lane, tone: LANE_RULES[card.lane].role });
|
|
52
|
+
// The pool has no plan to speak of and a finished card is history, so only a lane
|
|
53
|
+
// somebody is working in says whether the work was planned first. An unplanned card
|
|
54
|
+
// wears the tone the page's reason line gives the same fact.
|
|
55
|
+
const rule = LANE_RULES[card.lane];
|
|
56
|
+
if (!rule.ownerless && !rule.terminal) {
|
|
57
|
+
facts.push(
|
|
58
|
+
card.planPath === undefined
|
|
59
|
+
? { text: "unplanned", tone: REASON_TONE.contract }
|
|
60
|
+
: { text: "planned", tone: "muted" },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return facts;
|
|
35
64
|
}
|
|
36
65
|
|
|
37
|
-
/** One card: the id and title, with its
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
/** One card: the id and title, with its facts trailing. */
|
|
67
|
+
function cardRowText(card: Card, width: number, theme: Theme, self: boolean): string {
|
|
68
|
+
const head = paint(theme, self ? "accent" : "label", `${cardRef(card.id)} ${sanitizeLine(card.title)}`);
|
|
69
|
+
const gap = paint(theme, "meta", SEPARATORS.item);
|
|
70
|
+
const trailing = factsOf(card)
|
|
71
|
+
.map((fact) => paint(theme, TONE_ROLE[fact.tone], fact.text))
|
|
72
|
+
.join(gap);
|
|
73
|
+
return splitRow(head, clip(trailing, width), width);
|
|
45
74
|
}
|
|
46
75
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The row renderer every board section uses, for the session reading it.
|
|
78
|
+
*
|
|
79
|
+
* `me` matters: a `list` narrowed to this session's own cards should look like it, so the
|
|
80
|
+
* holder's rows wear the accent the page gives the card the reader is looking at. Passing
|
|
81
|
+
* no actor draws every row the same, which is what a card produced by an action gets.
|
|
82
|
+
*/
|
|
83
|
+
export function boardRowLineFor(me?: Actor): TailRowLine<Card> {
|
|
84
|
+
return (card, width, theme) => cardRowText(card, width, theme, me !== undefined && holds(card, me));
|
|
85
|
+
}
|