pi-sessions 0.16.0 → 0.17.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
@@ -74,6 +74,8 @@ Open the frontend implementation task in a session to the right.
74
74
  | Session Index | `/session-index` slash command | Shows index status and rebuilds the local session index |
75
75
  | Session Auto Title | in background, `/title` slash command | Give sessions titles |
76
76
 
77
+ Settings live under `sessions` in `~/.pi/agent/settings.json`. A project's `.pi/settings.json` overrides them the way pi merges its own settings: nested objects merge key by key, and the project wins.
78
+
77
79
  Every feature is on by default. Turn one off with `enable: false` under its own settings namespace. Index recovery and startup/switch sync always run, including when search is disabled. `hooks.enable` controls turn, tree, and compaction sync, not startup recovery.
78
80
 
79
81
  ```json
@@ -199,6 +201,7 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
199
201
  "thinkingLevel": "low",
200
202
  "roster": ["anthropic/*", "openai-codex/gpt-5.6-terra:high"],
201
203
  "deferred": {
204
+ "enable": true,
202
205
  "copyToClipboard": true
203
206
  }
204
207
  }
@@ -210,6 +213,8 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
210
213
 
211
214
  `roster` limits which models a handoff may launch a child session on. Defaults to pi's own `enabledModels` scoping, then to all configured models. May optionally include a thinking level; listing a model more than once adds to the levels it allows.
212
215
 
216
+ `deferred.enable` (default `true`) offers the `deferred` launch. Turn it off, along with `sessions.subagents.enable`, to leave only host and split launches.
217
+
213
218
  `deferred.copyToClipboard` (default `true`) controls whether deferred handoffs copy the resume command to the clipboard. When off, the resume command is only shown in the tool call.
214
219
 
215
220
  Subagents require the handoff and messaging features. Limit recursive delegation depth with `sessions.subagents.maxDepth` (default `2`), or cap the context a subagent runs in with `sessions.subagents.contextLimit`:
@@ -28,7 +28,11 @@ import {
28
28
  } from "./tool.ts";
29
29
  import { HANDOFF_TOOL_DETAILS_SCHEMA } from "./tool-contract.ts";
30
30
  import { HandoffToolComponent } from "./tool-renderer.ts";
31
- import { buildHandoffLaunchSchema, buildHandoffPromptGuidelines } from "./tool-schema.ts";
31
+ import {
32
+ buildHandoffLaunchSchema,
33
+ buildHandoffPromptGuidelines,
34
+ offersSubagentLaunch,
35
+ } from "./tool-schema.ts";
32
36
  import { buildHandoffToolView } from "./tool-view-model.ts";
33
37
  import { formatHandoffError } from "./ui.ts";
34
38
 
@@ -66,14 +70,16 @@ export function installHandoff(
66
70
  launchTargets: readonly HandoffLaunchTarget[],
67
71
  roster: HandoffRoster | undefined,
68
72
  ): void {
73
+ const offersSubagent = offersSubagentLaunch(launchTargets);
69
74
  pi.registerTool({
70
75
  name: "session_handoff",
71
76
  label: "Session Handoff",
72
77
  description: "Start a new Pi session with a self-contained task.",
73
78
  // Nested calls write no toolResult entry, so the board would never see the launch receipt.
74
79
  exposure: "model-only",
75
- promptSnippet:
76
- "Delegate bounded work to a background subagent or hand off context to another Pi session",
80
+ promptSnippet: offersSubagent
81
+ ? "Delegate bounded work to a background subagent or hand off context to another Pi session"
82
+ : "Hand off context to another Pi session",
77
83
  promptGuidelines: buildHandoffPromptGuidelines(launchTargets, models, roster),
78
84
  parameters: Type.Object({
79
85
  goal: Type.String({
@@ -93,8 +99,7 @@ export function installHandoff(
93
99
  ),
94
100
  requestResponse: Type.Optional(
95
101
  Type.Boolean({
96
- description:
97
- "Whether the child session should report completion/results of its task back to this session. Defaults to true for subagent launches and false otherwise.",
102
+ description: `Whether the child session should report completion/results of its task back to this session. ${offersSubagent ? "Defaults to true for subagent launches and false otherwise." : "Defaults to false."}`,
98
103
  }),
99
104
  ),
100
105
  provider: Type.Optional(
@@ -111,8 +116,9 @@ export function installHandoff(
111
116
  Type.Union(
112
117
  THINKING_LEVELS.map((level) => Type.Literal(level)),
113
118
  {
114
- description:
115
- "Thinking level for the child session. For directional or deferred, override only when the user requests it.",
119
+ description: offersSubagent
120
+ ? "Thinking level for the child session. For launches other than subagent, override only when the user requests it."
121
+ : "Thinking level for the child session. Override only when the user requests it.",
116
122
  },
117
123
  ),
118
124
  ),
@@ -215,7 +221,7 @@ export function installHandoff(
215
221
  createHandoffLaunchTargets({
216
222
  pi,
217
223
  splitBackend,
218
- copyDeferredToClipboard: settings.handoff.deferred.copyToClipboard,
224
+ deferred: settings.handoff.deferred,
219
225
  additionalTargets: deps.getLaunchTargets?.() ?? [],
220
226
  hosts: deps.getHosts?.() ?? [],
221
227
  }),
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { Host } from "../hosts/contract.ts";
3
+ import type { HandoffSettings } from "../shared/settings.ts";
3
4
  import { createDeferredLaunchBackend } from "./launch/deferred.ts";
4
5
  import type { SplitLaunchBackend } from "./launch/resolution.ts";
5
6
  import {
@@ -18,7 +19,7 @@ export interface ResolvedHandoffLaunchTarget {
18
19
  export function createHandoffLaunchTargets(options: {
19
20
  pi: ExtensionAPI;
20
21
  splitBackend: SplitLaunchBackend | undefined;
21
- copyDeferredToClipboard: boolean;
22
+ deferred: HandoffSettings["deferred"];
22
23
  additionalTargets: readonly HandoffLaunchTarget[];
23
24
  hosts?: readonly Host[];
24
25
  }): HandoffLaunchTarget[] {
@@ -35,11 +36,15 @@ export function createHandoffLaunchTargets(options: {
35
36
  ),
36
37
  )
37
38
  : [];
38
- const deferred = createBackendLaunchTarget(
39
- DEFERRED_LAUNCH,
40
- createDeferredLaunchBackend({ copyToClipboard: options.copyDeferredToClipboard }),
41
- "'deferred' creates the session and returns its resume command without opening anything.",
42
- );
39
+ const deferredTargets = options.deferred.enable
40
+ ? [
41
+ createBackendLaunchTarget(
42
+ DEFERRED_LAUNCH,
43
+ createDeferredLaunchBackend({ copyToClipboard: options.deferred.copyToClipboard }),
44
+ "'deferred' creates the session and returns its resume command without opening anything.",
45
+ ),
46
+ ]
47
+ : [];
43
48
  return [
44
49
  ...hosts.map((host) =>
45
50
  createBackendLaunchTarget(
@@ -50,7 +55,7 @@ export function createHandoffLaunchTargets(options: {
50
55
  ),
51
56
  ),
52
57
  ...splitTargets,
53
- deferred,
58
+ ...deferredTargets,
54
59
  ...options.additionalTargets,
55
60
  ];
56
61
  }
@@ -1,6 +1,11 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import { Type } from "typebox";
3
- import { type HandoffLaunchTarget, LAUNCH_DIRECTIONS, SUBAGENT_LAUNCH } from "./launch-target.ts";
3
+ import {
4
+ DEFERRED_LAUNCH,
5
+ type HandoffLaunchTarget,
6
+ LAUNCH_DIRECTIONS,
7
+ SUBAGENT_LAUNCH,
8
+ } from "./launch-target.ts";
4
9
  import { formatRosterEntry, type HandoffRoster } from "./roster.ts";
5
10
 
6
11
  export function buildHandoffLaunchSchema(targets: readonly HandoffLaunchTarget[]) {
@@ -23,9 +28,6 @@ const SUBAGENT_GUIDELINES = [
23
28
  "After launching a subagent, do not sleep, poll, or repeatedly check its progress; continue useful work or finish the current turn and wait for its report.",
24
29
  ];
25
30
 
26
- const LAUNCH_TARGET_GUIDELINE =
27
- "Use session_handoff directional or deferred launches only when the user requests one.";
28
-
29
31
  const MODEL_INHERITANCE_GUIDELINES = [
30
32
  "Leave provider and model unset to run the handoff on this session's current model.",
31
33
  "To run the handoff on a different model, set both provider and model together (both are required).",
@@ -35,7 +37,7 @@ const RESTRICTED_THINKING_GUIDELINE =
35
37
  'A ":level" suffix on a listed model limits it to those thinking levels; set thinkingLevel to one of them, or leave it unset when only one is listed.';
36
38
 
37
39
  const SUBAGENT_MODEL_OVERRIDE_GUIDELINE =
38
- "For subagents, choose the model that best fits the delegated task; for directional or deferred handoffs, only override the model when the task clearly warrants it. When unsure, ask the user for their preference.";
40
+ "For subagents, choose the model that best fits the delegated task; for other launches, only override the model when the task clearly warrants it. When unsure, ask the user for their preference.";
39
41
 
40
42
  const DEFAULT_MODEL_OVERRIDE_GUIDELINE =
41
43
  "Only override the model when the task clearly warrants it.";
@@ -45,20 +47,15 @@ export function buildHandoffPromptGuidelines(
45
47
  models: readonly Model<Api>[],
46
48
  roster: HandoffRoster | undefined,
47
49
  ): string[] {
48
- const hasSubagent = targets.some((target) => target.value === SUBAGENT_LAUNCH);
49
- const hasHost = targets.some(
50
- (target) =>
51
- target.value !== "subagent" &&
52
- target.value !== "deferred" &&
53
- !LAUNCH_DIRECTIONS.some((direction) => direction === target.value),
54
- );
50
+ const hasSubagent = offersSubagentLaunch(targets);
51
+ const userFacingKinds = describeUserFacingLaunchKinds(targets);
55
52
  const selectable =
56
53
  roster?.map(formatRosterEntry) ?? models.map((model) => `${model.provider}/${model.id}`);
57
54
  return [
58
55
  ...(hasSubagent ? SUBAGENT_GUIDELINES : []),
59
- hasHost
60
- ? "Use session_handoff host or deferred launches only when the user requests one."
61
- : LAUNCH_TARGET_GUIDELINE,
56
+ ...(userFacingKinds
57
+ ? [`Use session_handoff ${userFacingKinds} launches only when the user requests one.`]
58
+ : []),
62
59
  ...MODEL_INHERITANCE_GUIDELINES,
63
60
  hasSubagent ? SUBAGENT_MODEL_OVERRIDE_GUIDELINE : DEFAULT_MODEL_OVERRIDE_GUIDELINE,
64
61
  ...(selectable.length > 0
@@ -67,3 +64,24 @@ export function buildHandoffPromptGuidelines(
67
64
  ...(roster?.some((entry) => entry.thinkingLevels) ? [RESTRICTED_THINKING_GUIDELINE] : []),
68
65
  ];
69
66
  }
67
+
68
+ export function offersSubagentLaunch(targets: readonly HandoffLaunchTarget[]): boolean {
69
+ return targets.some((target) => target.value === SUBAGENT_LAUNCH);
70
+ }
71
+
72
+ function describeUserFacingLaunchKinds(targets: readonly HandoffLaunchTarget[]): string {
73
+ const isDirection = (value: string) => LAUNCH_DIRECTIONS.some((direction) => direction === value);
74
+ const kinds = [
75
+ targets.some((target) => isDirection(target.value)) ? "directional" : undefined,
76
+ targets.some(
77
+ (target) =>
78
+ target.value !== SUBAGENT_LAUNCH &&
79
+ target.value !== DEFERRED_LAUNCH &&
80
+ !isDirection(target.value),
81
+ )
82
+ ? "host"
83
+ : undefined,
84
+ targets.some((target) => target.value === DEFERRED_LAUNCH) ? "deferred" : undefined,
85
+ ];
86
+ return kinds.filter((kind) => kind !== undefined).join(" or ");
87
+ }
@@ -35,6 +35,7 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
35
35
  roster: Type.Optional(Type.Array(Type.String())),
36
36
  deferred: Type.Optional(
37
37
  Type.Object({
38
+ enable: Type.Optional(Type.Boolean()),
38
39
  copyToClipboard: Type.Optional(Type.Boolean()),
39
40
  }),
40
41
  ),
@@ -103,6 +104,7 @@ export interface HandoffSettings extends AgentModelSettings {
103
104
  /** Patterns naming the models a handoff may launch a child on. Empty means unrestricted. */
104
105
  roster: readonly ModelSelection[];
105
106
  deferred: {
107
+ enable: boolean;
106
108
  copyToClipboard: boolean;
107
109
  };
108
110
  }
@@ -228,9 +230,14 @@ function normalizeAutoTitlePrompt(value: string | undefined): string {
228
230
  return trimmed ? trimmed : DEFAULT_AUTO_TITLE_PROMPT;
229
231
  }
230
232
 
233
+ /**
234
+ * pi's merged view: project `.pi/settings.json` deep-merged over the global file.
235
+ * Extensions load before `pi.getSettings()` is bound, and the trust decision it
236
+ * honors is not visible at load time, so project settings apply regardless of trust.
237
+ */
231
238
  function loadSessionFileSettings(): SessionFileSettings {
232
- const globalSettings = SettingsManager.create(process.cwd()).getGlobalSettings();
233
- const parsed = parseTypeBoxValue(ROOT_SETTINGS_SCHEMA, globalSettings, "Invalid settings");
239
+ const mergedSettings = SettingsManager.create(process.cwd()).getSettings();
240
+ const parsed = parseTypeBoxValue(ROOT_SETTINGS_SCHEMA, mergedSettings, "Invalid settings");
234
241
  return parsed.sessions ?? {};
235
242
  }
236
243
 
@@ -263,6 +270,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
263
270
  persistRuns: fileSettings.handoff?.persistRuns ?? false,
264
271
  roster: normalizeRoster(fileSettings.handoff?.roster),
265
272
  deferred: {
273
+ enable: fileSettings.handoff?.deferred?.enable ?? true,
266
274
  copyToClipboard: fileSettings.handoff?.deferred?.copyToClipboard ?? true,
267
275
  },
268
276
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, messaging, auto-titling, and indexing tools",
6
6
  "license": "MIT",