@quickchatai/quickreview 1.1.0 → 1.2.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 +35 -19
- package/config.example.toml +41 -9
- package/dist/extensions/cli/login-route.js +5 -0
- package/dist/extensions/cli/login-route.js.map +1 -0
- package/dist/extensions/login-route.js +180 -0
- package/dist/extensions/login-route.js.map +1 -0
- package/dist/extensions/onboarding.js +29 -6
- package/dist/extensions/onboarding.js.map +1 -1
- package/dist/src/ciretry.js +2 -1
- package/dist/src/ciretry.js.map +1 -1
- package/dist/src/cli.js +43 -12
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliops.js +3 -2
- package/dist/src/cliops.js.map +1 -1
- package/dist/src/commands.js +25 -2
- package/dist/src/commands.js.map +1 -1
- package/dist/src/config.js +16 -2
- package/dist/src/config.js.map +1 -1
- package/dist/src/daemon.js +24 -8
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/debugroundpi.js +5 -1
- package/dist/src/debugroundpi.js.map +1 -1
- package/dist/src/dispatch.js +5 -4
- package/dist/src/dispatch.js.map +1 -1
- package/dist/src/engine/debugpi.js +17 -5
- package/dist/src/engine/debugpi.js.map +1 -1
- package/dist/src/execboundary.js +10 -3
- package/dist/src/execboundary.js.map +1 -1
- package/dist/src/github.js +51 -16
- package/dist/src/github.js.map +1 -1
- package/dist/src/githubtoken.js +346 -0
- package/dist/src/githubtoken.js.map +1 -0
- package/dist/src/loginhint.js +8 -3
- package/dist/src/loginhint.js.map +1 -1
- package/dist/src/loginrefresh/pirefresh.js +3 -0
- package/dist/src/loginrefresh/pirefresh.js.map +1 -1
- package/dist/src/npmverbs.js +4 -2
- package/dist/src/npmverbs.js.map +1 -1
- package/dist/src/onboard/choices.js +29 -1
- package/dist/src/onboard/choices.js.map +1 -1
- package/dist/src/onboard/engine.js +8 -23
- package/dist/src/onboard/engine.js.map +1 -1
- package/dist/src/onboard/launch.js +12 -20
- package/dist/src/onboard/launch.js.map +1 -1
- package/dist/src/onboard/pipin.js +22 -0
- package/dist/src/onboard/pipin.js.map +1 -0
- package/dist/src/onboard/stepforward.js +41 -0
- package/dist/src/onboard/stepforward.js.map +1 -0
- package/dist/src/onboard/steps/appslug.js +4 -1
- package/dist/src/onboard/steps/appslug.js.map +1 -1
- package/dist/src/onboard/steps/compose.js +29 -0
- package/dist/src/onboard/steps/compose.js.map +1 -1
- package/dist/src/onboard/steps/configfill.js +102 -32
- package/dist/src/onboard/steps/configfill.js.map +1 -1
- package/dist/src/onboard/steps/githubaccount.js +595 -0
- package/dist/src/onboard/steps/githubaccount.js.map +1 -0
- package/dist/src/onboard/steps/githubapp.js +65 -7
- package/dist/src/onboard/steps/githubapp.js.map +1 -1
- package/dist/src/onboard/steps/golive.js +2 -0
- package/dist/src/onboard/steps/golive.js.map +1 -1
- package/dist/src/onboard/steps/index.js +11 -0
- package/dist/src/onboard/steps/index.js.map +1 -1
- package/dist/src/onboard/steps/ingress.js +80 -25
- package/dist/src/onboard/steps/ingress.js.map +1 -1
- package/dist/src/onboard/steps/llmauth.js +103 -30
- package/dist/src/onboard/steps/llmauth.js.map +1 -1
- package/dist/src/onboard/steps/memory.js +53 -1
- package/dist/src/onboard/steps/memory.js.map +1 -1
- package/dist/src/onboard/steps/proof.js +366 -56
- package/dist/src/onboard/steps/proof.js.map +1 -1
- package/dist/src/onboard/steps/topology.js +6 -6
- package/dist/src/onboard/steps/topology.js.map +1 -1
- package/dist/src/onboard/steps/transport.js +9 -1
- package/dist/src/onboard/steps/transport.js.map +1 -1
- package/dist/src/onboard/steps/tuiroute.js +102 -0
- package/dist/src/onboard/steps/tuiroute.js.map +1 -0
- package/dist/src/onboard/steps/verifyup.js +17 -1
- package/dist/src/onboard/steps/verifyup.js.map +1 -1
- package/dist/src/onboard/toolnames.js +30 -0
- package/dist/src/onboard/toolnames.js.map +1 -0
- package/dist/src/operatorwrite.js +23 -0
- package/dist/src/operatorwrite.js.map +1 -0
- package/dist/src/outcomes.js +4 -1
- package/dist/src/outcomes.js.map +1 -1
- package/dist/src/posting-journal.js +2 -1
- package/dist/src/posting-journal.js.map +1 -1
- package/dist/src/posting-outbox.js +6 -2
- package/dist/src/posting-outbox.js.map +1 -1
- package/dist/src/posting-receipt.js +14 -1
- package/dist/src/posting-receipt.js.map +1 -1
- package/dist/src/recheck.js +2 -1
- package/dist/src/recheck.js.map +1 -1
- package/dist/src/reconcile.js +22 -2
- package/dist/src/reconcile.js.map +1 -1
- package/dist/src/reviewhistory.js +32 -6
- package/dist/src/reviewhistory.js.map +1 -1
- package/dist/src/reviewnow.js +45 -0
- package/dist/src/reviewnow.js.map +1 -0
- package/dist/src/state.js.map +1 -1
- package/dist/src/statemachine.js +4 -1
- package/dist/src/statemachine.js.map +1 -1
- package/dist/src/takeoverpi.js +13 -0
- package/dist/src/takeoverpi.js.map +1 -1
- package/dist/src/tui/app.js +49 -0
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/board.js +88 -12
- package/dist/src/tui/board.js.map +1 -1
- package/dist/src/tui/cmdbar.js +4 -1
- package/dist/src/tui/cmdbar.js.map +1 -1
- package/dist/src/tui/companion.js +74 -10
- package/dist/src/tui/companion.js.map +1 -1
- package/dist/src/tui/connect.js +7 -1
- package/dist/src/tui/connect.js.map +1 -1
- package/dist/src/tui/filters.js +5 -2
- package/dist/src/tui/filters.js.map +1 -1
- package/dist/src/tui/ghread.js +61 -5
- package/dist/src/tui/ghread.js.map +1 -1
- package/dist/src/tui/index.js +106 -20
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/memforward.js +39 -2
- package/dist/src/tui/memforward.js.map +1 -1
- package/dist/src/tui/model.js +25 -0
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/prpause.js +7 -0
- package/dist/src/tui/prpause.js.map +1 -1
- package/dist/src/tui/remote.js +7 -1
- package/dist/src/tui/remote.js.map +1 -1
- package/dist/src/tui/route.js +143 -0
- package/dist/src/tui/route.js.map +1 -0
- package/dist/src/tui/setupstate.js +29 -0
- package/dist/src/tui/setupstate.js.map +1 -0
- package/dist/src/tui/sshtriage.js +102 -5
- package/dist/src/tui/sshtriage.js.map +1 -1
- package/dist/src/tui/theme.js +2 -1
- package/dist/src/tui/theme.js.map +1 -1
- package/dist/src/tui/unwedge.js +2 -12
- package/dist/src/tui/unwedge.js.map +1 -1
- package/dist/src/verdicts.js +17 -5
- package/dist/src/verdicts.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/webhook.js +12 -1
- package/dist/src/webhook.js.map +1 -1
- package/dist/src/wire.js +28 -6
- package/dist/src/wire.js.map +1 -1
- package/dist/src/workerworld.js +7 -1
- package/dist/src/workerworld.js.map +1 -1
- package/docker/companion/entrypoint.sh +20 -1
- package/docker/entrypoint.sh +17 -0
- package/extensions/cli/login-route.ts +10 -0
- package/extensions/login-route.ts +220 -0
- package/extensions/onboarding.ts +35 -6
- package/package.json +2 -2
- package/prompts/onboarding.md +57 -18
- package/scripts/mint-token.sh +25 -4
- package/src/ciretry.ts +2 -1
- package/src/cli.ts +49 -14
- package/src/cliops.ts +3 -2
- package/src/commands.ts +28 -2
- package/src/config.ts +24 -4
- package/src/daemon.ts +32 -7
- package/src/debugroundpi.ts +11 -1
- package/src/dispatch.ts +5 -4
- package/src/engine/debugpi.ts +17 -5
- package/src/execboundary.ts +16 -4
- package/src/github.ts +57 -15
- package/src/githubtoken.ts +435 -0
- package/src/loginhint.ts +8 -3
- package/src/loginrefresh/pirefresh.ts +3 -0
- package/src/npmverbs.ts +4 -2
- package/src/onboard/choices.ts +34 -1
- package/src/onboard/engine.ts +22 -3
- package/src/onboard/launch.ts +17 -29
- package/src/onboard/pipin.ts +31 -0
- package/src/onboard/stepforward.ts +61 -0
- package/src/onboard/steps/appslug.ts +4 -1
- package/src/onboard/steps/compose.ts +31 -0
- package/src/onboard/steps/configfill.ts +121 -33
- package/src/onboard/steps/githubaccount.ts +716 -0
- package/src/onboard/steps/githubapp.ts +92 -8
- package/src/onboard/steps/golive.ts +2 -0
- package/src/onboard/steps/index.ts +11 -0
- package/src/onboard/steps/ingress.ts +98 -31
- package/src/onboard/steps/llmauth.ts +114 -29
- package/src/onboard/steps/memory.ts +58 -1
- package/src/onboard/steps/proof.ts +473 -55
- package/src/onboard/steps/topology.ts +7 -6
- package/src/onboard/steps/transport.ts +15 -0
- package/src/onboard/steps/tuiroute.ts +119 -0
- package/src/onboard/steps/verifyup.ts +20 -1
- package/src/onboard/toolnames.ts +34 -0
- package/src/operatorwrite.ts +25 -0
- package/src/outcomes.ts +8 -1
- package/src/posting-journal.ts +6 -1
- package/src/posting-outbox.ts +10 -2
- package/src/posting-receipt.ts +17 -0
- package/src/recheck.ts +2 -1
- package/src/reconcile.ts +29 -3
- package/src/reviewhistory.ts +20 -6
- package/src/reviewnow.ts +76 -0
- package/src/state.ts +18 -0
- package/src/statemachine.ts +4 -1
- package/src/takeoverpi.ts +13 -0
- package/src/tui/app.ts +47 -0
- package/src/tui/board.ts +111 -14
- package/src/tui/cmdbar.ts +4 -1
- package/src/tui/companion.ts +78 -10
- package/src/tui/connect.ts +7 -1
- package/src/tui/filters.ts +7 -3
- package/src/tui/ghread.ts +74 -7
- package/src/tui/index.ts +114 -23
- package/src/tui/memforward.ts +52 -3
- package/src/tui/model.ts +36 -2
- package/src/tui/prpause.ts +11 -2
- package/src/tui/remote.ts +9 -1
- package/src/tui/route.ts +173 -0
- package/src/tui/setupstate.ts +52 -0
- package/src/tui/sshtriage.ts +127 -6
- package/src/tui/theme.ts +2 -1
- package/src/tui/unwedge.ts +2 -14
- package/src/verdicts.ts +23 -5
- package/src/version.ts +1 -1
- package/src/webhook.ts +10 -1
- package/src/wire.ts +36 -7
- package/src/workerworld.ts +7 -1
package/src/reviewnow.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// "Review this PR now" — the operator's `review-now` command (src/commands.ts,
|
|
2
|
+
// root-sealed operator channel only).
|
|
3
|
+
//
|
|
4
|
+
// It queues ONE pull request for a human-asked round, the same state-machine
|
|
5
|
+
// event a fresh trigger label produces (`trigger`, source `manual`, so the
|
|
6
|
+
// round's provenance is the person's ask and dispatch reviews it in full),
|
|
7
|
+
// and then pumps dispatch. It makes no GitHub write to ask for the review:
|
|
8
|
+
// the label path stays the way later reviews are requested, and every write
|
|
9
|
+
// the round itself makes still goes through the one MutationGate.
|
|
10
|
+
//
|
|
11
|
+
// The PR is read from GitHub first (one GET), so a closed or merged PR, or a
|
|
12
|
+
// number that is not a PR of the reviewed repo, is refused with its reason
|
|
13
|
+
// instead of being adopted into the state store.
|
|
14
|
+
import type { PrRef } from "./github.js";
|
|
15
|
+
import type { FileOutcome } from "./improvements.js";
|
|
16
|
+
import { applyEvent } from "./statemachine.js";
|
|
17
|
+
import type { StateStore } from "./state.js";
|
|
18
|
+
|
|
19
|
+
export interface ReviewNowDeps {
|
|
20
|
+
store: StateStore;
|
|
21
|
+
/** The PR as GitHub has it (read-only). */
|
|
22
|
+
getPr(n: number): Promise<PrRef>;
|
|
23
|
+
pump(): Promise<void>;
|
|
24
|
+
/** The App's bot login, whose own PRs are never reviewed. Absent on the
|
|
25
|
+
* own-account route, where the "bot" is the person and their own PRs are
|
|
26
|
+
* exactly what they ask to have reviewed. */
|
|
27
|
+
botLogin?: string;
|
|
28
|
+
now?: () => Date;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function reviewNow(deps: ReviewNowDeps, pr: number): Promise<FileOutcome> {
|
|
32
|
+
let ref: PrRef;
|
|
33
|
+
try {
|
|
34
|
+
ref = await deps.getPr(pr);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: `could not read PR #${pr} from GitHub: ${err instanceof Error ? err.message : String(err)}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (ref.merged) return { ok: false, reason: `PR #${pr} is merged — there is nothing to review` };
|
|
42
|
+
if (ref.state !== "open") return { ok: false, reason: `PR #${pr} is closed` };
|
|
43
|
+
const local = deps.store.getPr(pr);
|
|
44
|
+
if (local?.paused)
|
|
45
|
+
return { ok: false, reason: `PR #${pr} is paused — resume it first (qcr resume-pr ${pr})` };
|
|
46
|
+
// The worker crash-loop breaker parks a PR after repeated worker failures
|
|
47
|
+
// (a missing model login is the usual first-install cause). Asking for this
|
|
48
|
+
// PR's review is the operator's word that it may run again, exactly as
|
|
49
|
+
// rerun-round's unpark is — without it the new round could never start.
|
|
50
|
+
if (deps.store.clearWorkerBreaker(pr))
|
|
51
|
+
deps.store.logEvent("worker_unparked", pr, { by: "review-now" });
|
|
52
|
+
const result = applyEvent(
|
|
53
|
+
deps.store,
|
|
54
|
+
pr,
|
|
55
|
+
{ type: "trigger", source: "manual" },
|
|
56
|
+
{
|
|
57
|
+
title: ref.title,
|
|
58
|
+
head_sha: ref.head_sha,
|
|
59
|
+
author: ref.author,
|
|
60
|
+
trigger_ts: (deps.now ?? (() => new Date()))().toISOString(),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
...(deps.botLogin ? { botLogin: deps.botLogin } : {}),
|
|
64
|
+
...(deps.now ? { now: deps.now } : {}),
|
|
65
|
+
reason: "operator:review-now",
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
if (result.skipped === "bot-author")
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
reason: `PR #${pr} is authored by the bot itself — it never reviews its own PRs`,
|
|
72
|
+
};
|
|
73
|
+
await deps.pump();
|
|
74
|
+
const state = deps.store.getPr(pr)?.state ?? result.to;
|
|
75
|
+
return { ok: true, pr, state, head_sha: ref.head_sha };
|
|
76
|
+
}
|
package/src/state.ts
CHANGED
|
@@ -7767,12 +7767,30 @@ export interface RoundStageBounds {
|
|
|
7767
7767
|
export const PR_RUNTIMES = ["tmux", "worker", "answer", "idle", "debug", "takeover"] as const;
|
|
7768
7768
|
export type PrRuntime = (typeof PR_RUNTIMES)[number];
|
|
7769
7769
|
|
|
7770
|
+
/** One open pull request as the board lists it (src/reconcile.ts lists them). */
|
|
7771
|
+
export interface OpenPrSummary {
|
|
7772
|
+
number: number;
|
|
7773
|
+
title: string;
|
|
7774
|
+
author: string;
|
|
7775
|
+
draft: boolean;
|
|
7776
|
+
updated_at: string;
|
|
7777
|
+
}
|
|
7778
|
+
|
|
7779
|
+
export interface OpenPrsSnapshot {
|
|
7780
|
+
listed_at: string;
|
|
7781
|
+
prs: OpenPrSummary[];
|
|
7782
|
+
}
|
|
7783
|
+
|
|
7770
7784
|
export interface StatusSnapshot {
|
|
7771
7785
|
written_at: string;
|
|
7772
7786
|
dry_run: boolean;
|
|
7773
7787
|
/** Where the daemon runs (src/runhost.ts). Absent: not reported (an older
|
|
7774
7788
|
* daemon, or one assembled without its host facts). */
|
|
7775
7789
|
run_host?: import("./runhost.js").RunHostStatus;
|
|
7790
|
+
/** Every open pull request of the reviewed repo, as the last successful
|
|
7791
|
+
* reconcile pass listed it (tracked or not): what the board offers to
|
|
7792
|
+
* review. Absent until a pass has listed them. */
|
|
7793
|
+
open_prs?: OpenPrsSnapshot;
|
|
7776
7794
|
prs: Array<
|
|
7777
7795
|
Pick<PrStateRow, "number" | "state" | "title" | "head_sha" | "stale_recheck"> &
|
|
7778
7796
|
Partial<Pick<PrStateRow, "trigger_ts" | "updated_at" | "author" | "paused">> & {
|
package/src/statemachine.ts
CHANGED
|
@@ -205,7 +205,10 @@ function applyEventTransaction(
|
|
|
205
205
|
const now = opts.now ?? (() => new Date());
|
|
206
206
|
const existing = store.getPr(pr);
|
|
207
207
|
const author = meta.author ?? existing?.author ?? "";
|
|
208
|
-
|
|
208
|
+
// `botLogin` is the bot ACCOUNT (identity.ts `botAccountLogin`): "" in
|
|
209
|
+
// person mode, where the person's own PRs are the target and only
|
|
210
|
+
// `[bot]`-suffixed accounts are skipped. Absent ⇒ nothing is skipped.
|
|
211
|
+
if (opts.botLogin !== undefined && author && isBotAuthor(author, opts.botLogin)) {
|
|
209
212
|
return {
|
|
210
213
|
pr,
|
|
211
214
|
from: existing?.state ?? null,
|
package/src/takeoverpi.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { chmodSync, copyFileSync, existsSync, readFileSync, unlinkSync } from "n
|
|
|
28
28
|
import { dirname, join } from "node:path";
|
|
29
29
|
import { fileURLToPath } from "node:url";
|
|
30
30
|
import { GITHUB_TOKEN_ENVS } from "@quickchatai/pi-quickreview/src/envstrip.js";
|
|
31
|
+
import { routeModelSpecForLogin } from "@quickchatai/pi-quickreview/src/modelroute.js";
|
|
31
32
|
import { CAP_FD_INDEX } from "./ipc/capability.js";
|
|
32
33
|
import {
|
|
33
34
|
PI_REFRESH_MARGIN_MS,
|
|
@@ -102,6 +103,15 @@ export function readTakeoverSpec(file: string): TakeoverSpec {
|
|
|
102
103
|
return raw as TakeoverSpec;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/** The login file in an agent dir, as text; null when there is none. */
|
|
107
|
+
export function readLogin(agentDir: string): string | null {
|
|
108
|
+
try {
|
|
109
|
+
return readFileSync(join(agentDir, "auth.json"), "utf8");
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
/**
|
|
106
116
|
* The pi's own agent dir: the owner's login borrowed as a refresh-blanked copy
|
|
107
117
|
* (0600, refused when its token would expire inside the lease's idle window plus
|
|
@@ -259,6 +269,9 @@ export async function runTakeover(specFile: string, deps: TakeoverRunDeps = {}):
|
|
|
259
269
|
return 3;
|
|
260
270
|
}
|
|
261
271
|
const env = takeoverEnv(procEnv, spec);
|
|
272
|
+
// An OpenAI model runs on the OpenAI login the borrowed copy holds
|
|
273
|
+
// (packages/pi-plugin/src/modelroute.ts): the spec's own provider when it is there.
|
|
274
|
+
spec = { ...spec, model: routeModelSpecForLogin(spec.model, readLogin(spec.borrow_dir), env) };
|
|
262
275
|
const pi = paneLaunch(takeoverArgv(spec), deps.piBin ?? spec.pi_bin);
|
|
263
276
|
const launch = toolIsolation(pi.bin, pi.argv, procEnv);
|
|
264
277
|
if ("refused" in launch) {
|
package/src/tui/app.ts
CHANGED
|
@@ -841,6 +841,13 @@ export class App {
|
|
|
841
841
|
if (pr !== this.selectedPr) this.msgDraft = ""; // a cancelled draft belongs to the PR it was typed for
|
|
842
842
|
this.selectedPr = pr;
|
|
843
843
|
if (this.view.progress?.pr !== pr) this.view.progress = undefined;
|
|
844
|
+
// An offered row (not tracked yet) has no detail, round or session to
|
|
845
|
+
// read: its pane is the PR list's own fields and the way to review it.
|
|
846
|
+
if (pr !== undefined && this.offeredSelected()) {
|
|
847
|
+
this.syncPiPane();
|
|
848
|
+
if (!this.view.piFocus) this.selectPane(undefined);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
844
851
|
void this.refreshDetail();
|
|
845
852
|
void this.refreshProgress();
|
|
846
853
|
this.followSelection();
|
|
@@ -967,6 +974,38 @@ export class App {
|
|
|
967
974
|
return sorted[this.view.selected]?.number;
|
|
968
975
|
}
|
|
969
976
|
|
|
977
|
+
/** Is the selected row an offered one (open on GitHub, not tracked yet)? */
|
|
978
|
+
private offeredSelected(): boolean {
|
|
979
|
+
return visiblePrs(this.view)[this.view.selected]?.offered === true;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** A PR verb on an offered row: there is no session or round to act on. */
|
|
983
|
+
private refuseOffered(): boolean {
|
|
984
|
+
if (!this.offeredSelected()) return false;
|
|
985
|
+
this.notice(`#${this.selectedPrNumber()} is not reviewed yet — R reviews it now`);
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** `R`: ask the daemon to review the selected PR now — the operator's
|
|
990
|
+
* `review-now` command over the sealed channel, the same as
|
|
991
|
+
* `qcr review-now <n>`. Any open PR of the repo, tracked or not. */
|
|
992
|
+
private reviewNow(): void {
|
|
993
|
+
const pr = this.selectedPrNumber();
|
|
994
|
+
if (pr === undefined) {
|
|
995
|
+
this.notice("select a pull request first (j/k), then R reviews it");
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (!this.hooks.operatorCommand) {
|
|
999
|
+
this.notice("review now: not wired (no remote runner)");
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
this.notice(`asking the daemon to review #${pr} now…`);
|
|
1003
|
+
void this.hooks
|
|
1004
|
+
.operatorCommand({ command: "review-now", pr }, this)
|
|
1005
|
+
.then(() => this.poller.tick())
|
|
1006
|
+
.catch(() => {});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
970
1009
|
/** Synthesize a keypress through the normal input path — the command bar's
|
|
971
1010
|
* buttons and any tab code that wants "exactly what the key would do". */
|
|
972
1011
|
pressKey(key: string): void {
|
|
@@ -1432,6 +1471,7 @@ export class App {
|
|
|
1432
1471
|
// the whole TUI (AGENTS.md invariant 10, the fixed layout). With no PR
|
|
1433
1472
|
// selected there is nothing to focus, and nothing else to open.
|
|
1434
1473
|
if (this.selectedPr !== undefined) {
|
|
1474
|
+
if (this.refuseOffered()) break;
|
|
1435
1475
|
this.focusPiPane();
|
|
1436
1476
|
this.repaint();
|
|
1437
1477
|
}
|
|
@@ -1443,6 +1483,7 @@ export class App {
|
|
|
1443
1483
|
case "message": {
|
|
1444
1484
|
const session = this.selectedSession();
|
|
1445
1485
|
if (!session) break;
|
|
1486
|
+
if (this.refuseOffered()) break;
|
|
1446
1487
|
if (this.hooks.message) this.hooks.message(session, this);
|
|
1447
1488
|
else this.openMessageBox(session);
|
|
1448
1489
|
break;
|
|
@@ -1457,6 +1498,7 @@ export class App {
|
|
|
1457
1498
|
// overseer command, where the extra deliberation is the point.
|
|
1458
1499
|
const pr = this.selectedPrNumber();
|
|
1459
1500
|
if (pr === undefined) break;
|
|
1501
|
+
if (this.refuseOffered()) break;
|
|
1460
1502
|
if (this.hooks.unwedge) this.hooks.unwedge(pr, this);
|
|
1461
1503
|
else this.notice("unwedge: not wired (no remote runner)");
|
|
1462
1504
|
break;
|
|
@@ -1482,6 +1524,11 @@ export class App {
|
|
|
1482
1524
|
this.setBoardFilter(null);
|
|
1483
1525
|
break;
|
|
1484
1526
|
}
|
|
1527
|
+
// R reviews the selected PR now — tracked or offered.
|
|
1528
|
+
if (key.ch === "R" && this.tab === "board") {
|
|
1529
|
+
this.reviewNow();
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1485
1532
|
// I starts setup — the companion takes it from there.
|
|
1486
1533
|
if (key.ch === "I" && this.tab === "board") {
|
|
1487
1534
|
if (this.hooks.setup) this.hooks.setup(this);
|
package/src/tui/board.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { cancelCauseLabel, isLoudCancelCause, type StatusSnapshot } from "../sta
|
|
|
11
11
|
import type { AlertBadge } from "./alerts.js";
|
|
12
12
|
import { barHeight, paintCmdBar, type BarButton, type LiveKeys } from "./cmdbar.js";
|
|
13
13
|
import { filterLabel, visiblePrs, type BoardFilter } from "./filters.js";
|
|
14
|
-
import { glyphFor, type EventLine, type SnapshotPr } from "./model.js";
|
|
14
|
+
import { glyphFor, OFFERED_GLYPH, type EventLine, type SnapshotPr } from "./model.js";
|
|
15
15
|
import { windowStart } from "./panes.js";
|
|
16
16
|
import {
|
|
17
17
|
ACCENT,
|
|
@@ -47,7 +47,8 @@ import {
|
|
|
47
47
|
type LiveAgentProgress,
|
|
48
48
|
type RoundProgressSnapshot,
|
|
49
49
|
} from "@quickchatai/pi-quickreview/src/round-progress.js";
|
|
50
|
-
import type
|
|
50
|
+
import { isLocalDiagnosis, type ConnectionDiagnosis } from "./sshtriage.js";
|
|
51
|
+
import { setupLine, type SetupState } from "./setupstate.js";
|
|
51
52
|
import { FG, RESET, fmtAge, legendLine, osc8, prUrl, stateColor, verdictChip } from "./theme.js";
|
|
52
53
|
|
|
53
54
|
/** Every clickable thing the board declares. */
|
|
@@ -109,6 +110,9 @@ export interface BoardView {
|
|
|
109
110
|
/** Active board filter (companion `board_filter` verb; F clears).
|
|
110
111
|
* Consumed through `visiblePrs` — never filter a sortPrs call directly. */
|
|
111
112
|
boardFilter?: BoardFilter | null;
|
|
113
|
+
/** Set while onboarding's checklist exists and is not complete
|
|
114
|
+
* (src/tui/setupstate.ts): the board says so before anything else. */
|
|
115
|
+
setup?: SetupState;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
// ---- layout ----------------------------------------------------------------
|
|
@@ -197,22 +201,26 @@ export function listWidth(prs: SnapshotPr[], cols: number, now: Date = new Date(
|
|
|
197
201
|
export function boardKeys(
|
|
198
202
|
view: Pick<BoardView, "snapshot" | "boardFilter" | "selected" | "repo">,
|
|
199
203
|
): LiveKeys {
|
|
200
|
-
const
|
|
204
|
+
const row = visiblePrs(view)[view.selected];
|
|
205
|
+
const selected = row !== undefined;
|
|
206
|
+
// An offered row (open on GitHub, not tracked yet) has no session, round or
|
|
207
|
+
// pane: `R` reviews it and `g` opens it, and nothing else addresses it.
|
|
208
|
+
const tracked = selected && row.offered !== true;
|
|
201
209
|
const bar: BarButton[] = [];
|
|
202
|
-
if (selected) bar.push({ key: "
|
|
210
|
+
if (selected) bar.push({ key: "R", label: "R review now" });
|
|
211
|
+
if (tracked) bar.push({ key: "\r", label: "⏎ pi pane" }, { key: "m", label: "m message" });
|
|
203
212
|
if (selected && view.repo) bar.push({ key: "g", label: "g github" });
|
|
204
213
|
bar.push({ key: "e", label: "e events" });
|
|
205
|
-
if (
|
|
214
|
+
if (tracked) bar.push({ key: "u", label: "u unwedge" });
|
|
206
215
|
// Setup is about the repo, not a PR — offered whatever is selected.
|
|
207
216
|
bar.push({ key: "I", label: "I setup" });
|
|
208
217
|
const inline: string[] = [];
|
|
209
|
-
if (
|
|
218
|
+
if (tracked) inline.push("a");
|
|
210
219
|
if (view.boardFilter != null) inline.push("F");
|
|
211
220
|
// A worker-hosted PR's pane is the transcript viewer: `[ ]` step its streams
|
|
212
221
|
// and `< >` its rounds. They are the pane header's keys (painted there), so
|
|
213
222
|
// they exist only while the selected PR's pane is the viewer.
|
|
214
|
-
|
|
215
|
-
if (pr !== undefined && paneSurface(pr.runtime, pr.number).kind === "viewer")
|
|
223
|
+
if (tracked && paneSurface(row.runtime, row.number).kind === "viewer")
|
|
216
224
|
inline.push("[", "]", "<", ">");
|
|
217
225
|
return { bar, inline };
|
|
218
226
|
}
|
|
@@ -287,6 +295,9 @@ const CONNECTION_LABEL: Record<ConnectionDiagnosis["kind"], string> = {
|
|
|
287
295
|
"wedged-master": "ssh master wedged",
|
|
288
296
|
"host-unreachable": "host down",
|
|
289
297
|
unknown: "cause unknown",
|
|
298
|
+
"local-down": "daemon container not running",
|
|
299
|
+
"local-docker": "local docker not answering",
|
|
300
|
+
unconfigured: "no ssh_host",
|
|
290
301
|
};
|
|
291
302
|
|
|
292
303
|
/**
|
|
@@ -297,12 +308,30 @@ const CONNECTION_LABEL: Record<ConnectionDiagnosis["kind"], string> = {
|
|
|
297
308
|
* the truth is "I could not ask"), the reason is named, and the remedy is a
|
|
298
309
|
* command the operator can paste into their own shell.
|
|
299
310
|
*/
|
|
300
|
-
export function unknownSourceLines(
|
|
311
|
+
export function unknownSourceLines(
|
|
312
|
+
conn: ConnectionDiagnosis | undefined,
|
|
313
|
+
width: number,
|
|
314
|
+
setup?: SetupState,
|
|
315
|
+
): string[] {
|
|
301
316
|
const lines = [
|
|
302
317
|
`${FG.anomaly}the daemon has not answered yet${RESET}`,
|
|
303
318
|
`${FG.dim}nothing below is a measurement — it is not known${RESET}`,
|
|
304
319
|
"",
|
|
305
320
|
];
|
|
321
|
+
if (setup) {
|
|
322
|
+
// A half-finished onboarding is the whole explanation: the stack is
|
|
323
|
+
// started by one of its later steps, so daemon or ssh troubleshooting
|
|
324
|
+
// here would send the operator after a fault that is not one.
|
|
325
|
+
for (const l of wrap(setupLine(setup), width)) lines.push(`${WARN}${l}${DRESET}`);
|
|
326
|
+
for (const l of wrap(
|
|
327
|
+
"the daemon, its memory and this board's connection are set up by the wizard's remaining steps",
|
|
328
|
+
width,
|
|
329
|
+
))
|
|
330
|
+
lines.push(`${FG.dim}${l}${RESET}`);
|
|
331
|
+
lines.push("");
|
|
332
|
+
lines.push(`${FG.dim}r refreshes${RESET}`);
|
|
333
|
+
return lines.map((l) => truncate(l, width));
|
|
334
|
+
}
|
|
306
335
|
if (conn && conn.kind !== "ok" && conn.kind !== "local") {
|
|
307
336
|
for (const l of wrap(stripAnsi(conn.cause), width)) lines.push(`${FG.dim}${l}${RESET}`);
|
|
308
337
|
if (conn.remedy !== "") {
|
|
@@ -337,7 +366,8 @@ export function unknownDetailLines(conn: ConnectionDiagnosis | undefined, width:
|
|
|
337
366
|
];
|
|
338
367
|
if (conn?.detail) {
|
|
339
368
|
lines.push("");
|
|
340
|
-
|
|
369
|
+
const who = isLocalDiagnosis(conn) ? "docker" : "ssh";
|
|
370
|
+
for (const l of wrap(`${who} said: ${stripAnsi(conn.detail)}`, width))
|
|
341
371
|
lines.push(`${FG.dim}${l}${RESET}`);
|
|
342
372
|
}
|
|
343
373
|
return lines.map((l) => truncate(l, width));
|
|
@@ -482,9 +512,17 @@ function barSegments(view: BoardView, alerts: boolean): string[] {
|
|
|
482
512
|
health = `${WARN}▲ DATA STALE (${view.health.dataAgeS}s)${DRESET}`;
|
|
483
513
|
break;
|
|
484
514
|
case "unreachable":
|
|
485
|
-
|
|
515
|
+
// Under an unfinished setup the setup segment that replaces this is the
|
|
516
|
+
// cause; a transport label would only point at a fault that is not one
|
|
517
|
+
// (the list still says, in full, that the daemon has not answered).
|
|
518
|
+
health = view.setup
|
|
519
|
+
? ""
|
|
520
|
+
: `${FG.anomaly}✖ unreachable${view.connection ? ` · ${CONNECTION_LABEL[view.connection.kind]}` : ""}${RESET}`;
|
|
486
521
|
break;
|
|
487
522
|
}
|
|
523
|
+
// Short on the bar, where every segment competes for the width; the list
|
|
524
|
+
// carries the full sentence with the next step.
|
|
525
|
+
const setupSeg = view.setup ? `${WARN}▲ setup not finished — qcr onboard${DRESET}` : "";
|
|
488
526
|
const s = view.snapshot;
|
|
489
527
|
const rounds = s ? `rounds ${s.running_rounds}/${s.max_concurrent_rounds ?? "?"}` : "";
|
|
490
528
|
const spend = s?.today_spend_usd !== undefined ? `today $${s.today_spend_usd.toFixed(2)}` : "";
|
|
@@ -512,6 +550,7 @@ function barSegments(view: BoardView, alerts: boolean): string[] {
|
|
|
512
550
|
`${HEADING}quickreview${DRESET}`,
|
|
513
551
|
mode,
|
|
514
552
|
health,
|
|
553
|
+
setupSeg,
|
|
515
554
|
mem?.loud ? mem.text : "",
|
|
516
555
|
loop?.loud ? loop.text : "",
|
|
517
556
|
curation,
|
|
@@ -549,6 +588,51 @@ export function alertZone(view: BoardView, cols: number): { x1: number; x2: numb
|
|
|
549
588
|
return { x1, x2: Math.min(cols - 1, x1 + width - 1) };
|
|
550
589
|
}
|
|
551
590
|
|
|
591
|
+
/**
|
|
592
|
+
* The board's own guide to the first review, under the PRS header. With open
|
|
593
|
+
* PRs offered and none tracked it says what to do; with no open PR at all it
|
|
594
|
+
* says that, naming the repo; once a PR is tracked it is silent. Absent
|
|
595
|
+
* `open_prs` (a daemon that has not listed yet, or an older one) is unknown,
|
|
596
|
+
* so the guide says nothing rather than "no open PRs".
|
|
597
|
+
*/
|
|
598
|
+
export function reviewGuideLines(
|
|
599
|
+
view: Pick<BoardView, "snapshot" | "boardFilter" | "repo">,
|
|
600
|
+
tracked: number,
|
|
601
|
+
offered: number,
|
|
602
|
+
width: number,
|
|
603
|
+
): string[] {
|
|
604
|
+
if (view.boardFilter != null || tracked > 0 || view.snapshot?.open_prs === undefined) return [];
|
|
605
|
+
if (offered === 0)
|
|
606
|
+
return [
|
|
607
|
+
truncate(
|
|
608
|
+
`${FG.dim}no open pull requests in ${view.repo ?? "the repository"} — open one, and it is listed here within a few minutes${RESET}`,
|
|
609
|
+
width,
|
|
610
|
+
),
|
|
611
|
+
];
|
|
612
|
+
return [
|
|
613
|
+
truncate(`${FG.accent}select a pull request (j/k), then R reviews it now${RESET}`, width),
|
|
614
|
+
];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** The detail pane of an offered row: what is known (the PR list's own
|
|
618
|
+
* fields) and the one thing to do with it. */
|
|
619
|
+
function offeredDetailLines(pr: SnapshotPr, width: number, now: Date): string[] {
|
|
620
|
+
return [
|
|
621
|
+
`${FG.dim}${OFFERED_GLYPH}${RESET} ${STRONG}#${pr.number}${RESET} ${STRONG}${truncate(sanitizeText(pr.title), width - 8 - String(pr.number).length)}${RESET}`,
|
|
622
|
+
truncate(
|
|
623
|
+
`${FG.author}${pr.author ?? ""}${RESET} ${FG.dim}· updated ${fmtAge(pr.updated_at, now)}${RESET}`,
|
|
624
|
+
width,
|
|
625
|
+
),
|
|
626
|
+
"",
|
|
627
|
+
"not reviewed yet",
|
|
628
|
+
"",
|
|
629
|
+
truncate(`${STRONG}R${RESET} reviews it now: the reviewer runs one full round`, width),
|
|
630
|
+
truncate("on this pull request, and its progress shows here.", width),
|
|
631
|
+
truncate(`${FG.dim}In dry-run (the top bar says so) the review is recorded,${RESET}`, width),
|
|
632
|
+
truncate(`${FG.dim}never posted to GitHub.${RESET}`, width),
|
|
633
|
+
];
|
|
634
|
+
}
|
|
635
|
+
|
|
552
636
|
function styledRow(
|
|
553
637
|
pr: SnapshotPr,
|
|
554
638
|
selected: boolean,
|
|
@@ -811,6 +895,7 @@ export function detailLines(
|
|
|
811
895
|
): string[] {
|
|
812
896
|
const lines: string[] = [];
|
|
813
897
|
if (!pr) return ["no PR selected"];
|
|
898
|
+
if (pr.offered) return offeredDetailLines(pr, width, now);
|
|
814
899
|
lines.push(
|
|
815
900
|
`${stateColor(pr.state)}${STRONG}#${pr.number}${RESET} ${STRONG}${truncate(sanitizeText(pr.title), width - 8 - String(pr.number).length)}${RESET}`,
|
|
816
901
|
);
|
|
@@ -1010,7 +1095,7 @@ export function detailLines(
|
|
|
1010
1095
|
export const HELP_LINES = [
|
|
1011
1096
|
"1…8 the eight surfaces: board · memory · digest · tickets · deploy · metrics · feedback · scan",
|
|
1012
1097
|
" the bar at the foot shows what THIS frame answers to; a button that is not there is a key that would do nothing",
|
|
1013
|
-
"r / s / ? / q refresh ·
|
|
1098
|
+
"r / s / ? / q refresh · re-check the connection (VM route: ssh, the one key-passphrase prompt; local route: the daemon container) · this overlay · quit (asks first)",
|
|
1014
1099
|
"Ctrl-c ×3 kill the TUI from anywhere (terminal restored) — the un-stuck hatch",
|
|
1015
1100
|
" an alert in the top bar: click it or ⏎ to open the companion",
|
|
1016
1101
|
"the companion column down the right edge is on every tab:",
|
|
@@ -1025,6 +1110,7 @@ export const HELP_LINES = [
|
|
|
1025
1110
|
"< / > past / later round in the viewer; PgUp / wheel parks it, End follows the tail again",
|
|
1026
1111
|
" its input line (focused pane): ⏎ steers the running round, or sends a message to an idle PR; Alt-⏎ queues a follow-up",
|
|
1027
1112
|
" Alt-p marks the next message may-post (shown, reset after send); Ctrl-c asks to abort the round (y confirms); [ ] < > are keys only while the line is empty",
|
|
1113
|
+
"R review the selected PR now — any open PR, tracked or not (○ not reviewed): one round, nothing posted in dry-run",
|
|
1028
1114
|
"m quick-message the selected PR's session",
|
|
1029
1115
|
"u unwedge: cancel the running round and re-queue it (nothing posted, no GitHub write)",
|
|
1030
1116
|
"e events overlay (j/k scroll, e/Esc close; click an event → its PR)",
|
|
@@ -1296,11 +1382,22 @@ export function renderBoard(view: BoardView, cols: number, rows: number): Frame
|
|
|
1296
1382
|
// detail pane says "no data" rather than "no PR selected".
|
|
1297
1383
|
const known = view.snapshot !== undefined;
|
|
1298
1384
|
const flabel = filterLabel(view.boardFilter);
|
|
1385
|
+
const offered = sorted.filter((p) => p.offered === true).length;
|
|
1299
1386
|
const listLines: string[] = known
|
|
1300
|
-
? [
|
|
1387
|
+
? [
|
|
1388
|
+
section(
|
|
1389
|
+
`PRS (${sorted.length})`,
|
|
1390
|
+
flabel !== ""
|
|
1391
|
+
? flabel
|
|
1392
|
+
: offered > 0
|
|
1393
|
+
? `${sorted.length - offered} tracked · ${offered} open, not reviewed`
|
|
1394
|
+
: "the daemon's live set",
|
|
1395
|
+
),
|
|
1396
|
+
...reviewGuideLines(view, sorted.length - offered, offered, listW - 1),
|
|
1397
|
+
]
|
|
1301
1398
|
: [
|
|
1302
1399
|
section("PRS (?)", "unknown — the daemon has not answered"),
|
|
1303
|
-
...unknownSourceLines(view.connection, listW - 1),
|
|
1400
|
+
...unknownSourceLines(view.connection, listW - 1, view.setup),
|
|
1304
1401
|
];
|
|
1305
1402
|
// The list scrolls with the selection: the verbs act on the selected PR, so
|
|
1306
1403
|
// it must never sit off-screen (`windowStart`, the tabs' shared rule). The
|
package/src/tui/cmdbar.ts
CHANGED
|
@@ -51,7 +51,9 @@ const NAV: ReadonlyArray<BarButton & { tab: Tab }> = TABS.map((tab) => ({
|
|
|
51
51
|
/** Row A right: always the same four, always in the same place. */
|
|
52
52
|
const GLOBALS: readonly BarButton[] = [
|
|
53
53
|
{ key: "r", label: "r refresh" },
|
|
54
|
-
|
|
54
|
+
// Transport-neutral: on the VM route `s` re-checks ssh (and offers the one
|
|
55
|
+
// key-passphrase prompt), on the local route the daemon container.
|
|
56
|
+
{ key: "s", label: "s check" },
|
|
55
57
|
{ key: "?", label: "? help" },
|
|
56
58
|
{ key: "q", label: "q quit" },
|
|
57
59
|
];
|
|
@@ -73,6 +75,7 @@ export const GLOBAL_KEYS: readonly string[] = [
|
|
|
73
75
|
*/
|
|
74
76
|
export const TAB_VOCABULARY: Readonly<Record<Tab, readonly BarButton[]>> = {
|
|
75
77
|
board: [
|
|
78
|
+
{ key: "R", label: "R review now" },
|
|
76
79
|
{ key: "\r", label: "⏎ pi pane" },
|
|
77
80
|
{ key: "m", label: "m message" },
|
|
78
81
|
{ key: "g", label: "g github" },
|