pi-sessions 0.13.0 → 0.14.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 CHANGED
@@ -34,7 +34,7 @@
34
34
 
35
35
  ## Install
36
36
 
37
- Requires Pi `0.87.1` or newer and Node `>=24 <26`.
37
+ Requires Pi `0.99.1` or newer and Node `>=24 <26`.
38
38
 
39
39
  **From npm** (recommended):
40
40
 
@@ -12,10 +12,12 @@ import { isExactSessionId } from "../shared/session-ui.ts";
12
12
  import type { SessionSettings } from "../shared/settings.ts";
13
13
  import { runSessionAskAgent } from "./agent.ts";
14
14
  import { renderSessionAskResult } from "./renderer.ts";
15
- import type {
16
- SessionAskProgressDetails,
17
- SessionAskRelevantFile,
18
- SessionAskResultDetails,
15
+ import {
16
+ SESSION_ASK_OUTPUT_SCHEMA,
17
+ type SessionAskOutput,
18
+ type SessionAskProgressDetails,
19
+ type SessionAskRelevantFile,
20
+ type SessionAskResultDetails,
19
21
  } from "./tool-contract.ts";
20
22
 
21
23
  interface SessionAskToolParams {
@@ -39,6 +41,7 @@ export function installAsk(
39
41
  name: "session_ask",
40
42
  label: "Session Ask",
41
43
  description: "Interrogate a Pi session transcript",
44
+ annotations: { readOnlyHint: true, openWorldHint: false },
42
45
  promptSnippet: "Recall information, decisions, or reasoning from a Pi session by id",
43
46
  promptGuidelines: [
44
47
  "Use session_ask with focused questions rather than broad recap requests.",
@@ -56,6 +59,7 @@ export function installAsk(
56
59
  description: "What to find in that session's transcript",
57
60
  }),
58
61
  }),
62
+ outputSchema: SESSION_ASK_OUTPUT_SCHEMA,
59
63
  async execute(_toolCallId, params: SessionAskToolParams, signal, onUpdate, ctx) {
60
64
  const sessionId = params.session.trim();
61
65
  if (!sessionId) {
@@ -110,6 +114,7 @@ export function installAsk(
110
114
  },
111
115
  ],
112
116
  details,
117
+ structuredContent: formatSessionAskOutput(details),
113
118
  };
114
119
  }
115
120
 
@@ -179,6 +184,7 @@ export function installAsk(
179
184
  },
180
185
  ],
181
186
  details,
187
+ structuredContent: formatSessionAskOutput(details),
182
188
  };
183
189
  },
184
190
  renderResult: renderSessionAskResult,
@@ -217,6 +223,15 @@ function resolveSessionAskTarget(
217
223
  }
218
224
  }
219
225
 
