pi-sessions 0.11.2 → 0.12.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
@@ -142,6 +142,7 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
142
142
  "pickerShortcut": "alt+p",
143
143
  "model": "openai-codex/gpt-5.6-terra",
144
144
  "thinkingLevel": "low",
145
+ "roster": ["anthropic/*", "openai-codex/gpt-5.6-terra:high"],
145
146
  "deferred": {
146
147
  "copyToClipboard": true
147
148
  }
@@ -152,6 +153,8 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
152
153
 
153
154
  `model` and `thinkingLevel` configure the agent that builds handoff prompts. They default to the new session's values when absent.
154
155
 
156
+ `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.
157
+
155
158
  `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.
156
159
 
157
160
  Subagents require the handoff and messaging features. Limit recursive delegation depth with `sessions.subagents.maxDepth` (default `2`):
@@ -26,7 +26,7 @@ import { type Static, Type } from "typebox";
26
26
  import { freshenModel } from "../shared/model-runtime.ts";
27
27
  import { getDefaultHandoffRunsDir, type HandoffSettings } from "../shared/settings.ts";
28
28
  import { parseTypeBoxValue } from "../shared/typebox.ts";
29
- import { resolveModelOverride } from "./model.ts";
29
+ import { resolveModelPattern } from "./model.ts";
30
30
 
31
31
  const MAX_HANDOFF_EXTRACTION_ATTEMPTS = 3;
32
32
  const HANDOFF_EXTRACTION_RETRY_PROMPT =
