pi-sessions 0.8.0 → 0.10.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 +74 -37
- package/dist/session-messaging/broker/process.js +234 -0
- package/dist/shared/session-broker/framing.js +58 -0
- package/dist/shared/session-broker/protocol.js +163 -0
- package/dist/shared/session-broker/socket-path.js +19 -0
- package/dist/shared/typebox.js +36 -0
- package/extensions/pi-sessions.ts +125 -0
- package/extensions/session-ask/agent.ts +34 -14
- package/extensions/{session-ask.ts → session-ask/install.ts} +73 -90
- package/extensions/session-ask/navigate.ts +3 -1
- package/extensions/session-ask/presenter.ts +23 -0
- package/extensions/session-ask/renderer.ts +63 -0
- package/extensions/session-ask/tool-contract.ts +27 -0
- package/extensions/session-ask/view-model.ts +18 -0
- package/extensions/session-auto-title/generate.ts +18 -18
- package/extensions/{session-auto-title.ts → session-auto-title/install.ts} +60 -53
- package/extensions/session-auto-title/model.ts +25 -17
- package/extensions/session-auto-title/retitle.ts +17 -18
- package/extensions/session-auto-title/wizard.ts +12 -11
- package/extensions/session-handoff/board-view-model.ts +179 -0
- package/extensions/session-handoff/board.ts +663 -0
- package/extensions/session-handoff/bootstrap.ts +172 -0
- package/extensions/session-handoff/extract.ts +129 -132
- package/extensions/session-handoff/install.ts +225 -0
- package/extensions/session-handoff/kickoff.ts +92 -0
- package/extensions/session-handoff/launch/backend.ts +16 -0
- package/extensions/session-handoff/launch/deferred.ts +20 -0
- package/extensions/session-handoff/launch/ghostty.ts +99 -0
- package/extensions/session-handoff/launch/resolution.ts +57 -0
- package/extensions/session-handoff/launch/shell.ts +7 -0
- package/extensions/session-handoff/launch/tmux.ts +31 -0
- package/extensions/session-handoff/launch-options.ts +61 -0
- package/extensions/session-handoff/launch-target.ts +91 -0
- package/extensions/session-handoff/metadata.ts +194 -56
- package/extensions/session-handoff/model.ts +58 -0
- package/extensions/session-handoff/query.ts +0 -1
- package/extensions/session-handoff/receipt.ts +96 -0
- package/extensions/session-handoff/review.ts +2 -28
- package/extensions/session-handoff/spawn.ts +76 -181
- package/extensions/session-handoff/strong-modal.ts +13 -0
- package/extensions/session-handoff/tool-contract.ts +12 -0
- package/extensions/session-handoff/tool-presenter.ts +35 -0
- package/extensions/session-handoff/tool-renderer.ts +66 -0
- package/extensions/session-handoff/tool-schema.ts +54 -0
- package/extensions/session-handoff/tool-view-model.ts +49 -0
- package/extensions/session-handoff/tool.ts +206 -0
- package/extensions/session-handoff/ui.ts +66 -0
- package/extensions/session-hooks/install.ts +75 -0
- package/extensions/{session-index.ts → session-index/install.ts} +9 -7
- package/extensions/session-messaging/broker/process.ts +21 -28
- package/extensions/session-messaging/broker/spawn.ts +12 -7
- package/extensions/session-messaging/install.ts +78 -0
- package/extensions/session-messaging/pi/cancel-session-presenter.ts +64 -0
- package/extensions/session-messaging/pi/incoming-message-presenter.ts +32 -0
- package/extensions/session-messaging/pi/incoming-message-view-model.ts +20 -0
- package/extensions/session-messaging/pi/incoming-runtime.ts +27 -23
- package/extensions/session-messaging/pi/message-contracts.ts +32 -0
- package/extensions/session-messaging/pi/renderer.ts +19 -12
- package/extensions/session-messaging/pi/send-message-presenter.ts +39 -0
- package/extensions/session-messaging/pi/send-message-view-model.ts +54 -0
- package/extensions/session-messaging/pi/service.ts +262 -81
- package/extensions/session-messaging/pi/tools.ts +184 -40
- package/extensions/session-search/extract.ts +60 -14
- package/extensions/session-search/hooks.ts +10 -13
- package/extensions/session-search/install.ts +373 -0
- package/extensions/session-search/renderer.ts +195 -0
- package/extensions/session-search/tool-contract.ts +51 -0
- package/extensions/shared/composition.ts +20 -0
- package/extensions/shared/errors.ts +3 -0
- package/extensions/shared/model-resolution.ts +73 -0
- package/extensions/shared/model-runtime.ts +33 -0
- package/extensions/shared/model.ts +7 -10
- package/extensions/shared/rendering/collapsible-text.ts +89 -0
- package/extensions/shared/rendering/expandable-content-layout.ts +84 -0
- package/extensions/shared/rendering/theme.ts +5 -0
- package/extensions/shared/session-broker/client.ts +30 -29
- package/extensions/shared/session-broker/protocol.ts +128 -10
- package/extensions/shared/session-index/common.ts +5 -5
- package/extensions/shared/session-index/lineage.ts +0 -3
- package/extensions/shared/session-index/schema.ts +0 -1
- package/extensions/shared/session-index/scoring.ts +1 -2
- package/extensions/shared/session-index/search.ts +11 -4
- package/extensions/shared/session-index/store.ts +4 -9
- package/extensions/shared/settings.ts +83 -40
- package/extensions/shared/text.ts +5 -0
- package/extensions/shared/thinking-levels.ts +22 -0
- package/extensions/shared/tmux.ts +205 -0
- package/extensions/subagents/cancel.ts +111 -0
- package/extensions/subagents/classify.ts +58 -0
- package/extensions/subagents/install.ts +229 -0
- package/extensions/subagents/launch-target.ts +75 -0
- package/extensions/subagents/ledger.ts +217 -0
- package/extensions/subagents/reconcile.ts +386 -0
- package/extensions/subagents/report-message-presenter.ts +36 -0
- package/extensions/subagents/report-message-renderer.ts +21 -0
- package/extensions/subagents/report-message-view-model.ts +29 -0
- package/extensions/subagents/report.ts +159 -0
- package/extensions/subagents/roster.ts +262 -0
- package/extensions/subagents/settle.ts +182 -0
- package/extensions/subagents/wake.ts +173 -0
- package/images/handoff-board-subagents.png +0 -0
- package/images/handoff-board-user-sessions.png +0 -0
- package/images/handoff-subagent-report.png +0 -0
- package/images/session-handoff-tool.png +0 -0
- package/package.json +16 -18
- package/extensions/session-handoff.ts +0 -741
- package/extensions/session-hooks.ts +0 -75
- package/extensions/session-messaging/pi/message-view.ts +0 -131
- package/extensions/session-messaging.ts +0 -30
- package/extensions/session-search.ts +0 -419
- package/extensions/shared/session-broker/active.ts +0 -26
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type ModelRuntime,
|
|
4
|
+
SessionManager,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { installAsk } from "./session-ask/install.ts";
|
|
7
|
+
import { installAutoTitle } from "./session-auto-title/install.ts";
|
|
8
|
+
import { installHandoff } from "./session-handoff/install.ts";
|
|
9
|
+
import { installHooks } from "./session-hooks/install.ts";
|
|
10
|
+
import { installIndex } from "./session-index/install.ts";
|
|
11
|
+
import { installMessaging } from "./session-messaging/install.ts";
|
|
12
|
+
import {
|
|
13
|
+
createSessionCancelTool,
|
|
14
|
+
createSessionSendMessageTool,
|
|
15
|
+
} from "./session-messaging/pi/tools.ts";
|
|
16
|
+
import { installSearch } from "./session-search/install.ts";
|
|
17
|
+
import type { SessionLifecycle } from "./shared/composition.ts";
|
|
18
|
+
import { createSessionModelRuntime, type ModelRuntimeProvider } from "./shared/model-runtime.ts";
|
|
19
|
+
import { loadSettings } from "./shared/settings.ts";
|
|
20
|
+
import { installSubagents } from "./subagents/install.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The single advertised entrypoint. It loads settings once, constructs each feature in
|
|
24
|
+
* dependency order, and wires them by constructor parameters.
|
|
25
|
+
* It also owns the single `session_start`/`session_shutdown` subscription so lifecycle order
|
|
26
|
+
* is deterministic: broker registration (messaging) resolves before any other feature hook.
|
|
27
|
+
*/
|
|
28
|
+
export default function piSessions(pi: ExtensionAPI): void {
|
|
29
|
+
const settings = loadSettings();
|
|
30
|
+
let sessionEpoch = 0;
|
|
31
|
+
|
|
32
|
+
// The mirrored ModelRuntime is expensive to build and only changes across session
|
|
33
|
+
// boundaries, so cache it per epoch (the root's own invalidation signal) instead of
|
|
34
|
+
// rebuilding it on every auto-title, /title, session_ask, and handoff draft.
|
|
35
|
+
let cachedModelRuntime: { epoch: number; runtime: Promise<ModelRuntime> } | undefined;
|
|
36
|
+
const getModelRuntime: ModelRuntimeProvider = (modelRegistry) => {
|
|
37
|
+
if (cachedModelRuntime?.epoch !== sessionEpoch) {
|
|
38
|
+
cachedModelRuntime = {
|
|
39
|
+
epoch: sessionEpoch,
|
|
40
|
+
runtime: createSessionModelRuntime(modelRegistry),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return cachedModelRuntime.runtime;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const index = installIndex(pi, { settings });
|
|
47
|
+
const messaging = settings.features.messaging
|
|
48
|
+
? installMessaging(pi, { settings, index })
|
|
49
|
+
: undefined;
|
|
50
|
+
|
|
51
|
+
const lifecycles: SessionLifecycle[] = [];
|
|
52
|
+
if (messaging) {
|
|
53
|
+
lifecycles.push(messaging);
|
|
54
|
+
}
|
|
55
|
+
const subagents =
|
|
56
|
+
settings.features.subagents && messaging
|
|
57
|
+
? installSubagents(pi, { settings, messaging })
|
|
58
|
+
: undefined;
|
|
59
|
+
if (subagents) {
|
|
60
|
+
lifecycles.push(subagents);
|
|
61
|
+
}
|
|
62
|
+
if (messaging) {
|
|
63
|
+
pi.registerTool(
|
|
64
|
+
createSessionSendMessageTool(subagents ?? messaging, {
|
|
65
|
+
wakeCapable: Boolean(subagents),
|
|
66
|
+
getCachedRelationTo: messaging.getCachedRelationTo,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
pi.registerTool(createSessionCancelTool(subagents ?? messaging));
|
|
70
|
+
}
|
|
71
|
+
if (settings.features.handoff) {
|
|
72
|
+
const board = {
|
|
73
|
+
roster: subagents?.roster,
|
|
74
|
+
cancelSubagent: subagents
|
|
75
|
+
? (sessionId: string) => subagents.cancelSession(sessionId)
|
|
76
|
+
: undefined,
|
|
77
|
+
listLiveSessions: messaging ? () => messaging.listSessions() : undefined,
|
|
78
|
+
readSessionEntries: (sessionFile: string) => SessionManager.open(sessionFile).getEntries(),
|
|
79
|
+
};
|
|
80
|
+
lifecycles.push(
|
|
81
|
+
installHandoff(pi, {
|
|
82
|
+
settings,
|
|
83
|
+
index,
|
|
84
|
+
getModelRuntime,
|
|
85
|
+
...(subagents ? { getLaunchTargets: () => subagents.getLaunchTargets() } : {}),
|
|
86
|
+
board,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (settings.features.search) {
|
|
91
|
+
installSearch(pi, { settings, index, messaging, roster: subagents?.roster });
|
|
92
|
+
}
|
|
93
|
+
if (settings.features.ask) {
|
|
94
|
+
installAsk(pi, { settings, index, getModelRuntime });
|
|
95
|
+
}
|
|
96
|
+
if (settings.features.autoTitle) {
|
|
97
|
+
lifecycles.push(
|
|
98
|
+
installAutoTitle(pi, {
|
|
99
|
+
settings,
|
|
100
|
+
getModelRuntime,
|
|
101
|
+
getSessionEpoch: () => sessionEpoch,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (settings.features.hooks) {
|
|
106
|
+
lifecycles.push(installHooks(pi, { settings, index }));
|
|
107
|
+
}
|
|
108
|
+
// Messaging leads every session_start so the broker connection is registered before any
|
|
109
|
+
// other feature hook runs. A side effect is that messaging's first relation snapshot can
|
|
110
|
+
// predate hooks' index sync for this session; getCachedRelationTo self-heals on miss, so
|
|
111
|
+
// this ordering is intentional — do not reorder to "fix" it.
|
|
112
|
+
pi.on("session_start", async (event, ctx) => {
|
|
113
|
+
sessionEpoch += 1;
|
|
114
|
+
for (const lifecycle of lifecycles) {
|
|
115
|
+
await lifecycle.onSessionStart?.(event, ctx);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
120
|
+
sessionEpoch += 1;
|
|
121
|
+
for (let index = lifecycles.length - 1; index >= 0; index -= 1) {
|
|
122
|
+
await lifecycles[index]?.onSessionShutdown?.(event, ctx);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExtensionContext, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import {
|
|
4
4
|
createAgentSession,
|
|
5
5
|
DefaultResourceLoader,
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
SessionManager,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { type Static, Type } from "typebox";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveAuthenticatedModel } from "../shared/model-resolution.ts";
|
|
12
|
+
import { freshenModel } from "../shared/model-runtime.ts";
|
|
12
13
|
import {
|
|
13
14
|
type SessionLineageRow,
|
|
14
15
|
searchSessionChunks,
|
|
@@ -122,6 +123,7 @@ export interface SessionAskAgentResult {
|
|
|
122
123
|
|
|
123
124
|
export async function runSessionAskAgent(params: {
|
|
124
125
|
ctx: ExtensionContext;
|
|
126
|
+
modelRuntime: ModelRuntime;
|
|
125
127
|
target: SessionLineageRow;
|
|
126
128
|
question: string;
|
|
127
129
|
indexPath: string;
|
|
@@ -129,13 +131,22 @@ export async function runSessionAskAgent(params: {
|
|
|
129
131
|
thinkingLevel: ThinkingLevel | undefined;
|
|
130
132
|
signal?: AbortSignal | undefined;
|
|
131
133
|
}): Promise<SessionAskAgentResult | undefined> {
|
|
134
|
+
params.signal?.throwIfAborted();
|
|
132
135
|
const navigationData = loadSessionNavigationData(params.target.sessionPath);
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
const configured = params.askSettings?.model
|
|
137
|
+
? resolveAuthenticatedModel({
|
|
138
|
+
modelRuntime: params.modelRuntime,
|
|
139
|
+
modelPattern: params.askSettings.model,
|
|
140
|
+
})
|
|
141
|
+
: undefined;
|
|
142
|
+
const currentModel = params.ctx.model
|
|
143
|
+
? freshenModel(params.modelRuntime, params.ctx.model)
|
|
144
|
+
: undefined;
|
|
145
|
+
const model = (configured?.ok ? configured.model : undefined) ?? currentModel;
|
|
136
146
|
if (!model) {
|
|
137
147
|
throw new Error("No active model is available for session_ask.");
|
|
138
148
|
}
|
|
149
|
+
const configuredThinkingLevel = configured?.ok ? configured.thinkingLevel : undefined;
|
|
139
150
|
|
|
140
151
|
let capturedArguments: ProvideResultsArgs | undefined;
|
|
141
152
|
const searchSessionTool = defineTool({
|
|
@@ -234,7 +245,7 @@ export async function runSessionAskAgent(params: {
|
|
|
234
245
|
description: "Return the final answer for session_ask.",
|
|
235
246
|
promptSnippet: "Provide session ask result",
|
|
236
247
|
promptGuidelines: [
|
|
237
|
-
"Call provide_results exactly once when you are ready to answer. It is the final tool call and ends the session_ask
|
|
248
|
+
"Call provide_results exactly once when you are ready to answer. It is the final tool call and ends the session_ask subagent turn.",
|
|
238
249
|
],
|
|
239
250
|
parameters: PROVIDE_RESULTS_PARAMETERS,
|
|
240
251
|
execute: async (_toolCallId, toolParams: ProvideResultsArgs) => {
|
|
@@ -261,9 +272,12 @@ export async function runSessionAskAgent(params: {
|
|
|
261
272
|
noSkills: true,
|
|
262
273
|
appendSystemPromptOverride: (base) => [...base, SESSION_ASK_NAVIGATION_SYSTEM_PROMPT],
|
|
263
274
|
});
|
|
275
|
+
params.signal?.throwIfAborted();
|
|
264
276
|
await resourceLoader.reload();
|
|
277
|
+
params.signal?.throwIfAborted();
|
|
265
278
|
|
|
266
|
-
const thinkingLevel =
|
|
279
|
+
const thinkingLevel =
|
|
280
|
+
params.askSettings?.thinkingLevel ?? configuredThinkingLevel ?? params.thinkingLevel;
|
|
267
281
|
const sessionManager = params.askSettings?.persistRuns
|
|
268
282
|
? SessionManager.create(cwd, getDefaultSessionAskRunsDir())
|
|
269
283
|
: SessionManager.inMemory(cwd);
|
|
@@ -273,7 +287,7 @@ export async function runSessionAskAgent(params: {
|
|
|
273
287
|
const { session } = await createAgentSession({
|
|
274
288
|
cwd,
|
|
275
289
|
model,
|
|
276
|
-
|
|
290
|
+
modelRuntime: params.modelRuntime,
|
|
277
291
|
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
278
292
|
tools: SESSION_ASK_AGENT_TOOLS,
|
|
279
293
|
customTools: [searchSessionTool, sessionReadTool, provideResultsTool],
|
|
@@ -281,29 +295,35 @@ export async function runSessionAskAgent(params: {
|
|
|
281
295
|
sessionManager,
|
|
282
296
|
});
|
|
283
297
|
|
|
284
|
-
|
|
285
|
-
|
|
298
|
+
// Exactly one session.abort() runs no matter how many abort paths fire.
|
|
299
|
+
let nestedAbort: Promise<void> | undefined;
|
|
300
|
+
const startNestedAbort = (): void => {
|
|
301
|
+
nestedAbort ??= Promise.resolve(session.abort()).catch(() => {});
|
|
286
302
|
};
|
|
287
303
|
|
|
288
304
|
try {
|
|
289
|
-
params.signal?.addEventListener("abort",
|
|
305
|
+
params.signal?.addEventListener("abort", startNestedAbort, { once: true });
|
|
290
306
|
if (params.signal?.aborted) {
|
|
291
|
-
|
|
292
|
-
return undefined;
|
|
307
|
+
startNestedAbort();
|
|
293
308
|
}
|
|
294
309
|
|
|
295
310
|
for (let attempt = 1; attempt <= MAX_SESSION_ASK_ATTEMPTS; attempt += 1) {
|
|
311
|
+
params.signal?.throwIfAborted();
|
|
296
312
|
const prompt =
|
|
297
313
|
attempt === 1
|
|
298
314
|
? buildNavigationPrompt(navigationData, params.question)
|
|
299
315
|
: `You did not call ${PROVIDE_RESULTS_TOOL_NAME}. Continue from your prior work and call ${PROVIDE_RESULTS_TOOL_NAME} exactly once with the final markdown answer.`;
|
|
300
316
|
await session.prompt(prompt);
|
|
317
|
+
params.signal?.throwIfAborted();
|
|
301
318
|
if (capturedArguments) {
|
|
302
319
|
break;
|
|
303
320
|
}
|
|
304
321
|
}
|
|
305
322
|
} finally {
|
|
306
|
-
params.signal?.removeEventListener("abort",
|
|
323
|
+
params.signal?.removeEventListener("abort", startNestedAbort);
|
|
324
|
+
if (nestedAbort) {
|
|
325
|
+
await nestedAbort;
|
|
326
|
+
}
|
|
307
327
|
session.dispose();
|
|
308
328
|
}
|
|
309
329
|
|
|
@@ -1,39 +1,38 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { getKeybindings, type Keybinding, Text } from "@earendil-works/pi-tui";
|
|
1
|
+
import { type ExtensionAPI, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import { Type } from "typebox";
|
|
4
|
-
import {
|
|
3
|
+
import { isSessionStarting } from "../session-handoff/metadata.ts";
|
|
4
|
+
import type { IndexHandle } from "../shared/composition.ts";
|
|
5
|
+
import type { ModelRuntimeProvider } from "../shared/model-runtime.ts";
|
|
5
6
|
import {
|
|
6
7
|
getSessionById,
|
|
7
8
|
type SessionLineageRow,
|
|
8
9
|
withSessionIndex,
|
|
9
|
-
} from "
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
} from "../shared/session-index/index.ts";
|
|
11
|
+
import { isExactSessionId } from "../shared/session-ui.ts";
|
|
12
|
+
import type { SessionSettings } from "../shared/settings.ts";
|
|
13
|
+
import { runSessionAskAgent } from "./agent.ts";
|
|
14
|
+
import { renderSessionAskResult } from "./renderer.ts";
|
|
15
|
+
import type {
|
|
16
|
+
SessionAskProgressDetails,
|
|
17
|
+
SessionAskRelevantFile,
|
|
18
|
+
SessionAskResultDetails,
|
|
19
|
+
} from "./tool-contract.ts";
|
|
14
20
|
|
|
15
21
|
interface SessionAskToolParams {
|
|
16
22
|
session: string;
|
|
17
23
|
question: string;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
sessionId?: string | undefined;
|
|
31
|
-
sessionName?: string | undefined;
|
|
32
|
-
sessionPath?: string | undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default function sessionAskExtension(pi: ExtensionAPI): void {
|
|
36
|
-
const settings = loadSettings();
|
|
26
|
+
export function installAsk(
|
|
27
|
+
pi: ExtensionAPI,
|
|
28
|
+
deps: {
|
|
29
|
+
settings: SessionSettings;
|
|
30
|
+
index: IndexHandle;
|
|
31
|
+
getModelRuntime: ModelRuntimeProvider;
|
|
32
|
+
},
|
|
33
|
+
): void {
|
|
34
|
+
const { settings } = deps;
|
|
35
|
+
const indexPath = deps.index.path;
|
|
37
36
|
|
|
38
37
|
pi.registerTool({
|
|
39
38
|
name: "session_ask",
|
|
@@ -60,12 +59,40 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
|
|
|
60
59
|
throw new Error("session_ask requires a question.");
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
const resolvedTarget = resolveSessionAskTarget(sessionId,
|
|
62
|
+
const resolvedTarget = resolveSessionAskTarget(sessionId, indexPath);
|
|
64
63
|
if (!resolvedTarget.resolved) {
|
|
65
64
|
throw new Error(resolvedTarget.error ?? "Unable to resolve session id.");
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
const
|
|
67
|
+
const startingAnswer = getStartingSessionAnswer(resolvedTarget.resolved.sessionPath);
|
|
68
|
+
if (startingAnswer) {
|
|
69
|
+
const details: SessionAskResultDetails = {
|
|
70
|
+
answer: startingAnswer,
|
|
71
|
+
relevantFiles: [],
|
|
72
|
+
sessionId: resolvedTarget.resolved.sessionId,
|
|
73
|
+
sessionName: resolvedTarget.resolved.sessionName,
|
|
74
|
+
sessionPath: resolvedTarget.resolved.sessionPath,
|
|
75
|
+
question,
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: [
|
|
82
|
+
formatSessionAskHeader(
|
|
83
|
+
resolvedTarget.resolved.sessionId,
|
|
84
|
+
resolvedTarget.resolved.sessionName,
|
|
85
|
+
question,
|
|
86
|
+
),
|
|
87
|
+
startingAnswer,
|
|
88
|
+
].join("\n\n"),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
details,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const progressDetails: SessionAskProgressDetails = {
|
|
69
96
|
question,
|
|
70
97
|
sessionId: resolvedTarget.resolved.sessionId,
|
|
71
98
|
sessionName: resolvedTarget.resolved.sessionName,
|
|
@@ -90,26 +117,26 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
|
|
|
90
117
|
details: progressDetails,
|
|
91
118
|
});
|
|
92
119
|
|
|
120
|
+
const modelRuntime = await deps.getModelRuntime(ctx.modelRegistry);
|
|
93
121
|
const agentResult = await runSessionAskAgent({
|
|
94
122
|
ctx,
|
|
123
|
+
modelRuntime,
|
|
95
124
|
target: resolvedTarget.resolved,
|
|
96
125
|
question,
|
|
97
|
-
indexPath
|
|
126
|
+
indexPath,
|
|
98
127
|
askSettings: settings.ask,
|
|
99
128
|
thinkingLevel: pi.getThinkingLevel(),
|
|
100
129
|
signal,
|
|
101
130
|
});
|
|
102
131
|
|
|
103
|
-
if (signal?.aborted) {
|
|
104
|
-
throw new Error("Session ask was cancelled.");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
132
|
const answer = agentResult?.answer || "Could not determine an answer from the session.";
|
|
108
133
|
const relevantFiles = agentResult?.relevantFiles ?? [];
|
|
109
134
|
|
|
110
|
-
const details:
|
|
135
|
+
const details: SessionAskResultDetails = {
|
|
111
136
|
answer,
|
|
112
|
-
|
|
137
|
+
...(agentResult?.debugSessionPath
|
|
138
|
+
? { debugSessionPath: agentResult.debugSessionPath }
|
|
139
|
+
: {}),
|
|
113
140
|
relevantFiles,
|
|
114
141
|
sessionId: resolvedTarget.resolved.sessionId,
|
|
115
142
|
sessionName: resolvedTarget.resolved.sessionName,
|
|
@@ -133,42 +160,21 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
|
|
|
133
160
|
details,
|
|
134
161
|
};
|
|
135
162
|
},
|
|
136
|
-
renderResult
|
|
137
|
-
const details = result.details as SessionAskToolDetails | undefined;
|
|
138
|
-
const content = result.content[0];
|
|
139
|
-
if (content?.type !== "text") {
|
|
140
|
-
return new Text(theme.fg("error", "No session output"), 0, 0);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (context.isError) {
|
|
144
|
-
return new Text(theme.fg("error", content.text), 0, 0);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (isPartial) {
|
|
148
|
-
const lines = [theme.bold(theme.fg("warning", "Reading session..."))];
|
|
149
|
-
if (details?.sessionId || details?.sessionName) {
|
|
150
|
-
const identity = formatSessionTitleOrShortId(details.sessionName, details.sessionId);
|
|
151
|
-
lines.push(`title: ${theme.fg("accent", identity)}`);
|
|
152
|
-
}
|
|
153
|
-
if (details?.question) {
|
|
154
|
-
lines.push(theme.fg("muted", `prompt: ${details.question}`));
|
|
155
|
-
}
|
|
156
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const answer = (details?.answer ?? "").trim() || "No answer generated.";
|
|
160
|
-
const identity = formatSessionTitleOrShortId(details?.sessionName, details?.sessionId);
|
|
161
|
-
const lines = [`title: ${theme.bold(identity)}`];
|
|
162
|
-
if (details?.question) {
|
|
163
|
-
lines.push(theme.fg("muted", `prompt: ${details.question}`));
|
|
164
|
-
lines.push("");
|
|
165
|
-
}
|
|
166
|
-
lines.push(...formatSessionAskAnswerPreview(answer, expanded, theme));
|
|
167
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
168
|
-
},
|
|
163
|
+
renderResult: renderSessionAskResult,
|
|
169
164
|
});
|
|
170
165
|
}
|
|
171
166
|
|
|
167
|
+
function getStartingSessionAnswer(sessionPath: string): string | undefined {
|
|
168
|
+
try {
|
|
169
|
+
if (isSessionStarting(SessionManager.open(sessionPath).getBranch())) {
|
|
170
|
+
return "This session is still starting: a handoff is in progress and it has not received its kickoff. Retry once the session is no longer starting.";
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// The navigation agent preserves the existing failure behavior for unreadable sessions.
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
172
178
|
function resolveSessionAskTarget(
|
|
173
179
|
sessionId: string,
|
|
174
180
|
indexPath: string,
|
|
@@ -220,26 +226,3 @@ function formatSessionAskAnswer(
|
|
|
220
226
|
}
|
|
221
227
|
return lines.join("\n");
|
|
222
228
|
}
|
|
223
|
-
|
|
224
|
-
function formatSessionAskAnswerPreview(answer: string, expanded: boolean, theme: Theme): string[] {
|
|
225
|
-
const lines = answer.split(/\r?\n/);
|
|
226
|
-
if (expanded || lines.length <= COLLAPSED_ANSWER_PREVIEW_ROWS) {
|
|
227
|
-
return lines;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return [
|
|
231
|
-
...lines.slice(0, COLLAPSED_ANSWER_PREVIEW_ROWS),
|
|
232
|
-
formatOverflowHint(lines.length - COLLAPSED_ANSWER_PREVIEW_ROWS, lines.length, theme),
|
|
233
|
-
];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function formatOverflowHint(remaining: number, total: number, theme: Theme): string {
|
|
237
|
-
return `${theme.fg("muted", `... (${remaining} more lines, ${total} total,`)} ${theme.fg(
|
|
238
|
-
"dim",
|
|
239
|
-
formatKeyHint("app.tools.expand"),
|
|
240
|
-
)}${theme.fg("muted", " to expand)")}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function formatKeyHint(keybinding: Keybinding): string {
|
|
244
|
-
return getKeybindings().getKeys(keybinding).join("/");
|
|
245
|
-
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SessionManager,
|
|
8
8
|
type SessionMessageEntry,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { HANDOFF_KICKOFF_CUSTOM_TYPE } from "../session-handoff/kickoff.ts";
|
|
10
11
|
import { contentToText, isRecord, truncateBlock, truncateInline } from "../shared/text.ts";
|
|
11
12
|
|
|
12
13
|
const MAX_TOOL_RESULT_PREVIEW_CHARS = 300;
|
|
@@ -726,7 +727,8 @@ function renderEntryContent(entry: SessionEntry, body: SessionReadBodyMode = "fu
|
|
|
726
727
|
return renderMessageEntry(entry, body);
|
|
727
728
|
}
|
|
728
729
|
if (entry.type === "custom_message") {
|
|
729
|
-
|
|
730
|
+
const label = entry.customType === HANDOFF_KICKOFF_CUSTOM_TYPE ? "Handoff kickoff" : "Custom";
|
|
731
|
+
return `[${label}]: ${String(entry.content ?? "")}`;
|
|
730
732
|
}
|
|
731
733
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
732
734
|
return String(entry.summary ?? "");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExpandableContentPresentation } from "../shared/rendering/expandable-content-layout.ts";
|
|
2
|
+
import type { RenderTheme } from "../shared/rendering/theme.ts";
|
|
3
|
+
import { formatSessionTitleOrShortId } from "../shared/session-ui.ts";
|
|
4
|
+
import type { SessionAskViewModel } from "./view-model.ts";
|
|
5
|
+
|
|
6
|
+
export function buildSessionAskPresentation(
|
|
7
|
+
model: SessionAskViewModel,
|
|
8
|
+
theme: RenderTheme,
|
|
9
|
+
): ExpandableContentPresentation {
|
|
10
|
+
const identity = formatSessionTitleOrShortId(model.sessionName, model.sessionId);
|
|
11
|
+
return {
|
|
12
|
+
header: `title: ${theme.bold(identity)}`,
|
|
13
|
+
...(model.question
|
|
14
|
+
? {
|
|
15
|
+
metadata: [theme.fg("muted", `prompt: ${model.question}`)],
|
|
16
|
+
}
|
|
17
|
+
: {}),
|
|
18
|
+
body: {
|
|
19
|
+
text: model.answer,
|
|
20
|
+
collapsedRows: 6,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
Theme,
|
|
4
|
+
ToolRenderResultOptions,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type Component, Text } from "@earendil-works/pi-tui";
|
|
7
|
+
import { ExpandableContentLayout } from "../shared/rendering/expandable-content-layout.ts";
|
|
8
|
+
import { formatSessionTitleOrShortId } from "../shared/session-ui.ts";
|
|
9
|
+
import { safeParseTypeBoxValue } from "../shared/typebox.ts";
|
|
10
|
+
import { buildSessionAskPresentation } from "./presenter.ts";
|
|
11
|
+
import {
|
|
12
|
+
SESSION_ASK_PROGRESS_DETAILS_SCHEMA,
|
|
13
|
+
SESSION_ASK_RESULT_DETAILS_SCHEMA,
|
|
14
|
+
} from "./tool-contract.ts";
|
|
15
|
+
import { buildSessionAskView } from "./view-model.ts";
|
|
16
|
+
|
|
17
|
+
interface SessionAskToolRendererState {
|
|
18
|
+
layout?: ExpandableContentLayout | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SessionAskToolRenderContext {
|
|
22
|
+
state: SessionAskToolRendererState;
|
|
23
|
+
isError: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderSessionAskResult(
|
|
27
|
+
result: AgentToolResult<unknown>,
|
|
28
|
+
options: ToolRenderResultOptions,
|
|
29
|
+
theme: Theme,
|
|
30
|
+
context: SessionAskToolRenderContext,
|
|
31
|
+
): Component {
|
|
32
|
+
const content = result.content[0];
|
|
33
|
+
if (content?.type !== "text") {
|
|
34
|
+
return new Text(theme.fg("error", "No session output"), 0, 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (context.isError) {
|
|
38
|
+
return new Text(theme.fg("error", content.text), 0, 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.isPartial) {
|
|
42
|
+
const details = safeParseTypeBoxValue(SESSION_ASK_PROGRESS_DETAILS_SCHEMA, result.details);
|
|
43
|
+
const lines = [theme.bold(theme.fg("warning", "Reading session..."))];
|
|
44
|
+
if (details) {
|
|
45
|
+
const identity = formatSessionTitleOrShortId(details.sessionName, details.sessionId);
|
|
46
|
+
lines.push(`title: ${theme.fg("accent", identity)}`);
|
|
47
|
+
}
|
|
48
|
+
if (details?.question) {
|
|
49
|
+
lines.push(theme.fg("muted", `prompt: ${details.question}`));
|
|
50
|
+
}
|
|
51
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const details = safeParseTypeBoxValue(SESSION_ASK_RESULT_DETAILS_SCHEMA, result.details);
|
|
55
|
+
if (!details) {
|
|
56
|
+
return new Text(content.text, 0, 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const layout = context.state.layout ?? new ExpandableContentLayout(theme);
|
|
60
|
+
context.state.layout = layout;
|
|
61
|
+
layout.update(buildSessionAskPresentation(buildSessionAskView(details), theme), options.expanded);
|
|
62
|
+
return layout;
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
export const SESSION_ASK_RELEVANT_FILE_SCHEMA = Type.Object({
|
|
4
|
+
path: Type.String(),
|
|
5
|
+
reason: Type.String(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const SESSION_ASK_PROGRESS_DETAILS_SCHEMA = Type.Object({
|
|
9
|
+
question: Type.String({ minLength: 1 }),
|
|
10
|
+
sessionId: Type.String({ minLength: 1 }),
|
|
11
|
+
sessionName: Type.String(),
|
|
12
|
+
sessionPath: Type.String({ minLength: 1 }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const SESSION_ASK_RESULT_DETAILS_SCHEMA = Type.Object({
|
|
16
|
+
answer: Type.String({ minLength: 1 }),
|
|
17
|
+
debugSessionPath: Type.Optional(Type.String({ minLength: 1 })),
|
|
18
|
+
question: Type.String({ minLength: 1 }),
|
|
19
|
+
relevantFiles: Type.Array(SESSION_ASK_RELEVANT_FILE_SCHEMA),
|
|
20
|
+
sessionId: Type.String({ minLength: 1 }),
|
|
21
|
+
sessionName: Type.String(),
|
|
22
|
+
sessionPath: Type.String({ minLength: 1 }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type SessionAskRelevantFile = Static<typeof SESSION_ASK_RELEVANT_FILE_SCHEMA>;
|
|
26
|
+
export type SessionAskProgressDetails = Static<typeof SESSION_ASK_PROGRESS_DETAILS_SCHEMA>;
|
|
27
|
+
export type SessionAskResultDetails = Static<typeof SESSION_ASK_RESULT_DETAILS_SCHEMA>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { normalizeOptionalText } from "../shared/text.ts";
|
|
2
|
+
import type { SessionAskResultDetails } from "./tool-contract.ts";
|
|
3
|
+
|
|
4
|
+
export interface SessionAskViewModel {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
sessionName?: string | undefined;
|
|
7
|
+
question: string;
|
|
8
|
+
answer: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildSessionAskView(details: SessionAskResultDetails): SessionAskViewModel {
|
|
12
|
+
return {
|
|
13
|
+
sessionId: details.sessionId.trim(),
|
|
14
|
+
sessionName: normalizeOptionalText(details.sessionName),
|
|
15
|
+
question: details.question.trim(),
|
|
16
|
+
answer: details.answer.trim(),
|
|
17
|
+
};
|
|
18
|
+
}
|