@huseynsnmz/pi-suite 0.0.0-stage → 0.1.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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +87 -2
- package/SECURITY.md +5 -0
- package/package.json +113 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +83 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +398 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2221 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +75 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +23 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +676 -0
- package/packages/frame/src/render/diff.ts +104 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +276 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +61 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +78 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// CI's last run on this branch and your open pull requests that wait on you, asked once per launch.
|
|
2
|
+
// Shows nothing without the forge's tool, network or a remote it serves, and starts only that tool.
|
|
3
|
+
import { isObject } from "@adeildo/pi-kit";
|
|
4
|
+
|
|
5
|
+
import { gitlabHosts, glabMayStart, readGitlab } from "./gitlab.ts";
|
|
6
|
+
import { processRunner, type Runner } from "./runner.ts";
|
|
7
|
+
|
|
8
|
+
const FAILED = new Set(["failure", "startup_failure", "timed_out"]);
|
|
9
|
+
/** Enough to tell a new comment from an old one. */
|
|
10
|
+
const PR_FIELDS = "number,title,author,reviewDecision,comments";
|
|
11
|
+
/** github.com, or an enterprise host like github.acme.dev. */
|
|
12
|
+
const GITHUB_REMOTE = /(?:@|\/\/)(?:[\w-]+\.)*github\.[\w.-]+[:/]/i;
|
|
13
|
+
|
|
14
|
+
interface CiFailure {
|
|
15
|
+
branch: string;
|
|
16
|
+
sha: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface OpenPullRequest {
|
|
21
|
+
number: number;
|
|
22
|
+
title: string;
|
|
23
|
+
waits: "changes requested" | "new comment" | "open threads";
|
|
24
|
+
/** How the forge names it. `PR #52` when unset. */
|
|
25
|
+
ref?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GithubState {
|
|
29
|
+
ci: CiFailure | undefined;
|
|
30
|
+
pr: OpenPullRequest | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Never rejects. `since` is the last session here, the bar for a comment to be news. */
|
|
34
|
+
export async function readGithub(
|
|
35
|
+
cwd: string,
|
|
36
|
+
since: number | undefined,
|
|
37
|
+
run: Runner = processRunner(cwd),
|
|
38
|
+
): Promise<GithubState> {
|
|
39
|
+
const remotes = (await ask(() => run("git", ["remote", "-v"]))) ?? "";
|
|
40
|
+
if (!GITHUB_REMOTE.test(remotes)) {
|
|
41
|
+
const hosts = gitlabHosts(remotes);
|
|
42
|
+
return glabMayStart(hosts) ? readGitlab(run, await branchOf(run)) : { ci: undefined, pr: undefined };
|
|
43
|
+
}
|
|
44
|
+
const [branch, pr] = await Promise.all([branchOf(run), pullRequest(run, since)]);
|
|
45
|
+
const ci = branch === undefined ? undefined : await failedRun(run, branch);
|
|
46
|
+
return { ci, pr };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function branchOf(run: Runner): Promise<string | undefined> {
|
|
50
|
+
const branch = (await ask(() => run("git", ["branch", "--show-current"])))?.trim();
|
|
51
|
+
return branch === undefined || branch === "" ? undefined : branch;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function failedRun(run: Runner, branch: string): Promise<CiFailure | undefined> {
|
|
55
|
+
const out = await ask(() =>
|
|
56
|
+
run("gh", [
|
|
57
|
+
"run",
|
|
58
|
+
"list",
|
|
59
|
+
"--branch",
|
|
60
|
+
branch,
|
|
61
|
+
"--limit",
|
|
62
|
+
"1",
|
|
63
|
+
"--json",
|
|
64
|
+
"conclusion,headSha,name,displayTitle",
|
|
65
|
+
]),
|
|
66
|
+
);
|
|
67
|
+
const [last] = rows(out);
|
|
68
|
+
if (last === undefined) return undefined;
|
|
69
|
+
const conclusion = field(last, "conclusion");
|
|
70
|
+
if (conclusion === undefined || !FAILED.has(conclusion)) return undefined;
|
|
71
|
+
return {
|
|
72
|
+
branch,
|
|
73
|
+
sha: (field(last, "headSha") ?? "").slice(0, 7),
|
|
74
|
+
name: field(last, "name") ?? field(last, "displayTitle") ?? "CI",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function pullRequest(
|
|
79
|
+
run: Runner,
|
|
80
|
+
since: number | undefined,
|
|
81
|
+
): Promise<OpenPullRequest | undefined> {
|
|
82
|
+
const out = await ask(() =>
|
|
83
|
+
run("gh", ["pr", "list", "--author", "@me", "--limit", "10", "--json", PR_FIELDS]),
|
|
84
|
+
);
|
|
85
|
+
for (const pr of rows(out)) {
|
|
86
|
+
const number = numberOf(pr);
|
|
87
|
+
const title = field(pr, "title");
|
|
88
|
+
if (number === undefined || title === undefined) continue;
|
|
89
|
+
if (field(pr, "reviewDecision") === "CHANGES_REQUESTED")
|
|
90
|
+
return { number, title, waits: "changes requested" };
|
|
91
|
+
if (hasNewComment(pr, since)) return { number, title, waits: "new comment" };
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasNewComment(pr: Record<string, unknown>, since: number | undefined): boolean {
|
|
97
|
+
if (since === undefined) return false;
|
|
98
|
+
const me = loginOf(pr["author"]);
|
|
99
|
+
const comments = pr["comments"];
|
|
100
|
+
if (!Array.isArray(comments)) return false;
|
|
101
|
+
return comments.some((comment) => {
|
|
102
|
+
if (!isObject(comment)) return false;
|
|
103
|
+
const at = Date.parse(field(comment, "createdAt") ?? "");
|
|
104
|
+
const login = loginOf(comment["author"]);
|
|
105
|
+
return Number.isFinite(at) && at > since && login !== undefined && login !== me;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function loginOf(value: unknown): string | undefined {
|
|
110
|
+
return isObject(value) ? field(value, "login") : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function ask(question: () => Promise<string>): Promise<string | undefined> {
|
|
114
|
+
try {
|
|
115
|
+
return await question();
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function rows(out: string | undefined): Record<string, unknown>[] {
|
|
122
|
+
if (out === undefined) return [];
|
|
123
|
+
let value: unknown;
|
|
124
|
+
try {
|
|
125
|
+
value = JSON.parse(out);
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
return Array.isArray(value) ? value.filter(isObject) : [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function field(row: Record<string, unknown>, key: string): string | undefined {
|
|
133
|
+
const value = row[key];
|
|
134
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function numberOf(pr: Record<string, unknown>, key = "number"): number | undefined {
|
|
138
|
+
const value = pr[key];
|
|
139
|
+
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
140
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The GitLab version of github.ts: the branch's last pipeline, and your merge requests that wait on you.
|
|
2
|
+
// glab sends a token from the environment to whichever host it is pointed at, and the repository
|
|
3
|
+
// chooses that host, so with such a token it starts only for gitlab.com or GITLAB_HOST.
|
|
4
|
+
import { ask, field, type GithubState, numberOf, rows } from "./github.ts";
|
|
5
|
+
import type { Runner } from "./runner.ts";
|
|
6
|
+
|
|
7
|
+
/** gitlab.com, or a self-hosted name like gitlab.acme.dev. */
|
|
8
|
+
const GITLAB_HOST = /(?:@|\/\/)((?:[\w-]+\.)*gitlab\.[\w.-]+)[:/]/gi;
|
|
9
|
+
const TOKENS = ["GITLAB_TOKEN", "GITLAB_ACCESS_TOKEN", "OAUTH_TOKEN"];
|
|
10
|
+
const WAITS = {
|
|
11
|
+
requested_changes: "changes requested",
|
|
12
|
+
discussions_not_resolved: "open threads",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export function gitlabHosts(remotes: string): string[] {
|
|
16
|
+
return [...remotes.matchAll(GITLAB_HOST)].map((match) => (match[1] ?? "").toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function glabMayStart(
|
|
20
|
+
hosts: readonly string[],
|
|
21
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
22
|
+
): boolean {
|
|
23
|
+
if (hosts.length === 0) return false;
|
|
24
|
+
if (!TOKENS.some((name) => env[name])) return true;
|
|
25
|
+
const trusted = ["gitlab.com", env["GITLAB_HOST"]?.replace(/^https?:\/\//, "").toLowerCase()];
|
|
26
|
+
return hosts.every((host) => trusted.includes(host));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Never rejects. */
|
|
30
|
+
export async function readGitlab(run: Runner, branch: string | undefined): Promise<GithubState> {
|
|
31
|
+
const [ci, pr] = await Promise.all([
|
|
32
|
+
branch === undefined ? undefined : failedPipeline(run, branch),
|
|
33
|
+
mergeRequest(run),
|
|
34
|
+
]);
|
|
35
|
+
return { ci, pr };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function failedPipeline(run: Runner, branch: string): Promise<GithubState["ci"]> {
|
|
39
|
+
const out = await ask(() =>
|
|
40
|
+
run("glab", ["ci", "list", "--ref", branch, "--per-page", "1", "--output", "json"]),
|
|
41
|
+
);
|
|
42
|
+
const [last] = rows(out);
|
|
43
|
+
if (last === undefined || field(last, "status") !== "failed") return undefined;
|
|
44
|
+
return {
|
|
45
|
+
branch,
|
|
46
|
+
sha: (field(last, "sha") ?? "").slice(0, 7),
|
|
47
|
+
name: field(last, "name") ?? "pipeline",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function mergeRequest(run: Runner): Promise<GithubState["pr"]> {
|
|
52
|
+
const out = await ask(() =>
|
|
53
|
+
run("glab", ["mr", "list", "--author", "@me", "--per-page", "10", "--output", "json"]),
|
|
54
|
+
);
|
|
55
|
+
for (const mr of rows(out)) {
|
|
56
|
+
const number = numberOf(mr, "iid");
|
|
57
|
+
const title = field(mr, "title");
|
|
58
|
+
const waits = WAITS[field(mr, "detailed_merge_status") as keyof typeof WAITS];
|
|
59
|
+
if (number !== undefined && title !== undefined && waits !== undefined)
|
|
60
|
+
return { number, title, waits, ref: `MR !${number}` };
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The mode pi-modes reports. It only publishes a status, so the text is the contract.
|
|
2
|
+
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
const MODE_STATUS = "pi-modes";
|
|
5
|
+
const REPORTED = /^mode: (normal|auto|plan|read-only|yolo)$/;
|
|
6
|
+
|
|
7
|
+
type Mode = "normal" | "auto" | "plan" | "read-only" | "yolo";
|
|
8
|
+
|
|
9
|
+
/** The same colors pi-modes gives the word. In `normal`, the frame stays pi's own. */
|
|
10
|
+
const FRAME_COLORS = { auto: "warning", plan: "accent", "read-only": "success", yolo: "error" } as const;
|
|
11
|
+
|
|
12
|
+
export function modeOf(statuses: ReadonlyMap<string, string>): Mode | undefined {
|
|
13
|
+
const status = statuses.get(MODE_STATUS);
|
|
14
|
+
if (status === undefined) return undefined;
|
|
15
|
+
return REPORTED.exec(stripTerminalSequences(status).trim())?.[1] as Mode | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function frameColorOf(
|
|
19
|
+
statuses: ReadonlyMap<string, string>,
|
|
20
|
+
): (typeof FRAME_COLORS)[keyof typeof FRAME_COLORS] | undefined {
|
|
21
|
+
const mode = modeOf(statuses);
|
|
22
|
+
return mode === undefined || mode === "normal" ? undefined : FRAME_COLORS[mode];
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const NEWEST_FILES = 20;
|
|
5
|
+
const MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
6
|
+
const MAX_PROMPTS = 1000;
|
|
7
|
+
|
|
8
|
+
function promptOf(line: string): string | undefined {
|
|
9
|
+
if (!line.includes('"role":"user"')) return undefined;
|
|
10
|
+
try {
|
|
11
|
+
const entry = JSON.parse(line) as { type?: string; message?: { role?: string; content?: unknown } };
|
|
12
|
+
if (entry.type !== "message" || entry.message?.role !== "user") return undefined;
|
|
13
|
+
const content = entry.message.content;
|
|
14
|
+
if (typeof content === "string") return content.trim() || undefined;
|
|
15
|
+
if (!Array.isArray(content)) return undefined;
|
|
16
|
+
const text = content
|
|
17
|
+
.filter((part): part is { type: "text"; text: string } => part?.type === "text" && typeof part.text === "string")
|
|
18
|
+
.map((part) => part.text)
|
|
19
|
+
.join("\n")
|
|
20
|
+
.trim();
|
|
21
|
+
return text || undefined;
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What was typed in this folder's recent sessions, newest first. A file it can't read is skipped. */
|
|
28
|
+
export function pastPrompts(sessionDir: string): string[] {
|
|
29
|
+
const prompts: string[] = [];
|
|
30
|
+
let files: { path: string; modified: number; size: number }[] = [];
|
|
31
|
+
try {
|
|
32
|
+
files = readdirSync(sessionDir)
|
|
33
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
34
|
+
.map((name) => {
|
|
35
|
+
const path = join(sessionDir, name);
|
|
36
|
+
const { mtimeMs, size } = statSync(path);
|
|
37
|
+
return { path, modified: mtimeMs, size };
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
files.sort((a, b) => b.modified - a.modified);
|
|
43
|
+
for (const { path, size } of files.slice(0, NEWEST_FILES)) {
|
|
44
|
+
if (size > MAX_FILE_BYTES || prompts.length >= MAX_PROMPTS) continue;
|
|
45
|
+
try {
|
|
46
|
+
const found: string[] = [];
|
|
47
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
48
|
+
const prompt = promptOf(line);
|
|
49
|
+
if (prompt) found.push(prompt);
|
|
50
|
+
}
|
|
51
|
+
prompts.push(...found.reverse());
|
|
52
|
+
} catch {
|
|
53
|
+
/* it may be gone, or still being written */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return prompts.slice(0, MAX_PROMPTS);
|
|
57
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/** The start card is never worth a longer wait. */
|
|
4
|
+
const TIMEOUT_MS = 3000;
|
|
5
|
+
/** Room for a page of answers, like your open pull requests with their comments. */
|
|
6
|
+
const MAX_OUTPUT = 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
/** No download for a partial clone, whose transport the repository's config chooses, and no prompt. */
|
|
9
|
+
const QUIET_GIT = {
|
|
10
|
+
...process.env,
|
|
11
|
+
GIT_NO_LAZY_FETCH: "1",
|
|
12
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
13
|
+
GLAB_CHECK_UPDATE: "false",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Rejects when it fails or takes too long. */
|
|
17
|
+
export type Runner = (file: string, args: readonly string[]) => Promise<string>;
|
|
18
|
+
|
|
19
|
+
export function processRunner(cwd: string, timeoutMs: number = TIMEOUT_MS): Runner {
|
|
20
|
+
return (file, args) =>
|
|
21
|
+
new Promise((resolve, reject) => {
|
|
22
|
+
execFile(
|
|
23
|
+
file,
|
|
24
|
+
[...args],
|
|
25
|
+
{ cwd, timeout: timeoutMs, maxBuffer: MAX_OUTPUT, env: QUIET_GIT },
|
|
26
|
+
(error, stdout) => {
|
|
27
|
+
if (error === null) resolve(stdout);
|
|
28
|
+
else reject(error instanceof Error ? error : new Error(String(error)));
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
|
|
3
|
+
import type { AccountState, Board, PeerMark, RoomMark, WritingMark } from "@adeildo/pi-kit";
|
|
4
|
+
import { VERSION, type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import { plainText } from "../render/safe.ts";
|
|
7
|
+
|
|
8
|
+
import type { GitState } from "./git.ts";
|
|
9
|
+
import type { RequestView, RunView } from "./telemetry.ts";
|
|
10
|
+
import {
|
|
11
|
+
type Averages,
|
|
12
|
+
averagesOf,
|
|
13
|
+
cacheHitPercent,
|
|
14
|
+
cacheWarming,
|
|
15
|
+
emptyTotals,
|
|
16
|
+
type Totals,
|
|
17
|
+
totalsOf,
|
|
18
|
+
} from "./totals.ts";
|
|
19
|
+
|
|
20
|
+
interface ModelInfo {
|
|
21
|
+
name: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
reasoning: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ContextInfo {
|
|
27
|
+
percent: number;
|
|
28
|
+
tokens: number;
|
|
29
|
+
window: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** pi-sandbox sets this while it's on, and empties it when it goes off. */
|
|
33
|
+
export const SANDBOX_STATUS = "sandbox";
|
|
34
|
+
|
|
35
|
+
export interface Snapshot {
|
|
36
|
+
now: number;
|
|
37
|
+
cwd: string;
|
|
38
|
+
home: string | undefined;
|
|
39
|
+
branch: string | null;
|
|
40
|
+
git: GitState | undefined;
|
|
41
|
+
host: string | undefined;
|
|
42
|
+
sessionName: string | undefined;
|
|
43
|
+
/** The word the room calls this session by, in its color once it is in a room. */
|
|
44
|
+
me: { handle: string; color?: string } | undefined;
|
|
45
|
+
version: string;
|
|
46
|
+
model: ModelInfo | undefined;
|
|
47
|
+
account: AccountState | undefined;
|
|
48
|
+
thinking: string | undefined;
|
|
49
|
+
context: ContextInfo | undefined;
|
|
50
|
+
totals: Totals;
|
|
51
|
+
cacheHit: number | undefined;
|
|
52
|
+
cacheWarming: boolean;
|
|
53
|
+
subscription: boolean;
|
|
54
|
+
request: RequestView | undefined;
|
|
55
|
+
last: RequestView | undefined;
|
|
56
|
+
run: RunView | undefined;
|
|
57
|
+
averages: Averages;
|
|
58
|
+
statuses: ReadonlyMap<string, string>;
|
|
59
|
+
/** Whether pi-sandbox is confining commands. Undefined when it isn't installed. */
|
|
60
|
+
sandbox: "on" | "off" | undefined;
|
|
61
|
+
/** When a board feature keeps it. */
|
|
62
|
+
board: Board | undefined;
|
|
63
|
+
/** When a peers feature keeps them. */
|
|
64
|
+
peers: PeerMark[];
|
|
65
|
+
/** Empty when the peers feature publishes only `peers`. */
|
|
66
|
+
peersRooms: RoomMark[];
|
|
67
|
+
peersUnread: number;
|
|
68
|
+
peersWriting: WritingMark[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function emptySnapshot(): Snapshot {
|
|
72
|
+
return {
|
|
73
|
+
now: 0,
|
|
74
|
+
cwd: "",
|
|
75
|
+
home: undefined,
|
|
76
|
+
branch: null,
|
|
77
|
+
git: undefined,
|
|
78
|
+
host: undefined,
|
|
79
|
+
sessionName: undefined,
|
|
80
|
+
me: undefined,
|
|
81
|
+
version: VERSION,
|
|
82
|
+
model: undefined,
|
|
83
|
+
account: undefined,
|
|
84
|
+
thinking: undefined,
|
|
85
|
+
context: undefined,
|
|
86
|
+
totals: emptyTotals(),
|
|
87
|
+
cacheHit: undefined,
|
|
88
|
+
cacheWarming: false,
|
|
89
|
+
subscription: false,
|
|
90
|
+
request: undefined,
|
|
91
|
+
last: undefined,
|
|
92
|
+
run: undefined,
|
|
93
|
+
averages: { decode: undefined, prefill: undefined },
|
|
94
|
+
statuses: new Map(),
|
|
95
|
+
sandbox: undefined,
|
|
96
|
+
board: undefined,
|
|
97
|
+
peers: [],
|
|
98
|
+
peersRooms: [],
|
|
99
|
+
peersUnread: 0,
|
|
100
|
+
peersWriting: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface LiveInputs {
|
|
105
|
+
branch: string | null;
|
|
106
|
+
git: GitState | undefined;
|
|
107
|
+
account?: AccountState | undefined;
|
|
108
|
+
request: RequestView | undefined;
|
|
109
|
+
last: RequestView | undefined;
|
|
110
|
+
run: RunView | undefined;
|
|
111
|
+
statuses: ReadonlyMap<string, string>;
|
|
112
|
+
sandboxInstalled?: boolean;
|
|
113
|
+
board?: Board | undefined;
|
|
114
|
+
me?: { handle: string; color?: string } | undefined;
|
|
115
|
+
peers?: PeerMark[] | undefined;
|
|
116
|
+
peersRooms?: RoomMark[] | undefined;
|
|
117
|
+
peersUnread?: number | undefined;
|
|
118
|
+
peersWriting?: WritingMark[] | undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface Derived {
|
|
122
|
+
totals: Totals;
|
|
123
|
+
cacheHit: number | undefined;
|
|
124
|
+
cacheWarming: boolean;
|
|
125
|
+
averages: Averages;
|
|
126
|
+
context: ContextInfo | undefined;
|
|
127
|
+
answeredBy: string | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const VIRTUAL_API = "pi-virtual";
|
|
131
|
+
|
|
132
|
+
export class SnapshotReader {
|
|
133
|
+
#key: string | undefined;
|
|
134
|
+
#derived: Derived | undefined;
|
|
135
|
+
readonly #host = hostname().split(".")[0];
|
|
136
|
+
|
|
137
|
+
read(ctx: ExtensionContext, live: LiveInputs): Snapshot {
|
|
138
|
+
const model = ctx.model;
|
|
139
|
+
const derived = this.#derive(ctx);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
now: Date.now(),
|
|
143
|
+
cwd: ctx.cwd,
|
|
144
|
+
home: process.env.HOME ?? process.env.USERPROFILE,
|
|
145
|
+
branch: live.branch,
|
|
146
|
+
git: live.git,
|
|
147
|
+
host: this.#host,
|
|
148
|
+
sessionName: ctx.sessionManager.getSessionName(),
|
|
149
|
+
me: live.me,
|
|
150
|
+
version: VERSION,
|
|
151
|
+
model:
|
|
152
|
+
model === undefined
|
|
153
|
+
? undefined
|
|
154
|
+
: {
|
|
155
|
+
name:
|
|
156
|
+
model.api === VIRTUAL_API && derived.answeredBy !== undefined
|
|
157
|
+
? `${model.name} \u2192 ${derived.answeredBy}`
|
|
158
|
+
: (model.name ?? model.id),
|
|
159
|
+
provider: ctx.modelRegistry.getProviderDisplayName(model.provider),
|
|
160
|
+
reasoning: model.reasoning,
|
|
161
|
+
},
|
|
162
|
+
thinking: ctx.thinkingLevel,
|
|
163
|
+
account: live.account,
|
|
164
|
+
...derived,
|
|
165
|
+
subscription: model === undefined ? false : ctx.modelRegistry.isUsingOAuth(model),
|
|
166
|
+
request: live.request,
|
|
167
|
+
last: live.last,
|
|
168
|
+
run: live.run,
|
|
169
|
+
statuses: new Map(
|
|
170
|
+
[...live.statuses.entries()]
|
|
171
|
+
.toSorted(([a], [b]) => a.localeCompare(b))
|
|
172
|
+
.map(([key, text]): [string, string] => [key, sanitize(text)])
|
|
173
|
+
.filter(([, text]) => text !== ""),
|
|
174
|
+
),
|
|
175
|
+
sandbox: !live.sandboxInstalled ? undefined : live.statuses.get(SANDBOX_STATUS) ? "on" : "off",
|
|
176
|
+
board: live.board,
|
|
177
|
+
peers: live.peers ?? [],
|
|
178
|
+
peersRooms: live.peersRooms ?? [],
|
|
179
|
+
peersUnread: live.peersUnread ?? 0,
|
|
180
|
+
peersWriting: live.peersWriting ?? [],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#derive(ctx: ExtensionContext): Derived {
|
|
185
|
+
// Entries are appended at the leaf, so the sums change only when the leaf or the model does.
|
|
186
|
+
const key = `${ctx.sessionManager.getLeafId() ?? ""}\u0000${ctx.model?.provider}/${ctx.model?.id}`;
|
|
187
|
+
if (this.#derived !== undefined && key === this.#key) return this.#derived;
|
|
188
|
+
|
|
189
|
+
const entries: readonly SessionEntry[] = ctx.sessionManager.getBranch();
|
|
190
|
+
this.#key = key;
|
|
191
|
+
this.#derived = {
|
|
192
|
+
totals: totalsOf(entries),
|
|
193
|
+
cacheHit: cacheHitPercent(entries),
|
|
194
|
+
cacheWarming: cacheWarming(entries),
|
|
195
|
+
averages: averagesOf(entries),
|
|
196
|
+
context: contextOf(ctx),
|
|
197
|
+
answeredBy: answeredBy(ctx, entries),
|
|
198
|
+
};
|
|
199
|
+
return this.#derived;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A virtual model only says what was selected. The latest answer says which model it chose. */
|
|
204
|
+
function answeredBy(ctx: ExtensionContext, entries: readonly SessionEntry[]): string | undefined {
|
|
205
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
206
|
+
const entry = entries[index];
|
|
207
|
+
if (entry?.type !== "message" || entry.message.role !== "assistant") continue;
|
|
208
|
+
if (entry.message.stopReason === "error" || entry.message.stopReason === "aborted") continue;
|
|
209
|
+
const { provider, model } = entry.message;
|
|
210
|
+
return plainText(ctx.modelRegistry.find(provider, model)?.name ?? model).replace(/\s+/g, " ");
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function contextOf(ctx: ExtensionContext): ContextInfo | undefined {
|
|
216
|
+
const usage = ctx.getContextUsage();
|
|
217
|
+
if (usage === undefined || usage.percent === null || usage.tokens === null) return undefined;
|
|
218
|
+
return { percent: usage.percent, tokens: usage.tokens, window: usage.contextWindow };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sanitize(text: string): string {
|
|
222
|
+
return text.replace(/[\r\n\t]+/g, " ").trim();
|
|
223
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Under the folder and the model, only what changed or waits on you since the last session here.
|
|
2
|
+
import { formatClock, GLYPH } from "@adeildo/pi-kit";
|
|
3
|
+
|
|
4
|
+
import type { GithubState } from "./github.ts";
|
|
5
|
+
import type { Snapshot } from "./snapshot.ts";
|
|
6
|
+
|
|
7
|
+
const PLAN_LIMIT = 70;
|
|
8
|
+
|
|
9
|
+
export interface LastVisit {
|
|
10
|
+
at: number;
|
|
11
|
+
version: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StartNote {
|
|
15
|
+
mark: string;
|
|
16
|
+
color: string;
|
|
17
|
+
text: string;
|
|
18
|
+
detail?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function startNotes(
|
|
22
|
+
snapshot: Pick<Snapshot, "peers" | "account" | "git" | "branch">,
|
|
23
|
+
version: string,
|
|
24
|
+
last: LastVisit | undefined,
|
|
25
|
+
github?: GithubState,
|
|
26
|
+
): StartNote[] {
|
|
27
|
+
const notes: StartNote[] = [];
|
|
28
|
+
const { account, git } = snapshot;
|
|
29
|
+
|
|
30
|
+
if (account?.needsLogin === true)
|
|
31
|
+
notes.push({
|
|
32
|
+
mark: GLYPH.cross,
|
|
33
|
+
color: "error",
|
|
34
|
+
text: `${account.label} needs a sign-in`,
|
|
35
|
+
detail: "/accounts",
|
|
36
|
+
});
|
|
37
|
+
if (git !== undefined && git.conflicted > 0)
|
|
38
|
+
notes.push({ mark: GLYPH.cross, color: "error", text: `${git.conflicted} files in conflict` });
|
|
39
|
+
if (git !== undefined && git.behind > 0)
|
|
40
|
+
notes.push({
|
|
41
|
+
mark: GLYPH.down,
|
|
42
|
+
color: "warning",
|
|
43
|
+
text: `${snapshot.branch ?? "this branch"} is ${git.behind} behind`,
|
|
44
|
+
detail: "pull before you start",
|
|
45
|
+
});
|
|
46
|
+
if (github?.ci !== undefined)
|
|
47
|
+
notes.push({
|
|
48
|
+
mark: GLYPH.cross,
|
|
49
|
+
color: "error",
|
|
50
|
+
text: `CI failed on ${github.ci.branch}`,
|
|
51
|
+
detail: `${github.ci.sha} ${github.ci.name}`,
|
|
52
|
+
});
|
|
53
|
+
if (github?.pr !== undefined)
|
|
54
|
+
notes.push({
|
|
55
|
+
mark: GLYPH.flag,
|
|
56
|
+
color: "warning",
|
|
57
|
+
text: `${github.pr.ref ?? `PR #${github.pr.number}`} ${github.pr.title}`,
|
|
58
|
+
detail: github.pr.waits,
|
|
59
|
+
});
|
|
60
|
+
for (const peer of snapshot.peers)
|
|
61
|
+
notes.push({
|
|
62
|
+
mark: GLYPH.diamond,
|
|
63
|
+
color: peer.color,
|
|
64
|
+
text: `@${peer.handle}`,
|
|
65
|
+
detail: peerState(peer.state),
|
|
66
|
+
});
|
|
67
|
+
for (const window of account?.windows ?? []) {
|
|
68
|
+
if (window.used < PLAN_LIMIT) continue;
|
|
69
|
+
notes.push({
|
|
70
|
+
mark: GLYPH.up,
|
|
71
|
+
color: window.used >= 90 ? "error" : "warning",
|
|
72
|
+
text: `${window.name} at ${Math.round(window.used)}%`,
|
|
73
|
+
detail: window.resetsIn === undefined ? undefined : `resets in ${window.resetsIn}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (last !== undefined && last.version !== version)
|
|
77
|
+
notes.push({
|
|
78
|
+
mark: GLYPH.up,
|
|
79
|
+
color: "accent",
|
|
80
|
+
text: `${GLYPH.pi}-frame ${version} is new`,
|
|
81
|
+
detail: `was ${last.version}`,
|
|
82
|
+
});
|
|
83
|
+
return notes;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function peerState(state: "running" | "idle" | "waiting"): string {
|
|
87
|
+
switch (state) {
|
|
88
|
+
case "running":
|
|
89
|
+
return "works here";
|
|
90
|
+
case "waiting":
|
|
91
|
+
return "waits for you";
|
|
92
|
+
case "idle":
|
|
93
|
+
return "is open here";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Local time, or the day when it was not today. */
|
|
98
|
+
export function since(at: number, now: number): string {
|
|
99
|
+
const then = new Date(at);
|
|
100
|
+
const time = formatClock(at);
|
|
101
|
+
if (new Date(now).toDateString() === then.toDateString()) return time;
|
|
102
|
+
const days = Math.round((now - at) / 86_400_000);
|
|
103
|
+
return days <= 1 ? `yesterday ${time}` : `${days} days ago`;
|
|
104
|
+
}
|