@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/web.ts
CHANGED
|
@@ -57,7 +57,7 @@ export interface PlanReviewSession {
|
|
|
57
57
|
export interface PlanReviewOptions {
|
|
58
58
|
/** Abort signal; aborting closes the session as cancelled. */
|
|
59
59
|
signal?: AbortSignal;
|
|
60
|
-
/** Whether the browser may approve the plan (only the `
|
|
60
|
+
/** Whether the browser may approve the plan (only the `plan` tool's `exit` flow). */
|
|
61
61
|
canApprove?: boolean;
|
|
62
62
|
/** The section digests the previous review showed, so this page can mark changed sections. */
|
|
63
63
|
baseline?: SectionDigest[];
|
|
@@ -131,17 +131,6 @@ const DROP_GRACE_MS = 1500;
|
|
|
131
131
|
*/
|
|
132
132
|
const PLAN_POLL_MS = 1000;
|
|
133
133
|
|
|
134
|
-
/**
|
|
135
|
-
* The deps a caller that injects its own review leaves out.
|
|
136
|
-
*
|
|
137
|
-
* A review needs a place on the session's server, so a caller with no wiring says so
|
|
138
|
-
* rather than binding a socket of its own: the start is refused with a sentence.
|
|
139
|
-
*/
|
|
140
|
-
export const NO_SERVER_DEPS: PlanWebReviewDeps = {
|
|
141
|
-
mount: async () => ({ kind: "refused", reason: "this review has no server wired" }),
|
|
142
|
-
session: () => "",
|
|
143
|
-
};
|
|
144
|
-
|
|
145
134
|
export function createPlanWebReview(store: PlanStore, deps: PlanWebReviewDeps): PlanWebReview {
|
|
146
135
|
const sessions = new Set<PlanReviewSession>();
|
|
147
136
|
// What each running review can do about sharing; the session itself stays the
|
|
@@ -14,7 +14,6 @@ import { ASK_USER_QUESTION_TOOL } from "../../lib/tool-names.ts";
|
|
|
14
14
|
import { UNAVAILABLE_TEXT, formatAnswerText, optionSummary, questionHeaders, unavailableResult } from "./answers.ts";
|
|
15
15
|
import { askViaDialogs, type QuestionUI } from "./dialogs.ts";
|
|
16
16
|
import { AskUserQuestionParams, normalizeQuestions, type AskUserQuestionArgs } from "./schema.ts";
|
|
17
|
-
import { askViaTui } from "./tui.ts";
|
|
18
17
|
import type { AskResult } from "./types.ts";
|
|
19
18
|
|
|
20
19
|
export const TOOL_NAME = ASK_USER_QUESTION_TOOL;
|
|
@@ -59,7 +58,12 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
59
58
|
// A remote-answered turn falls back to dialogs; only a local TUI turn uses
|
|
60
59
|
// the full-screen questionnaire.
|
|
61
60
|
result = await askHuman(ctx, {
|
|
62
|
-
custom:
|
|
61
|
+
custom: async () => {
|
|
62
|
+
// The questionnaire is the TUI's: a remote or headless answer below loads
|
|
63
|
+
// neither it nor the rails wrapper.
|
|
64
|
+
const { askViaTui } = await import("./view/question-screen.ts");
|
|
65
|
+
return withRailsSuppressed(pi, () => askViaTui(ctx, questions, signal));
|
|
66
|
+
},
|
|
63
67
|
dialogs: () => askViaDialogs(dialogUI(ctx), questions, signal),
|
|
64
68
|
});
|
|
65
69
|
}
|
|
@@ -15,15 +15,15 @@ import {
|
|
|
15
15
|
type TuiMouseEvent,
|
|
16
16
|
type TuiMouseEventResult,
|
|
17
17
|
} from "@earendil-works/pi-tui";
|
|
18
|
-
import { sanitizeLine } from "
|
|
19
|
-
import { flowCells } from "
|
|
20
|
-
import { clampScroll, formatRange, keepVisible, selectionMarker, wheelDelta } from "
|
|
21
|
-
import { editorTheme, screenHint, type ScreenHint } from "
|
|
22
|
-
import { GLYPHS } from "
|
|
23
|
-
import { checkbox } from "
|
|
24
|
-
import { plainRule, titleRule } from "
|
|
25
|
-
import { paint } from "
|
|
26
|
-
import { OTHER_LABEL, cancelledResult, collectAnswers, completedResult, describeSelection } from "
|
|
18
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
19
|
+
import { flowCells } from "../../../lib/tui/columns.ts";
|
|
20
|
+
import { clampScroll, formatRange, keepVisible, selectionMarker, wheelDelta } from "../../../lib/tui/cursor.ts";
|
|
21
|
+
import { editorTheme, screenHint, type ScreenHint } from "../../../lib/tui/chrome.ts";
|
|
22
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
23
|
+
import { checkbox } from "../../../lib/tui/rows.ts";
|
|
24
|
+
import { plainRule, titleRule } from "../../../lib/tui/rules.ts";
|
|
25
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
26
|
+
import { OTHER_LABEL, cancelledResult, collectAnswers, completedResult, describeSelection } from "../answers.ts";
|
|
27
27
|
import {
|
|
28
28
|
allAnswered,
|
|
29
29
|
applyCustomText,
|
|
@@ -34,8 +34,8 @@ import {
|
|
|
34
34
|
type Effect,
|
|
35
35
|
type KeyAction,
|
|
36
36
|
type TuiState,
|
|
37
|
-
} from "
|
|
38
|
-
import type { AskResult, Question, Selection } from "
|
|
37
|
+
} from "../tui-state.ts";
|
|
38
|
+
import type { AskResult, Question, Selection } from "../types.ts";
|
|
39
39
|
|
|
40
40
|
type Outcome =
|
|
41
41
|
| { type: "cancelled"; selections: Map<string, Selection> }
|
|
@@ -10,15 +10,14 @@
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { withRailsSuppressed } from "../../../lib/rails.ts";
|
|
12
12
|
import { formatRewindListText, formatRewindRow } from "../text/format.ts";
|
|
13
|
-
import { openReview } from "./review.ts";
|
|
14
|
-
import { rewindTo } from "./rewind.ts";
|
|
15
13
|
import type { RewindRuntime } from "../state/runtime.ts";
|
|
16
|
-
import { RewindListComponent } from "../view/rewind-tui.ts";
|
|
17
14
|
import { buildRewindPoints, type RewindPoint } from "../state/timeline.ts";
|
|
18
15
|
|
|
19
16
|
/** Pick a point from the timeline, or undefined when the user cancels. */
|
|
20
17
|
async function choosePoint(ctx: ExtensionCommandContext, points: RewindPoint[]): Promise<RewindPoint | undefined> {
|
|
21
18
|
if (ctx.mode === "tui") {
|
|
19
|
+
// The timeline is the TUI's: the picker below and registration load neither.
|
|
20
|
+
const { RewindListComponent } = await import("../view/rewind-tui.ts");
|
|
22
21
|
return ctx.ui.custom<RewindPoint | undefined>(
|
|
23
22
|
(tui, theme, _keybindings, done) =>
|
|
24
23
|
new RewindListComponent({
|
|
@@ -62,6 +61,9 @@ async function openRewind(pi: ExtensionAPI, runtime: RewindRuntime, ctx: Extensi
|
|
|
62
61
|
|
|
63
62
|
const point = await withRailsSuppressed(pi, () => choosePoint(ctx, points));
|
|
64
63
|
if (!point) return;
|
|
64
|
+
// The rewind run and the review gate both load on the verb that needs them: the
|
|
65
|
+
// listing above and registration load neither.
|
|
66
|
+
const { rewindTo } = await import("./rewind.ts");
|
|
65
67
|
await rewindTo(pi, runtime, ctx, root, point);
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -76,6 +78,7 @@ export function registerCommands(pi: ExtensionAPI, runtime: RewindRuntime): void
|
|
|
76
78
|
pi.registerCommand("review", {
|
|
77
79
|
description: "Review the latest turn's changes and revert a file or the whole turn",
|
|
78
80
|
handler: async (_args, ctx) => {
|
|
81
|
+
const { openReview } = await import("./review.ts");
|
|
79
82
|
await openReview(pi, runtime, ctx);
|
|
80
83
|
},
|
|
81
84
|
});
|
|
@@ -24,7 +24,7 @@ import { createOperationQueue } from "../transport/queue.ts";
|
|
|
24
24
|
import { applyRestore, planRestore, type PlanResult, type RestoreInput } from "../transport/restore.ts";
|
|
25
25
|
import type { PathStat } from "../text/paths.ts";
|
|
26
26
|
import { createSnapshot } from "../transport/snapshot.ts";
|
|
27
|
-
import { createStatusChip } from "../
|
|
27
|
+
import { createStatusChip } from "../status.ts";
|
|
28
28
|
import { belongsToRoot, listSnapshots, pruneSnapshots } from "../transport/store.ts";
|
|
29
29
|
import { createTempIndexStore } from "../transport/temp-index.ts";
|
|
30
30
|
import { lastUserEntryId } from "./timeline.ts";
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { GLYPHS, STATUS_KEYS } from "
|
|
12
|
-
import { paint } from "
|
|
13
|
-
import type { RewindConfig } from "
|
|
14
|
-
import { userMessagesFromBranch } from "
|
|
11
|
+
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
12
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
13
|
+
import type { RewindConfig } from "./config/config.ts";
|
|
14
|
+
import { userMessagesFromBranch } from "./state/timeline.ts";
|
|
15
15
|
|
|
16
16
|
const STATUS_KEY = STATUS_KEYS.rewind;
|
|
17
17
|
const STATUS_GLYPH = GLYPHS.rewind;
|
|
@@ -193,10 +193,10 @@ export class ReviewListComponent extends ChecklistScreen<ReviewEntry> {
|
|
|
193
193
|
protected hint(width: number): string {
|
|
194
194
|
return screenHint(this.theme, width, [
|
|
195
195
|
"Esc close",
|
|
196
|
-
{ text: "Enter diff", color: "accent" },
|
|
196
|
+
{ text: "Enter diff", color: "accent" as const },
|
|
197
197
|
"Space select",
|
|
198
|
-
{ text: "r revert", color: "
|
|
199
|
-
{ text: "R all", color: "
|
|
198
|
+
{ text: "r revert", color: "warn" },
|
|
199
|
+
{ text: "R all", color: "warn" },
|
|
200
200
|
"a/n all/none",
|
|
201
201
|
]);
|
|
202
202
|
}
|
|
@@ -314,7 +314,7 @@ export class ReviewDiffComponent implements Component {
|
|
|
314
314
|
return screenHint(this.theme, width, [
|
|
315
315
|
"Esc back",
|
|
316
316
|
...(this.options.canRevertHunks ? [{ text: "h hunks", color: "accent" as const }] : []),
|
|
317
|
-
{ text: "r revert file", color: "
|
|
317
|
+
{ text: "r revert file", color: "warn" },
|
|
318
318
|
"j/k scroll",
|
|
319
319
|
"g/G top/end",
|
|
320
320
|
]);
|
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
* The session's one port: the `serve` section of the shared `bytefly.json` (global
|
|
3
3
|
* then project, project wins).
|
|
4
4
|
*
|
|
5
|
-
* `0` — the default — takes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* `0` — the default — takes the project's own port: its repository key hashed into the
|
|
6
|
+
* band `lib/http/port.ts` holds, so a session that starts again lands where a bookmark
|
|
7
|
+
* already points and two repositories do not compete for one number. A positive value
|
|
8
|
+
* pins the session's server, so every page it serves keeps the address a bookmark
|
|
9
|
+
* already points at. Either way a port another process holds costs the start its
|
|
10
|
+
* OS-chosen fallback — unless it was pinned, which fails the start rather than moving
|
|
11
|
+
* the pages out from under the bookmark.
|
|
10
12
|
*
|
|
11
|
-
* Pin it in the project document when
|
|
12
|
-
*
|
|
13
|
+
* Pin it in the project document when a repository needs a number of its own: a global
|
|
14
|
+
* value is one port for every session.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import { clampInteger, loadConfigSection } from "../../lib/config.ts";
|
|
16
18
|
|
|
17
19
|
/** The `serve` section. */
|
|
18
20
|
export interface ServeConfig {
|
|
19
|
-
/** TCP port on `127.0.0.1`; `0`
|
|
21
|
+
/** TCP port on `127.0.0.1`; `0` takes the project's port, a positive value pins it. */
|
|
20
22
|
port: number;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* own. `plan`, `board`, and `file-browse` ask for a path prefix on
|
|
6
6
|
* {@link HTTP_MOUNT_CHANNEL} and the registry answers with the session's origin, so
|
|
7
7
|
* a session has one port, one close path, and one chip instead of one socket per
|
|
8
|
-
* extension. Binding happens on the first page that asks, never in the factory
|
|
8
|
+
* extension. Binding happens on the first page that asks, never in the factory, and the
|
|
9
|
+
* port it binds is the project's own number unless `serve.port` pins another — so a
|
|
10
|
+
* bookmarked page URL survives a restart.
|
|
9
11
|
*
|
|
10
12
|
* Load with: pi --extension ./extensions/serve
|
|
11
13
|
*/
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
import { networkInterfaces } from "node:os";
|
|
14
16
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
17
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
16
|
-
import { HTTP_MOUNT_CHANNEL, type MountRequest } from "../../lib/http/
|
|
18
|
+
import { HTTP_MOUNT_CHANNEL, type MountRequest } from "../../lib/http/mount.ts";
|
|
17
19
|
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
18
20
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
19
21
|
import { loadServeConfig } from "./config.ts";
|
|
@@ -26,6 +28,9 @@ export default function serve(pi: ExtensionAPI): void {
|
|
|
26
28
|
/** Each session's context, which is where the port chip is drawn. */
|
|
27
29
|
const contexts = new Map<string, ExtensionContext>();
|
|
28
30
|
|
|
31
|
+
/** The directory a session serves: its config and its port both come from it. */
|
|
32
|
+
const cwdOf = (session: string): string => cwds.get(session) ?? process.cwd();
|
|
33
|
+
|
|
29
34
|
/**
|
|
30
35
|
* Publish the session's port, or clear the chip with `undefined`. The chip belongs
|
|
31
36
|
* to the server, not to a page: one number, shown while anything is served on it.
|
|
@@ -45,7 +50,16 @@ export default function serve(pi: ExtensionAPI): void {
|
|
|
45
50
|
};
|
|
46
51
|
|
|
47
52
|
const registry = createServeRegistry({
|
|
48
|
-
portFor: (session) => loadServeConfig(
|
|
53
|
+
portFor: (session) => loadServeConfig(cwdOf(session)).port,
|
|
54
|
+
pickPort: async (session) => {
|
|
55
|
+
// The repository key is a git answer, so it is paid on the first bind rather than
|
|
56
|
+
// at registration: a session that opens no page never asks for it.
|
|
57
|
+
const [{ repoKeyFor }, { projectPort }] = await Promise.all([
|
|
58
|
+
import("../../lib/repo-key.ts"),
|
|
59
|
+
import("../../lib/http/port.ts"),
|
|
60
|
+
]);
|
|
61
|
+
return projectPort((await repoKeyFor(pi, cwdOf(session))).key);
|
|
62
|
+
},
|
|
49
63
|
interfaces: () => networkInterfaces(),
|
|
50
64
|
onPort: setChip,
|
|
51
65
|
});
|
|
@@ -8,26 +8,26 @@
|
|
|
8
8
|
* and closes a session's socket with its last mount. A page that goes away takes
|
|
9
9
|
* only its own prefix with it.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The port is a preference, not a reservation: a session's first bind tries the port
|
|
12
|
+
* it is pinned to, or the project's own number, and falls back to an OS-chosen one when
|
|
13
|
+
* that port is taken. Pinning is the session's, not a page's: `serve.port` belongs to the
|
|
14
|
+
* first session that binds, so a second session in this process (a WeChat peer loads every
|
|
15
|
+
* other extension, `extensions/wechat/peer/peer-session.ts`) falls back to an OS-chosen
|
|
14
16
|
* port instead of failing on the host's pin, and a second *process* is told the pin
|
|
15
17
|
* is in use rather than moved off the address a bookmark names.
|
|
16
18
|
*
|
|
17
19
|
* Value-only: every byte of state is in the returned closure.
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
randomPort,
|
|
30
|
-
type NetworkInterfaces,
|
|
22
|
+
import { lazy } from "../../lib/lazy.ts";
|
|
23
|
+
import type {
|
|
24
|
+
HttpMount,
|
|
25
|
+
MountOutcome,
|
|
26
|
+
MountRequest,
|
|
27
|
+
NetworkInterfaces,
|
|
28
|
+
ServedMount,
|
|
29
|
+
ServedServer,
|
|
30
|
+
SharedListener,
|
|
31
31
|
} from "../../lib/http/index.ts";
|
|
32
32
|
|
|
33
33
|
/** One prefix as this session's server holds it: the mount, and the spec that remounts it. */
|
|
@@ -52,12 +52,17 @@ interface Served {
|
|
|
52
52
|
export interface ServeRegistryDeps {
|
|
53
53
|
/** The port a session is pinned to, or `0` to pick one. */
|
|
54
54
|
portFor(session: string): number;
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The port a session's first bind tries, before the OS-chosen fallback: the project's
|
|
57
|
+
* own number, so a session that starts again lands on the one a bookmark names.
|
|
58
|
+
*/
|
|
59
|
+
pickPort(session: string): number | Promise<number>;
|
|
57
60
|
/** The machine's interfaces; a test injects a snapshot instead. */
|
|
58
61
|
interfaces: () => NetworkInterfaces;
|
|
59
62
|
/** Told when a session's port comes up and when it goes away, so a chip can say so. */
|
|
60
63
|
onPort?: (session: string, port: number | undefined) => void;
|
|
64
|
+
/** The served-page kit; loaded on the first bind, so registration never pays for it. */
|
|
65
|
+
loadServe?: () => Promise<typeof import("../../lib/http/index.ts")>;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
/** The session's servers, as the extension's bus listener drives them. */
|
|
@@ -91,12 +96,15 @@ export function createServeRegistry(deps: ServeRegistryDeps): ServeRegistry {
|
|
|
91
96
|
const remembered = new Map<string, number>();
|
|
92
97
|
/** The session the pinned port belongs to; every later session picks its own. */
|
|
93
98
|
let pinnedBy: string | undefined;
|
|
99
|
+
/** The served-page kit: one load, on the first bind, shared by every later one. */
|
|
100
|
+
const http = lazy(deps.loadServe ?? (() => import("../../lib/http/index.ts")));
|
|
94
101
|
|
|
95
102
|
/** Bind `session`'s server on the pin when it is this session's to take. */
|
|
96
103
|
const bind = async (session: string): Promise<Opened> => {
|
|
104
|
+
const { serveHttp } = await http.get();
|
|
97
105
|
const pinned = deps.portFor(session);
|
|
98
106
|
const usePin = pinned > 0 && (pinnedBy === undefined || pinnedBy === session);
|
|
99
|
-
const preferred = remembered.get(session) ?? (deps.pickPort
|
|
107
|
+
const preferred = remembered.get(session) ?? (await deps.pickPort(session));
|
|
100
108
|
try {
|
|
101
109
|
const server = await serveHttp({ ports: usePin ? [pinned] : [preferred, 0] });
|
|
102
110
|
if (usePin) pinnedBy = session;
|
|
@@ -163,6 +171,7 @@ export function createServeRegistry(deps: ServeRegistryDeps): ServeRegistry {
|
|
|
163
171
|
};
|
|
164
172
|
|
|
165
173
|
const share = async (session: string, base: string): Promise<SharedListener | undefined> => {
|
|
174
|
+
const { serveHttp, lanAddresses } = await http.get();
|
|
166
175
|
const served = sessions.get(session);
|
|
167
176
|
const held = served?.mounts.get(base);
|
|
168
177
|
if (served === undefined || held === undefined) return undefined;
|
|
@@ -12,11 +12,9 @@
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { messageOf } from "../../lib/errors.ts";
|
|
14
14
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
15
|
-
import { withPolledScreen } from "../../lib/screen-session.ts";
|
|
16
15
|
import { formatJoin, formatStatus } from "./format.ts";
|
|
17
16
|
import { COMMAND_NAME } from "./protocol.ts";
|
|
18
17
|
import type { SessionBus } from "./runtime.ts";
|
|
19
|
-
import { SessionListComponent } from "./tui.ts";
|
|
20
18
|
|
|
21
19
|
/** How often the open board re-reads the bus. */
|
|
22
20
|
const BOARD_POLL_MS = 1_000;
|
|
@@ -29,7 +27,7 @@ const BOARD_POLL_MS = 1_000;
|
|
|
29
27
|
* record fresh, so a poll never has to write. Both reads are read-only: the
|
|
30
28
|
* snapshot neither prunes nor refreshes, and the peek neither consumes nor marks.
|
|
31
29
|
*/
|
|
32
|
-
function createBoardReader(bus: SessionBus): (screen:
|
|
30
|
+
function createBoardReader(bus: SessionBus): (screen: SessionsScreen) => Promise<void> {
|
|
33
31
|
let busy = false;
|
|
34
32
|
return async (screen) => {
|
|
35
33
|
if (busy) return;
|
|
@@ -45,12 +43,19 @@ function createBoardReader(bus: SessionBus): (screen: SessionListComponent) => P
|
|
|
45
43
|
};
|
|
46
44
|
}
|
|
47
45
|
|
|
46
|
+
/** The mounted board's type, without loading the screen at registration. */
|
|
47
|
+
type SessionsScreen = InstanceType<typeof import("./view/session-list.ts")["SessionListComponent"]>;
|
|
48
|
+
|
|
48
49
|
/** Open the TUI board, and resolve when it closes. */
|
|
49
50
|
async function openSessionsScreen(pi: ExtensionAPI, ctx: ExtensionCommandContext, bus: SessionBus): Promise<void> {
|
|
51
|
+
// The board is the TUI's: the text paths below and registration load neither it
|
|
52
|
+
// nor the dock screen's poll loop.
|
|
53
|
+
const { withPolledScreen } = await import("../../lib/screen-session.ts");
|
|
54
|
+
const { SessionListComponent } = await import("./view/session-list.ts");
|
|
50
55
|
await bus.start(ctx);
|
|
51
56
|
const [status, messages] = await Promise.all([bus.snapshot(), bus.peek()]);
|
|
52
57
|
const first = { status, messages };
|
|
53
|
-
let read: ((screen:
|
|
58
|
+
let read: ((screen: SessionsScreen) => Promise<void>) | undefined;
|
|
54
59
|
await withPolledScreen(
|
|
55
60
|
pi,
|
|
56
61
|
ctx,
|
|
@@ -59,9 +64,9 @@ async function openSessionsScreen(pi: ExtensionAPI, ctx: ExtensionCommandContext
|
|
|
59
64
|
read = reader;
|
|
60
65
|
// The verbs repaint the board they belong to; their callbacks only run after
|
|
61
66
|
// construction, so a holder keeps the wiring one-directional.
|
|
62
|
-
const holder: { screen?:
|
|
67
|
+
const holder: { screen?: SessionsScreen } = {};
|
|
63
68
|
const component = new SessionListComponent({
|
|
64
|
-
|
|
69
|
+
board: first,
|
|
65
70
|
theme: screen.theme,
|
|
66
71
|
callbacks: {
|
|
67
72
|
toggleWatch: async (armed) => {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* The answer always comes: every path here returns a result, including a failure, so
|
|
11
11
|
* a caller never waits on a listener that threw. A caller that asks for `name` also gets
|
|
12
12
|
* Pi's session name, because `/team join <label>` is the one door that means "call me
|
|
13
|
-
* this from now on"; every
|
|
13
|
+
* this from now on"; the footer draws the roster label after every join either way, so a
|
|
14
|
+
* plain join changes the name on the bar without renaming the session.
|
|
14
15
|
*
|
|
15
16
|
* The claim is the answer, not the result: the bus starts and joins asynchronously, so
|
|
16
17
|
* this answers with the promise and stays claimed while it runs — which is what keeps
|
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { RepeatTimer } from "../../lib/timer.ts";
|
|
15
|
-
import type { BusMessage } from "./types.ts";
|
|
15
|
+
import type { BusMessage, InboxPeek } from "./types.ts";
|
|
16
16
|
|
|
17
17
|
export interface MailWatchOptions {
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* This session's inbox, read-only: up to a page of mail whose ids are not `known`, and
|
|
20
|
+
* the ids of every message still there. Nothing is consumed or marked.
|
|
21
|
+
*/
|
|
22
|
+
peek(known: ReadonlySet<string>): Promise<InboxPeek>;
|
|
20
23
|
/** The repeating timer behind the poll. */
|
|
21
24
|
interval: RepeatTimer;
|
|
22
25
|
/** Poll interval; 0 disables the ticker (tests). */
|
|
@@ -54,7 +57,10 @@ export function createMailWatch(options: MailWatchOptions): MailWatch {
|
|
|
54
57
|
let wake = false;
|
|
55
58
|
let stopTicker: (() => void) | undefined;
|
|
56
59
|
let pendingMail: BusMessage[] = [];
|
|
57
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Ids already queued, reported, or consumed — pruned by what is still on disk, so it
|
|
62
|
+
* holds exactly the announced mail an `inbox` call has not removed yet.
|
|
63
|
+
*/
|
|
58
64
|
let reported = new Set<string>();
|
|
59
65
|
let onWake: (() => void) | undefined;
|
|
60
66
|
|
|
@@ -68,24 +74,27 @@ export function createMailWatch(options: MailWatchOptions): MailWatch {
|
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
/**
|
|
71
|
-
* Queue what
|
|
72
|
-
*
|
|
77
|
+
* Queue what this watch has not announced, once, and wake a turn when this session
|
|
78
|
+
* asked for one.
|
|
73
79
|
*
|
|
74
|
-
* The
|
|
75
|
-
* the
|
|
76
|
-
*
|
|
80
|
+
* The queue and the memory are pruned by what is still on disk, so `reported` is
|
|
81
|
+
* bounded by the inbox rather than by a count: an id that leaves the inbox leaves the
|
|
82
|
+
* set, and no id still on disk can be forgotten while it is there. A count was how a
|
|
83
|
+
* backlog larger than one read made the watch forget an id it had already announced and
|
|
84
|
+
* announce it again.
|
|
77
85
|
*/
|
|
78
86
|
async function pollOnce(): Promise<void> {
|
|
79
87
|
if (!armed) return;
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
//
|
|
88
|
+
const known = new Set(reported);
|
|
89
|
+
for (const message of pendingMail) known.add(message.id);
|
|
90
|
+
const { mail, unread } = await peek(known);
|
|
91
|
+
// The read names everything still in the inbox, which is a different question from
|
|
92
|
+
// what one page covered: a message that expired or was consumed leaves the queue and
|
|
93
|
+
// the memory with its file.
|
|
94
|
+
for (const id of reported) if (!unread.has(id)) reported.delete(id);
|
|
84
95
|
pendingMail = pendingMail.filter((message) => unread.has(message.id));
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (fresh.length === 0) return;
|
|
88
|
-
pendingMail = [...pendingMail, ...fresh];
|
|
96
|
+
if (mail.length === 0) return;
|
|
97
|
+
pendingMail = [...pendingMail, ...mail];
|
|
89
98
|
if (wake) onWake?.();
|
|
90
99
|
}
|
|
91
100
|
|
|
@@ -65,16 +65,28 @@ export function normalizeLimit(raw: unknown): number {
|
|
|
65
65
|
return Math.min(MAX_LIMIT, Math.max(1, Math.floor(raw)));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Repository-relative, posix-normalized paths, or an error.
|
|
70
|
+
*
|
|
71
|
+
* A path that resolves to the repository root is handed on in the caller's own spelling
|
|
72
|
+
* rather than refused here: what it resolves to is the whole repository, and that
|
|
73
|
+
* refusal, with the sentence it is worded in, belongs to `resolve.ts` — one rule, one
|
|
74
|
+
* wording, and one module that owns both.
|
|
75
|
+
*/
|
|
69
76
|
export function normalizePaths(repoRoot: string, cwd: string, raw: unknown): { paths: string[] } | { error: string } {
|
|
70
77
|
if (!Array.isArray(raw) || raw.length === 0) return { error: "paths must contain at least one path." };
|
|
71
78
|
if (raw.length > MAX_PATHS) return { error: `at most ${MAX_PATHS} paths are allowed.` };
|
|
72
79
|
const paths: string[] = [];
|
|
73
80
|
for (const entry of raw) {
|
|
74
81
|
if (typeof entry !== "string" || entry.trim() === "") return { error: "every path must be a non-empty string." };
|
|
75
|
-
const
|
|
82
|
+
const given = entry.trim();
|
|
83
|
+
const absolute = resolve(cwd, given);
|
|
76
84
|
const rel = relative(repoRoot, absolute);
|
|
77
|
-
if (rel === ""
|
|
85
|
+
if (rel === "") {
|
|
86
|
+
if (!paths.includes(given)) paths.push(given);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
78
90
|
return { error: `path "${entry}" is outside the repository.` };
|
|
79
91
|
}
|
|
80
92
|
const normalized = rel.split(sep).join("/");
|
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
* spelling inside the repository and the file it names are one claim, and a path
|
|
7
7
|
* that leaves the repository is refused before anything is written.
|
|
8
8
|
*
|
|
9
|
+
* The caller hands in paths that are already relative to the repository (the
|
|
10
|
+
* spelling rule is `protocol.ts`'s), so what is decided here is what they *resolve*
|
|
11
|
+
* to: the root itself, a target outside it, or a claim.
|
|
12
|
+
*
|
|
9
13
|
* `lib/path.ts` owns the containment predicates; this owns the claim's shape.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
|
-
import {
|
|
16
|
+
import { join, relative, sep } from "node:path";
|
|
13
17
|
import { isInside, realPathOfNearest } from "../../lib/path.ts";
|
|
14
18
|
|
|
15
19
|
/** A path this module declined, with the reason a caller can print. */
|
|
@@ -40,9 +44,6 @@ const ROOT_ITSELF = "is the repository, not a path in it";
|
|
|
40
44
|
/** What a path outside the root resolves to. */
|
|
41
45
|
const OUTSIDE = "does not resolve to a file in the repository";
|
|
42
46
|
|
|
43
|
-
/** What an absolute path is refused for: it is not the claim's to place. */
|
|
44
|
-
const ABSOLUTE = "is absolute; a claim is relative to the repository root";
|
|
45
|
-
|
|
46
47
|
/**
|
|
47
48
|
* Resolve `paths` against `root`, or refuse the first one that is not in it.
|
|
48
49
|
*
|
|
@@ -53,9 +54,6 @@ export function resolveClaimPaths(root: string, paths: readonly string[]): Resol
|
|
|
53
54
|
const realRoot = realPathOfNearest(root);
|
|
54
55
|
const canonical: string[] = [];
|
|
55
56
|
for (const path of paths) {
|
|
56
|
-
// `join` would re-root an absolute path under the repository, which is not what
|
|
57
|
-
// a caller meant: refuse it rather than claim a directory it never named.
|
|
58
|
-
if (isAbsolute(path)) return { error: { path, reason: ABSOLUTE } };
|
|
59
57
|
const target = realPathOfNearest(join(root, path));
|
|
60
58
|
const rel = relative(realRoot, target);
|
|
61
59
|
if (rel === "") return { error: { path, reason: ROOT_ITSELF } };
|