@@ -111,7 +111,7 @@ export async function generateHandoffDraftFromSessionManager({
111
111
  requestResponse?: boolean | undefined;
112
112
  }): Promise<HandoffDraftResult | undefined> {
113
113
  const modelOverride = settings.model
114
- ? resolveModelOverride(modelRuntime, settings.model, settings.thinkingLevel)
114
+ ? resolveModelPattern(modelRuntime, settings.model, settings.thinkingLevel)
115
115
  : undefined;
116
116
  let model = modelOverride?.model;
117
117
  if (!model) {
@@ -19,6 +19,7 @@ import type { HandoffLaunchTarget } from "./launch-target.ts";
19
19
  import { HANDOFF_BOOTSTRAP_FAILED_CUSTOM_TYPE } from "./metadata.ts";
20
20
  import { openSessionReferencePicker } from "./picker.ts";
21
21
  import { SESSION_TOKEN_PREFIX } from "./query.ts";
22
+ import { type HandoffRoster, resolveHandoffRoster } from "./roster.ts";
22
23
  import {
23
24
  executeSessionHandoffTool,
24
25
  type HandoffToolParams,
@@ -57,6 +58,7 @@ export function installHandoff(
57
58
  function registerHandoffTool(
58
59
  models: readonly Model<Api>[],
59
60
  launchTargets: readonly HandoffLaunchTarget[],
61
+ roster: HandoffRoster | undefined,
60
62
  ): void {
61
63
  pi.registerTool({
62
64
  name: "session_handoff",
@@ -64,7 +66,7 @@ export function installHandoff(
64
66
  description: "Start a new Pi session with a self-contained task.",
65
67
  promptSnippet:
66
68
  "Delegate bounded work to a background subagent or hand off context to another Pi session",
67
- promptGuidelines: buildHandoffPromptGuidelines(launchTargets, models),
69
+ promptGuidelines: buildHandoffPromptGuidelines(launchTargets, models, roster),
68
70
  parameters: Type.Object({
69
71
  goal: Type.String({
70
72
  description:
@@ -141,6 +143,7 @@ export function installHandoff(
141
143
  ctx,
142
144
  modelRuntime,
143
145
  launchTargets,
146
+ roster,
144
147
  (sessionId, status) => clipboardStatusBySessionId.set(sessionId, status),
145
148
  );
146
149
  },
@@ -202,14 +205,20 @@ export function installHandoff(
202
205
 
203
206
  return {
204
207
  async onSessionStart(_event, ctx) {
208
+ const models = ctx.modelRegistry.getAvailable();
205
209
  registerHandoffTool(
206
- ctx.modelRegistry.getAvailable(),
210
+ models,
207
211
  createHandoffLaunchTargets({
208
212
  pi,
209
213
  splitBackend,
210
214
  copyDeferredToClipboard: settings.handoff.deferred.copyToClipboard,
211
215
  additionalTargets: deps.getLaunchTargets?.() ?? [],
212
216
  }),
217
+ resolveHandoffRoster({
218
+ models,
219
+ patterns: settings.handoff.roster,
220
+ scopedModels: ctx.scopedModels,
221
+ }),
213
222
  );
214
223
 
215
224
  await consumePendingHandoffBootstrap(
@@ -1,34 +1,20 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { Api, Model } from "@earendil-works/pi-ai";
3
3
  import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
4
+ import {
5
+ type ExactModelReference,
6
+ findModelByReference,
7
+ formatModelSelection,
8
+ selectModel,
9
+ } from "../shared/model.ts";
4
10
  import { formatAvailableModelList, resolveAuthenticatedModel } from "../shared/model-resolution.ts";
5
- import { isThinkingLevel } from "../shared/thinking-levels.ts";
11
+ import { type HandoffRoster, selectFromRoster } from "./roster.ts";
6
12
 
7
13
  export function formatModelArgument(
8
14
  model: Model<Api> | undefined,
9
15
  thinkingLevel: ThinkingLevel | undefined,
10
16
  ): string | undefined {
11
- if (!model) {
12
- return undefined;
13
- }
14
-
15
- const base = `${model.provider}/${model.id}`;
16
- return thinkingLevel ? `${base}:${thinkingLevel}` : base;
17
- }
18
-
19
- export function parseModelArgument(value: string): {
20
- model: string;
21
- thinkingLevel?: ThinkingLevel | undefined;
22
- } {
23
- const separator = value.lastIndexOf(":");
24
- const suffix = separator >= 0 ? value.slice(separator + 1) : undefined;
25
- if (!isThinkingLevel(suffix)) {
26
- return { model: value };
27
- }
28
- return {
29
- model: value.slice(0, separator),
30
- thinkingLevel: suffix,
31
- };
17
+ return model ? formatModelSelection(selectModel(model, thinkingLevel)) : undefined;
32
18
  }
33
19
 
34
20
  export interface HandoffModelOverride {
@@ -36,7 +22,41 @@ export interface HandoffModelOverride {
36
22
  thinkingLevel: ThinkingLevel | undefined;
37
23
  }
38
24
 
39
- export function resolveModelOverride(
25
+ /**
26
+ * Resolves the model a handoff launches its child on. A roster replaces Pi's CLI
27
+ * matching entirely: fuzzy aliases resolve across every authenticated provider,
28
+ * which is how delegated work lands on a metered provider that happens to serve
29
+ * the same model id, so a rostered handoff accepts only an exact roster entry.
30
+ */
31
+ export function resolveChildModel(
32
+ modelRuntime: ModelRuntime,
33
+ roster: HandoffRoster | undefined,
34
+ requested: ExactModelReference,
35
+ thinkingLevel: ThinkingLevel | undefined,
36
+ ): HandoffModelOverride {
37
+ if (!roster) {
38
+ return resolveModelPattern(
39
+ modelRuntime,
40
+ formatModelSelection({ ...requested, thinkingLevel: undefined }),
41
+ thinkingLevel,
42
+ );
43
+ }
44
+
45
+ const level = selectFromRoster(roster, requested, thinkingLevel);
46
+ const model = findModelByReference(modelRuntime.getAvailableSnapshot(), requested);
47
+ if (!model) {
48
+ throw new Error(
49
+ `Model "${requested.provider}/${requested.modelId}" is no longer an available authenticated model.`,
50
+ );
51
+ }
52
+ return { model, thinkingLevel: level };
53
+ }
54
+
55
+ /**
56
+ * Resolves a user-configured model pattern with Pi's CLI grammar, including fuzzy
57
+ * aliases. Only for settings the user writes by hand; never for a model's choice.
58
+ */
59
+ export function resolveModelPattern(
40
60
  modelRuntime: ModelRuntime,
41
61
  modelPattern: string,
42
62
  thinkingLevel?: ThinkingLevel | undefined,
@@ -0,0 +1,162 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type { Api, Model } from "@earendil-works/pi-ai";
3
+ import type { ScopedModel } from "@earendil-works/pi-coding-agent";
4
+ import type { ExactModelReference, ModelSelection } from "../shared/model.ts";
5
+
6
+ export interface HandoffRosterEntry extends ExactModelReference {
7
+ /** The thinking levels this model may run at, or undefined for the agent's choice. */
8
+ thinkingLevels: readonly ThinkingLevel[] | undefined;
9
+ }
10
+
11
+ export type HandoffRoster = readonly HandoffRosterEntry[];
12
+
13
+ /**
14
+ * Chooses the roster a handoff selects from, in descending priority: an explicit
15
+ * `sessions.handoff.roster`, the session's own scoped models (`enabledModels`),
16
+ * then everything authenticated. The middle tier lets a curated model cycle
17
+ * govern delegation too; the top tier exists for when the two should disagree.
18
+ */
19
+ export function resolveHandoffRoster(options: {
20
+ models: readonly Model<Api>[];
21
+ patterns: readonly ModelSelection[];
22
+ scopedModels: readonly ScopedModel[];
23
+ }): HandoffRoster | undefined {
24
+ const configured = buildHandoffRoster(options.models, options.patterns);
25
+ if (configured) {
26
+ return configured;
27
+ }
28
+ return options.scopedModels.length > 0 ? options.scopedModels.map(scopedEntry) : undefined;
29
+ }
30
+
31
+ function scopedEntry(scoped: ScopedModel): HandoffRosterEntry {
32
+ return {
33
+ provider: scoped.model.provider,
34
+ modelId: scoped.model.id,
35
+ thinkingLevels: scoped.thinkingLevel ? [scoped.thinkingLevel] : undefined,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Expands configured roster patterns against the authenticated models. A roster
41
+ * only ever narrows what a handoff may launch, so patterns that match nothing
42
+ * are dropped rather than raised; the resulting roster is still safe, just
43
+ * smaller. An unconfigured roster returns undefined, deferring to the next tier.
44
+ */
45
+ export function buildHandoffRoster(
46
+ models: readonly Model<Api>[],
47
+ patterns: readonly ModelSelection[],
48
+ ): HandoffRoster | undefined {
49
+ if (patterns.length === 0) {
50
+ return undefined;
51
+ }
52
+
53
+ const entries = new Map<string, HandoffRosterEntry>();
54
+ for (const pattern of patterns) {
55
+ const provider = globMatcher(pattern.provider);
56
+ const modelId = globMatcher(pattern.modelId);
57
+ for (const model of models) {
58
+ if (!provider.test(model.provider) || !modelId.test(model.id)) {
59
+ continue;
60
+ }
61
+ const key = `${model.provider}/${model.id}`;
62
+ const existing = entries.get(key);
63
+ entries.set(key, {
64
+ provider: model.provider,
65
+ modelId: model.id,
66
+ thinkingLevels: mergeThinkingLevels(existing, pattern.thinkingLevel),
67
+ });
68
+ }
69
+ }
70
+
71
+ return [...entries.values()];
72
+ }
73
+
74
+ /**
75
+ * Narrows a requested model to its roster entry. An entry listing a single
76
+ * thinking level pins it, so the agent may leave the level out; an entry listing
77
+ * several makes the choice mandatory.
78
+ */
79
+ export function selectFromRoster(
80
+ roster: HandoffRoster,
81
+ requested: ExactModelReference,
82
+ thinkingLevel: ThinkingLevel | undefined,
83
+ ): ThinkingLevel | undefined {
84
+ if (roster.length === 0) {
85
+ throw new Error(
86
+ "session_handoff cannot override the model: sessions.handoff.roster matches no authenticated model.",
87
+ );
88
+ }
89
+
90
+ const entry = roster.find(
91
+ (candidate) =>
92
+ candidate.provider === requested.provider && candidate.modelId === requested.modelId,
93
+ );
94
+ if (!entry) {
95
+ throw new Error(
96
+ `Model "${requested.provider}/${requested.modelId}" is not on the handoff roster. Allowed models: ${formatRoster(roster)}.`,
97
+ );
98
+ }
99
+
100
+ const allowed = entry.thinkingLevels;
101
+ if (!allowed) {
102
+ return thinkingLevel;
103
+ }
104
+ if (!thinkingLevel) {
105
+ if (allowed.length > 1) {
106
+ throw new Error(
107
+ `The handoff roster requires a thinking level for "${formatEntryReference(entry)}": one of ${allowed.join(", ")}.`,
108
+ );
109
+ }
110
+ return allowed[0];
111
+ }
112
+ if (!allowed.includes(thinkingLevel)) {
113
+ throw new Error(
114
+ `The handoff roster does not allow thinking level ${thinkingLevel} for "${formatEntryReference(entry)}"; allowed: ${allowed.join(", ")}.`,
115
+ );
116
+ }
117
+ return thinkingLevel;
118
+ }
119
+
120
+ export function formatRoster(roster: HandoffRoster): string {
121
+ return roster.map(formatRosterEntry).join(", ");
122
+ }
123
+
124
+ export function formatRosterEntry(entry: HandoffRosterEntry): string {
125
+ const reference = formatEntryReference(entry);
126
+ if (!entry.thinkingLevels) {
127
+ return reference;
128
+ }
129
+ return entry.thinkingLevels.length === 1
130
+ ? `${reference}:${entry.thinkingLevels[0]}`
131
+ : `${reference}:{${entry.thinkingLevels.join(",")}}`;
132
+ }
133
+
134
+ function formatEntryReference(entry: ExactModelReference): string {
135
+ return `${entry.provider}/${entry.modelId}`;
136
+ }
137
+
138
+ function mergeThinkingLevels(
139
+ existing: HandoffRosterEntry | undefined,
140
+ added: ThinkingLevel | undefined,
141
+ ): readonly ThinkingLevel[] | undefined {
142
+ if (!added) {
143
+ return undefined;
144
+ }
145
+ if (!existing) {
146
+ return [added];
147
+ }
148
+ if (!existing.thinkingLevels) {
149
+ return undefined;
150
+ }
151
+ return existing.thinkingLevels.includes(added)
152
+ ? existing.thinkingLevels
153
+ : [...existing.thinkingLevels, added];
154
+ }
155
+
156
+ function globMatcher(pattern: string): RegExp {
157
+ const source = pattern
158
+ .split("*")
159
+ .map((segment) => segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
160
+ .join(".*");
161
+ return new RegExp(`^${source}$`);
162
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import { Type } from "typebox";
3
- import { formatAvailableModelList } from "../shared/model-resolution.ts";
4
3
  import { type HandoffLaunchTarget, SUBAGENT_LAUNCH } from "./launch-target.ts";
4
+ import { formatRosterEntry, type HandoffRoster } from "./roster.ts";
5
5
 
6
6
  export function buildHandoffLaunchSchema(targets: readonly HandoffLaunchTarget[]) {
7
7
  const descriptions = targets.flatMap((target) =>
@@ -31,6 +31,9 @@ const MODEL_INHERITANCE_GUIDELINES = [
31
31
  "To run the handoff on a different model, set both provider and model together (both are required).",
32
32
  ];
33
33
 
34
+ const RESTRICTED_THINKING_GUIDELINE =
35
+ '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
+
34
37
  const SUBAGENT_MODEL_OVERRIDE_GUIDELINE =
35
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.";
36
39
 
@@ -40,15 +43,19 @@ const DEFAULT_MODEL_OVERRIDE_GUIDELINE =
40
43
  export function buildHandoffPromptGuidelines(
41
44
  targets: readonly HandoffLaunchTarget[],
42
45
  models: readonly Model<Api>[],
46
+ roster: HandoffRoster | undefined,
43
47
  ): string[] {
44
48
  const hasSubagent = targets.some((target) => target.value === SUBAGENT_LAUNCH);
49
+ const selectable =
50
+ roster?.map(formatRosterEntry) ?? models.map((model) => `${model.provider}/${model.id}`);
45
51
  return [
46
52
  ...(hasSubagent ? SUBAGENT_GUIDELINES : []),
47
53
  LAUNCH_TARGET_GUIDELINE,
48
54
  ...MODEL_INHERITANCE_GUIDELINES,
49
55
  hasSubagent ? SUBAGENT_MODEL_OVERRIDE_GUIDELINE : DEFAULT_MODEL_OVERRIDE_GUIDELINE,
50
- ...(models.length > 0
51
- ? [`Available models, given as provider/model-id: ${formatAvailableModelList(models)}.`]
56
+ ...(selectable.length > 0
57
+ ? [`Available models, given as provider/model-id: ${selectable.join(", ")}.`]
52
58
  : []),
59
+ ...(roster?.some((entry) => entry.thinkingLevels) ? [RESTRICTED_THINKING_GUIDELINE] : []),
53
60
  ];
54
61
  }
@@ -12,8 +12,9 @@ import {
12
12
  type HandoffLaunchValue,
13
13
  } from "./launch-target.ts";
14
14
  import { createChildGeneratedHandoffBootstrap } from "./metadata.ts";
15
- import { formatModelArgument, resolveModelOverride } from "./model.ts";
15
+ import { formatModelArgument, resolveChildModel } from "./model.ts";
16
16
  import { buildLaunchReceipt } from "./receipt.ts";
17
+ import type { HandoffRoster } from "./roster.ts";
17
18
  import { formatHandoffLaunchFailure, prepareHandoffLaunch } from "./spawn.ts";
18
19
  import type { HandoffToolDetails } from "./tool-contract.ts";
19
20
 
@@ -39,6 +40,7 @@ export async function executeSessionHandoffTool(
39
40
  ctx: ExtensionContext,
40
41
  modelRuntime: ModelRuntime,
41
42
  launchTargets: readonly HandoffLaunchTarget[],
43
+ roster: HandoffRoster | undefined,
42
44
  recordClipboardStatus: (sessionId: string, status: ClipboardStatus) => void,
43
45
  ) {
44
46
  const goal = params.goal.trim();
@@ -90,7 +92,7 @@ export async function executeSessionHandoffTool(
90
92
  }
91
93
  const override =
92
94
  provider && modelId
93
- ? resolveModelOverride(modelRuntime, `${provider}/${modelId}`, params.thinkingLevel)
95
+ ? resolveChildModel(modelRuntime, roster, { provider, modelId }, params.thinkingLevel)
94
96
  : undefined;
95
97
  const childModel = override?.model ?? ctx.model;
96
98
  const thinkingLevel = override?.thinkingLevel ?? params.thinkingLevel ?? pi.getThinkingLevel();
@@ -1,10 +1,51 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
1
2
  import type { Api, Model } from "@earendil-works/pi-ai";
3
+ import { isThinkingLevel } from "./thinking-levels.ts";
2
4
 
3
5
  export interface ExactModelReference {
4
6
  provider: string;
5
7
  modelId: string;
6
8
  }
7
9
 
10
+ /**
11
+ * The structured representation of "which model, at which thinking level". Pi's
12
+ * CLI grammar spells this `provider/model-id[:level]`; that string is an edge
13
+ * format, parsed on the way in and rebuilt on the way out.
14
+ */
15
+ export interface ModelSelection extends ExactModelReference {
16
+ thinkingLevel: ThinkingLevel | undefined;
17
+ }
18
+
19
+ export function parseModelSelection(value: string): ModelSelection {
20
+ const separator = value.lastIndexOf(":");
21
+ const suffix = separator >= 0 ? value.slice(separator + 1) : undefined;
22
+ const thinkingLevel = isThinkingLevel(suffix) ? suffix : undefined;
23
+ const reference = thinkingLevel ? value.slice(0, separator) : value;
24
+
25
+ const boundary = reference.indexOf("/");
26
+ if (boundary <= 0 || boundary === reference.length - 1) {
27
+ throw new Error(`Expected a "provider/model-id" reference; got "${value}".`);
28
+ }
29
+
30
+ return {
31
+ provider: reference.slice(0, boundary),
32
+ modelId: reference.slice(boundary + 1),
33
+ thinkingLevel,
34
+ };
35
+ }
36
+
37
+ export function formatModelSelection(selection: ModelSelection): string {
38
+ const reference = `${selection.provider}/${selection.modelId}`;
39
+ return selection.thinkingLevel ? `${reference}:${selection.thinkingLevel}` : reference;
40
+ }
41
+
42
+ export function selectModel(
43
+ model: Model<Api>,
44
+ thinkingLevel: ThinkingLevel | undefined,
45
+ ): ModelSelection {
46
+ return { provider: model.provider, modelId: model.id, thinkingLevel };
47
+ }
48
+
8
49
  export function findModelByReference(
9
50
  availableModels: readonly Model<Api>[],
10
51
  reference: ExactModelReference,
@@ -4,6 +4,7 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
4
4
  import { getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
5
5
  import type { KeyId } from "@earendil-works/pi-tui";
6
6
  import { type Static, Type } from "typebox";
7
+ import { type ModelSelection, parseModelSelection } from "./model.ts";
7
8
  import { isThinkingLevel } from "./thinking-levels.ts";
8
9
  import { parseTypeBoxValue } from "./typebox.ts";
9
10
 
@@ -30,6 +31,7 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
30
31
  persistRuns: Type.Optional(Type.Boolean()),
31
32
  model: Type.Optional(Type.String()),
32
33
  thinkingLevel: Type.Optional(Type.String()),
34
+ roster: Type.Optional(Type.Array(Type.String())),
33
35
  deferred: Type.Optional(
34
36
  Type.Object({
35
37
  copyToClipboard: Type.Optional(Type.Boolean()),
@@ -97,6 +99,8 @@ export interface AskSettings extends AgentModelSettings {
97
99
  export interface HandoffSettings extends AgentModelSettings {
98
100
  pickerShortcut: KeyId;
99
101
  persistRuns: boolean;
102
+ /** Patterns naming the models a handoff may launch a child on. Empty means unrestricted. */
103
+ roster: readonly ModelSelection[];
100
104
  deferred: {
101
105
  copyToClipboard: boolean;
102
106
  };
@@ -182,6 +186,19 @@ function normalizePickerShortcut(value: string | undefined): KeyId {
182
186
  return (trimmed ? trimmed : "alt+o") as KeyId;
183
187
  }
184
188
 
189
+ function normalizeRoster(value: readonly string[] | undefined): readonly ModelSelection[] {
190
+ return (value ?? [])
191
+ .map((pattern) => pattern.trim())
192
+ .filter(Boolean)
193
+ .map((pattern) => {
194
+ try {
195
+ return parseModelSelection(pattern);
196
+ } catch (error) {
197
+ throw new Error(`Invalid sessions.handoff.roster entry: ${(error as Error).message}`);
198
+ }
199
+ });
200
+ }
201
+
185
202
  function parseThinkingLevel(value: string | undefined): ThinkingLevel | undefined {
186
203
  const trimmed = value?.trim();
187
204
  return isThinkingLevel(trimmed) ? trimmed : undefined;
@@ -238,6 +255,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
238
255
  ...resolveAgentModelSettings(fileSettings.handoff),
239
256
  pickerShortcut: normalizePickerShortcut(fileSettings.handoff?.pickerShortcut),
240
257
  persistRuns: fileSettings.handoff?.persistRuns ?? false,
258
+ roster: normalizeRoster(fileSettings.handoff?.roster),
241
259
  deferred: {
242
260
  copyToClipboard: fileSettings.handoff?.deferred?.copyToClipboard ?? true,
243
261
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, messaging, auto-titling, and indexing tools",
6
6
  "license": "MIT",