pi-ui-extend 0.1.34 → 0.1.35

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.
Files changed (58) hide show
  1. package/README.md +20 -0
  2. package/dist/app/app.js +4 -2
  3. package/dist/app/constants.d.ts +2 -1
  4. package/dist/app/constants.js +6 -1
  5. package/dist/app/input/input-controller.d.ts +4 -1
  6. package/dist/app/input/input-controller.js +95 -16
  7. package/dist/app/input/input-paste-handler.js +3 -1
  8. package/dist/app/input/terminal-edit-shortcuts.d.ts +20 -0
  9. package/dist/app/input/terminal-edit-shortcuts.js +50 -16
  10. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  11. package/dist/app/rendering/conversation-entry-renderer.js +1 -1
  12. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  13. package/dist/app/rendering/conversation-tool-renderer.js +21 -0
  14. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  15. package/dist/app/rendering/conversation-viewport.js +41 -5
  16. package/dist/app/rendering/editor-layout-renderer.js +3 -2
  17. package/dist/app/rendering/editor-panels.js +27 -10
  18. package/dist/app/runtime.d.ts +1 -0
  19. package/dist/app/runtime.js +33 -14
  20. package/dist/app/session/session-event-controller.d.ts +7 -0
  21. package/dist/app/session/session-event-controller.js +78 -0
  22. package/dist/app/session/tabs-controller.js +3 -1
  23. package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
  24. package/dist/app/subagents/subagents-widget-controller.js +141 -70
  25. package/dist/app/terminal/terminal-controller.d.ts +10 -0
  26. package/dist/app/terminal/terminal-controller.js +91 -2
  27. package/dist/app/todo/todo-model.js +2 -0
  28. package/dist/app/todo/todo-widget-controller.d.ts +2 -0
  29. package/dist/app/todo/todo-widget-controller.js +17 -7
  30. package/dist/app/types.d.ts +4 -0
  31. package/dist/bundled-extensions/question/tui.js +8 -1
  32. package/dist/bundled-extensions/session-title/index.js +65 -14
  33. package/dist/input-editor-files.js +23 -4
  34. package/dist/markdown-format.d.ts +4 -1
  35. package/dist/markdown-format.js +76 -9
  36. package/external/pi-tools-suite/README.md +71 -1
  37. package/external/pi-tools-suite/package.json +3 -3
  38. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
  39. package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
  40. package/external/pi-tools-suite/src/context-usage.ts +6 -1
  41. package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
  42. package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
  43. package/external/pi-tools-suite/src/dcp/config.ts +142 -6
  44. package/external/pi-tools-suite/src/dcp/index.ts +20 -8
  45. package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
  46. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
  47. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
  48. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  49. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
  50. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
  51. package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
  52. package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
  53. package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
  54. package/external/pi-tools-suite/src/todo/index.ts +87 -16
  55. package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
  56. package/external/pi-tools-suite/src/todo/todo.ts +49 -6
  57. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  58. package/package.json +7 -5
@@ -4,6 +4,24 @@ import { loadSessionTitleConfig } from "./config.js";
4
4
  import { fallbackSessionTitleFromInput, firstUserMessageText as firstUserMessageTextFromEntries, generateSessionTitle, sessionTitleModelRefs, } from "./title-generation.js";
5
5
  export { fallbackSessionTitleFromInput, generateSessionTitle, sessionTitleModelRefs, sanitizeSessionTitle } from "./title-generation.js";
6
6
  const DEFAULT_TERMINAL_TITLE = "pi";
