@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
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import { sanitizeLine, stripControlChars } from "
|
|
18
|
-
import type { ScreenHint, ViewportRowsSource } from "
|
|
19
|
-
import { DockListScreen, type DetailContents } from "
|
|
20
|
-
import { GLYPHS, SEPARATORS } from "
|
|
21
|
-
import { keyValueRow, statusRow } from "
|
|
22
|
-
import { paint, type TuiRole } from "
|
|
23
|
-
import type { BackgroundRun, BackgroundStatus } from "
|
|
24
|
-
import { canCollect } from "
|
|
25
|
-
import { formatBackgroundDuration, formatUsageStats } from "
|
|
17
|
+
import { sanitizeLine, stripControlChars } from "../../../lib/format.ts";
|
|
18
|
+
import type { ScreenHint, ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
19
|
+
import { DockListScreen, type DetailContents } from "../../../lib/tui/dock-screen.ts";
|
|
20
|
+
import { GLYPHS, SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
21
|
+
import { keyValueRow, statusRow } from "../../../lib/tui/rows.ts";
|
|
22
|
+
import { paint, type TuiRole } from "../../../lib/tui/tokens.ts";
|
|
23
|
+
import type { BackgroundRun, BackgroundStatus } from "../background.ts";
|
|
24
|
+
import { canCollect } from "../background.ts";
|
|
25
|
+
import { formatBackgroundDuration, formatUsageStats } from "../format.ts";
|
|
26
26
|
|
|
27
27
|
/** One status's glyph, role, and readable label. */
|
|
28
28
|
function statusView(status: BackgroundStatus): { glyph: string; role: TuiRole; label: string } {
|
|
@@ -164,7 +164,7 @@ export class AgentsBoardComponent extends DockListScreen<BackgroundRun> {
|
|
|
164
164
|
const focused = this.focusedRow;
|
|
165
165
|
const hints: ScreenHint[] = ["Esc close", { text: "Enter detail", color: "accent" }];
|
|
166
166
|
if (focused && canCollect(focused)) hints.push({ text: "c collect", color: "accent" });
|
|
167
|
-
if (focused?.status === "running") hints.push({ text: "x cancel", color: "
|
|
167
|
+
if (focused?.status === "running") hints.push({ text: "x cancel", color: "warn" });
|
|
168
168
|
hints.push("↑/↓ select");
|
|
169
169
|
return hints;
|
|
170
170
|
}
|
|
@@ -172,7 +172,7 @@ export class AgentsBoardComponent extends DockListScreen<BackgroundRun> {
|
|
|
172
172
|
protected detailHints(run: BackgroundRun | undefined): ScreenHint[] {
|
|
173
173
|
const hints: ScreenHint[] = ["Esc back"];
|
|
174
174
|
if (run && canCollect(run)) hints.push({ text: "c collect", color: "accent" });
|
|
175
|
-
if (run?.status === "running") hints.push({ text: "x cancel", color: "
|
|
175
|
+
if (run?.status === "running") hints.push({ text: "x cancel", color: "warn" });
|
|
176
176
|
hints.push("j/k scroll");
|
|
177
177
|
return hints;
|
|
178
178
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* open a browser to see what needs a decision, so the command prints the rollup the
|
|
6
6
|
* tool returns, the work a dead owner left behind, and one line saying who is live. It
|
|
7
7
|
* reads the cards and the bus's records and writes nothing: acting on the round is
|
|
8
|
-
* `team`'s tool (`assign`, `reclaim`) or `board`'s.
|
|
8
|
+
* `team`'s tool (`assign`, `offer`, `reclaim`, `claim`) or `board`'s.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -13,8 +13,9 @@ import { type Card, cardRef } from "../../lib/board/card.ts";
|
|
|
13
13
|
import { buildRound } from "../../lib/board/round.ts";
|
|
14
14
|
import { formatRound, plural } from "../../lib/board/round-text.ts";
|
|
15
15
|
import type { BoardSession } from "../../lib/board/session.ts";
|
|
16
|
-
import { createTeamJoin, type JoinResult, type TeamJoin } from "../../lib/session-tool.ts";
|
|
16
|
+
import { createTeamJoin, type JoinResult, type TeamJoin, type TeamJoinInput } from "../../lib/session-tool.ts";
|
|
17
17
|
import { readRoster, type RosterSession, rosterStateOf } from "../../lib/session-roster.ts";
|
|
18
|
+
import { sessionNameOf } from "../../lib/session-label.ts";
|
|
18
19
|
import { formatJoined, formatNotJoined } from "./format.ts";
|
|
19
20
|
|
|
20
21
|
/** The command's name, which is also the word a user types. */
|
|
@@ -40,7 +41,13 @@ export function registerCommands(pi: ExtensionAPI, session: BoardSession, deps:
|
|
|
40
41
|
const [word = "", ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
41
42
|
if (word === "join") {
|
|
42
43
|
const label = rest.join(" ").trim();
|
|
43
|
-
|
|
44
|
+
// A typed label is the one naming action: it is also Pi's session name, which is what
|
|
45
|
+
// `/team join` is for. With no label, a session Pi already names keeps that name and
|
|
46
|
+
// the bus derives it; one with no name of its own draws a preset teammate name
|
|
47
|
+
// instead of landing on the repository's basename, and the drawn name renames nothing
|
|
48
|
+
// because it is the team's, not the user's.
|
|
49
|
+
const input: TeamJoinInput = label === "" ? await unnamedJoin(session, ctx) : { label, name: true };
|
|
50
|
+
const joined = await team.join(ctx, input);
|
|
44
51
|
ctx.ui.notify(joinedLine(joined), joined.ok ? "info" : "warning");
|
|
45
52
|
return;
|
|
46
53
|
}
|
|
@@ -61,6 +68,18 @@ export function registerCommands(pi: ExtensionAPI, session: BoardSession, deps:
|
|
|
61
68
|
});
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
/**
|
|
72
|
+
* What a bare `/team join` asks for: nothing, when Pi already names the session and the
|
|
73
|
+
* bus can derive that, and otherwise the name the board draws for it.
|
|
74
|
+
*
|
|
75
|
+
* A drawn name is the team's, not the user's, so `name` is never set here: only a typed
|
|
76
|
+
* label is meant to rename the footer.
|
|
77
|
+
*/
|
|
78
|
+
async function unnamedJoin(session: BoardSession, ctx: ExtensionCommandContext): Promise<TeamJoinInput> {
|
|
79
|
+
if ((sessionNameOf(ctx) ?? "").trim() !== "") return {};
|
|
80
|
+
return { label: (await session.who(ctx, ctx.cwd)).label };
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
/** `/team join`'s line: who this session is on the team, or why it is on none. */
|
|
65
84
|
export function joinedLine(result: JoinResult): string {
|
|
66
85
|
return result.ok ? formatJoined(result.label, result.watching) : formatNotJoined(result.error);
|
|
@@ -74,7 +93,7 @@ export function joinedLine(result: JoinResult): string {
|
|
|
74
93
|
*/
|
|
75
94
|
export function roundText(cards: readonly Card[], now: number, roster?: readonly RosterSession[]): string {
|
|
76
95
|
if (cards.length === 0) return "team: nothing to report — the board is empty";
|
|
77
|
-
const round = formatRound(cards, buildRound(cards, now)).split("\n");
|
|
96
|
+
const round = formatRound(cards, buildRound(cards, now), roster).split("\n");
|
|
78
97
|
const shown = round.slice(0, MAX_ROUND_LINES);
|
|
79
98
|
if (round.length > MAX_ROUND_LINES) shown.push(`… ${plural(round.length - MAX_ROUND_LINES, "line")} more`);
|
|
80
99
|
for (const line of goneLines(cards, roster, now)) shown.push(line);
|
|
@@ -99,7 +118,7 @@ function goneLines(cards: readonly Card[], roster: readonly RosterSession[] | un
|
|
|
99
118
|
if (gone.size === 0) return [];
|
|
100
119
|
const byOwner = new Map<string, string[]>();
|
|
101
120
|
for (const card of cards) {
|
|
102
|
-
if (card.owner === undefined || card.
|
|
121
|
+
if (card.owner === undefined || card.lane === "done") continue;
|
|
103
122
|
const label = gone.get(card.owner.label.toLowerCase());
|
|
104
123
|
if (label === undefined) continue;
|
|
105
124
|
byOwner.set(label, [...(byOwner.get(label) ?? []), cardRef(card.id)]);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import type { Card } from "../../lib/board/card.ts";
|
|
16
16
|
import { cardRef, type CardOwner } from "../../lib/board/card.ts";
|
|
17
|
+
import type { Actor } from "../../lib/board/rules.ts";
|
|
17
18
|
import { listPeers, MAX_MESSAGE_TEXT, type Peer, send as sendTo } from "../../lib/session-tool.ts";
|
|
18
19
|
|
|
19
20
|
/** How a handoff reaches a peer, and what the bus can say about the peers there are. */
|
|
@@ -42,9 +43,9 @@ export function handoffText(card: Card, to: string, note?: string): string {
|
|
|
42
43
|
const lines = [
|
|
43
44
|
`Handoff: ${cardRef(card.id)} "${card.title}" is yours, ${to}.`,
|
|
44
45
|
card.paths.length > 0 ? `Paths: ${card.paths.join(", ")}` : undefined,
|
|
45
|
-
card.
|
|
46
|
+
card.planPath === undefined ? undefined : `Plan: ${card.planPath}`,
|
|
46
47
|
note === undefined ? undefined : `Note: ${note}`,
|
|
47
|
-
`It is already assigned to you —
|
|
48
|
+
`It is already assigned to you — write its contract with board action "contract" if it has none, then note each phase and move it to execute, blocked, or done.`,
|
|
48
49
|
].filter((line): line is string => line !== undefined);
|
|
49
50
|
return withinLimit(lines.join("\n"));
|
|
50
51
|
}
|
|
@@ -60,9 +61,9 @@ export function offerText(card: Card, note?: string): string {
|
|
|
60
61
|
const lines = [
|
|
61
62
|
`Offer: ${cardRef(card.id)} "${card.title}" is unclaimed.`,
|
|
62
63
|
card.paths.length > 0 ? `Paths: ${card.paths.join(", ")}` : undefined,
|
|
63
|
-
card.
|
|
64
|
+
card.planPath === undefined ? undefined : `Plan: ${card.planPath}`,
|
|
64
65
|
note === undefined ? undefined : `Note: ${note}`,
|
|
65
|
-
`First to claim it takes it: run board action "claim" with card "${card.id}", then
|
|
66
|
+
`First to claim it takes it: run board action "claim" with card "${card.id}", then write its contract and move it on from plan.`,
|
|
66
67
|
].filter((line): line is string => line !== undefined);
|
|
67
68
|
return withinLimit(lines.join("\n"));
|
|
68
69
|
}
|
|
@@ -123,6 +124,19 @@ export function notListening(peers: readonly Peer[] | undefined, labels: readonl
|
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The actor a handoff names: the peer the bus lists, else the label that was asked for.
|
|
129
|
+
*
|
|
130
|
+
* `team assign` checks the card against this before it sends anything and records the same
|
|
131
|
+
* actor afterwards, so the check and the write cannot disagree about who `to` is. That is
|
|
132
|
+
* what makes an id the holder: a card records the id the bus gave it, `holds` compares ids
|
|
133
|
+
* first, and only the resolved actor carries one.
|
|
134
|
+
*/
|
|
135
|
+
export function actorFor(peers: readonly Peer[] | undefined, to: string): Actor {
|
|
136
|
+
const peer = peerFor(peers, to);
|
|
137
|
+
return peer === undefined ? { label: to.trim() } : { id: peer.id, label: peer.label };
|
|
138
|
+
}
|
|
139
|
+
|
|
126
140
|
/**
|
|
127
141
|
* The peer a handoff names, as the card records it when the bus cannot say who that
|
|
128
142
|
* is: the label a message file was written for, else the one that was asked for.
|
|
@@ -113,7 +113,22 @@ export function formatRacedAssignment(card: Card, to: string, reason: string): s
|
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
/** A card taken back to
|
|
116
|
+
/** A card taken back to the pool. */
|
|
117
117
|
export function formatReclaimed(card: Card, previous: string): string {
|
|
118
|
-
return `${cardRef(card.id)} reclaimed from ${previous} and back in
|
|
118
|
+
return `${cardRef(card.id)} reclaimed from ${previous} and back in Backlog; its notes are kept.\n ${card.title}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A card the lead took itself.
|
|
123
|
+
*
|
|
124
|
+
* The next step is the board's, so it is named: a claim is the one round action that
|
|
125
|
+
* leaves a card in `plan` waiting on its owner, and a line that stopped at "yours" would
|
|
126
|
+
* leave the lead holding a card nobody said what to do with.
|
|
127
|
+
*/
|
|
128
|
+
export function formatClaimed(card: Card): string {
|
|
129
|
+
return (
|
|
130
|
+
`${cardRef(card.id)} is yours, in plan.\n` +
|
|
131
|
+
` ${card.title}\n` +
|
|
132
|
+
` next: design it in plan mode, \`plan\` action \`write\`, then \`board\` action \`contract\``
|
|
133
|
+
);
|
|
119
134
|
}
|
package/extensions/team/index.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* team — the lead's round over the shared board.
|
|
3
3
|
*
|
|
4
|
-
* `board` owns the cards
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `board` owns the cards; `team` owns the leads round over them. `assign` hands a card to
|
|
5
|
+
* a session that is already running and sends it a handoff — dispatch cannot start a
|
|
6
|
+
* peer's turn, and the page and the docs say so — `offer` broadcasts one to whoever is
|
|
7
|
+
* free, `reclaim` takes one back when its owner is gone or has gone quiet, and `claim`
|
|
8
|
+
* takes one for the lead itself. The card's life after that is `board`'s.
|
|
8
9
|
* Its one
|
|
9
10
|
* command, `/team`, prints the round the board's Team tab draws, without a browser.
|
|
10
11
|
*
|
|
11
|
-
* Either extension works without the other: `board` is the work list
|
|
12
|
-
*
|
|
13
|
-
* first use.
|
|
12
|
+
* Either extension works without the other: `board` is the work list, and `team` is
|
|
13
|
+
* the lead's half of it; both are opt-in, so a session names the ones it runs. The
|
|
14
|
+
* factory only registers; the store is opened on first use.
|
|
14
15
|
*
|
|
15
|
-
* Load with: pi --extension ./extensions/team --extension ./extensions/session-bus
|
|
16
|
+
* Load with: PI_ENABLED_EXTENSIONS=board,team pi --extension ./extensions/team --extension ./extensions/session-bus
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `team` tool's parameters:
|
|
2
|
+
* The `team` tool's parameters: four actions over one card.
|
|
3
3
|
*
|
|
4
|
-
* `assign` names the peer a handoff goes to, and `reclaim` takes a card back
|
|
5
|
-
* take the card the way `board` does — `4` or `#4` —
|
|
6
|
-
* the same board.
|
|
4
|
+
* `assign` names the peer a handoff goes to, and `reclaim` takes a card back; `claim`
|
|
5
|
+
* takes one for the lead. All of them take the card the way `board` does — `4` or `#4` —
|
|
6
|
+
* because a lead reads ids off the same board.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Type } from "typebox";
|
|
@@ -16,7 +16,8 @@ export const TeamParams = Type.Object({
|
|
|
16
16
|
description:
|
|
17
17
|
"`assign` hands a card to a running session and sends it a handoff; `offer` broadcasts an unclaimed card to " +
|
|
18
18
|
"every other live session and writes nothing, so the first to claim it takes it; `reclaim` takes a card back " +
|
|
19
|
-
"to
|
|
19
|
+
"to Backlog; `claim` takes one for the lead itself, which then plans and records it with `board`. They are " +
|
|
20
|
+
"the round's four verbs: `board` creates, plans, moves, and finishes the cards, and the page is a view.",
|
|
20
21
|
},
|
|
21
22
|
),
|
|
22
23
|
card: Type.String({ description: 'The card to act on, e.g. "4" or "#4".' }),
|
package/extensions/team/tools.ts
CHANGED
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `team` tool: the lead's round over the same cards `board` owns.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* is, so the lead's next call
|
|
4
|
+
* Four actions, because they are the four a lead takes at the round: hand a card out,
|
|
5
|
+
* offer it, take one back, and take one itself. `assign` sends the handoff **first** and
|
|
6
|
+
* sets the owner **second**: a card assigned without a message is a card nobody knows
|
|
7
|
+
* about, while a message for a card that then changed is one the peer can still claim.
|
|
8
|
+
* Either failure is reported with the card as it actually is, so the lead's next call
|
|
9
|
+
* starts from the truth.
|
|
9
10
|
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
11
|
+
* `claim` is the lead's own work, and it is the same store operation `board`'s claim
|
|
12
|
+
* makes, so the lane rules, the WIP limit, and the offer record cleared are the store's —
|
|
13
|
+
* not a second answer the round would keep in step by hand. Everything after the claim
|
|
14
|
+
* (plan, contract, move, finish) is `board`'s, because that is the card's life and not
|
|
15
|
+
* the round's.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
19
|
import type { Static } from "typebox";
|
|
17
20
|
import { type Card, cardRef, normalizeCardId } from "../../lib/board/card.ts";
|
|
18
21
|
import { indexOf } from "../../lib/board/links.ts";
|
|
19
|
-
import { type Actor,
|
|
22
|
+
import { type Actor, mayOp } from "../../lib/board/rules.ts";
|
|
20
23
|
import type { BoardSession } from "../../lib/board/session.ts";
|
|
21
24
|
import type { BoardStore } from "../../lib/board/store.ts";
|
|
22
25
|
import { messageOf } from "../../lib/errors.ts";
|
|
26
|
+
import type { TeamJoin } from "../../lib/session-tool.ts";
|
|
23
27
|
import { failedResult, type ToolFailure, textResult } from "../../lib/tool-result.ts";
|
|
24
28
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
25
29
|
import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
|
|
26
30
|
import { plural } from "../../lib/board/round-text.ts";
|
|
27
31
|
import { strayField } from "../../lib/stray-fields.ts";
|
|
28
|
-
import { type Dispatch, handoffText, notListening, offerText, ownerFor, peerFor, sessionDispatch } from "./dispatch.ts";
|
|
29
32
|
import {
|
|
33
|
+
actorFor,
|
|
34
|
+
type Dispatch,
|
|
35
|
+
handoffText,
|
|
36
|
+
notListening,
|
|
37
|
+
offerText,
|
|
38
|
+
ownerFor,
|
|
39
|
+
sessionDispatch,
|
|
40
|
+
} from "./dispatch.ts";
|
|
41
|
+
import {
|
|
42
|
+
formatClaimed,
|
|
30
43
|
formatHandoff,
|
|
31
44
|
formatHandoffFailure,
|
|
32
45
|
formatOffer,
|
|
@@ -64,10 +77,19 @@ function callSubject(action: TeamAction, args: Record<string, unknown>): string
|
|
|
64
77
|
export interface TeamToolsDeps {
|
|
65
78
|
/** Test seam: the handoff transport. */
|
|
66
79
|
dispatch?: (ctx: Parameters<typeof sessionDispatch>[0]) => Dispatch;
|
|
80
|
+
/** Test seam: the join a claim makes, so a suite records it instead of asking a bus. */
|
|
81
|
+
team?: TeamJoin;
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
export function registerTools(pi: ExtensionAPI, session: BoardSession, deps: TeamToolsDeps = {}): void {
|
|
70
85
|
const dispatchFor = deps.dispatch ?? sessionDispatch;
|
|
86
|
+
/** The join, built on the first claim: a round that never takes a card pays nothing. */
|
|
87
|
+
let joinBuilder: Promise<TeamJoin> | undefined;
|
|
88
|
+
const teamFor = (): Promise<TeamJoin> => {
|
|
89
|
+
if (deps.team !== undefined) return Promise.resolve(deps.team);
|
|
90
|
+
joinBuilder ??= import("../../lib/session-tool.ts").then(({ createTeamJoin }) => createTeamJoin(pi));
|
|
91
|
+
return joinBuilder;
|
|
92
|
+
};
|
|
71
93
|
|
|
72
94
|
pi.registerTool({
|
|
73
95
|
name: TEAM_TOOL,
|
|
@@ -75,13 +97,16 @@ export function registerTools(pi: ExtensionAPI, session: BoardSession, deps: Tea
|
|
|
75
97
|
description:
|
|
76
98
|
"Run a round of work over the shared board: `assign` hands a card to a running session and sends it a handoff " +
|
|
77
99
|
"(`to` is a teammate's label or id, as `/team` lists them); `offer` broadcasts an unclaimed card to every other " +
|
|
78
|
-
"live session and writes nothing, so the first to claim it takes it; `reclaim` takes a card back to
|
|
79
|
-
"sends nothing, so it needs no team
|
|
80
|
-
"message waits in its inbox, and a peer that is
|
|
81
|
-
|
|
100
|
+
"live session and writes nothing, so the first to claim it takes it; `reclaim` takes a card back to Backlog and " +
|
|
101
|
+
"sends nothing, so it needs no team; `claim` takes one for yourself, and the card's plan, contract, moves, and " +
|
|
102
|
+
"finish are `board`'s. Nothing here starts a peer's turn: a message waits in its inbox, and a peer that is " +
|
|
103
|
+
"listening reports it at its next turn.",
|
|
104
|
+
promptSnippet:
|
|
105
|
+
"Run a round over the board: assign a card to a running session, offer one, reclaim one, or claim one.",
|
|
82
106
|
promptGuidelines: [
|
|
83
107
|
"Open the board with `/board` or work a card first, so this session and its peers are on the team.",
|
|
84
|
-
"Offer a card nobody has taken; assign one you mean a particular peer to hold
|
|
108
|
+
"Offer a card nobody has taken; assign one you mean a particular peer to hold; claim one you will take " +
|
|
109
|
+
"yourself, then plan and record it with `board`.",
|
|
85
110
|
"When a result says `Not listening`, say so: the message waits for that peer's next turn.",
|
|
86
111
|
"Reclaim a card whose owner is gone or has gone quiet rather than editing someone else's work. A " +
|
|
87
112
|
"`gone:` owner's process is not running, so what it held is anyone's to reclaim.",
|
|
@@ -102,12 +127,13 @@ export function registerTools(pi: ExtensionAPI, session: BoardSession, deps: Tea
|
|
|
102
127
|
if (stray !== undefined) return refusal(action, stray);
|
|
103
128
|
const id = normalizeCardId(args.card);
|
|
104
129
|
if (id === undefined) return refusal(action, `"${String(args.card)}" is not a card id.`);
|
|
105
|
-
const me = session.who(ctx, ctx.cwd);
|
|
130
|
+
const me = await session.who(ctx, ctx.cwd);
|
|
106
131
|
try {
|
|
107
132
|
const store = await session.store(ctx.cwd);
|
|
108
133
|
const now = session.now();
|
|
109
134
|
if (action === "reclaim") return await reclaimCard(store, me, id, now);
|
|
110
135
|
if (action === "offer") return await offerCard(store, me, id, args.note, now, dispatchFor(ctx));
|
|
136
|
+
if (action === "claim") return await claimCard(store, session, teamFor, ctx, me, id, now);
|
|
111
137
|
const to = typeof args.to === "string" ? args.to.trim() : "";
|
|
112
138
|
if (to === "") return refusal("assign", 'assign needs a peer: pass `to`, e.g. "billing-core".');
|
|
113
139
|
return await assignCard(store, me, id, to, args.note, now, dispatchFor(ctx));
|
|
@@ -134,8 +160,9 @@ export function registerTools(pi: ExtensionAPI, session: BoardSession, deps: Tea
|
|
|
134
160
|
/**
|
|
135
161
|
* `assign`: check the preconditions, send the handoff, then set the owner.
|
|
136
162
|
*
|
|
137
|
-
* The preconditions are checked with the same
|
|
138
|
-
*
|
|
163
|
+
* The preconditions are checked with `mayOp`, the same decision the store's `applyOp` will
|
|
164
|
+
* make — and with the store's own limits, so a configured WIP cap is the one the precheck
|
|
165
|
+
* sees — so an assignment the rules would refuse never sends a message at all.
|
|
139
166
|
*/
|
|
140
167
|
async function assignCard(
|
|
141
168
|
store: BoardStore,
|
|
@@ -148,11 +175,21 @@ async function assignCard(
|
|
|
148
175
|
): Promise<ReturnType<typeof textResult<TeamDetails>> | ToolFailure<TeamDetails>> {
|
|
149
176
|
const card = await store.read(id);
|
|
150
177
|
if (!card) return refusal("assign", `${cardRef(id)} does not exist.`);
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
178
|
+
// A handoff to yourself is not a claim: it writes a message into your own inbox and
|
|
179
|
+
// records you as the owner without the claim's next step. `claim` is that act.
|
|
180
|
+
if (namesSelf(me, to))
|
|
181
|
+
return refusal("assign", `${cardRef(id)} cannot be handed to yourself: \`claim\` takes one.`, card);
|
|
182
|
+
// Who `to` names, asked before the check and once: a card records the id the bus gave its
|
|
183
|
+
// holder, and `holds` compares ids first, so an id the caller typed has to be resolved to
|
|
184
|
+
// the peer it names rather than compared as a label. The same actor is what the write
|
|
185
|
+
// records, so the check and the write cannot disagree about who `to` is.
|
|
186
|
+
const peers = await dispatch.peers().catch(() => undefined);
|
|
187
|
+
const target = actorFor(peers, to);
|
|
188
|
+
// The board, not just the card: a card that waits on unfinished work, or a `plan` lane
|
|
189
|
+
// with no room, is refused here, so a handoff for it is never sent. `mayOp` is the
|
|
190
|
+
// store's own check without the write, and the board's settings are the store's, not a default.
|
|
191
|
+
const refused = mayOp(card, { kind: "assign", to: target }, me, indexOf(await store.list()), store.config());
|
|
192
|
+
if (refused !== undefined) return refusal("assign", refused, card);
|
|
156
193
|
|
|
157
194
|
let delivered: string[];
|
|
158
195
|
try {
|
|
@@ -161,14 +198,12 @@ async function assignCard(
|
|
|
161
198
|
return refusal("assign", formatHandoffFailure(card, to, messageOf(error)), card);
|
|
162
199
|
}
|
|
163
200
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
// says whether that peer will be woken by the message it just got.
|
|
167
|
-
const peers = await dispatch.peers().catch(() => undefined);
|
|
168
|
-
const peer = peerFor(peers, to);
|
|
201
|
+
// The owner a handoff records: the resolved peer, whose id survives the peer renaming
|
|
202
|
+
// itself, or the label a message file was written for when the bus could not say who that
|
|
203
|
+
// is. The same read also says whether that peer will be woken by the message it just got.
|
|
169
204
|
const applied = await store.mutate(
|
|
170
205
|
id,
|
|
171
|
-
{ kind: "assign", to:
|
|
206
|
+
{ kind: "assign", to: target.id === undefined ? ownerFor(to, delivered) : target },
|
|
172
207
|
me,
|
|
173
208
|
now,
|
|
174
209
|
);
|
|
@@ -184,15 +219,6 @@ async function assignCard(
|
|
|
184
219
|
});
|
|
185
220
|
}
|
|
186
221
|
|
|
187
|
-
/**
|
|
188
|
-
* The actor an offer's preconditions are judged as: nobody in particular.
|
|
189
|
-
*
|
|
190
|
-
* `claim` is what the offer asks a peer to do, so the dry run is a claim by someone who
|
|
191
|
-
* holds nothing — the rules' own refusals decide whether a card may be offered at all,
|
|
192
|
-
* without a second copy of the sentences.
|
|
193
|
-
*/
|
|
194
|
-
const OFFER_CLAIMANT: Actor = { label: "*" };
|
|
195
|
-
|
|
196
222
|
/**
|
|
197
223
|
* `offer`: check the claim's preconditions, broadcast the card, then record the offer.
|
|
198
224
|
*
|
|
@@ -212,10 +238,11 @@ async function offerCard(
|
|
|
212
238
|
): Promise<ReturnType<typeof textResult<TeamDetails>> | ToolFailure<TeamDetails>> {
|
|
213
239
|
const card = await store.read(id);
|
|
214
240
|
if (!card) return refusal("offer", `${cardRef(id)} does not exist.`);
|
|
215
|
-
// The board, not just the card: a card that waits on unfinished work,
|
|
216
|
-
// holds, is refused here, so a broadcast for it is never
|
|
217
|
-
|
|
218
|
-
|
|
241
|
+
// The board, not just the card: a card that waits on unfinished work, one somebody
|
|
242
|
+
// holds, or a `plan` lane with no room is refused here, so a broadcast for it is never
|
|
243
|
+
// sent. This is the offer's own rule, asked without the write.
|
|
244
|
+
const refused = mayOp(card, { kind: "offer" }, me, indexOf(await store.list()), store.config());
|
|
245
|
+
if (refused !== undefined) return refusal("offer", refused, card);
|
|
219
246
|
|
|
220
247
|
let delivered: string[];
|
|
221
248
|
try {
|
|
@@ -237,7 +264,41 @@ async function offerCard(
|
|
|
237
264
|
});
|
|
238
265
|
}
|
|
239
266
|
|
|
240
|
-
/** `
|
|
267
|
+
/** Whether `to` names this session: its id, or its label ignoring case. */
|
|
268
|
+
function namesSelf(me: Actor, to: string): boolean {
|
|
269
|
+
const target = to.trim();
|
|
270
|
+
return (me.id !== undefined && target === me.id) || target.toLowerCase() === me.label.toLowerCase();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* `claim`: the lead takes a card itself, through the store operation `board`'s claim makes.
|
|
275
|
+
*
|
|
276
|
+
* The lane rules, the WIP limit, the waiting refusal, and the offer record cleared are the
|
|
277
|
+
* store's, so the two doors cannot answer them differently. The join is the claim's one
|
|
278
|
+
* side effect outside the card: taking work is what makes this session a teammate, and it
|
|
279
|
+
* is the same ask-once call `board` makes.
|
|
280
|
+
*/
|
|
281
|
+
async function claimCard(
|
|
282
|
+
store: BoardStore,
|
|
283
|
+
session: BoardSession,
|
|
284
|
+
teamFor: () => Promise<TeamJoin>,
|
|
285
|
+
ctx: ExtensionContext,
|
|
286
|
+
me: Actor,
|
|
287
|
+
id: string,
|
|
288
|
+
now: number,
|
|
289
|
+
): Promise<ReturnType<typeof textResult<TeamDetails>> | ToolFailure<TeamDetails>> {
|
|
290
|
+
const before = await store.read(id);
|
|
291
|
+
if (!before) return refusal("claim", `${cardRef(id)} does not exist.`);
|
|
292
|
+
const result = await store.mutate(id, { kind: "claim" }, me, now);
|
|
293
|
+
if (!result.ok) return refusal("claim", result.error, result.card ?? before);
|
|
294
|
+
// Both loaded here, not at registration: a round that never takes a card pays nothing
|
|
295
|
+
// for the bus, and the registration graph stays the size it was.
|
|
296
|
+
const { joinOnce } = await import("../../lib/board/join.ts");
|
|
297
|
+
await joinOnce(session.team, await teamFor(), ctx, me.label);
|
|
298
|
+
return textResult(formatClaimed(result.card), { action: "claim", card: result.card });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** `reclaim`: the owner clears and the card returns to the pool, notes and all. */
|
|
241
302
|
async function reclaimCard(
|
|
242
303
|
store: BoardStore,
|
|
243
304
|
me: Actor,
|
|
@@ -257,11 +318,12 @@ async function reclaimCard(
|
|
|
257
318
|
function summaryOf(details: TeamDetails): string {
|
|
258
319
|
const card = details.card;
|
|
259
320
|
if (!card) return details.action === "reclaim" ? "reclaimed" : "handed off";
|
|
321
|
+
if (details.action === "claim") return `${cardRef(card.id)} claimed`;
|
|
260
322
|
if (details.action === "offer") {
|
|
261
323
|
const count = details.delivered?.length ?? 0;
|
|
262
324
|
return `${cardRef(card.id)} offered to ${plural(count, "session")}`;
|
|
263
325
|
}
|
|
264
326
|
const who = details.assigned?.label ?? card.owner?.label;
|
|
265
327
|
const target = details.action === "assign" && who !== undefined ? ` → ${who}` : "";
|
|
266
|
-
return `${cardRef(card.id)} ${card.
|
|
328
|
+
return `${cardRef(card.id)} ${card.lane}${target}`;
|
|
267
329
|
}
|
package/extensions/team/types.ts
CHANGED
|
@@ -13,8 +13,8 @@ import type { CardOwner } from "../../lib/board/card.ts";
|
|
|
13
13
|
/** The tool's name. `board` owns the cards; `team` owns the round. */
|
|
14
14
|
export const TEAM_TOOL = "team";
|
|
15
15
|
|
|
16
|
-
/** The
|
|
17
|
-
export const TEAM_ACTIONS = ["assign", "offer", "reclaim"] as const;
|
|
16
|
+
/** The four actions: the lead's round — hand out, offer, take back, and take one itself. */
|
|
17
|
+
export const TEAM_ACTIONS = ["assign", "offer", "reclaim", "claim"] as const;
|
|
18
18
|
|
|
19
19
|
/** One action. */
|
|
20
20
|
export type TeamAction = (typeof TEAM_ACTIONS)[number];
|
|
@@ -57,6 +57,7 @@ export const ACTION_FIELDS: Record<TeamAction, readonly string[]> = {
|
|
|
57
57
|
assign: ["card", "to", "note"],
|
|
58
58
|
offer: ["card", "note"],
|
|
59
59
|
reclaim: ["card"],
|
|
60
|
+
claim: ["card"],
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
/** Whether a value is one of the actions, so an unknown one is a refusal and not a crash. */
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
7
6
|
import { formatTodoList, progressSummary } from "./format.ts";
|
|
8
7
|
import type { TodoRuntime } from "./runtime.ts";
|
|
9
|
-
import { TodoListComponent } from "./tui.ts";
|
|
10
8
|
|
|
11
9
|
export function registerCommands(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
12
10
|
pi.registerCommand("todos", {
|
|
@@ -17,6 +15,10 @@ export function registerCommands(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
17
15
|
ctx.ui.notify(todos.length === 0 ? "No todos." : `${progressSummary(todos)}\n${formatTodoList(todos)}`, "info");
|
|
18
16
|
return;
|
|
19
17
|
}
|
|
18
|
+
// The screen and its rails wrapper are the TUI's: the listing above and
|
|
19
|
+
// registration load neither.
|
|
20
|
+
const { withRailsSuppressed } = await import("../../lib/rails.ts");
|
|
21
|
+
const { TodoListComponent } = await import("./view/todo-screen.ts");
|
|
20
22
|
let unsubscribe: (() => void) | undefined;
|
|
21
23
|
try {
|
|
22
24
|
await withRailsSuppressed(pi, () =>
|
|
@@ -5,11 +5,22 @@
|
|
|
5
5
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
7
7
|
import { GLYPH_GAP, GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
8
|
+
import { listRow } from "../../lib/tui/rows.ts";
|
|
8
9
|
import { overflowHint } from "../../lib/tui/tool.ts";
|
|
9
10
|
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
10
11
|
import { completedCount, currentTodo } from "./state.ts";
|
|
11
12
|
import type { Todo, TodoStatus } from "./types.ts";
|
|
12
13
|
|
|
14
|
+
/** One indented item line, clipped to `width`. */
|
|
15
|
+
export function todoRow(todo: Todo, theme: Theme, width: number): string {
|
|
16
|
+
return listRow(theme, width, {
|
|
17
|
+
glyph: todoGlyphChar(todo),
|
|
18
|
+
glyphRole: todoGlyphRole(todo),
|
|
19
|
+
label: todoLabelText(todo),
|
|
20
|
+
labelRole: todoLabelRole(todo),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
/** Sort order so the widget and transcript surface current work before finished rows. */
|
|
14
25
|
const STATUS_ORDER: Record<TodoStatus, number> = { in_progress: 0, pending: 1, completed: 2 };
|
|
15
26
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `TodoResult` transcript block: the indented glyph rail over the todo list.
|
|
3
|
+
*
|
|
4
|
+
* `render` runs on every frame Pi draws, for every block in the transcript, so
|
|
5
|
+
* the rail is memoized on the width (`RenderCache`) and rebuilt only when
|
|
6
|
+
* `setInput` hands over a new list or `invalidate` says the theme moved.
|
|
7
|
+
*
|
|
8
|
+
* This module is on the registration path — the `todo` tool's `renderResult`
|
|
9
|
+
* needs the class — while the `/todos` screen beside it is not.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
14
|
+
import { RenderCache } from "../../lib/tui/render-cache.ts";
|
|
15
|
+
import { todoRailLines, type TodoRailInput } from "./format.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Transcript result block: the indented glyph rail, wrapping at the render width.
|
|
19
|
+
*
|
|
20
|
+
* `render` runs on every frame Pi draws, for every block in the transcript, so the
|
|
21
|
+
* rail is memoized on the width (`RenderCache`) and rebuilt only when
|
|
22
|
+
* `setInput` hands over a new list or `invalidate` says the theme moved. The
|
|
23
|
+
* caller must go through `setInput`: mutating a list in place would leave the
|
|
24
|
+
* block showing what it drew last.
|
|
25
|
+
*/
|
|
26
|
+
export class TodoResult implements Component {
|
|
27
|
+
private input: TodoRailInput;
|
|
28
|
+
private theme: Theme;
|
|
29
|
+
private readonly cache = new RenderCache();
|
|
30
|
+
|
|
31
|
+
constructor(input: TodoRailInput, theme: Theme) {
|
|
32
|
+
this.input = input;
|
|
33
|
+
this.theme = theme;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Update in place so the transcript can reuse this component across renders. */
|
|
37
|
+
setInput(input: TodoRailInput, theme: Theme): void {
|
|
38
|
+
this.input = input;
|
|
39
|
+
this.theme = theme;
|
|
40
|
+
this.cache.invalidate();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
invalidate(): void {
|
|
44
|
+
this.cache.invalidate();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
render(width: number): string[] {
|
|
48
|
+
const w = Math.max(1, width);
|
|
49
|
+
return this.cache.lines(w, undefined, () => this.build(w));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private build(w: number): string[] {
|
|
53
|
+
// The leading blank line separates the rail from the call header, matching
|
|
54
|
+
// the built-ins' header/output gap.
|
|
55
|
+
return ["", ...todoRailLines(this.input, this.theme, w)];
|
|
56
|
+
}
|
|
57
|
+
}
|