@gavin-hu/my-pi-agent 0.21.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 -31
- package/README.zh-CN.md +40 -26
- 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 -24
- package/docs/extensions/board.md +361 -147
- package/docs/extensions/bytefly.md +152 -111
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +30 -31
- 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 +166 -126
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +34 -18
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +72 -33
- 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 +56 -15
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +5 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +4 -40
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +13 -11
- package/extensions/board/result.ts +30 -16
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +42 -10
- package/extensions/board/snapshot.ts +2 -1
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +131 -44
- package/extensions/board/types.ts +36 -32
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +33 -15
- 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 +89 -21
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +23 -9
- package/extensions/board/view/render.ts +208 -271
- package/extensions/board/view/styles.ts +123 -31
- 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 +4 -2
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/lines.ts +7 -4
- package/extensions/bytefly/footer/render/segments.ts +37 -10
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- 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/{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/index.ts +67 -36
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -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 +3 -3
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
- 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/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +53 -52
- package/extensions/plan/plan-index.ts +4 -2
- 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 +49 -39
- 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-client.ts +3 -0
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +9 -67
- package/extensions/plan/web-payload.ts +3 -3
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web.ts +1 -12
- 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 +10 -8
- package/extensions/serve/index.ts +17 -3
- package/extensions/serve/registry.ts +26 -17
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +2 -1
- 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/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- 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 -7
- 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/view/todo-screen.ts +122 -0
- 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 +27 -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 +1 -1
- 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 +136 -54
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +87 -30
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +70 -58
- 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 +14 -12
- package/lib/http/port.ts +15 -10
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +39 -10
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +8 -8
- package/lib/session-label.ts +111 -5
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +5 -4
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +23 -8
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +9 -7
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +3 -3
- package/lib/web/README.md +21 -0
- 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 +2 -1
- package/lib/web/runtime.ts +9 -10
- package/package.json +6 -1
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/seat.ts +0 -28
- package/extensions/todo/tui.ts +0 -212
- 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/plan/tools.ts
CHANGED
|
@@ -1,43 +1,201 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model-facing registration for the plan
|
|
2
|
+
* Model-facing registration for the `plan` tool.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* returns its path
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and on approval seeds the `todo` tool with the plan's steps (through
|
|
10
|
-
* `ctx.executeTool`, so the todo widget and validation run normally).
|
|
11
|
-
* `set_plan_steps` then keeps the plan current: after approval the model ticks
|
|
12
|
-
* each step off in place as it finishes it, and the todo item approval seeded
|
|
13
|
-
* from that step is ticked with it. The file is the source of truth: the model
|
|
14
|
-
* executes from it.
|
|
4
|
+
* One tool with five actions, like `board` and `memory`: `enter` asks the user to
|
|
5
|
+
* plan before touching code, `write` saves the plan artifact in the plans
|
|
6
|
+
* directory and returns its path, `exit` reads that file back and presents it for
|
|
7
|
+
* approval, `steps` keeps an approved plan current, and `cancel` leaves planning
|
|
8
|
+
* without approving, so a phone user is never trapped in read-only mode.
|
|
15
9
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* `exit` presents the plan through `review.ts` (a scrollable review screen in the
|
|
11
|
+
* TUI, a select dialog elsewhere) and, on approval, seeds the `todo` tool with the
|
|
12
|
+
* plan's steps through `ctx.executeTool`, so the todo widget and validation run
|
|
13
|
+
* normally. After approval the model ticks each step off in place with `steps`, and
|
|
14
|
+
* the todo item approval seeded from that step is ticked with it. The file is the
|
|
15
|
+
* source of truth: the model executes from it.
|
|
19
16
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* Each action lives in `tools/`: its execute handler and the block it draws stay
|
|
18
|
+
* together, and the approval-time helpers they share live next to them. This
|
|
19
|
+
* module only composes them, in the order the description names them.
|
|
20
|
+
*
|
|
21
|
+
* Without an interactive UI the two actions that need one (`enter`, `exit`) fail
|
|
22
|
+
* with an actionable message instead of deciding for the user.
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
26
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
27
|
+
import { Type } from "typebox";
|
|
28
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
29
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
30
|
+
import { detailsOrLine, messageText, callLine } from "../../lib/tui/tool.ts";
|
|
31
|
+
import { MAX_STEPS, PLAN_OUTLINE } from "./steps.ts";
|
|
32
|
+
import { PLAN_ACTIONS, PLAN_TOOL, type PlanAction, type PlanRuntime } from "./runtime.ts";
|
|
33
|
+
import { callCancel, executeCancel, textCancel } from "./tools/cancel.ts";
|
|
34
|
+
import { callEnter, executeEnter, textEnter } from "./tools/enter.ts";
|
|
35
|
+
import { callExit, executeExit, textExit } from "./tools/exit.ts";
|
|
36
|
+
import { callSteps, executeSteps, StepUpdateParams, textSteps } from "./tools/steps.ts";
|
|
37
|
+
import { callWrite, executeWrite, textWrite } from "./tools/write.ts";
|
|
38
|
+
import type { PlanDetails } from "./types.ts";
|
|
39
|
+
import { createLazyPlanWebReview, NO_SERVER_DEPS } from "./web-lazy.ts";
|
|
40
|
+
import type { PlanWebReview } from "./web.ts";
|
|
41
|
+
|
|
42
|
+
const PlanParams = Type.Object({
|
|
43
|
+
action: StringEnum(PLAN_ACTIONS, {
|
|
44
|
+
description:
|
|
45
|
+
"`enter` asks to plan, `write` saves the plan file, `exit` presents it for approval, `steps` ticks its steps, `cancel` leaves without approving.",
|
|
46
|
+
}),
|
|
47
|
+
title: Type.Optional(
|
|
48
|
+
Type.String({
|
|
49
|
+
description: '`write`: short title for the plan, used as the file-name slug, e.g. "Add rate limiting".',
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
content: Type.Optional(
|
|
53
|
+
Type.String({
|
|
54
|
+
description: "`write`: the complete plan as markdown in the documented outline, including every step.",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
plan_path: Type.Optional(
|
|
58
|
+
Type.String({
|
|
59
|
+
description:
|
|
60
|
+
"`write`: overwrite an existing plan file (one an earlier `write` returned) to refine it instead of creating a new one. " +
|
|
61
|
+
"`exit`: the file `write` returned, which the user reviews. " +
|
|
62
|
+
"`steps`: the approved plan file, as `exit` returned it.",
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
updates: Type.Optional(
|
|
66
|
+
Type.Array(StepUpdateParams, {
|
|
67
|
+
minItems: 1,
|
|
68
|
+
description:
|
|
69
|
+
"`steps`: the steps to set; each entry sets one step's checkbox, and the last entry wins for a duplicate step.",
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const DESCRIPTION =
|
|
75
|
+
"Plan mode and the plan file it is built around, in one tool. `enter` asks the user to enter read-only plan mode; " +
|
|
76
|
+
"`write` saves the plan you designed as markdown in the plans directory; `exit` presents the saved plan for approval; " +
|
|
77
|
+
"`steps` ticks its steps off in place; `cancel` leaves plan mode without approving. Each action refuses outside its " +
|
|
78
|
+
"mode, and every field but `action` says which action reads it.\n\n" +
|
|
79
|
+
"`write` takes the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
|
|
80
|
+
`${PLAN_OUTLINE}\n\n` +
|
|
81
|
+
"Each section has one job: lead it with its outcome, keep a prose section to a few bullets or two or three " +
|
|
82
|
+
"sentences, give each step one verifiable outcome with the detail in a nested note, and put the sketch, files, " +
|
|
83
|
+
"and commands in inline code or a fence rather than pasted prose. " +
|
|
84
|
+
"When the change alters a surface that already exists, Visual shows it before and after in one fence each; " +
|
|
85
|
+
"tag a fence only when every line in it is code. " +
|
|
86
|
+
"The outline is a guide, not a form: drop Context, Approach, Visual, or Risks when there is nothing to say. " +
|
|
87
|
+
"Keep Steps and Verification — the steps are read from that section (without it, the shallowest list " +
|
|
88
|
+
"anywhere becomes the steps), and Verification becomes the last todo item. " +
|
|
89
|
+
"In Steps, each top-level bullet is one outcome in the order you will do them; " +
|
|
90
|
+
"an indented line continues that outcome, and a deeper bullet is a note. " +
|
|
91
|
+
`A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it ` +
|
|
92
|
+
"cannot track. `write` returns the path to pass to `exit`; plan_path overwrites an existing file when refining.\n\n" +
|
|
93
|
+
"`steps` sets the checkboxes of the given top-level steps of an approved plan in one write, and their todo items " +
|
|
94
|
+
"with them. Each step number matches the order the plan was approved in; a step can be marked completed or reopened. " +
|
|
95
|
+
"Only the checkboxes change; the step text is left as written. The file is rewritten once, and nothing changes when " +
|
|
96
|
+
"any step is out of range or an entry's expected text no longer matches the file.\n\n" +
|
|
97
|
+
"`exit` reads the plan file back and presents it with approve, keep, refine, and exit choices; on approval plan mode " +
|
|
98
|
+
"ends, write access returns, and the plan's steps go into the todo list. The plan file stays the source of truth. " +
|
|
99
|
+
"`cancel` is the escape hatch that needs no plan file: write access returns and nothing is recorded.";
|
|
100
|
+
|
|
101
|
+
/** The verb a call names, or undefined for a transcript recorded before the fold. */
|
|
102
|
+
function planActionOf(raw: unknown): PlanAction | undefined {
|
|
103
|
+
return typeof raw === "string" ? PLAN_ACTIONS.find((action) => action === raw) : undefined;
|
|
104
|
+
}
|
|
32
105
|
|
|
33
106
|
export function registerTools(
|
|
34
107
|
pi: ExtensionAPI,
|
|
35
108
|
runtime: PlanRuntime,
|
|
36
|
-
web: PlanWebReview =
|
|
109
|
+
web: PlanWebReview = createLazyPlanWebReview(runtime.plans, NO_SERVER_DEPS),
|
|
37
110
|
): void {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
111
|
+
pi.registerTool<typeof PlanParams, PlanDetails>({
|
|
112
|
+
name: PLAN_TOOL,
|
|
113
|
+
label: "Plan",
|
|
114
|
+
description: DESCRIPTION,
|
|
115
|
+
promptSnippet: "Enter read-only plan mode, save the plan, present it for approval, tick its steps, or leave it.",
|
|
116
|
+
promptGuidelines: [
|
|
117
|
+
"Call plan with action enter before a non-trivial implementation task; skip it for small, obvious changes.",
|
|
118
|
+
"While plan mode is active, write the full plan with action write, then call action exit with the returned plan_path.",
|
|
119
|
+
"When refining with plan_path, keep every section heading as it was: the review marks changed sections by heading, " +
|
|
120
|
+
"so a rename reads as an edit.",
|
|
121
|
+
"After a plan is approved, call action steps as you complete steps, passing each step's text as expected so a " +
|
|
122
|
+
"reordered plan is refused instead of mis-ticked.",
|
|
123
|
+
"Call action cancel when the user asks to leave or abandon planning without approving a plan.",
|
|
124
|
+
],
|
|
125
|
+
parameters: PlanParams,
|
|
126
|
+
executionMode: "sequential",
|
|
127
|
+
// One annotation set for five verbs, so it takes the strictest of them: `write`
|
|
128
|
+
// overwrites a plan file with plan_path. The actions that only change the mode
|
|
129
|
+
// are exempted by the guard in `index.ts`, which is the only place that can
|
|
130
|
+
// still tell them apart.
|
|
131
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
132
|
+
|
|
133
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
134
|
+
switch (params.action) {
|
|
135
|
+
case "enter":
|
|
136
|
+
return await executeEnter(runtime, ctx);
|
|
137
|
+
case "write":
|
|
138
|
+
return await executeWrite(runtime, params, ctx);
|
|
139
|
+
case "exit":
|
|
140
|
+
return await executeExit(pi, runtime, web, params, ctx, signal);
|
|
141
|
+
case "steps":
|
|
142
|
+
return await executeSteps(runtime, params, ctx);
|
|
143
|
+
case "cancel":
|
|
144
|
+
return await executeCancel(runtime, ctx);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
renderCall(args, theme, context) {
|
|
149
|
+
const last = context?.lastComponent;
|
|
150
|
+
switch (planActionOf(args.action)) {
|
|
151
|
+
case "enter":
|
|
152
|
+
return callEnter(theme, last);
|
|
153
|
+
case "write":
|
|
154
|
+
return callWrite(args, theme, last);
|
|
155
|
+
case "exit":
|
|
156
|
+
return callExit(args, theme, last);
|
|
157
|
+
case "steps":
|
|
158
|
+
return callSteps(args, theme, last);
|
|
159
|
+
case "cancel":
|
|
160
|
+
return callCancel(theme, last);
|
|
161
|
+
case undefined:
|
|
162
|
+
return callLine(theme, last, PLAN_TOOL, "");
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
renderResult(result, options, theme, context) {
|
|
167
|
+
const text = reuseText(context?.lastComponent);
|
|
168
|
+
const details = toolDetails<PlanDetails>(result.details);
|
|
169
|
+
const outcome = detailsOrLine(theme, result, options.expanded, details);
|
|
170
|
+
if ("line" in outcome) {
|
|
171
|
+
text.setText(outcome.line);
|
|
172
|
+
return text;
|
|
173
|
+
}
|
|
174
|
+
const found = outcome.details;
|
|
175
|
+
// No `action` means a refusal (which reports only its verb and reason), or a
|
|
176
|
+
// result recorded before the tool was folded; its own text is what is left.
|
|
177
|
+
if (!("action" in found)) {
|
|
178
|
+
text.setText(paint(theme, "toolOutput", messageText(result)));
|
|
179
|
+
return text;
|
|
180
|
+
}
|
|
181
|
+
switch (found.action) {
|
|
182
|
+
case "enter":
|
|
183
|
+
text.setText(textEnter(found, theme));
|
|
184
|
+
break;
|
|
185
|
+
case "write":
|
|
186
|
+
text.setText(textWrite(found, theme));
|
|
187
|
+
break;
|
|
188
|
+
case "exit":
|
|
189
|
+
text.setText(textExit(found, options.expanded, theme));
|
|
190
|
+
break;
|
|
191
|
+
case "steps":
|
|
192
|
+
text.setText(textSteps(found, theme));
|
|
193
|
+
break;
|
|
194
|
+
case "cancel":
|
|
195
|
+
text.setText(textCancel(found, theme));
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
return text;
|
|
199
|
+
},
|
|
200
|
+
});
|
|
43
201
|
}
|
package/extensions/plan/tui.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal rendering for the plan review screen.
|
|
3
3
|
*
|
|
4
|
-
* `PlanViewComponent` is the scrollable review screen `
|
|
4
|
+
* `PlanViewComponent` is the scrollable review screen the `exit` action opens
|
|
5
5
|
* for approval. The plan body is rendered by Pi's own `Markdown` component with
|
|
6
6
|
* the active Markdown theme, so headings, lists, code, links, and quotes match
|
|
7
7
|
* the rest of the application; plan-authored control characters are stripped
|
|
@@ -100,10 +100,10 @@ const GAUGE_SEGMENTS = 10;
|
|
|
100
100
|
/** Footer hints shared by the review and the inline editor's primary actions. */
|
|
101
101
|
const PRIMARY_HINTS: ScreenHint[] = [
|
|
102
102
|
"Esc keep plan",
|
|
103
|
-
{ text: "a approve", color: "
|
|
103
|
+
{ text: "a approve", color: "ok" },
|
|
104
104
|
{ text: "r refine", color: "accent" },
|
|
105
105
|
{ text: "w browser", color: "accent" },
|
|
106
|
-
{ text: "x exit plan", color: "
|
|
106
|
+
{ text: "x exit plan", color: "warn" },
|
|
107
107
|
];
|
|
108
108
|
/** Scroll keys appended to the review footer while the plan overflows. */
|
|
109
109
|
const SCROLL_HINTS: ScreenHint[] = ["j/k scroll", "space page", "g/G ends"];
|
|
@@ -168,7 +168,7 @@ function scrollRow(
|
|
|
168
168
|
return truncateToWidth(` ${head}`, width);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
/** Scrollable plan review screen opened by `
|
|
171
|
+
/** Scrollable plan review screen opened by the `exit` action. */
|
|
172
172
|
export class PlanViewComponent implements Component {
|
|
173
173
|
private offset = 0;
|
|
174
174
|
private visible = DEFAULT_ROWS;
|
|
@@ -407,7 +407,7 @@ export class PlanViewComponent implements Component {
|
|
|
407
407
|
lines.push(truncateToWidth(` ${paint(theme, "meta", "Refine the plan:")}`, w));
|
|
408
408
|
for (const line of editorLines) lines.push(truncateToWidth(` ${line}`, w));
|
|
409
409
|
lines.push("");
|
|
410
|
-
lines.push(screenHint(theme, w, ["Esc back to plan", { text: "Enter submit", color: "
|
|
410
|
+
lines.push(screenHint(theme, w, ["Esc back to plan", { text: "Enter submit", color: "ok" }]));
|
|
411
411
|
} else {
|
|
412
412
|
if (this.maxOffset > 0) lines.push(scrollRow(theme, w, this.offset, end, body.length, sectionLabel));
|
|
413
413
|
else lines.push("");
|
package/extensions/plan/types.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import type { PlanAnnotation } from "./feedback.ts";
|
|
6
6
|
import type { SectionDigest } from "./plan-text.ts";
|
|
7
|
+
import type { PlanAction } from "./runtime.ts";
|
|
8
|
+
|
|
9
|
+
export type { PlanAction };
|
|
7
10
|
import type { StepUpdate } from "./steps.ts";
|
|
8
11
|
|
|
9
12
|
/**
|
|
@@ -65,8 +68,10 @@ export interface SeededStepsEntry {
|
|
|
65
68
|
steps: string[];
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
/** Structured result carried in `
|
|
71
|
+
/** Structured result carried in the `plan` tool's `details` for `action: "enter"`. */
|
|
69
72
|
export interface EnterPlanModeDetails {
|
|
73
|
+
/** The verb this result answers, so a renderer reads the result instead of the call. */
|
|
74
|
+
action: "enter";
|
|
70
75
|
/** Whether the user agreed to enter plan mode. */
|
|
71
76
|
entered: boolean;
|
|
72
77
|
/** True when plan mode was already enabled before this call. */
|
|
@@ -75,8 +80,10 @@ export interface EnterPlanModeDetails {
|
|
|
75
80
|
unavailable?: boolean;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
/** Structured result carried in `
|
|
83
|
+
/** Structured result carried in the `plan` tool's `details` for `action: "write"`. */
|
|
79
84
|
export interface WritePlanDetails {
|
|
85
|
+
/** The verb this result answers. */
|
|
86
|
+
action: "write";
|
|
80
87
|
/** Absolute path of the written plan file. */
|
|
81
88
|
path: string;
|
|
82
89
|
/** How the path is shown: `~`-shortened, since plans live outside the project. */
|
|
@@ -85,8 +92,10 @@ export interface WritePlanDetails {
|
|
|
85
92
|
bytes: number;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
/** Structured result carried in `
|
|
95
|
+
/** Structured result carried in the `plan` tool's `details` for `action: "steps"`. */
|
|
89
96
|
export interface SetPlanStepsDetails {
|
|
97
|
+
/** The verb this result answers. */
|
|
98
|
+
action: "steps";
|
|
90
99
|
/** Absolute path of the plan file that was updated. */
|
|
91
100
|
path: string;
|
|
92
101
|
/** How the path is shown: `~`-shortened. */
|
|
@@ -99,8 +108,10 @@ export interface SetPlanStepsDetails {
|
|
|
99
108
|
total: number;
|
|
100
109
|
}
|
|
101
110
|
|
|
102
|
-
/** Structured result carried in `
|
|
111
|
+
/** Structured result carried in the `plan` tool's `details` for `action: "exit"`. */
|
|
103
112
|
export interface ExitPlanModeDetails {
|
|
113
|
+
/** The verb this result answers. */
|
|
114
|
+
action: "exit";
|
|
104
115
|
/** Whether the user approved the plan. */
|
|
105
116
|
approved: boolean;
|
|
106
117
|
/** The plan the model submitted, read from its file. */
|
|
@@ -121,8 +132,40 @@ export interface ExitPlanModeDetails {
|
|
|
121
132
|
unavailable?: boolean;
|
|
122
133
|
}
|
|
123
134
|
|
|
124
|
-
/** Structured result carried in `
|
|
135
|
+
/** Structured result carried in the `plan` tool's `details` for `action: "cancel"`. */
|
|
125
136
|
export interface CancelPlanModeDetails {
|
|
137
|
+
/** The verb this result answers. */
|
|
138
|
+
action: "cancel";
|
|
126
139
|
/** True when plan mode was on and has now been turned off. */
|
|
127
140
|
cancelled: boolean;
|
|
128
141
|
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A refused call: the reason, and the verb it named. It carries no `action`
|
|
145
|
+
* field on purpose — a refusal has none of that verb's state to report, and the
|
|
146
|
+
* absent discriminant is what tells a renderer it is looking at a rejection.
|
|
147
|
+
*/
|
|
148
|
+
export interface RefusedPlanDetails {
|
|
149
|
+
/** The verb the call named. */
|
|
150
|
+
refused: PlanAction;
|
|
151
|
+
/** Why the call was refused, as the model reads it. */
|
|
152
|
+
error: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* What the `plan` tool's result carries: one of the five verbs, told apart by
|
|
157
|
+
* `action`, or a refusal that carried no state at all.
|
|
158
|
+
*
|
|
159
|
+
* The details name their own verb so a renderer draws the result it was given
|
|
160
|
+
* rather than re-reading the call's arguments, which a resumed transcript may
|
|
161
|
+
* no longer spell the same way. A details block recorded before the fold has no
|
|
162
|
+
* `action` at all, and a refusal never reaches the per-verb renderers; both take
|
|
163
|
+
* the same fallback.
|
|
164
|
+
*/
|
|
165
|
+
export type PlanDetails =
|
|
166
|
+
| EnterPlanModeDetails
|
|
167
|
+
| WritePlanDetails
|
|
168
|
+
| ExitPlanModeDetails
|
|
169
|
+
| SetPlanStepsDetails
|
|
170
|
+
| CancelPlanModeDetails
|
|
171
|
+
| RefusedPlanDetails;
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
CLIENT_BASE_JS,
|
|
24
24
|
CLIENT_RUNTIME_JS,
|
|
25
25
|
dialogScript,
|
|
26
|
+
INLINE_MARKDOWN_JS,
|
|
26
27
|
keysDialogScript,
|
|
27
28
|
THEME_TOGGLE_JS,
|
|
28
29
|
} from "../../lib/web/index.ts";
|
|
@@ -125,6 +126,8 @@ ${WEB_COMMENTS_JS}
|
|
|
125
126
|
|
|
126
127
|
${WEB_COLLAB_JS}
|
|
127
128
|
|
|
129
|
+
${INLINE_MARKDOWN_JS}
|
|
130
|
+
|
|
128
131
|
${WEB_MARKDOWN_JS}
|
|
129
132
|
|
|
130
133
|
// Review facts: the plan's step progress, and the comments per section.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan review server, loaded when a review first asks for it.
|
|
3
|
+
*
|
|
4
|
+
* The browser review owns `lib/http`, the web kit, and the plan page; none of it
|
|
5
|
+
* is needed to register `/plan` or the plan tools. `index.ts`, `commands.ts`, and
|
|
6
|
+
* `tools.ts` therefore share this proxy over `lib/lazy`: the first review loads
|
|
7
|
+
* `web.ts` once, and `closeAll` on shutdown only closes a review that was already
|
|
8
|
+
* opened, so a session that never reviewed a plan imports none of the server.
|
|
9
|
+
*
|
|
10
|
+
* The synchronous reads (`current`, `shareState`) answer `undefined` until the
|
|
11
|
+
* module is loaded, which is the truth: no review can be open before a load.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { lazy } from "../../lib/lazy.ts";
|
|
15
|
+
import type { PlanStore } from "./plans.ts";
|
|
16
|
+
import type { PlanWebReview, PlanWebReviewDeps } from "./web.ts";
|
|
17
|
+
|
|
18
|
+
/** The review module, as the proxy loads it. */
|
|
19
|
+
export type PlanWebModule = typeof import("./web.ts");
|
|
20
|
+
|
|
21
|
+
/** How the proxy reaches the review module; production uses the dynamic import. */
|
|
22
|
+
export type PlanWebLoader = () => Promise<PlanWebModule>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The deps a caller that injects its own review leaves out.
|
|
26
|
+
*
|
|
27
|
+
* A review needs a place on the session's server, so a caller with no wiring says so
|
|
28
|
+
* rather than binding a socket of its own: the start is refused with a sentence.
|
|
29
|
+
*/
|
|
30
|
+
export const NO_SERVER_DEPS: PlanWebReviewDeps = {
|
|
31
|
+
mount: async () => ({ kind: "refused", reason: "this review has no server wired" }),
|
|
32
|
+
session: () => "",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function createLazyPlanWebReview(
|
|
36
|
+
store: PlanStore,
|
|
37
|
+
deps: PlanWebReviewDeps,
|
|
38
|
+
load: PlanWebLoader = () => import("./web.ts"),
|
|
39
|
+
): PlanWebReview {
|
|
40
|
+
const review = lazy(async () => (await load()).createPlanWebReview(store, deps));
|
|
41
|
+
return {
|
|
42
|
+
start: async (cwd, plan, options) => (await review.get()).start(cwd, plan, options),
|
|
43
|
+
current: () => review.peek()?.current(),
|
|
44
|
+
share: async () => (await review.get()).share(),
|
|
45
|
+
stopSharing: async () => (await review.get()).stopSharing(),
|
|
46
|
+
shareState: () => review.peek()?.shareState(),
|
|
47
|
+
// Shutdown must not start a load: a review that never opened has nothing to close.
|
|
48
|
+
closeAll: async () => {
|
|
49
|
+
const opened = review.peek();
|
|
50
|
+
if (opened !== undefined) await opened.closeAll();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -1,76 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The block half of the plan review's Markdown: tables, fenced code, headings, quotes,
|
|
3
|
+
* and lists.
|
|
3
4
|
*
|
|
4
5
|
* It is a fragment of the same IIFE `web-client.ts` ships (`WEB_MARKDOWN_JS` is
|
|
5
6
|
* interpolated into `REVIEW_CLIENT_JS`), kept in its own module so the client's
|
|
6
|
-
* event wiring and its document rendering are read one at a time.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* event wiring and its document rendering are read one at a time. The text inside a
|
|
8
|
+
* block is rendered by `renderInline`, which crosses from `lib/web/markdown.ts` with
|
|
9
|
+
* the board's card notes — one inline grammar and one link policy for both pages.
|
|
10
|
+
*
|
|
11
|
+
* Every value it writes is a DOM text node: plan and comment text never becomes markup,
|
|
12
|
+
* and the only HTML it copies is the server's own `tok-*` spans.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
|
-
export const WEB_MARKDOWN_JS = String.raw` //
|
|
12
|
-
// the review origin is what catches a target that only looks relative: the
|
|
13
|
-
// URL parser strips a tab or newline, so "/\t/evil.com" is cross-origin once
|
|
14
|
-
// it is parsed, and a disguised target must stay text.
|
|
15
|
-
function safeHref(url, base) {
|
|
16
|
-
var trimmed = String(url).trim();
|
|
17
|
-
if (!trimmed || /^\/\//.test(trimmed)) return null;
|
|
18
|
-
var resolved;
|
|
19
|
-
try { resolved = new URL(trimmed, base); } catch (error) { return null; }
|
|
20
|
-
if (resolved.protocol === "mailto:") return trimmed;
|
|
21
|
-
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return null;
|
|
22
|
-
if (resolved.username || resolved.password) return null;
|
|
23
|
-
if (resolved.origin === new URL(base).origin) return resolved.href;
|
|
24
|
-
return /^https?:\/\//i.test(trimmed) ? resolved.href : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Inline token order matters: code, emphasis, and links are matched before a
|
|
28
|
-
// bare URL, so a URL inside a link target or a code span is never re-split.
|
|
29
|
-
var INLINE = /(\x60[^\x60]+\x60|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\)|(?:https?:\/\/|www\.)[^\s<]+)/g;
|
|
30
|
-
|
|
31
|
-
function renderInline(container, text) {
|
|
32
|
-
String(text).split(INLINE).forEach(function (part) {
|
|
33
|
-
if (!part) return;
|
|
34
|
-
var match;
|
|
35
|
-
if ((match = /^\x60([^\x60]+)\x60$/.exec(part))) {
|
|
36
|
-
var code = el("code"); code.textContent = match[1]; container.appendChild(code); return;
|
|
37
|
-
}
|
|
38
|
-
if ((match = /^\*\*([^*]+)\*\*$/.exec(part))) {
|
|
39
|
-
var strong = el("strong"); strong.textContent = match[1]; container.appendChild(strong); return;
|
|
40
|
-
}
|
|
41
|
-
if ((match = /^\*([^*]+)\*$/.exec(part))) {
|
|
42
|
-
var em = el("em"); em.textContent = match[1]; container.appendChild(em); return;
|
|
43
|
-
}
|
|
44
|
-
if ((match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part))) {
|
|
45
|
-
var href = safeHref(match[2], location.href);
|
|
46
|
-
if (!href) { container.appendChild(document.createTextNode(part)); return; }
|
|
47
|
-
var link = el("a"); link.textContent = match[1]; link.href = href; link.target = "_blank"; link.rel = "noreferrer noopener";
|
|
48
|
-
container.appendChild(link); return;
|
|
49
|
-
}
|
|
50
|
-
// GFM extended autolink: a bare http(s) or www. URL in running text.
|
|
51
|
-
if ((match = /^(?:https?:\/\/|www\.)[^\s<]+$/.exec(part))) {
|
|
52
|
-
var url = match[0].replace(/[.,;:!?*_~]+$/, "");
|
|
53
|
-
while (url.charAt(url.length - 1) === ")" && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) {
|
|
54
|
-
url = url.slice(0, -1).replace(/[.,;:!?*_~]+$/, "");
|
|
55
|
-
}
|
|
56
|
-
var dot = url.indexOf(".");
|
|
57
|
-
var target = /^www\./i.test(url) ? "http://" + url : url;
|
|
58
|
-
var safe = dot > 0 && /\.[^./]/.test(url.slice(dot)) ? safeHref(target, location.href) : null;
|
|
59
|
-
if (safe) {
|
|
60
|
-
var auto = el("a");
|
|
61
|
-
auto.textContent = url;
|
|
62
|
-
auto.href = safe;
|
|
63
|
-
auto.target = "_blank";
|
|
64
|
-
auto.rel = "noreferrer noopener";
|
|
65
|
-
container.appendChild(auto);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
container.appendChild(document.createTextNode(part));
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Copy only text and tok-* spans out of server-highlighted HTML, so a code
|
|
15
|
+
export const WEB_MARKDOWN_JS = String.raw` // Copy only text and tok-* spans out of server-highlighted HTML, so a code
|
|
74
16
|
// line can never introduce markup even though it arrives as a string.
|
|
75
17
|
function copyTokens(from, to) {
|
|
76
18
|
Array.prototype.forEach.call(from.childNodes, function (node) {
|
|
@@ -60,7 +60,7 @@ export interface ReviewPageData {
|
|
|
60
60
|
outline: ReviewOutlineItem[];
|
|
61
61
|
/**
|
|
62
62
|
* The plan's step progress, from the same `extractPlanSteps` the approval and
|
|
63
|
-
* `
|
|
63
|
+
* the `steps` action use: the `Steps` section when there is one, so the page
|
|
64
64
|
* agrees with what approval seeds rather than counting every checkbox.
|
|
65
65
|
*/
|
|
66
66
|
steps: { done: number; total: number };
|
|
@@ -71,7 +71,7 @@ export interface ReviewPageData {
|
|
|
71
71
|
* count, comment length, and quote length, from `feedback.ts`.
|
|
72
72
|
*/
|
|
73
73
|
limits: { comments: number; comment: number; quote: number };
|
|
74
|
-
/** Whether the review may approve the plan (only the `
|
|
74
|
+
/** Whether the review may approve the plan (only the `plan` tool's `exit` flow). */
|
|
75
75
|
canApprove: boolean;
|
|
76
76
|
/** Who is looking: `owner` may approve, `reviewer` may only comment. */
|
|
77
77
|
role: ReviewRole;
|
|
@@ -150,7 +150,7 @@ export function reviewPageData(input: ReviewPayloadInput): ReviewPageData {
|
|
|
150
150
|
},
|
|
151
151
|
],
|
|
152
152
|
);
|
|
153
|
-
// The same step list approval seeds and
|
|
153
|
+
// The same step list approval seeds and the `steps` action indexes, so the page's
|
|
154
154
|
// progress cannot disagree with the todo list.
|
|
155
155
|
const stepList = extractPlanSteps(current);
|
|
156
156
|
// Highlight code blocks once, at the boundary: the client copies only the tok-*
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The request router for browser plan review.
|
|
3
3
|
*
|
|
4
|
-
* A request is validated (method, `Host`, origin, token
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A request is validated (method, `Host`, origin, token, and — on a route that
|
|
5
|
+
* reads a body — that the body is JSON), matched to a route, and answered with a
|
|
6
|
+
* plain {@link ServeResponse}, so every route can be tested without a socket. The
|
|
7
|
+
* `/events` route is a marker: the node server in `web.ts` keeps that response open
|
|
8
|
+
* as a Server-Sent Events stream.
|
|
8
9
|
*
|
|
9
10
|
* The token is a per-session secret carried in the page URL, and on a shared
|
|
10
11
|
* review it is the only credential a reviewer has: it names the caller — the
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
pageHeaders,
|
|
21
22
|
parseRequestUrl,
|
|
22
23
|
postOnly,
|
|
24
|
+
requireJsonBody,
|
|
23
25
|
scriptHeaders,
|
|
24
26
|
type ServeResponse,
|
|
25
27
|
} from "../../lib/http/index.ts";
|
|
@@ -40,7 +42,7 @@ export interface ReviewContext {
|
|
|
40
42
|
caller: (token: string) => ReviewCaller | undefined;
|
|
41
43
|
/** The host names this server answers to now, so a rebound page is refused. */
|
|
42
44
|
hosts: () => readonly string[];
|
|
43
|
-
/** Whether the browser may approve the plan (only the `
|
|
45
|
+
/** Whether the browser may approve the plan (only the `plan` tool's `exit` flow). */
|
|
44
46
|
canApprove: boolean;
|
|
45
47
|
/** Build the page data for the caller (recomputed per request). */
|
|
46
48
|
page: (caller: ReviewCaller) => Promise<ReviewPageData>;
|
|
@@ -157,7 +159,9 @@ export async function handleReviewRequest(
|
|
|
157
159
|
}
|
|
158
160
|
if (pathname === "/api/share/revoke") {
|
|
159
161
|
const refusal =
|
|
160
|
-
postOnly(method, "revoke a link") ??
|
|
162
|
+
postOnly(method, "revoke a link") ??
|
|
163
|
+
requireJsonBody(headers["content-type"]) ??
|
|
164
|
+
roleOnly(caller, ["owner"], "Only the plan owner can revoke a link.");
|
|
161
165
|
if (refusal) return refusal;
|
|
162
166
|
const id = linkIdOf(body);
|
|
163
167
|
if (id === undefined) return jsonError(400, "The request body must name the link's id.");
|
|
@@ -169,7 +173,9 @@ export async function handleReviewRequest(
|
|
|
169
173
|
// A reviewer's whole surface: persist the comments they wrote and end
|
|
170
174
|
// nothing. Only the owner's own save decides the review.
|
|
171
175
|
const refusal =
|
|
172
|
-
postOnly(method, "post comments") ??
|
|
176
|
+
postOnly(method, "post comments") ??
|
|
177
|
+
requireJsonBody(headers["content-type"]) ??
|
|
178
|
+
roleOnly(caller, ["reviewer"], "Only a shared reviewer posts comments.");
|
|
173
179
|
if (refusal) return refusal;
|
|
174
180
|
const annotations = parseAnnotations(body);
|
|
175
181
|
if (isResponse(annotations)) return annotations;
|
|
@@ -182,6 +188,7 @@ export async function handleReviewRequest(
|
|
|
182
188
|
// `/api/comment`, so this route can never settle a shared review.
|
|
183
189
|
const refusal =
|
|
184
190
|
postOnly(method, "save feedback") ??
|
|
191
|
+
requireJsonBody(headers["content-type"]) ??
|
|
185
192
|
roleOnly(caller, ["owner"], "Only the plan owner saves this review; a reviewer posts comments.");
|
|
186
193
|
if (refusal) return refusal;
|
|
187
194
|
const annotations = parseAnnotations(body);
|
|
@@ -192,10 +199,12 @@ export async function handleReviewRequest(
|
|
|
192
199
|
return jsonResponse(200, { ok: true });
|
|
193
200
|
}
|
|
194
201
|
if (pathname === "/api/approve") {
|
|
195
|
-
// Only the owner's own page approves, and only the
|
|
202
|
+
// Only the owner's own page approves, and only the `exit` flow owns
|
|
196
203
|
// the side effects: a reviewer token is refused whatever it posts.
|
|
197
204
|
const refusal =
|
|
198
|
-
postOnly(method, "approve") ??
|
|
205
|
+
postOnly(method, "approve") ??
|
|
206
|
+
requireJsonBody(headers["content-type"]) ??
|
|
207
|
+
roleOnly(caller, ["owner"], "A shared review cannot approve the plan.");
|
|
199
208
|
if (refusal) return refusal;
|
|
200
209
|
if (!ctx.canApprove) return jsonError(403, "This review cannot be approved from the browser.");
|
|
201
210
|
const annotations = parseAnnotations(body);
|