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.
- package/README.md +20 -0
- package/dist/app/app.js +4 -2
- package/dist/app/constants.d.ts +2 -1
- package/dist/app/constants.js +6 -1
- package/dist/app/input/input-controller.d.ts +4 -1
- package/dist/app/input/input-controller.js +95 -16
- package/dist/app/input/input-paste-handler.js +3 -1
- package/dist/app/input/terminal-edit-shortcuts.d.ts +20 -0
- package/dist/app/input/terminal-edit-shortcuts.js +50 -16
- package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-entry-renderer.js +1 -1
- package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-tool-renderer.js +21 -0
- package/dist/app/rendering/conversation-viewport.d.ts +3 -0
- package/dist/app/rendering/conversation-viewport.js +41 -5
- package/dist/app/rendering/editor-layout-renderer.js +3 -2
- package/dist/app/rendering/editor-panels.js +27 -10
- package/dist/app/runtime.d.ts +1 -0
- package/dist/app/runtime.js +33 -14
- package/dist/app/session/session-event-controller.d.ts +7 -0
- package/dist/app/session/session-event-controller.js +78 -0
- package/dist/app/session/tabs-controller.js +3 -1
- package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
- package/dist/app/subagents/subagents-widget-controller.js +141 -70
- package/dist/app/terminal/terminal-controller.d.ts +10 -0
- package/dist/app/terminal/terminal-controller.js +91 -2
- package/dist/app/todo/todo-model.js +2 -0
- package/dist/app/todo/todo-widget-controller.d.ts +2 -0
- package/dist/app/todo/todo-widget-controller.js +17 -7
- package/dist/app/types.d.ts +4 -0
- package/dist/bundled-extensions/question/tui.js +8 -1
- package/dist/bundled-extensions/session-title/index.js +65 -14
- package/dist/input-editor-files.js +23 -4
- package/dist/markdown-format.d.ts +4 -1
- package/dist/markdown-format.js +76 -9
- package/external/pi-tools-suite/README.md +71 -1
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
- package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
- package/external/pi-tools-suite/src/context-usage.ts +6 -1
- package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
- package/external/pi-tools-suite/src/dcp/config.ts +142 -6
- package/external/pi-tools-suite/src/dcp/index.ts +20 -8
- package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
- package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
- package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
- package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
- package/external/pi-tools-suite/src/todo/index.ts +87 -16
- package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
- package/external/pi-tools-suite/src/todo/todo.ts +49 -6
- package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
224
|
+
const pendingSessionId = pendingGeneration.sessionId;
|
|
189
225
|
const generation = { ...pendingGeneration, modelRef };
|
|
190
226
|
void (async () => {
|
|
191
227
|
try {
|
|
192
|
-
const
|
|
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 !==
|
|
236
|
+
if (sessionId !== pendingSessionId)
|
|
237
|
+
return;
|
|
238
|
+
if (pendingSessionId !== currentSessionId(ctx))
|
|
196
239
|
return;
|
|
197
240
|
if (!shouldGeneratePendingTitle(ctx))
|
|
198
241
|
return;
|
|
199
|
-
|
|
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 !==
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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;
|
package/dist/markdown-format.js
CHANGED
|
@@ -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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
176
|
-
pi.sendUserMessage
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|