226
+ function formatSessionAskOutput(details: SessionAskResultDetails): SessionAskOutput {
227
+ return {
228
+ sessionId: details.sessionId,
229
+ sessionName: details.sessionName,
230
+ answer: details.answer,
231
+ relevantFiles: details.relevantFiles,
232
+ };
233
+ }
234
+
220
235
  function formatSessionAskHeader(sessionId: string, sessionName: string, question: string): string {
221
236
  return [
222
237
  `session: ${sessionId}`,
@@ -22,6 +22,14 @@ export const SESSION_ASK_RESULT_DETAILS_SCHEMA = Type.Object({
22
22
  sessionPath: Type.String({ minLength: 1 }),
23
23
  });
24
24
 
25
+ export const SESSION_ASK_OUTPUT_SCHEMA = Type.Object({
26
+ sessionId: Type.String(),
27
+ sessionName: Type.String(),
28
+ answer: Type.String(),
29
+ relevantFiles: Type.Array(SESSION_ASK_RELEVANT_FILE_SCHEMA),
30
+ });
31
+
25
32
  export type SessionAskRelevantFile = Static<typeof SESSION_ASK_RELEVANT_FILE_SCHEMA>;
26
33
  export type SessionAskProgressDetails = Static<typeof SESSION_ASK_PROGRESS_DETAILS_SCHEMA>;
27
34
  export type SessionAskResultDetails = Static<typeof SESSION_ASK_RESULT_DETAILS_SCHEMA>;
35
+ export type SessionAskOutput = Static<typeof SESSION_ASK_OUTPUT_SCHEMA>;
@@ -165,10 +165,7 @@ class RetitleWizardPanel implements Focusable {
165
165
  }
166
166
  if (result) return result;
167
167
  if (event.type === "wheel" && this.step.kind === "scope" && event.wheelDelta) {
168
- this.selectedIndex = Math.max(
169
- 0,
170
- Math.min(2, this.selectedIndex + Math.sign(event.wheelDelta)),
171
- );
168
+ this.selectedIndex = Math.max(0, Math.min(2, this.selectedIndex + event.wheelDelta));
172
169
  this.requestRender();
173
170
  return { handled: true };
174
171
  }
@@ -203,7 +203,7 @@ export class HandoffBoard implements Focusable {
203
203
  return { handled: true };
204
204
  }
205
205
  if (event.type === "wheel" && event.wheelDelta && this.rowHitAt(event)) {
206
- if (!this.busy) this.moveSelection(Math.sign(event.wheelDelta));
206
+ if (!this.busy) this.moveSelection(event.wheelDelta);
207
207
  return { handled: true };
208
208
  }
209
209
  return undefined;
@@ -31,6 +31,10 @@ import { buildHandoffLaunchSchema, buildHandoffPromptGuidelines } from "./tool-s
31
31
  import { buildHandoffToolView } from "./tool-view-model.ts";
32
32
  import { formatHandoffError } from "./ui.ts";
33
33
 
34
+ const SESSION_TOKEN_PROMPT_SECTION = "pi_sessions_session_token";
35
+ const SESSION_TOKEN_PROMPT =
36
+ "When the user references @session:<uuid>, treat it as a session token. If you call session_ask, pass only the UUID value, not the @session: prefix.";
37
+
34
38
  interface HandoffToolRendererState {
35
39
  callComponent?: HandoffToolComponent | undefined;
36
40
  }
@@ -64,6 +68,8 @@ export function installHandoff(
64
68
  name: "session_handoff",
65
69
  label: "Session Handoff",
66
70
  description: "Start a new Pi session with a self-contained task.",
71
+ // Nested calls write no toolResult entry, so the board would never see the launch receipt.
72
+ exposure: "model-only",
67
73
  promptSnippet:
68
74
  "Delegate bounded work to a background subagent or hand off context to another Pi session",
69
75
  promptGuidelines: buildHandoffPromptGuidelines(launchTargets, models, roster),
@@ -196,11 +202,7 @@ export function installHandoff(
196
202
  (await splitBackend.identifyTerminalId(ctx.cwd)) ?? identifiedGhosttyTerminalId;
197
203
  }
198
204
 
199
- return {
200
- systemPrompt:
201
- event.systemPrompt +
202
- "\n\nWhen the user references @session:<uuid>, treat it as a session token. If you call session_ask, pass only the UUID value, not the @session: prefix.",
203
- };
205
+ event.systemPromptOptions.sections[SESSION_TOKEN_PROMPT_SECTION] = SESSION_TOKEN_PROMPT;
204
206
  });
205
207
 
206
208
  return {
@@ -178,7 +178,7 @@ export class SessionReferencePickerComponent implements Focusable {
178
178
  return undefined;
179
179
  }
180
180
  if (event.type === "wheel" && event.wheelDelta && this.sessionAt(event)) {
181
- this.moveSelection(event.wheelDelta < 0 ? -1 : 1);
181
+ this.moveSelection(event.wheelDelta);
182
182
  return { handled: true };
183
183
  }
184
184
  if (event.type !== "click" || event.button !== "left") return undefined;
@@ -63,8 +63,8 @@ export function formatHandoffLaunchFailure(error: string, prepared: PreparedHand
63
63
  return `${error} Created handoff session ${prepared.sessionId}; start it manually with: ${prepared.resumeCommand}`;
64
64
  }
65
65
 
66
- // Pi intentionally defers writing a new session until an assistant response
67
- // exists, so a prepared child needs one explicit initial flush of the
66
+ // Pi intentionally defers writing a new session until it holds a user or
67
+ // assistant message, so a prepared child needs one explicit initial flush of the
68
68
  // manager-assembled state to be discoverable by `pi --session-id`.
69
69
  function flushPreparedSession(manager: SessionManager): void {
70
70
  const sessionFile = manager.getSessionFile();
@@ -77,6 +77,7 @@ export function createSessionReachableTool(deps: SessionReachableDeps): ToolDefi
77
77
  "Before session_send_message, use session_reachable to find the target session id.",
78
78
  ],
79
79
  parameters: subagentsAvailable ? SESSION_REACHABLE_PARAMS : SESSION_REACHABLE_USER_PARAMS,
80
+ outputSchema: SESSION_REACHABLE_TOOL_DETAILS_SCHEMA,
80
81
  renderResult(result, options, theme, context) {
81
82
  const output = result.content.find((item) => item.type === "text")?.text ?? "";
82
83
  if (context.isError) {
@@ -108,6 +109,7 @@ export function createSessionReachableTool(deps: SessionReachableDeps): ToolDefi
108
109
  return {
109
110
  content: [{ type: "text" as const, text: JSON.stringify(details, null, 2) }],
110
111
  details,
112
+ structuredContent: details,
111
113
  };
112
114
  },
113
115
  });
@@ -633,11 +633,17 @@ function extractMessageFileTouches(
633
633
  message: AgentMessage,
634
634
  cwd: string,
635
635
  ): SessionFileTouch[] {
636
- if (message.role !== "assistant" || !Array.isArray(message.content)) {
636
+ let toolCalls: { name: string; arguments?: Record<string, unknown> | undefined }[];
637
+ if (message.role === "assistant" && Array.isArray(message.content)) {
638
+ toolCalls = message.content.filter(isToolCallBlock);
639
+ } else if (message.role === "toolResult" && message.nestedCalls) {
640
+ // Calls made through ctx.executeTool(), e.g. from a codemode script, only appear here.
641
+ toolCalls = message.nestedCalls.calls;
642
+ } else {
637
643
  return [];
638
644
  }
639
645
 
640
- return message.content.filter(isToolCallBlock).flatMap((toolCall) => {
646
+ return toolCalls.flatMap((toolCall) => {
641
647
  const rawPath = stringValue(toolCall.arguments?.path);
642
648
  const op = getToolCallFileTouchOp(toolCall.name);
643
649
  if (!rawPath || !op) {
@@ -10,10 +10,12 @@ import {
10
10
  } from "../shared/session-index/index.ts";
11
11
  import type { SessionSettings } from "../shared/settings.ts";
12
12
  import { renderSessionSearchResult } from "./renderer.ts";
13
- import type {
14
- SessionSearchResult,
15
- SessionSearchToolDetails,
16
- SessionSearchToolParams,
13
+ import {
14
+ SESSION_SEARCH_OUTPUT_SCHEMA,
15
+ type SessionSearchOutput,
16
+ type SessionSearchResult,
17
+ type SessionSearchToolDetails,
18
+ type SessionSearchToolParams,
17
19
  } from "./tool-contract.ts";
18
20
 
19
21
  const DEFAULT_SESSION_SEARCH_LIMIT = 6;
@@ -30,6 +32,7 @@ export function installSearch(pi: ExtensionAPI, deps: SearchInstallDeps): void {
30
32
  name: "session_search",
31
33
  label: "Session Search",
32
34
  description: "Search Pi sessions",
35
+ annotations: { readOnlyHint: true, openWorldHint: false },
33
36
  promptSnippet: "Locate Pi sessions for follow-up",
34
37
  promptGuidelines: [
35
38
  "Omit queries in session_search to list matching sessions chronologically.",
@@ -104,6 +107,7 @@ export function installSearch(pi: ExtensionAPI, deps: SearchInstallDeps): void {
104
107
  }),
105
108
  ),
106
109
  }),
110
+ outputSchema: SESSION_SEARCH_OUTPUT_SCHEMA,
107
111
  async execute(_toolCallId, params: SessionSearchToolParams, _signal, onUpdate, ctx) {
108
112
  const validationError = validateSearchParams(params);
109
113
  if (validationError) {
@@ -130,6 +134,7 @@ export function installSearch(pi: ExtensionAPI, deps: SearchInstallDeps): void {
130
134
  },
131
135
  ],
132
136
  details,
137
+ structuredContent: formatSearchOutput(results),
133
138
  };
134
139
  });
135
140
  },
@@ -170,6 +175,24 @@ function formatSearchResultsForModel(details: SessionSearchToolDetails): string
170
175
  );
171
176
  }
172
177
 
178
+ function formatSearchOutput(results: SessionSearchResult[]): SessionSearchOutput {
179
+ return {
180
+ results: results.map((result) => ({
181
+ sessionId: result.sessionId,
182
+ sessionName: result.sessionName,
183
+ cwd: result.cwd,
184
+ startedAt: result.startedAt,
185
+ modifiedAt: result.modifiedAt,
186
+ messageCount: result.messageCount,
187
+ snippet: stripSearchSnippetMarkers(result.snippet) ?? result.snippet,
188
+ hitCount: result.hitCount,
189
+ ...(result.sessionOrigin ? { sessionOrigin: result.sessionOrigin } : {}),
190
+ ...(result.relation ? { relation: result.relation } : {}),
191
+ ...(result.parentSessionId ? { parentSessionId: result.parentSessionId } : {}),
192
+ })),
193
+ };
194
+ }
195
+
173
196
  function formatSearchResultForModel(result: SessionSearchResult): SessionSearchResult {
174
197
  return {
175
198
  ...result,
@@ -1,8 +1,11 @@
1
- import type {
2
- SearchSessionResult,
3
- SearchSort,
4
- SessionIndexStatus,
5
- SessionKind,
1
+ import { type Static, Type } from "typebox";
2
+ import {
3
+ SESSION_LINEAGE_RELATION_SCHEMA,
4
+ SESSION_ORIGIN_SCHEMA,
5
+ type SearchSessionResult,
6
+ type SearchSort,
7
+ type SessionIndexStatus,
8
+ type SessionKind,
6
9
  } from "../shared/session-index/index.ts";
7
10
 
8
11
  export interface SessionSearchToolParams {
@@ -29,3 +32,23 @@ export interface SessionSearchToolDetails {
29
32
  results: SessionSearchResult[];
30
33
  status?: SessionIndexStatus | undefined;
31
34
  }
35
+
36
+ export const SESSION_SEARCH_OUTPUT_SCHEMA = Type.Object({
37
+ results: Type.Array(
38
+ Type.Object({
39
+ sessionId: Type.String(),
40
+ sessionName: Type.String(),
41
+ cwd: Type.String(),
42
+ startedAt: Type.String(),
43
+ modifiedAt: Type.String(),
44
+ messageCount: Type.Number(),
45
+ snippet: Type.String(),
46
+ hitCount: Type.Number(),
47
+ sessionOrigin: Type.Optional(SESSION_ORIGIN_SCHEMA),
48
+ relation: Type.Optional(SESSION_LINEAGE_RELATION_SCHEMA),
49
+ parentSessionId: Type.Optional(Type.String()),
50
+ }),
51
+ ),
52
+ });
53
+
54
+ export type SessionSearchOutput = Static<typeof SESSION_SEARCH_OUTPUT_SCHEMA>;
@@ -43,6 +43,7 @@ export function createSubmitTaskReportTool(
43
43
  label: "Submit task report",
44
44
  description:
45
45
  "Send a report to the parent session and end the current turn. Use it when the delegated task or requested follow-up is done, blocked, or cannot be completed.",
46
+ exposure: "model-only",
46
47
  promptSnippet: "Submit the current task report to the parent session and end the turn",
47
48
  promptGuidelines: [
48
49
  "Call submit_task_report exactly once as the final tool call for every delegated task or follow-up that expects a response.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, messaging, auto-titling, and indexing tools",
6
6
  "license": "MIT",
@@ -38,10 +38,10 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@biomejs/biome": "^2.5.10",
41
- "@earendil-works/pi-agent-core": "^0.87.1",
42
- "@earendil-works/pi-ai": "^0.87.1",
43
- "@earendil-works/pi-coding-agent": "^0.87.1",
44
- "@earendil-works/pi-tui": "^0.87.1",
41
+ "@earendil-works/pi-agent-core": "^0.99.1",
42
+ "@earendil-works/pi-ai": "^0.99.1",
43
+ "@earendil-works/pi-coding-agent": "^0.99.1",
44
+ "@earendil-works/pi-tui": "^0.99.1",
45
45
  "@types/node": "^24.13.3",
46
46
  "strip-ansi": "^7.2.0",
47
47
  "typebox": "1.3.27",
@@ -49,9 +49,10 @@
49
49
  "vitest": "^4.1.11"
50
50
  },
51
51
  "peerDependencies": {
52
- "@earendil-works/pi-agent-core": ">=0.87.1",
53
- "@earendil-works/pi-ai": ">=0.87.1",
54
- "@earendil-works/pi-coding-agent": ">=0.87.1",
55
- "@earendil-works/pi-tui": ">=0.87.1"
52
+ "@earendil-works/pi-agent-core": "*",
53
+ "@earendil-works/pi-ai": "*",
54
+ "@earendil-works/pi-coding-agent": "*",
55
+ "@earendil-works/pi-tui": "*",
56
+ "typebox": "*"
56
57
  }
57
58
  }