@zhuxixi/pi-agent-board 0.6.2 → 0.8.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 +43 -0
- package/README.md +6 -3
- package/VERIFY.md +2 -1
- package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +3 -1
- package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +917 -0
- package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +603 -0
- package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +252 -0
- package/docs/superpowers/plans/2026-09-10-reader-consistency.md +115 -0
- package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +469 -0
- package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +92 -0
- package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +771 -0
- package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +299 -0
- package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +609 -0
- package/docs/superpowers/plans/2026-09-14-terminal-model.md +145 -0
- package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +224 -0
- package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +341 -0
- package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +875 -0
- package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +116 -0
- package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +517 -0
- package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +130 -0
- package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +298 -0
- package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +114 -0
- package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +120 -0
- package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +146 -0
- package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +116 -0
- package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +84 -0
- package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +92 -0
- package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +123 -0
- package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +204 -0
- package/package.json +3 -2
- package/runner/job-runner-legacy.mjs +68 -0
- package/runner/job-runner.mjs +371 -67
- package/runner/pty-runner-legacy.mjs +50 -0
- package/runner/pty-runner.mjs +685 -58
- package/runner/state-coordinator.mjs +429 -0
- package/runner/state-runner.mjs +90 -15
- package/scripts/run-perf-gate.mjs +40 -0
- package/src/commands/agent-board.ts +8 -8
- package/src/commands/attach-flow.ts +5 -5
- package/src/commands/bg.ts +2 -1
- package/src/core/control-protocol.mjs +482 -0
- package/src/core/coordinator-client.mjs +313 -0
- package/src/core/coordinator-journal.mjs +282 -0
- package/src/core/coordinator-protocol.mjs +12 -0
- package/src/core/editor-state-reporter.mjs +11 -1
- package/src/core/foreground-preview-cache.mjs +117 -0
- package/src/core/host-protocol.mjs +24 -0
- package/src/core/launch.mjs +15 -0
- package/src/core/locks.mjs +68 -14
- package/src/core/paths.mjs +48 -0
- package/src/core/pid.mjs +32 -1
- package/src/core/pty-attach-jiggle-controller.mjs +83 -6
- package/src/core/pty-attach-reconnect.mjs +13 -6
- package/src/core/pty-attach-render.mjs +50 -0
- package/src/core/state-commands.mjs +699 -0
- package/src/core/status-consistency.mjs +98 -0
- package/src/core/store.mjs +59 -13
- package/src/core/terminal-attach-client.mjs +803 -0
- package/src/core/terminal-attach-protocol.mjs +252 -0
- package/src/core/terminal-model.mjs +222 -0
- package/src/core/terminal-snapshot.mjs +440 -0
- package/src/core/types.mjs +2 -0
- package/src/index.ts +12 -4
- package/src/runtime/service.mjs +694 -121
- package/src/ui/dashboard.ts +67 -92
- package/src/ui/pty-attach.ts +298 -72
- package/src/core/pty-input.mjs +0 -47
package/src/ui/dashboard.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
|
11
11
|
import type { Component, EditorTheme, KeybindingsManager, TUI } from "@earendil-works/pi-tui";
|
|
12
12
|
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
13
13
|
import { requestDashboardRender } from "../core/dashboard-render.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import { normalizeGenericStatusText } from "../core/derive.mjs";
|
|
15
15
|
import { firstSentence, truncate } from "../core/heuristics.mjs";
|
|
16
16
|
import {
|
|
17
17
|
canonicalModelRef,
|
|
@@ -31,7 +31,7 @@ import { filterRows, groupRowsByFolder, rowState, stateGlyph } from "../core/row
|
|
|
31
31
|
import { loadSessionView } from "../core/session-view.mjs";
|
|
32
32
|
import { GROUP_LABELS } from "../core/types.mjs";
|
|
33
33
|
import { buildEvidencePanel } from "./dashboard-evidence.mjs";
|
|
34
|
-
import {
|
|
34
|
+
import { type Row } from "../core/store.mjs";
|
|
35
35
|
import type { createService } from "../runtime/service.mjs";
|
|
36
36
|
|
|
37
37
|
type Service = ReturnType<typeof createService>;
|
|
@@ -440,7 +440,7 @@ export class DashboardComponent implements Component {
|
|
|
440
440
|
const page = Math.max(1, termRows - 8);
|
|
441
441
|
if (matchesKey(data, Key.left) || matchesKey(data, Key.escape) || data === "<") {
|
|
442
442
|
const row = this.selectedRow();
|
|
443
|
-
if (row) this.deps.service.markVisited?.(row.meta.id);
|
|
443
|
+
if (row) void this.deps.service.markVisited?.(row.meta.id)?.catch(() => {});
|
|
444
444
|
this.mode = "list";
|
|
445
445
|
return;
|
|
446
446
|
}
|
|
@@ -847,13 +847,28 @@ export class DashboardComponent implements Component {
|
|
|
847
847
|
const launchCwd = launchOpts?.cwd ?? this.deps.defaultCwd;
|
|
848
848
|
const launchModel = launchOpts?.model ?? (this.launch?.model ? canonicalModelRef(this.launch.model) : null);
|
|
849
849
|
const launchThinking = launchOpts?.thinkingLevel ?? this.launch?.thinking ?? this.deps.currentThinkingLevel;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
850
|
+
// Clear synchronously, BEFORE the async dispatch: closes the double-submit
|
|
851
|
+
// window — a second Enter during the coordinator round-trip (mark_queued
|
|
852
|
+
// lands before the runner spawns, issue #91) must not re-dispatch.
|
|
853
|
+
this.setInput("");
|
|
854
|
+
this.launch = null;
|
|
855
|
+
this.mode = "list";
|
|
856
|
+
this.inputNotice = null;
|
|
857
|
+
this.refresh();
|
|
858
|
+
// dispatch is async (mark_queued routes through the view-state coordinator
|
|
859
|
+
// before the runner spawns, issue #91); notices/attach land when it settles.
|
|
860
|
+
void Promise.resolve(
|
|
861
|
+
this.deps.service.dispatch(text, {
|
|
862
|
+
cwd: launchCwd,
|
|
863
|
+
model: launchModel,
|
|
864
|
+
thinkingLevel: launchThinking,
|
|
865
|
+
}),
|
|
866
|
+
).then((res) => {
|
|
867
|
+
if (!res.ok) {
|
|
868
|
+
this.notice(res.error ?? "Dispatch failed", "error");
|
|
869
|
+
this.refresh();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
857
872
|
this.lastLaunchPrefs = { ...this.deps.service.getLaunchPrefs?.(), cwd: launchCwd, model: launchModel, thinkingLevel: launchThinking };
|
|
858
873
|
try {
|
|
859
874
|
recordCwdLaunch(this.deps.root, launchCwd);
|
|
@@ -867,10 +882,6 @@ export class DashboardComponent implements Component {
|
|
|
867
882
|
}
|
|
868
883
|
this.selectedId = res.viewId ?? this.selectedId;
|
|
869
884
|
if (launchOpts?.attach && res.hostMode === "pty" && res.viewId) {
|
|
870
|
-
this.setInput("");
|
|
871
|
-
this.launch = null;
|
|
872
|
-
this.mode = "list";
|
|
873
|
-
this.inputNotice = null;
|
|
874
885
|
this.done({ action: "attach", viewId: res.viewId, stopFirst: false });
|
|
875
886
|
return;
|
|
876
887
|
}
|
|
@@ -879,12 +890,14 @@ export class DashboardComponent implements Component {
|
|
|
879
890
|
} else {
|
|
880
891
|
this.notice(`Dispatched: ${truncate(text, 40)}`, "info");
|
|
881
892
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
893
|
+
this.refresh();
|
|
894
|
+
}).catch(() => {
|
|
895
|
+
// dispatch() is not expected to reject (its failures surface as
|
|
896
|
+
// {ok:false}); this guards fs-class throws from escaping as unhandled
|
|
897
|
+
// rejections (issue #91 hygiene).
|
|
898
|
+
this.notice("Dispatch failed", "error");
|
|
899
|
+
this.refresh();
|
|
900
|
+
});
|
|
888
901
|
}
|
|
889
902
|
|
|
890
903
|
private async submitReply(): Promise<void> {
|
|
@@ -947,10 +960,11 @@ export class DashboardComponent implements Component {
|
|
|
947
960
|
this.pending = {
|
|
948
961
|
prompt: `Mark "${row.meta.name}" as done? (y/N)`,
|
|
949
962
|
onYes: () => {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
963
|
+
void this.markCompleted(row).then((res) => {
|
|
964
|
+
if (!res.ok) this.notice(res.error ?? "Mark done failed", "error");
|
|
965
|
+
else this.notice("Marked done", "info");
|
|
966
|
+
this.refresh();
|
|
967
|
+
});
|
|
954
968
|
},
|
|
955
969
|
};
|
|
956
970
|
this.mode = "confirm";
|
|
@@ -966,57 +980,25 @@ export class DashboardComponent implements Component {
|
|
|
966
980
|
prompt: `Mark ${doneIds.length} selected session${doneIds.length === 1 ? "" : "s"} as done?${skipped ? ` ${skipped} will be skipped.` : ""} (y/N)`,
|
|
967
981
|
returnMode: "select",
|
|
968
982
|
onYes: () => {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
983
|
+
void Promise.resolve(this.deps.service.markCompletedMany?.(doneIds) ?? { ok: true, completed: 0, skipped: doneIds.length, completedIds: [] }).then((res) => {
|
|
984
|
+
if (!res.ok) this.notice("Mark done failed", "error");
|
|
985
|
+
else {
|
|
986
|
+
if (res.completedIds?.length) this.selectedId = res.completedIds[0];
|
|
987
|
+
this.notice(`Moved ${res.completed} to Done${res.skipped ? ` · skipped ${res.skipped}` : ""}`, "info");
|
|
988
|
+
}
|
|
989
|
+
this.refresh();
|
|
990
|
+
});
|
|
976
991
|
},
|
|
977
992
|
};
|
|
978
993
|
this.mode = "confirm";
|
|
979
994
|
}
|
|
980
995
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const state = readState(service.root, row.meta.id) ?? row.state ?? {
|
|
988
|
-
version: 1,
|
|
989
|
-
viewId: row.meta.id,
|
|
990
|
-
currentRunId: null,
|
|
991
|
-
semanticState: "idle",
|
|
992
|
-
processState: "exited",
|
|
993
|
-
summary: "Needs instructions",
|
|
994
|
-
lastActivityAt: Date.now(),
|
|
995
|
-
updatedAt: Date.now(),
|
|
996
|
-
needsInput: false,
|
|
997
|
-
hasError: false,
|
|
998
|
-
latestAssistantPreview: "",
|
|
999
|
-
latestTool: null,
|
|
1000
|
-
question: null,
|
|
1001
|
-
pendingQuestions: [],
|
|
1002
|
-
error: null,
|
|
1003
|
-
lastVisitedAt: null,
|
|
1004
|
-
lastAgentActivityAt: null,
|
|
1005
|
-
autoState: null,
|
|
1006
|
-
};
|
|
1007
|
-
state.semanticState = "completed";
|
|
1008
|
-
state.processState = "exited";
|
|
1009
|
-
state.needsInput = false;
|
|
1010
|
-
state.hasError = false;
|
|
1011
|
-
state.question = null;
|
|
1012
|
-
state.pendingQuestions = [];
|
|
1013
|
-
state.error = null;
|
|
1014
|
-
state.autoState = null;
|
|
1015
|
-
state.summary = completedSummary(state.summary, state.latestAssistantPreview);
|
|
1016
|
-
state.lastActivityAt = Date.now();
|
|
1017
|
-
state.updatedAt = Date.now();
|
|
1018
|
-
writeState(service.root, state);
|
|
1019
|
-
return { ok: true };
|
|
996
|
+
// Routes through the View State Coordinator (issue #91): the service's
|
|
997
|
+
// markCompleted submits a fenced state command. No compat fallback for stale
|
|
998
|
+
// pre-coordinator service objects — the window is transient and self-heals
|
|
999
|
+
// on the next dashboard reload.
|
|
1000
|
+
private markCompleted(row: Row): Promise<{ ok: boolean; error?: string }> {
|
|
1001
|
+
return this.deps.service.markCompleted(row.meta.id);
|
|
1020
1002
|
}
|
|
1021
1003
|
|
|
1022
1004
|
private handleDeleteKey(): void {
|
|
@@ -1025,10 +1007,13 @@ export class DashboardComponent implements Component {
|
|
|
1025
1007
|
const now = Date.now();
|
|
1026
1008
|
if (this.deleteArm !== null && this.deleteArm.id === row.meta.id && now - this.deleteArm.at <= DELETE_DOUBLE_PRESS_MS) {
|
|
1027
1009
|
this.deleteArm = null;
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1010
|
+
// archive is async (routes through the view-state coordinator, issue #91);
|
|
1011
|
+
// the notice + refresh land when the command settles.
|
|
1012
|
+
void Promise.resolve(this.deps.service.archive(row.meta.id)).then((res) => {
|
|
1013
|
+
if (!res.ok) this.notice(res.error ?? "Delete failed", "error");
|
|
1014
|
+
else this.notice(`Deleted "${row.meta.name}"`, "info");
|
|
1015
|
+
this.refresh();
|
|
1016
|
+
});
|
|
1032
1017
|
return;
|
|
1033
1018
|
}
|
|
1034
1019
|
this.deleteArm = { id: row.meta.id, at: now };
|
|
@@ -1063,11 +1048,13 @@ export class DashboardComponent implements Component {
|
|
|
1063
1048
|
prompt: `Delete ${rows.length} done session${rows.length === 1 ? "" : "s"}? Session files are preserved. (y/N)`,
|
|
1064
1049
|
returnMode: "select",
|
|
1065
1050
|
onYes: () => {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1051
|
+
// archiveMany is async (coordinator-routed, issue #91).
|
|
1052
|
+
void Promise.resolve(this.deps.service.archiveMany?.(rows.map((row) => row.meta.id)) ?? { ok: true, archived: 0, skipped: rows.length }).then((res) => {
|
|
1053
|
+
if (!res.ok) this.notice("Delete failed", "error");
|
|
1054
|
+
else this.notice(`Deleted ${res.archived}${res.skipped ? ` · skipped ${res.skipped}` : ""}`, "info");
|
|
1055
|
+
this.exitSelectionMode(true);
|
|
1056
|
+
this.refresh();
|
|
1057
|
+
});
|
|
1071
1058
|
},
|
|
1072
1059
|
};
|
|
1073
1060
|
this.mode = "confirm";
|
|
@@ -1088,7 +1075,7 @@ export class DashboardComponent implements Component {
|
|
|
1088
1075
|
|
|
1089
1076
|
private openSessionView(): void {
|
|
1090
1077
|
if (!this.selectedId) return;
|
|
1091
|
-
this.deps.service.markVisited?.(this.selectedId);
|
|
1078
|
+
void this.deps.service.markVisited?.(this.selectedId)?.catch(() => {});
|
|
1092
1079
|
this.sessionScrollTop = 0;
|
|
1093
1080
|
this.mode = "session";
|
|
1094
1081
|
}
|
|
@@ -2009,18 +1996,6 @@ function isAgentBusy(row: Row): boolean {
|
|
|
2009
1996
|
return Boolean(row.alive && (st === "queued" || st === "working" || waitingOnTool));
|
|
2010
1997
|
}
|
|
2011
1998
|
|
|
2012
|
-
function completedSummary(summary: string, _latestAssistantPreview: string): string {
|
|
2013
|
-
if (!isGenericStatusText(summary)) return compactCompletedSummary(summary);
|
|
2014
|
-
return "Done";
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
function compactCompletedSummary(text: string): string {
|
|
2018
|
-
const cleaned = String(text || "").replace(/\s+/g, " ").trim();
|
|
2019
|
-
if (!cleaned) return "Done";
|
|
2020
|
-
const first = firstSentence(cleaned);
|
|
2021
|
-
return truncate(first.length >= 12 ? first : cleaned, 80);
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
1999
|
function displayPath(path: string): string {
|
|
2025
2000
|
const home = process.env.HOME;
|
|
2026
2001
|
if (home && path.startsWith(home)) return `~${path.slice(home.length)}` || "~";
|