@fitchmultz/pi-subagents 0.46.1 → 0.47.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 +29 -0
- package/README.md +1 -1
- package/dist/agents/agent-management.js +8 -2
- package/dist/agents/agents.js +8 -8
- package/dist/extension/build-info.js +1 -1
- package/dist/extension/index.js +11 -5
- package/dist/extension/result-snapshot.js +0 -18
- package/dist/history/ingest.js +40 -31
- package/dist/history/prune.js +1 -1
- package/dist/history/run-projection.js +1 -2
- package/dist/history/search-queries.js +8 -2
- package/dist/history/store.js +20 -4
- package/dist/history/text-chunks.js +1 -2
- package/dist/history/transcript-queries.js +12 -6
- package/dist/intercom/result-intercom.js +0 -74
- package/dist/pi-intercom/bridge-config.js +35 -26
- package/dist/pi-intercom/bridge-files.js +47 -0
- package/dist/pi-intercom/bridge-mailbox.js +324 -0
- package/dist/pi-intercom/bridge-presence.js +100 -0
- package/dist/pi-intercom/bridge-routes.js +27 -3
- package/dist/pi-intercom/bridge-session.js +60 -64
- package/dist/pi-intercom/bridge-wake.js +158 -0
- package/dist/pi-intercom/bridge.js +55 -7
- package/dist/pi-intercom/broker/broker.js +33 -16
- package/dist/pi-intercom/broker/framing.js +67 -17
- package/dist/pi-intercom/broker/snapshot-reader.js +29 -8
- package/dist/pi-intercom/broker/snapshots.js +144 -41
- package/dist/pi-intercom/session-targets.js +0 -1
- package/dist/runs/background/async-run-discovery.js +4 -6
- package/dist/runs/background/completion-dedupe.js +4 -55
- package/dist/runs/background/notify.js +1 -9
- package/dist/runs/background/result-watcher.js +1 -1
- package/dist/runs/background/runner-child-observer.js +12 -7
- package/dist/runs/background/runner-monitor.js +2 -0
- package/dist/runs/background/runner-parallel.js +2 -3
- package/dist/runs/shared/child-observation-spool.js +8 -3
- package/dist/runs/shared/run-restoration.js +4 -5
- package/dist/runs/shared/supervisor-questions.js +7 -4
- package/dist/shared/journal-frames.js +1 -1
- package/dist/slash/command-discovery.js +13 -12
- package/dist/tui/agent-history.js +0 -14
- package/dist/tui/agent-view.js +0 -1
- package/dist/tui/conversation-history.js +10 -1
- package/dist/tui/history-cards.js +8 -22
- package/docs/intercom-bridge.md +39 -17
- package/docs/intercom.md +5 -1
- package/docs/performance.md +72 -3
- package/package.json +1 -1
- package/scripts/intercom-bridge-credentials.mjs +54 -12
- package/scripts/intercom-remote.mjs +25 -1
- package/scripts/package-smoke.mjs +1 -1
- package/src/agents/agent-management.ts +8 -4
- package/src/agents/agents.ts +17 -12
- package/src/extension/index.ts +12 -5
- package/src/extension/result-snapshot.ts +1 -27
- package/src/history/ingest.ts +57 -49
- package/src/history/prune.ts +1 -1
- package/src/history/run-projection.ts +2 -4
- package/src/history/search-queries.ts +9 -2
- package/src/history/store.ts +21 -5
- package/src/history/text-chunks.ts +7 -2
- package/src/history/transcript-queries.ts +16 -10
- package/src/intercom/result-intercom.ts +0 -103
- package/src/pi-intercom/bridge-config.ts +46 -36
- package/src/pi-intercom/bridge-files.ts +59 -0
- package/src/pi-intercom/bridge-mailbox.ts +395 -0
- package/src/pi-intercom/bridge-presence.ts +128 -0
- package/src/pi-intercom/bridge-routes.ts +41 -4
- package/src/pi-intercom/bridge-session.ts +73 -71
- package/src/pi-intercom/bridge-wake.ts +188 -0
- package/src/pi-intercom/bridge.ts +58 -7
- package/src/pi-intercom/broker/broker.ts +34 -20
- package/src/pi-intercom/broker/framing.ts +71 -20
- package/src/pi-intercom/broker/snapshot-reader.ts +47 -9
- package/src/pi-intercom/broker/snapshots.ts +184 -51
- package/src/pi-intercom/session-targets.ts +0 -2
- package/src/runs/background/async-run-discovery.ts +4 -10
- package/src/runs/background/completion-dedupe.ts +6 -69
- package/src/runs/background/notify.ts +2 -12
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/runner-child-observer.ts +19 -11
- package/src/runs/background/runner-monitor.ts +2 -0
- package/src/runs/background/runner-parallel.ts +2 -3
- package/src/runs/shared/child-observation-spool.ts +8 -3
- package/src/runs/shared/run-restoration.ts +4 -4
- package/src/runs/shared/supervisor-questions.ts +10 -10
- package/src/shared/journal-frames.ts +1 -1
- package/src/slash/command-discovery.ts +13 -12
- package/src/tui/agent-history.ts +1 -18
- package/src/tui/agent-view.ts +0 -1
- package/src/tui/conversation-history.ts +15 -1
- package/src/tui/history-cards.ts +8 -26
|
@@ -2,29 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { isRecord, hasText } from "./async-value.ts";
|
|
3
3
|
|
|
4
4
|
interface CompletionDataLike {
|
|
5
|
-
completionId?:
|
|
6
|
-
id
|
|
7
|
-
agent?: unknown;
|
|
8
|
-
timestamp?: unknown;
|
|
9
|
-
sessionId?: unknown;
|
|
10
|
-
taskIndex?: unknown;
|
|
11
|
-
totalTasks?: unknown;
|
|
12
|
-
success?: unknown;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function asNonEmptyString(value: unknown): string | undefined {
|
|
16
|
-
if (typeof value !== "string") {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
const trimmed = value.trim();
|
|
20
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function asFiniteNumber(value: unknown): number | undefined {
|
|
24
|
-
if (typeof value !== "number") {
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
return Number.isFinite(value) ? value : undefined;
|
|
5
|
+
completionId?: string;
|
|
6
|
+
id: string;
|
|
28
7
|
}
|
|
29
8
|
|
|
30
9
|
function stableStringify(value: unknown): string {
|
|
@@ -48,32 +27,11 @@ function payloadDigest(value: unknown): string {
|
|
|
48
27
|
return createHash("sha256").update(stableStringify(value)).digest("hex").slice(0, 16);
|
|
49
28
|
}
|
|
50
29
|
|
|
51
|
-
export function buildCompletionKey(data: Readonly<CompletionDataLike
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return `completion:${completionId}`;
|
|
55
|
-
}
|
|
56
|
-
const id = asNonEmptyString(data.id);
|
|
57
|
-
if (hasText(id)) {
|
|
58
|
-
return `id:${id}:${payloadDigest(data)}`;
|
|
30
|
+
export function buildCompletionKey(data: Readonly<CompletionDataLike>): string {
|
|
31
|
+
if (hasText(data.completionId)) {
|
|
32
|
+
return `completion:${data.completionId.trim()}`;
|
|
59
33
|
}
|
|
60
|
-
|
|
61
|
-
const agent = asNonEmptyString(data.agent) ?? "unknown";
|
|
62
|
-
const timestamp = asFiniteNumber(data.timestamp);
|
|
63
|
-
const taskIndex = asFiniteNumber(data.taskIndex);
|
|
64
|
-
const totalTasks = asFiniteNumber(data.totalTasks);
|
|
65
|
-
const success = data.success === true ? "1" : "?";
|
|
66
|
-
const successIdentity = data.success === false ? "0" : success;
|
|
67
|
-
return [
|
|
68
|
-
"meta",
|
|
69
|
-
sessionId,
|
|
70
|
-
agent,
|
|
71
|
-
timestamp !== undefined ? String(timestamp) : "no-ts",
|
|
72
|
-
taskIndex !== undefined ? String(taskIndex) : "-",
|
|
73
|
-
totalTasks !== undefined ? String(totalTasks) : "-",
|
|
74
|
-
successIdentity,
|
|
75
|
-
fallback,
|
|
76
|
-
].join(":");
|
|
34
|
+
return `id:${data.id.trim()}:${payloadDigest(data)}`;
|
|
77
35
|
}
|
|
78
36
|
|
|
79
37
|
/** Mutable identity store owned by the completion-admission lifecycle. */
|
|
@@ -105,24 +63,3 @@ export function markSeenWithTtl(
|
|
|
105
63
|
seen.set(key, now);
|
|
106
64
|
return false;
|
|
107
65
|
}
|
|
108
|
-
|
|
109
|
-
function isSeenMap(value: unknown): value is Map<string, number> {
|
|
110
|
-
if (!(value instanceof Map)) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
const entries: Map<unknown, unknown> = value;
|
|
114
|
-
return [...entries].every(
|
|
115
|
-
([key, timestamp]) => typeof key === "string" && typeof timestamp === "number",
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function getGlobalSeenMap(storeKey: string): Map<string, number> {
|
|
120
|
-
const globalStore = globalThis as Record<string, unknown>;
|
|
121
|
-
const existing = globalStore[storeKey];
|
|
122
|
-
if (isSeenMap(existing)) {
|
|
123
|
-
return existing;
|
|
124
|
-
}
|
|
125
|
-
const map = new Map<string, number>();
|
|
126
|
-
globalStore[storeKey] = map;
|
|
127
|
-
return map;
|
|
128
|
-
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Subagent completion notifications. */
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
4
3
|
import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
|
|
5
4
|
import { hasText, isRecord } from "./async-value.ts";
|
|
6
5
|
import type { ReadonlyInput } from "../../shared/types/inputs.ts";
|
|
@@ -144,7 +143,7 @@ function isUnsubscribe(value: unknown): value is () => void {
|
|
|
144
143
|
|
|
145
144
|
export default function registerSubagentNotify(
|
|
146
145
|
pi: ReadonlyInput<ExtensionAPI>,
|
|
147
|
-
onQueued
|
|
146
|
+
onQueued: (completionKey: string, runId?: string) => void,
|
|
148
147
|
): () => void {
|
|
149
148
|
const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
|
|
150
149
|
const globalStore = globalThis as Record<string, unknown>;
|
|
@@ -156,7 +155,6 @@ export default function registerSubagentNotify(
|
|
|
156
155
|
// Best effort cleanup for stale handlers from an older reload.
|
|
157
156
|
}
|
|
158
157
|
}
|
|
159
|
-
const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
|
|
160
158
|
const handleComplete = (data: unknown): void => {
|
|
161
159
|
if (
|
|
162
160
|
!isResult(data) ||
|
|
@@ -165,17 +163,9 @@ export default function registerSubagentNotify(
|
|
|
165
163
|
) {
|
|
166
164
|
return;
|
|
167
165
|
}
|
|
168
|
-
const key = buildCompletionKey(data, "notify");
|
|
169
|
-
// Owned completion keys are governed by the shared queue/journal lifecycle, not a TTL.
|
|
170
|
-
if (
|
|
171
|
-
(onQueued === undefined || !hasText(data.completionKey)) &&
|
|
172
|
-
markSeenWithTtl(seen, key, Date.now(), 10 * 60 * 1000)
|
|
173
|
-
) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
166
|
pi.sendMessage(buildNotification(data), { triggerTurn: true });
|
|
177
167
|
if (hasText(data.completionKey)) {
|
|
178
|
-
onQueued
|
|
168
|
+
onQueued(data.completionKey, data.runId);
|
|
179
169
|
}
|
|
180
170
|
};
|
|
181
171
|
const unsubscribe = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
|
|
@@ -292,7 +292,7 @@ class ResultWatcher {
|
|
|
292
292
|
if (this.foreign(envelope)) {
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
|
-
const key = buildCompletionKey({ ...envelope.data, id: envelope.runId }
|
|
295
|
+
const key = buildCompletionKey({ ...envelope.data, id: envelope.runId });
|
|
296
296
|
if (this.alreadyHandled(envelope, key)) {
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
@@ -59,6 +59,7 @@ export class RunnerChildObserver {
|
|
|
59
59
|
this.id = config.id;
|
|
60
60
|
this.hooks = hooks;
|
|
61
61
|
this.mutatingFailureStates = statusPayload.steps.map(() => createMutatingFailureState());
|
|
62
|
+
this.syncTopLevelCurrentTool();
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
expandChildren(flatIndex: number, count: number): void {
|
|
@@ -69,16 +70,21 @@ export class RunnerChildObserver {
|
|
|
69
70
|
);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
const active =
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
syncTopLevelCurrentTool(): void {
|
|
74
|
+
const active =
|
|
75
|
+
this.statusPayload.state === "running" || this.statusPayload.state === "queued"
|
|
76
|
+
? this.statusPayload.steps
|
|
77
|
+
.filter(
|
|
78
|
+
(step) =>
|
|
79
|
+
step.status === "running" &&
|
|
80
|
+
typeof step.currentTool === "string" &&
|
|
81
|
+
step.currentTool.length > 0,
|
|
82
|
+
)
|
|
83
|
+
.sort(
|
|
84
|
+
(left, right) => (right.currentToolStartedAt ?? 0) - (left.currentToolStartedAt ?? 0),
|
|
85
|
+
)
|
|
86
|
+
.at(0)
|
|
87
|
+
: undefined;
|
|
82
88
|
this.statusPayload.currentTool = active?.currentTool;
|
|
83
89
|
this.statusPayload.currentToolStartedAt = active?.currentToolStartedAt;
|
|
84
90
|
this.statusPayload.currentPath = active?.currentPath;
|
|
@@ -106,7 +112,9 @@ export class RunnerChildObserver {
|
|
|
106
112
|
}
|
|
107
113
|
step.streamingText = updateStreamingText(step.streamingText, event);
|
|
108
114
|
this.recordChildActivity(flatIndex, event, toolSnapshot, now);
|
|
109
|
-
|
|
115
|
+
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
116
|
+
this.syncTopLevelCurrentTool();
|
|
117
|
+
}
|
|
110
118
|
step.lastActivityAt = now;
|
|
111
119
|
this.statusPayload.lastActivityAt = now;
|
|
112
120
|
this.statusPayload.lastUpdate = now;
|
|
@@ -166,6 +166,8 @@ export class RunnerMonitor {
|
|
|
166
166
|
clearTimeout(this.statusWriteTimer);
|
|
167
167
|
this.statusWriteTimer = undefined;
|
|
168
168
|
}
|
|
169
|
+
// Step completion, interruption and expansion can change activity without a tool-end event.
|
|
170
|
+
this.childObserver.syncTopLevelCurrentTool();
|
|
169
171
|
this.refreshWorkflowGraph();
|
|
170
172
|
writeAtomicJson(this.statusPath, this.statusPayload);
|
|
171
173
|
if (this.config.runtimeVersion !== 2) {
|
|
@@ -2,7 +2,6 @@ import * as path from "node:path";
|
|
|
2
2
|
import { inspect } from "node:util";
|
|
3
3
|
import type { ReadonlyDeep } from "type-fest";
|
|
4
4
|
import { appendJsonl } from "../../shared/artifacts.ts";
|
|
5
|
-
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
6
5
|
import { writeInitialProgressFile } from "../../shared/settings.ts";
|
|
7
6
|
import { completeWorkflowStep, runParallelTasks } from "../shared/workflow-policy.ts";
|
|
8
7
|
import {
|
|
@@ -194,7 +193,7 @@ export class RunnerParallelGroup {
|
|
|
194
193
|
status.currentStep = position.flatIndex;
|
|
195
194
|
status.lastUpdate = failedAt;
|
|
196
195
|
status.outputFile = path.join(this.config.asyncDir, `output-${position.flatIndex}.log`);
|
|
197
|
-
|
|
196
|
+
this.monitor.writeStatusPayload();
|
|
198
197
|
this.append({
|
|
199
198
|
type: "subagent.parallel.completed",
|
|
200
199
|
ts: failedAt,
|
|
@@ -224,7 +223,7 @@ export class RunnerParallelGroup {
|
|
|
224
223
|
status.lastActivityAt = startedAt;
|
|
225
224
|
status.lastUpdate = startedAt;
|
|
226
225
|
status.outputFile = path.join(this.config.asyncDir, `output-${position.flatIndex}.log`);
|
|
227
|
-
|
|
226
|
+
this.monitor.writeStatusPayload();
|
|
228
227
|
this.append({
|
|
229
228
|
type: "subagent.parallel.started",
|
|
230
229
|
ts: startedAt,
|
|
@@ -51,8 +51,11 @@ export class ChildObservationSpool {
|
|
|
51
51
|
fs.writeFileSync(this.stderr, text);
|
|
52
52
|
}
|
|
53
53
|
resetText(): void {
|
|
54
|
-
this.textOffset
|
|
54
|
+
if (this.textOffset === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
55
57
|
fs.ftruncateSync(this.text, 0);
|
|
58
|
+
this.textOffset = 0;
|
|
56
59
|
}
|
|
57
60
|
appendText(text: string): void {
|
|
58
61
|
const bytes = Buffer.from(text);
|
|
@@ -72,14 +75,16 @@ export class ChildObservationSpool {
|
|
|
72
75
|
this.textOffset += bytes.length;
|
|
73
76
|
}
|
|
74
77
|
replayText(consume: (text: string) => void): void {
|
|
75
|
-
|
|
78
|
+
if (this.textOffset > 0) {
|
|
79
|
+
this.decode(this.text, 0, this.textOffset, consume);
|
|
80
|
+
}
|
|
76
81
|
}
|
|
77
82
|
replayWire(start: number, end: number, consume: (text: string) => void): void {
|
|
78
83
|
this.decode(this.wire, start, end, consume);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
private copy(fd: number, start: number, end: number, consume: (bytes: Buffer) => void): void {
|
|
82
|
-
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
87
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, end - start));
|
|
83
88
|
for (let position = start; position < end;) {
|
|
84
89
|
const count = fs.readSync(fd, buffer, 0, Math.min(buffer.length, end - position), position);
|
|
85
90
|
if (count === 0) {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
getRunMetadataDir,
|
|
24
24
|
migrateSupervisorQuestionSteps,
|
|
25
|
+
readQuestionOwner,
|
|
25
26
|
readRunJson,
|
|
26
27
|
saveQuestionOwner,
|
|
27
28
|
saveRunStatus,
|
|
@@ -254,14 +255,13 @@ function nativeLaunchCwd(run: OwnedRun, details: ReadonlyDetails): OwnedRun {
|
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
function repairQuestionOwner(run: OwnedRun): void {
|
|
257
|
-
let
|
|
258
|
+
let sessionId: string | undefined;
|
|
258
259
|
try {
|
|
259
|
-
|
|
260
|
+
sessionId = readQuestionOwner(run.runId)?.sessionId;
|
|
260
261
|
} catch {
|
|
261
262
|
// A genuine receipt can repair unusable supplemental metadata.
|
|
262
263
|
}
|
|
263
|
-
|
|
264
|
-
if (typeof sessionId !== "string" || sessionId.trim() === "") {
|
|
264
|
+
if (sessionId === undefined || sessionId.trim() === "") {
|
|
265
265
|
saveQuestionOwner(run.runId, run.ownerSessionId);
|
|
266
266
|
}
|
|
267
267
|
}
|
|
@@ -216,7 +216,10 @@ export function createSupervisorQuestion(
|
|
|
216
216
|
return question;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
export function listRunQuestions(
|
|
219
|
+
export function listRunQuestions(
|
|
220
|
+
runDir: string,
|
|
221
|
+
ownerSessionId?: string,
|
|
222
|
+
): SupervisorQuestionView[] {
|
|
220
223
|
let entries: fs.Dirent[];
|
|
221
224
|
try {
|
|
222
225
|
entries = fs.readdirSync(path.join(runDir, "questions"), { withFileTypes: true });
|
|
@@ -233,7 +236,10 @@ export function listRunQuestions(runDir: string): SupervisorQuestionView[] {
|
|
|
233
236
|
path.join(runDir, "questions", entry.name, "question.json"),
|
|
234
237
|
parseSupervisorQuestion,
|
|
235
238
|
);
|
|
236
|
-
return question
|
|
239
|
+
return question &&
|
|
240
|
+
(ownerSessionId === undefined || question.ownerSessionId === ownerSessionId)
|
|
241
|
+
? [readQuestionState(question, path.dirname(runDir))]
|
|
242
|
+
: [];
|
|
237
243
|
});
|
|
238
244
|
}
|
|
239
245
|
|
|
@@ -243,9 +249,7 @@ export function listOwnedRunQuestions(
|
|
|
243
249
|
runId: string,
|
|
244
250
|
): SupervisorQuestionView[] {
|
|
245
251
|
migrateSupervisorQuestions(ownerSessionId, runId);
|
|
246
|
-
return listRunQuestions(getRunMetadataDir(runId))
|
|
247
|
-
(question) => question.ownerSessionId === ownerSessionId,
|
|
248
|
-
);
|
|
252
|
+
return listRunQuestions(getRunMetadataDir(runId), ownerSessionId);
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
export function pendingSupervisorQuestion(input: {
|
|
@@ -321,11 +325,7 @@ function* supervisorQuestionSteps(
|
|
|
321
325
|
for (const entry of runs) {
|
|
322
326
|
yield;
|
|
323
327
|
try {
|
|
324
|
-
questions.push(
|
|
325
|
-
...listRunQuestions(path.join(root, entry.name)).filter(
|
|
326
|
-
(question) => question.ownerSessionId === ownerSessionId,
|
|
327
|
-
),
|
|
328
|
-
);
|
|
328
|
+
questions.push(...listRunQuestions(path.join(root, entry.name), ownerSessionId));
|
|
329
329
|
} catch (error) {
|
|
330
330
|
if (runId !== undefined) {
|
|
331
331
|
throw error;
|
|
@@ -172,7 +172,7 @@ export function readJsonProjection(file: string, select: Projection): UnknownRec
|
|
|
172
172
|
const projection = new JsonProjection(select, undefined, () => Infinity);
|
|
173
173
|
try {
|
|
174
174
|
const end = fs.fstatSync(fd).size;
|
|
175
|
-
const bytes = Buffer.allocUnsafe(64 * 1024);
|
|
175
|
+
const bytes = Buffer.allocUnsafe(Math.min(64 * 1024, end));
|
|
176
176
|
for (let offset = 0; offset < end;) {
|
|
177
177
|
const count = fs.readSync(fd, bytes, 0, Math.min(bytes.length, end - offset), offset);
|
|
178
178
|
if (count === 0) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { discoverAgents,
|
|
2
|
+
import { discoverAgents, discoverChains } from "../agents/agents.ts";
|
|
3
3
|
import type { ChainConfig } from "../shared/types/config.ts";
|
|
4
4
|
import { resolveExecutionCwd } from "../shared/execution-cwd.ts";
|
|
5
5
|
|
|
@@ -51,21 +51,19 @@ export function makeAgentCompletions(
|
|
|
51
51
|
multiAgent: boolean,
|
|
52
52
|
): (prefix: string) => Completion[] | null {
|
|
53
53
|
return (prefix) => {
|
|
54
|
+
const position = multiAgent ? completionPosition(prefix) : { word: prefix, before: "" };
|
|
55
|
+
if (!position || (!multiAgent && prefix.includes(" "))) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
54
58
|
const cwd = completionCwd(pi, state);
|
|
55
59
|
if (cwd === null || cwd.length === 0) {
|
|
56
60
|
return null;
|
|
57
61
|
}
|
|
58
62
|
const agents = discoverAgents(cwd, "both", { projectTrusted: projectTrusted(state) }).agents;
|
|
59
63
|
if (!multiAgent) {
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
:
|
|
63
|
-
.filter((agent) => agent.name.startsWith(prefix))
|
|
64
|
-
.map((agent) => ({ value: agent.name, label: agent.name }));
|
|
65
|
-
}
|
|
66
|
-
const position = completionPosition(prefix);
|
|
67
|
-
if (!position) {
|
|
68
|
-
return null;
|
|
64
|
+
return agents
|
|
65
|
+
.filter((agent) => agent.name.startsWith(prefix))
|
|
66
|
+
.map((agent) => ({ value: agent.name, label: agent.name }));
|
|
69
67
|
}
|
|
70
68
|
const { word: lastWord, before: beforeLastWord } = position;
|
|
71
69
|
if (lastWord === "->") {
|
|
@@ -79,7 +77,7 @@ export function makeAgentCompletions(
|
|
|
79
77
|
|
|
80
78
|
export function discoverSavedChains(cwd: string, trusted: boolean): ChainConfig[] {
|
|
81
79
|
const chains = new Map<string, ChainConfig>();
|
|
82
|
-
for (const chain of
|
|
80
|
+
for (const chain of discoverChains(cwd, { projectTrusted: trusted }).chains) {
|
|
83
81
|
const existing = chains.get(chain.name);
|
|
84
82
|
const projectOverride = existing?.source === "user" && chain.source === "project";
|
|
85
83
|
const jsonOverride =
|
|
@@ -98,8 +96,11 @@ export function makeChainCompletions(
|
|
|
98
96
|
state: SlashDiscoveryState,
|
|
99
97
|
): (prefix: string) => Completion[] | null {
|
|
100
98
|
return (prefix) => {
|
|
99
|
+
if (prefix.includes(" ")) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
101
102
|
const cwd = completionCwd(pi, state);
|
|
102
|
-
if (
|
|
103
|
+
if (cwd === null || cwd.length === 0) {
|
|
103
104
|
return null;
|
|
104
105
|
}
|
|
105
106
|
return discoverSavedChains(cwd, projectTrusted(state))
|
package/src/tui/agent-history.ts
CHANGED
|
@@ -1,27 +1,10 @@
|
|
|
1
|
-
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { stripAcceptanceReport } from "../runs/shared/acceptance-reports.ts";
|
|
3
|
-
import {
|
|
4
|
-
import type { DisplayEntry } from "./history-record.ts";
|
|
2
|
+
import type { AgentHistory, AgentHistoryItem } from "./history-display.ts";
|
|
5
3
|
import { readableText } from "./history-text.ts";
|
|
6
4
|
export { readableText } from "./history-text.ts";
|
|
7
5
|
export { indexedHistory } from "./indexed-history.ts";
|
|
8
6
|
export type { AgentHistory, AgentHistoryItem } from "./history-display.ts";
|
|
9
7
|
|
|
10
|
-
/** Read native entries, not a compacted model context: earlier messages remain inspectable. */
|
|
11
|
-
export function historyItems(entries: readonly SessionEntry[]): AgentHistory {
|
|
12
|
-
const visible: DisplayEntry[] = [];
|
|
13
|
-
for (const entry of entries) {
|
|
14
|
-
if (
|
|
15
|
-
entry.type === "message" ||
|
|
16
|
-
entry.type === "custom_message" ||
|
|
17
|
-
entry.type === "compaction" ||
|
|
18
|
-
entry.type === "branch_summary"
|
|
19
|
-
) {
|
|
20
|
-
visible.push(entry.type === "message" ? { ...entry, native: entry.message } : entry);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return displayHistory(visible);
|
|
24
|
-
}
|
|
25
8
|
function finalMatch(history: AgentHistory, text: string): string | undefined {
|
|
26
9
|
if (history.finalId !== undefined) {
|
|
27
10
|
return history.finalId;
|
package/src/tui/agent-view.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errorText } from "./text-values.ts";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import {
|
|
3
4
|
historyStatus,
|
|
4
5
|
pageNotices,
|
|
@@ -183,9 +184,22 @@ export class ConversationHistory {
|
|
|
183
184
|
? { ...item, load: () => this.controller.savedResult(this.key, item.id) }
|
|
184
185
|
: item,
|
|
185
186
|
);
|
|
187
|
+
const previous = task.page,
|
|
188
|
+
sameSource =
|
|
189
|
+
previous !== undefined &&
|
|
190
|
+
previous.sourceId === result.page.sourceId &&
|
|
191
|
+
previous.generation === result.page.generation &&
|
|
192
|
+
previous.sessionId === result.page.sessionId &&
|
|
193
|
+
previous.sourceState === result.page.sourceState &&
|
|
194
|
+
isDeepStrictEqual(
|
|
195
|
+
previous.entries.map((entry) => entry.ref),
|
|
196
|
+
result.page.entries.map((entry) => entry.ref),
|
|
197
|
+
);
|
|
186
198
|
this.seenSource ||= result.page.count > 0;
|
|
187
199
|
this.controller.applyHistoryPage(this.key, { ...history, items }, result.page, this.seenSource);
|
|
188
|
-
|
|
200
|
+
if (!sameSource) {
|
|
201
|
+
this.source.clearCards();
|
|
202
|
+
}
|
|
189
203
|
}
|
|
190
204
|
|
|
191
205
|
assignment(): AgentHistoryItem {
|
package/src/tui/history-cards.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import {
|
|
3
4
|
AssistantMessageComponent,
|
|
4
5
|
UserMessageComponent,
|
|
@@ -47,29 +48,9 @@ interface CardLine {
|
|
|
47
48
|
readonly contentStart: number;
|
|
48
49
|
readonly end: number;
|
|
49
50
|
}
|
|
50
|
-
function plainItem(item: AgentHistoryItem): boolean {
|
|
51
|
-
return (
|
|
52
|
-
item.load === undefined &&
|
|
53
|
-
item.assistant === undefined &&
|
|
54
|
-
item.previewAssistant === undefined &&
|
|
55
|
-
item.call === undefined &&
|
|
56
|
-
item.result === undefined
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
function plainSignature(item: AgentHistoryItem): string {
|
|
60
|
-
return JSON.stringify([
|
|
61
|
-
item.kind,
|
|
62
|
-
item.title,
|
|
63
|
-
item.text,
|
|
64
|
-
item.details,
|
|
65
|
-
item.diff,
|
|
66
|
-
item.model,
|
|
67
|
-
item.timestamp,
|
|
68
|
-
item.messageId,
|
|
69
|
-
]);
|
|
70
|
-
}
|
|
71
51
|
function sameDisplay(left: AgentHistoryItem, right: AgentHistoryItem): boolean {
|
|
72
|
-
|
|
52
|
+
// Detail loaders stay fresh in contentItems; only the native display component is reused.
|
|
53
|
+
return isDeepStrictEqual({ ...left, load: undefined }, { ...right, load: undefined });
|
|
73
54
|
}
|
|
74
55
|
function previewStop(reason: string | undefined): AssistantMessage["stopReason"] {
|
|
75
56
|
if (reason === "error" || reason === "aborted" || reason === "length") {
|
|
@@ -191,10 +172,11 @@ export class HistoryCards {
|
|
|
191
172
|
private card(item: AgentHistoryItem, expanded: boolean, detail: boolean): Component {
|
|
192
173
|
const signature = `${expanded}:${detail}`,
|
|
193
174
|
cached = this.components.get(item.id);
|
|
194
|
-
if (
|
|
195
|
-
cached
|
|
196
|
-
|
|
197
|
-
) {
|
|
175
|
+
if (cached?.signature === signature && cached.item === item) {
|
|
176
|
+
return cached.component;
|
|
177
|
+
}
|
|
178
|
+
if (cached?.signature === signature && sameDisplay(cached.item, item)) {
|
|
179
|
+
this.components.set(item.id, { ...cached, item });
|
|
198
180
|
return cached.component;
|
|
199
181
|
}
|
|
200
182
|
const component = this.create(item, { expanded, detail });
|