7
+ function isStaleExtensionContextError(error) {
8
+ if (!(error instanceof Error))
9
+ return false;
10
+ return /ctx is stale|stale ctx|stale after session replacement|stale after.*reload/i.test(error.message);
11
+ }
12
+ function ignoreStaleExtensionContextError(error) {
13
+ if (!isStaleExtensionContextError(error))
14
+ throw error;
15
+ }
16
+ function staleSafe(callback) {
17
+ try {
18
+ return callback();
19
+ }
20
+ catch (error) {
21
+ ignoreStaleExtensionContextError(error);
22
+ return undefined;
23
+ }
24
+ }
7
25
  function imageAttachmentLabel(images) {
8
26
  if (images.length === 0)
9
27
  return undefined;
@@ -76,14 +94,23 @@ export default function sessionTitle(pi) {
76
94
  clearTimeout(timer);
77
95
  refreshTimers.clear();
78
96
  }
97
+ function safeCtxCall(callback) {
98
+ return staleSafe(callback);
99
+ }
100
+ function safePiCall(callback) {
101
+ return staleSafe(callback);
102
+ }
103
+ function currentSessionId(ctx) {
104
+ return safeCtxCall(() => ctx.sessionManager.getSessionId());
105
+ }
79
106
  function currentSessionName(ctx) {
80
- const name = pi.getSessionName() ?? ctx?.sessionManager.getSessionName?.();
107
+ const name = safePiCall(() => pi.getSessionName()) ?? safeCtxCall(() => ctx?.sessionManager.getSessionName?.());
81
108
  return name?.trim() || undefined;
82
109
  }
83
110
  function shouldGeneratePendingTitle(ctx) {
84
111
  if (!pendingGeneration)
85
112
  return false;
86
- if (pendingGeneration.sessionId !== ctx.sessionManager.getSessionId())
113
+ if (pendingGeneration.sessionId !== currentSessionId(ctx))
87
114
  return false;
88
115
  const name = currentSessionName(ctx);
89
116
  if (!name)
@@ -110,7 +137,12 @@ export default function sessionTitle(pi) {
110
137
  const safeTitle = terminalSafeText(title) || DEFAULT_TERMINAL_TITLE;
111
138
  if (!force && safeTitle === lastRenderedTitle)
112
139
  return;
113
- ctx.ui.setTitle(safeTitle);
140
+ const rendered = safeCtxCall(() => {
141
+ ctx.ui.setTitle(safeTitle);
142
+ return true;
143
+ });
144
+ if (!rendered)
145
+ return;
114
146
  lastRenderedTitle = safeTitle;
115
147
  }
116
148
  function refreshSessionUi(ctx, options = {}) {
@@ -130,7 +162,7 @@ export default function sessionTitle(pi) {
130
162
  for (const delayMs of [0, 100, 500, 1500, 3000]) {
131
163
  const timer = setTimeout(() => {
132
164
  refreshTimers.delete(timer);
133
- refreshSessionUi(ctx, { reapplyTitle: true });
165
+ safeCtxCall(() => refreshSessionUi(ctx, { reapplyTitle: true }));
134
166
  }, delayMs);
135
167
  timer.unref?.();
136
168
  refreshTimers.add(timer);
@@ -146,7 +178,7 @@ export default function sessionTitle(pi) {
146
178
  return;
147
179
  retryTimer = setTimeout(() => {
148
180
  retryTimer = undefined;
149
- startTitleGeneration(ctx, currentConfig);
181
+ safeCtxCall(() => startTitleGeneration(ctx, currentConfig));
150
182
  }, currentConfig.retryDelayMs);
151
183
  retryTimer.unref?.();
152
184
  }
@@ -157,7 +189,11 @@ export default function sessionTitle(pi) {
157
189
  const fallbackTitle = fallbackSessionTitleFromInput(input, currentConfig.maxTitleChars);
158
190
  if (!fallbackTitle)
159
191
  return false;
160
- pi.setSessionName(fallbackTitle);
192
+ if (!safePiCall(() => {
193
+ pi.setSessionName(fallbackTitle);
194
+ return true;
195
+ }))
196
+ return false;
161
197
  refreshSessionUi(ctx, { force: true });
162
198
  scheduleSessionUiRefresh(ctx);
163
199
  return true;
@@ -185,36 +221,47 @@ export default function sessionTitle(pi) {
185
221
  abortCurrentRequest();
186
222
  controller = new AbortController();
187
223
  const requestController = controller;
188
- const currentSessionId = pendingGeneration.sessionId;
224
+ const pendingSessionId = pendingGeneration.sessionId;
189
225
  const generation = { ...pendingGeneration, modelRef };
190
226
  void (async () => {
191
227
  try {
192
- const title = await generateSessionTitle(generation.input, ctx.modelRegistry, currentConfig, generation.modelRef, requestController.signal, currentConfig.debug && ctx.hasUI ? (message) => ctx.ui.notify(message, "warning") : undefined);
228
+ const notifyDebug = currentConfig.debug && ctx.hasUI
229
+ ? (message) => {
230
+ safeCtxCall(() => ctx.ui.notify(message, "warning"));
231
+ }
232
+ : undefined;
233
+ const title = await generateSessionTitle(generation.input, ctx.modelRegistry, currentConfig, generation.modelRef, requestController.signal, notifyDebug);
193
234
  if (!title || requestController.signal.aborted)
194
235
  return;
195
- if (sessionId !== currentSessionId)
236
+ if (sessionId !== pendingSessionId)
237
+ return;
238
+ if (pendingSessionId !== currentSessionId(ctx))
196
239
  return;
197
240
  if (!shouldGeneratePendingTitle(ctx))
198
241
  return;
199
- pi.setSessionName(title);
242
+ if (!safePiCall(() => {
243
+ pi.setSessionName(title);
244
+ return true;
245
+ }))
246
+ return;
200
247
  pendingGeneration = undefined;
201
248
  refreshSessionUi(ctx, { force: true });
202
249
  scheduleSessionUiRefresh(ctx);
203
250
  if (currentConfig.notify && ctx.hasUI)
204
- ctx.ui.notify(`Session named: ${title}`, "info");
251
+ safeCtxCall(() => ctx.ui.notify(`Session named: ${title}`, "info"));
205
252
  }
206
253
  catch (error) {
207
254
  if (requestController.signal.aborted)
208
255
  return;
209
256
  if (currentConfig.debug && ctx.hasUI) {
210
257
  const message = error instanceof Error ? error.message : String(error);
211
- ctx.ui.notify(`Session title generation failed: ${message}`, "warning");
258
+ safeCtxCall(() => ctx.ui.notify(`Session title generation failed: ${message}`, "warning"));
212
259
  }
213
260
  }
214
261
  finally {
215
262
  if (controller === requestController)
216
263
  controller = undefined;
217
- if (requestController.signal.aborted || pendingGeneration?.sessionId !== currentSessionId)
264
+ if (requestController.signal.aborted || pendingGeneration?.sessionId !== pendingSessionId)
218
265
  return;
219
266
  if (shouldGeneratePendingTitle(ctx)) {
220
267
  if (!advancePendingGeneration(currentConfig)) {
@@ -363,7 +410,11 @@ export default function sessionTitle(pi) {
363
410
  : fallbackInput;
364
411
  const provisionalSessionName = fallbackSessionTitleFromInput(fallbackTitleInput, currentConfig.maxTitleChars);
365
412
  if (provisionalSessionName && (!currentName || activeForkTitleState)) {
366
- pi.setSessionName(provisionalSessionName);
413
+ if (!safePiCall(() => {
414
+ pi.setSessionName(provisionalSessionName);
415
+ return true;
416
+ }))
417
+ return { action: "continue" };
367
418
  refreshSessionUi(ctx, { force: true });
368
419
  scheduleSessionUiRefresh(ctx);
369
420
  }
@@ -1,5 +1,24 @@
1
+ import { createRequire } from "node:module";
2
+ import { dirname, join } from "node:path";
3
+ import { pathToFileURL } from "node:url";
1
4
  import { resizeImage } from "@earendil-works/pi-coding-agent";
2
- import { hasImage, getImageBinary } from "@mariozechner/clipboard";
5
+ const moduleRequire = createRequire(import.meta.url);
6
+ const executableDirRequire = createRequire(pathToFileURL(join(dirname(process.execPath), "package.json")).href);
7
+ function loadClipboardNative(requires = [moduleRequire, executableDirRequire]) {
8
+ for (const requireClipboard of requires) {
9
+ try {
10
+ return requireClipboard("@mariozechner/clipboard");
11
+ }
12
+ catch {
13
+ // Try the next resolution root. This mirrors pi's packaged-binary fallback,
14
+ // where native sidecars may resolve relative to the executable directory.
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+ const nativeClipboard = !process.env.TERMUX_VERSION && (process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY))
20
+ ? loadClipboardNative()
21
+ : null;
3
22
  /**
4
23
  * Read an image from the system clipboard.
5
24
  * Uses the native @mariozechner/clipboard N-API module for direct clipboard
@@ -7,12 +26,12 @@ import { hasImage, getImageBinary } from "@mariozechner/clipboard";
7
26
  */
8
27
  export async function readClipboardImage() {
9
28
  try {
10
- if (!hasImage())
29
+ if (!nativeClipboard?.hasImage())
11
30
  return null;
12
- const bytes = await getImageBinary();
31
+ const bytes = await nativeClipboard.getImageBinary();
13
32
  if (!bytes || bytes.length === 0)
14
33
  return null;
15
- const uint8 = new Uint8Array(bytes);
34
+ const uint8 = bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes);
16
35
  try {
17
36
  const resized = await resizeImage(uint8, "image/png", { maxWidth: 2000, maxHeight: 2000 });
18
37
  if (resized)
@@ -22,8 +22,11 @@ export type RenderedMarkdownTextLine = {
22
22
  syntaxHighlight?: SyntaxLineHighlight | undefined;
23
23
  heading?: boolean;
24
24
  };
25
+ export type RenderMarkdownTextLinesOptions = {
26
+ preserveWrappedWordSeparator?: boolean;
27
+ };
25
28
  export declare function formatMarkdownTables(text: string, maxWidth?: number): string;
26
29
  export declare function renderMarkdownLine(text: string, start?: number): RenderedMarkdownLine;
27
- export declare function renderMarkdownTextLines(text: string, width: number, start?: number): RenderedMarkdownTextLine[];
30
+ export declare function renderMarkdownTextLines(text: string, width: number, start?: number, options?: RenderMarkdownTextLinesOptions): RenderedMarkdownTextLine[];
28
31
  export declare function markdownSyntaxHighlightsForText(text: string, startColumn?: number): ToolBodySyntaxHighlights;
29
32
  export declare function isOnlyHiddenMetadata(text: string): boolean;
@@ -1,5 +1,6 @@
1
1
  import { expandTabs, stringDisplayWidth } from "./terminal-width.js";
2
2
  import { syntaxHighlightLanguageForMarkdownFence, } from "./syntax-highlight.js";
3
+ const MIN_TRAILING_WORD_WIDTH_TO_REBALANCE = 5;
3
4
  export function formatMarkdownTables(text, maxWidth) {
4
5
  const lines = text.split("\n");
5
6
  const formatted = [];
@@ -70,7 +71,7 @@ export function renderMarkdownLine(text, start = 0) {
70
71
  }
71
72
  return { text: rendered, segments, ...(isHeading ? { heading: true } : {}) };
72
73
  }
73
- export function renderMarkdownTextLines(text, width, start = 0) {
74
+ export function renderMarkdownTextLines(text, width, start = 0, options = {}) {
74
75
  const lines = [];
75
76
  let fence;
76
77
  const formattedText = formatMarkdownTables(sanitizeMarkdownText(text), width);
@@ -83,7 +84,7 @@ export function renderMarkdownTextLines(text, width, start = 0) {
83
84
  const syntaxHighlight = markdownLineSyntaxHighlight(fence, Boolean(opensFence || closesFence), start);
84
85
  const isHeadingLine = !fence && /^\s{0,3}#{1,6}\s/.test(rawLine);
85
86
  const markdownLine = syntaxHighlight?.language === "markdown" || isHeadingLine ? renderMarkdownLine(rawLine) : undefined;
86
- for (const wrapped of wrapRenderedMarkdownLine(markdownLine ?? { text: rawLine, segments: [] }, width)) {
87
+ for (const wrapped of wrapRenderedMarkdownLine(markdownLine ?? { text: rawLine, segments: [] }, width, options)) {
87
88
  lines.push({
88
89
  text: wrapped.text,
89
90
  ...(wrapped.copyText === undefined ? {} : { copyText: wrapped.copyText }),
@@ -122,11 +123,11 @@ export function markdownSyntaxHighlightsForText(text, startColumn = 0) {
122
123
  }
123
124
  return highlights;
124
125
  }
125
- function wrapRenderedMarkdownLine(line, width) {
126
+ function wrapRenderedMarkdownLine(line, width, options) {
126
127
  const safeWidth = Math.max(1, width);
127
128
  if (stringDisplayWidth(line.text) <= safeWidth)
128
129
  return [line];
129
- const ranges = wrapDisplayLineByWordsWithRanges(line.text, safeWidth);
130
+ const ranges = wrapDisplayLineByWordsWithRanges(line.text, safeWidth, options);
130
131
  return ranges.map((range, index) => ({
131
132
  text: range.text,
132
133
  copyText: line.text.slice(range.start, ranges[index + 1]?.start ?? range.end),
@@ -134,7 +135,7 @@ function wrapRenderedMarkdownLine(line, width) {
134
135
  segments: line.segments.flatMap((segment) => shiftSegmentToRange(segment, range.start, range.end)),
135
136
  }));
136
137
  }
137
- function wrapDisplayLineByWordsWithRanges(text, width) {
138
+ function wrapDisplayLineByWordsWithRanges(text, width, options) {
138
139
  const chunks = [];
139
140
  let chunkText = "";
140
141
  let chunkStart = 0;
@@ -165,10 +166,19 @@ function wrapDisplayLineByWordsWithRanges(text, width) {
165
166
  chunkEnd = token.end;
166
167
  }
167
168
  else {
168
- chunks.push(trimTrailingWhitespaceChunk(chunkText, chunkStart));
169
- chunkText = "";
170
- chunkStart = token.end;
171
- chunkEnd = token.end;
169
+ const rewrapped = options.preserveWrappedWordSeparator
170
+ ? splitChunkBeforeTrailingWord(chunkText, chunkStart, token, width)
171
+ : undefined;
172
+ if (rewrapped) {
173
+ chunks.push(rewrapped.previous);
174
+ setChunk(rewrapped.next);
175
+ }
176
+ else {
177
+ chunks.push(trimTrailingWhitespaceChunk(chunkText, chunkStart));
178
+ chunkText = "";
179
+ chunkStart = token.end;
180
+ chunkEnd = chunkStart;
181
+ }
172
182
  }
173
183
  continue;
174
184
  }
@@ -182,6 +192,14 @@ function wrapDisplayLineByWordsWithRanges(text, width) {
182
192
  chunkEnd = token.end;
183
193
  continue;
184
194
  }
195
+ const rewrapped = options.preserveWrappedWordSeparator
196
+ ? splitChunkBeforeTrailingWordWithNextToken(chunkText, chunkStart, token, width)
197
+ : undefined;
198
+ if (rewrapped) {
199
+ chunks.push(rewrapped.previous);
200
+ setChunk(rewrapped.next);
201
+ continue;
202
+ }
185
203
  chunks.push(trimTrailingWhitespaceChunk(chunkText, chunkStart));
186
204
  appendTokenToEmptyChunk(token);
187
205
  }
@@ -238,6 +256,55 @@ function trimTrailingWhitespaceChunk(text, start) {
238
256
  const trimmed = text.replace(/\s+$/u, "");
239
257
  return { text: trimmed, start, end: start + trimmed.length };
240
258
  }
259
+ function splitChunkBeforeTrailingWord(chunkText, chunkStart, separator, width) {
260
+ const match = /^(.*\S)(\s+)(\S+)$/u.exec(chunkText);
261
+ if (!match)
262
+ return undefined;
263
+ const prefix = match[1] ?? "";
264
+ const whitespace = match[2] ?? "";
265
+ const trailingWord = match[3] ?? "";
266
+ if (!prefix || !whitespace || !trailingWord)
267
+ return undefined;
268
+ const nextText = `${trailingWord}${separator.text}`;
269
+ if (stringDisplayWidth(nextText) > width)
270
+ return undefined;
271
+ const previous = trimTrailingWhitespaceChunk(prefix, chunkStart);
272
+ if (!previous.text)
273
+ return undefined;
274
+ const trailingWordStart = chunkStart + prefix.length + whitespace.length;
275
+ return {
276
+ previous,
277
+ next: { text: nextText, start: trailingWordStart, end: separator.end },
278
+ };
279
+ }
280
+ function splitChunkBeforeTrailingWordWithNextToken(chunkText, chunkStart, nextToken, width) {
281
+ const match = /^(.*\S)(\s+)(\S+)(\s+)$/u.exec(chunkText);
282
+ if (!match)
283
+ return undefined;
284
+ const prefix = match[1] ?? "";
285
+ const whitespace = match[2] ?? "";
286
+ const trailingWord = match[3] ?? "";
287
+ const trailingWhitespace = match[4] ?? "";
288
+ if (!prefix || !whitespace || !trailingWord || !trailingWhitespace)
289
+ return undefined;
290
+ if (!shouldMoveTrailingWordToPreserveSeparator(prefix, trailingWord, width))
291
+ return undefined;
292
+ const nextText = `${trailingWord}${trailingWhitespace}${nextToken.text}`;
293
+ if (stringDisplayWidth(nextText) > width)
294
+ return undefined;
295
+ const previous = trimTrailingWhitespaceChunk(prefix, chunkStart);
296
+ if (!previous.text)
297
+ return undefined;
298
+ const trailingWordStart = chunkStart + prefix.length + whitespace.length;
299
+ return {
300
+ previous,
301
+ next: { text: nextText, start: trailingWordStart, end: nextToken.end },
302
+ };
303
+ }
304
+ function shouldMoveTrailingWordToPreserveSeparator(prefix, trailingWord, width) {
305
+ return stringDisplayWidth(prefix) >= Math.floor(width / 2)
306
+ && stringDisplayWidth(trailingWord) >= MIN_TRAILING_WORD_WIDTH_TO_REBALANCE;
307
+ }
241
308
  function shiftSegmentToRange(segment, rangeStart, rangeEnd) {
242
309
  const start = Math.max(segment.start, rangeStart);
243
310
  const end = Math.min(segment.end, rangeEnd);
@@ -68,12 +68,62 @@ DCP settings are stored only under `dcp` in the user shared config file `~/.conf
68
68
  "nudgeFrequency": 1,
69
69
  "iterationNudgeThreshold": 6,
70
70
  "protectedTools": ["compress", "write", "edit", "subagents"]
71
+ },
72
+ "modelOverrides": {
73
+ "openai-codex/gpt-5.5": {
74
+ "compress": {
75
+ "minContextPercent": "28%",
76
+ "maxContextPercent": "48%"
77
+ }
78
+ },
79
+ "openai-codex/gpt-5.4-mini": {
80
+ "compress": {
81
+ "minContextPercent": "20%",
82
+ "maxContextPercent": "38%"
83
+ }
84
+ },
85
+ "zai/*": {
86
+ "compress": {
87
+ "minContextPercent": "16%",
88
+ "maxContextPercent": "30%"
89
+ }
90
+ },
91
+ "antigravity/*sonnet*": {
92
+ "compress": {
93
+ "minContextPercent": "22%",
94
+ "maxContextPercent": "40%"
95
+ }
96
+ },
97
+ "antigravity/gemini-3.1-pro*": {
98
+ "compress": {
99
+ "minContextPercent": "24%",
100
+ "maxContextPercent": "42%"
101
+ }
102
+ },
103
+ "antigravity/gemini-3-flash*": {
104
+ "compress": {
105
+ "minContextPercent": "18%",
106
+ "maxContextPercent": "34%"
107
+ }
108
+ },
109
+ "antigravity/gemini-2.5-flash*": {
110
+ "compress": {
111
+ "minContextPercent": "18%",
112
+ "maxContextPercent": "32%"
113
+ }
114
+ },
115
+ "antigravity/antigravity-claude-opus-4-6-thinking": {
116
+ "compress": {
117
+ "minContextPercent": "26%",
118
+ "maxContextPercent": "44%"
119
+ }
120
+ }
71
121
  }
72
122
  }
73
123
  }
74
124
  ```
75
125
 
76
- `minContextPercent` / `maxContextPercent` accept legacy fractions (`0.25`), percent strings (`"25%"`), or absolute token counts when Pi knows the current model context window. `minContextLimit` / `maxContextLimit` and `modelMinContextLimits` / `modelMaxContextLimits` are explicit absolute-or-percent aliases. If `compress.protectUserMessages` is enabled, range compression appends selected user messages verbatim instead of rejecting the range; individual message compression still skips protected raw user messages. Protected tool outputs are copied into summaries for tools protected by name or `protectedFilePatterns`; protected `subagents` result reads also try to include the saved `result.md` artifact when available.
126
+ `minContextPercent` / `maxContextPercent` accept legacy fractions (`0.25`), percent strings (`"25%"`), or absolute token counts when Pi knows the current model context window. `minContextLimit` / `maxContextLimit` and `modelMinContextLimits` / `modelMaxContextLimits` are explicit absolute-or-percent aliases. `modelOverrides` and the `modelMin*` / `modelMax*` maps support exact model keys plus `*` / `?` wildcard patterns; matching is applied from generic to specific so exact bare-model matches override bare wildcards, and exact `provider/model` matches override provider wildcards. Array fields are union-merged, so model-specific `protectedTools` extend the defaults instead of replacing them. If `compress.protectUserMessages` is enabled, range compression appends selected user messages verbatim instead of rejecting the range; individual message compression still skips protected raw user messages. Protected tool outputs are copied into summaries for tools protected by name or `protectedFilePatterns`; protected `subagents` result reads also try to include the saved `result.md` artifact when available.
77
127
 
78
128
  ## LSP setup
79
129
 
@@ -271,6 +321,26 @@ npm run test:e2e
271
321
 
272
322
  Supporting docs and historical standalone README content are kept in `docs/`; third-party license texts are kept in `licenses/`.
273
323
 
324
+ ## SDK pin
325
+
326
+ This suite runs inside the Pi host process, so its `@earendil-works/*`
327
+ peerDependencies (`pi-ai`, `pi-coding-agent`, `pi-tui`) must match the host Pi
328
+ SDK version exactly. Otherwise npm can resolve a stale copy in this package's
329
+ own `node_modules` and cause a double-load (e.g. `0.75.4` here vs `0.79.4` in
330
+ the host).
331
+
332
+ The host repo keeps these aligned: `npm run sync:sdk-pin` rewrites these
333
+ peerDeps to the host version, and `npm run sync:sdk-pin:check` reports drift
334
+ (non-zero exit). When you bump the Pi SDK in the host `package.json`, the host
335
+ runs `sync:sdk-pin` and then you reinstall here:
336
+
337
+ ```bash
338
+ npm install --ignore-scripts
339
+ ```
340
+
341
+ The suite deliberately does not bump its own `version` field for SDK changes;
342
+ its peerDeps carry the version.
343
+
274
344
  ## Third-party notices
275
345
 
276
346
  Parts of this extension suite are based on or adapted from code by other vendors and projects. The corresponding license texts and notices are included in `licenses/`.
@@ -38,9 +38,9 @@
38
38
  "vscode-languageserver-protocol": "^3.17.5"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-ai": "*",
42
- "@earendil-works/pi-coding-agent": "*",
43
- "@earendil-works/pi-tui": "*",
41
+ "@earendil-works/pi-ai": "0.79.4",
42
+ "@earendil-works/pi-coding-agent": "0.79.4",
43
+ "@earendil-works/pi-tui": "0.79.4",
44
44
  "typebox": "*"
45
45
  },
46
46
  "devDependencies": {
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { ignoreStaleExtensionContextError } from "../context-usage.js";
4
5
  import {
5
6
  copySubagentConfigSample,
6
7
  ensureSessionFileLink,
@@ -172,12 +173,17 @@ async function triggerOrchestrationPrompt(
172
173
  ? `${basePrompt}\n\nObjective:\n${objective}`
173
174
  : basePrompt;
174
175
 
175
- if (typeof pi.sendUserMessage === "function") {
176
- pi.sendUserMessage(prompt);
177
- } else if (typeof pi.sendMessage === "function") {
178
- pi.sendMessage({ customType: `async-subagents-${modeName}`, content: prompt, display: false }, { triggerTurn: true, deliverAs: "followUp" });
179
- } else {
180
- ctx.ui.notify(`Cannot trigger /${modeName}: this Pi runtime does not expose sendUserMessage/sendMessage.`, "error");
176
+ try {
177
+ if (typeof pi.sendUserMessage === "function") {
178
+ pi.sendUserMessage(prompt);
179
+ } else if (typeof pi.sendMessage === "function") {
180
+ pi.sendMessage({ customType: `async-subagents-${modeName}`, content: prompt, display: false }, { triggerTurn: true, deliverAs: "followUp" });
181
+ } else {
182
+ ctx.ui.notify(`Cannot trigger /${modeName}: this Pi runtime does not expose sendUserMessage/sendMessage.`, "error");
183
+ return;
184
+ }
185
+ } catch (error) {
186
+ ignoreStaleExtensionContextError(error);
181
187
  return;
182
188
  }
183
189