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
|
@@ -26,7 +26,9 @@ When multiple independent stale sections exist, prefer several focused compressi
|
|
|
26
26
|
When one older message is huge but the surrounding context is still useful, use message-mode compression for that single message instead of compressing a broad range.
|
|
27
27
|
Summaries should be proportional to future usefulness, not proportional to the amount of text being removed.
|
|
28
28
|
|
|
29
|
-
Use \`compress\` as steady housekeeping while you work.
|
|
29
|
+
Use \`compress\` as steady housekeeping while you work. Closed slice => compress now. Do not start a new search, file-read batch, test, verification run, or web lookup while older completed work remains raw.
|
|
30
|
+
|
|
31
|
+
A closed slice is any finished implementation, verification, config edit, answered exploration, dead-end debugging branch, or test/log inspection. Passing logs are summary-only: preserve command, pass/fail, key failures if any, and whether follow-up is needed; never keep a full passing log in live context. Treat large shell/read/repo/web outputs as disposable evidence once their facts are extracted.
|
|
30
32
|
|
|
31
33
|
Before compressing while work is unfinished, ensure one \`todo in_progress\` captures the active objective and next step.
|
|
32
34
|
|
|
@@ -38,6 +40,7 @@ CADENCE, SIGNALS, AND LATENCY
|
|
|
38
40
|
- Prioritize closedness and independence over raw size
|
|
39
41
|
- Prefer smaller, regular compressions over infrequent massive compressions for better latency and summary quality
|
|
40
42
|
- When multiple independent stale sections are ready, batch compressions in parallel
|
|
43
|
+
- Before more exploration, ask whether an older completed slice can become summary-only; if yes, compress first
|
|
41
44
|
|
|
42
45
|
COMPRESS WHEN
|
|
43
46
|
|
|
@@ -45,6 +48,8 @@ A section is genuinely closed and the raw conversation has served its purpose:
|
|
|
45
48
|
|
|
46
49
|
- Research concluded and findings are clear
|
|
47
50
|
- Implementation finished and verified
|
|
51
|
+
- Config/doc edit finished
|
|
52
|
+
- Test, lint, or CI output has been understood, especially passing logs
|
|
48
53
|
- Exploration exhausted and patterns understood
|
|
49
54
|
- Dead-end noise can be discarded without waiting for a whole chapter to close
|
|
50
55
|
|
|
@@ -77,7 +82,10 @@ It is your responsibility to keep a sharp, high-quality context window for optim
|
|
|
77
82
|
export const COMPRESS_RANGE_DESCRIPTION = `Collapse one or more ranges of the conversation into detailed summaries.
|
|
78
83
|
|
|
79
84
|
AUTONOMOUS HOUSEKEEPING
|
|
80
|
-
Do not wait for context emergencies.
|
|
85
|
+
Do not wait for context emergencies. Closed slice => compress now. A closed slice includes completed implementation, verification, config/doc edit, answered exploration, dead-end debugging, or finished test/log inspection. Compress before starting the next search/read/test/web lookup unless exact raw text is still needed.
|
|
86
|
+
|
|
87
|
+
PASSING LOGS AND LARGE OUTPUTS
|
|
88
|
+
Passing check/test/lint/tsc logs are summary-only after you know the result. Preserve command, pass/fail, key failures if any, and follow-up status; drop full passing output. Treat large shell/read/repo/web outputs as disposable evidence once important facts are extracted.
|
|
81
89
|
|
|
82
90
|
DCP REMINDERS
|
|
83
91
|
If a \`<dcp-system-reminder>\` is present in context, treat it as a direct instruction to evaluate compression immediately. If a safe closed range exists, call this tool before further exploration. Skipping compression is acceptable only when the newest raw context is still needed for the next concrete edit, test, or answer.
|
|
@@ -87,7 +95,7 @@ Your summary must be COMPLETE FOR CONTINUATION, not a transcript rewrite. Preser
|
|
|
87
95
|
|
|
88
96
|
If active unfinished work exists, start with \`Active objective\` and \`Next step\`.
|
|
89
97
|
|
|
90
|
-
Default to a compact structured summary (roughly
|
|
98
|
+
Default to a compact structured summary (roughly 4-10 bullets for a normal completed work slice). Grow beyond that only when the compressed range contains multiple independent decisions, unresolved blockers, or precise state that is genuinely required to continue.
|
|
91
99
|
|
|
92
100
|
Do not copy long raw code, JSON, diffs, logs, or tool output into summaries. Prefer semantic descriptions such as “updated foo.json so scene_assets_1.zai-svg has maxConcurrentRuns set to 5.” Include exact snippets only when the literal text is required for safe continuation, and keep them short and single-line.
|
|
93
101
|
|
|
@@ -98,7 +106,7 @@ USER INTENT FIDELITY
|
|
|
98
106
|
When the compressed range includes user messages, preserve the user's intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
|
|
99
107
|
Directly quote user messages when they are short enough to include safely. Direct quotes are preferred when they best preserve exact meaning.
|
|
100
108
|
|
|
101
|
-
Be LEAN. Strip away
|
|
109
|
+
Be LEAN. Strip away full logs, repeated search/read output, duplicate summaries, incidental failed attempts, and line-by-line edit history. What remains should be pure signal with enough detail to resume work confidently.
|
|
102
110
|
|
|
103
111
|
TWO COMPRESSION MODES
|
|
104
112
|
You may use either or both modes in one call:
|
|
@@ -174,7 +182,7 @@ You are at or beyond the configured max context threshold. This is an emergency
|
|
|
174
182
|
You MUST use the \`compress\` tool now. Do not continue normal exploration until compression is handled.
|
|
175
183
|
|
|
176
184
|
If you are in the middle of a critical atomic operation, finish that atomic step first, then compress immediately.
|
|
177
|
-
If
|
|
185
|
+
If any closed slice exists (finished implementation, verification, config/doc edit, answered exploration, dead end, or test/log inspection), compress it before replying or starting another task. Passing logs should become command + pass/fail + follow-up status only.
|
|
178
186
|
|
|
179
187
|
RANGE STRATEGY (MANDATORY)
|
|
180
188
|
Prioritize one large, closed, high-yield compression range first.
|
|
@@ -201,9 +209,9 @@ ACTION REQUIRED: Context usage is high.
|
|
|
201
209
|
|
|
202
210
|
Before doing more exploration, look for a closed, self-contained range that no longer needs to stay raw and compress it now.
|
|
203
211
|
|
|
204
|
-
Do not treat this as optional housekeeping. If any completed research, implementation, verification, CI-log inspection, or dead-end debugging slice is present, call the \`compress\` tool before continuing normal work.
|
|
212
|
+
Do not treat this as optional housekeeping. If any completed research, implementation, verification, config/doc edit, CI-log inspection, or dead-end debugging slice is present, call the \`compress\` tool before continuing normal work.
|
|
205
213
|
If a completed implementation+verification slice exists, compress it before replying or starting another task.
|
|
206
|
-
High-priority stale
|
|
214
|
+
High-priority stale shell/read/repo/web outputs must be compressed once no exact raw text is needed. Passing logs should not remain raw after they are understood.
|
|
207
215
|
|
|
208
216
|
RANGE SELECTION
|
|
209
217
|
Prefer older, resolved history. Avoid the newest active working slice unless it is clearly done.
|
|
@@ -225,7 +233,7 @@ If any range is cleanly closed and unlikely to be needed again, use the \`compre
|
|
|
225
233
|
If direction has shifted, compress earlier ranges that are now less relevant.
|
|
226
234
|
|
|
227
235
|
Do not defer this across another batch of searches, reads, CI log fetches, or tests. The next safe action should be compression whenever a closed slice exists.
|
|
228
|
-
High-priority stale
|
|
236
|
+
High-priority stale shell/read/repo/web outputs and understood passing logs must be compressed once no exact raw text is needed.
|
|
229
237
|
|
|
230
238
|
Prefer small, closed-range compressions over one broad compression.
|
|
231
239
|
Use message-mode compression for isolated large stale messages.
|
|
@@ -239,7 +247,7 @@ Keep active context uncompressed.
|
|
|
239
247
|
export const ITERATION_NUDGE = `<dcp-system-reminder>
|
|
240
248
|
ACTION REQUIRED: You've been iterating for a while after the last user message.
|
|
241
249
|
|
|
242
|
-
Pause before the next non-atomic tool call. If there is a closed portion that is unlikely to be referenced immediately (for example, finished research before implementation, completed CI-log triage, a verified fix, or a dead-end investigation), use the \`compress\` tool on it now.
|
|
250
|
+
Pause before the next non-atomic tool call. If there is a closed portion that is unlikely to be referenced immediately (for example, finished research before implementation, completed config edit, completed CI-log triage, a verified fix, or a dead-end investigation), use the \`compress\` tool on it now.
|
|
243
251
|
|
|
244
252
|
Do not keep accumulating tool outputs while a completed slice remains raw. If a range is closed, compression is the next safe action.
|
|
245
253
|
If a completed implementation+verification slice exists, compress it before replying or starting another task.
|
|
@@ -17,6 +17,55 @@ interface CandidateBoundary {
|
|
|
17
17
|
isSystemReminder: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function hasAddressableSnapshot(state: DcpState): boolean {
|
|
21
|
+
return state.messageMetaSnapshot.size > 0 || state.messageIdSnapshot.size > 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isActiveBlockId(blockId: number, state: DcpState): boolean {
|
|
25
|
+
return state.compressionBlocks.some((block) => block.id === blockId && block.active);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function findCurrentMessageId(msg: any, state: DcpState): string | undefined {
|
|
29
|
+
const role = msg?.role ?? "";
|
|
30
|
+
const timestamp = msg?.timestamp;
|
|
31
|
+
if (!Number.isFinite(timestamp)) return undefined;
|
|
32
|
+
|
|
33
|
+
for (const [id, meta] of state.messageMetaSnapshot) {
|
|
34
|
+
if (meta.timestamp === timestamp && meta.role === role && meta.blockId === undefined) return id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [id, ts] of state.messageIdSnapshot) {
|
|
38
|
+
if (ts === timestamp) return id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveAddressableBoundaryId(
|
|
45
|
+
msg: any,
|
|
46
|
+
state: DcpState,
|
|
47
|
+
options: { allowBlocks: boolean },
|
|
48
|
+
): { id: string; blockId?: number; text: string } | null {
|
|
49
|
+
const text = messageText(msg);
|
|
50
|
+
const blockId = extractBlockId(text);
|
|
51
|
+
if (blockId !== undefined) {
|
|
52
|
+
if (options.allowBlocks && isActiveBlockId(blockId, state)) return { id: `b${blockId}`, blockId, text };
|
|
53
|
+
if (!hasAddressableSnapshot(state) && options.allowBlocks) return { id: `b${blockId}`, blockId, text };
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const messageId = extractMessageId(text);
|
|
58
|
+
if (!messageId) return null;
|
|
59
|
+
if (!hasAddressableSnapshot(state)) return { id: messageId, text };
|
|
60
|
+
|
|
61
|
+
if (state.messageMetaSnapshot.has(messageId) || state.messageIdSnapshot.has(messageId)) {
|
|
62
|
+
return { id: messageId, text };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const currentId = findCurrentMessageId(msg, state);
|
|
66
|
+
return currentId ? { id: currentId, text } : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
20
69
|
export function detectCompressionCandidate(
|
|
21
70
|
messages: any[],
|
|
22
71
|
_state: DcpState,
|
|
@@ -29,19 +78,16 @@ export function detectCompressionCandidate(
|
|
|
29
78
|
|
|
30
79
|
const boundaries: CandidateBoundary[] = [];
|
|
31
80
|
for (const msg of messages) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const messageId = extractMessageId(text);
|
|
35
|
-
const id = blockId !== undefined ? `b${blockId}` : messageId;
|
|
36
|
-
if (!id) continue;
|
|
81
|
+
const boundary = resolveAddressableBoundaryId(msg, _state, { allowBlocks: true });
|
|
82
|
+
if (!boundary) continue;
|
|
37
83
|
if (!Number.isFinite(msg.timestamp)) continue;
|
|
38
84
|
boundaries.push({
|
|
39
|
-
id,
|
|
85
|
+
id: boundary.id,
|
|
40
86
|
role: msg.role ?? "",
|
|
41
87
|
timestamp: msg.timestamp,
|
|
42
88
|
tokenEstimate: estimateMessageTokens(msg),
|
|
43
|
-
blockId,
|
|
44
|
-
isSystemReminder: text.includes("<dcp-system-reminder>"),
|
|
89
|
+
blockId: boundary.blockId,
|
|
90
|
+
isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
|
|
45
91
|
});
|
|
46
92
|
}
|
|
47
93
|
|
|
@@ -102,6 +148,7 @@ export function formatCompressionCandidateHint(candidate: CompressionCandidate):
|
|
|
102
148
|
|
|
103
149
|
export function detectMessageCompressionCandidates(
|
|
104
150
|
messages: any[],
|
|
151
|
+
state: DcpState,
|
|
105
152
|
config: DcpConfig,
|
|
106
153
|
contextPercent: number,
|
|
107
154
|
): MessageCompressionCandidate[] {
|
|
@@ -111,18 +158,15 @@ export function detectMessageCompressionCandidates(
|
|
|
111
158
|
|
|
112
159
|
const boundaries: CandidateBoundary[] = [];
|
|
113
160
|
for (const msg of messages) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const messageId = extractMessageId(text);
|
|
117
|
-
if (!messageId || blockId !== undefined) continue;
|
|
161
|
+
const boundary = resolveAddressableBoundaryId(msg, state, { allowBlocks: false });
|
|
162
|
+
if (!boundary || boundary.blockId !== undefined) continue;
|
|
118
163
|
if (!Number.isFinite(msg.timestamp)) continue;
|
|
119
164
|
boundaries.push({
|
|
120
|
-
id:
|
|
165
|
+
id: boundary.id,
|
|
121
166
|
role: msg.role ?? "",
|
|
122
167
|
timestamp: msg.timestamp,
|
|
123
168
|
tokenEstimate: estimateMessageTokens(msg),
|
|
124
|
-
|
|
125
|
-
isSystemReminder: text.includes("<dcp-system-reminder>"),
|
|
169
|
+
isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
|
|
126
170
|
});
|
|
127
171
|
}
|
|
128
172
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { matchingModelEntries, type DcpConfig } from "./config.js";
|
|
2
2
|
import type { DcpState } from "./state.js";
|
|
3
3
|
import type { NudgeThresholds } from "./pruner-types.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
@@ -107,8 +107,6 @@ export function resolveContextThresholds(
|
|
|
107
107
|
modelKeys: Array<string | undefined> = [],
|
|
108
108
|
contextWindow?: number,
|
|
109
109
|
): Required<NudgeThresholds> {
|
|
110
|
-
const candidates = modelKeys.filter((key): key is string => typeof key === "string" && key.length > 0);
|
|
111
|
-
|
|
112
110
|
const resolveThresholdValue = (value: number | string | undefined): number | undefined => {
|
|
113
111
|
if (typeof value === "string") {
|
|
114
112
|
const trimmed = value.trim();
|
|
@@ -130,12 +128,12 @@ export function resolveContextThresholds(
|
|
|
130
128
|
};
|
|
131
129
|
|
|
132
130
|
const resolveOverride = (map: Record<string, number | string> | undefined): number | undefined => {
|
|
133
|
-
|
|
134
|
-
for (const
|
|
135
|
-
const value = resolveThresholdValue(
|
|
136
|
-
if (value !== undefined)
|
|
131
|
+
let resolved: number | undefined;
|
|
132
|
+
for (const [, rawValue] of matchingModelEntries(map, modelKeys)) {
|
|
133
|
+
const value = resolveThresholdValue(rawValue);
|
|
134
|
+
if (value !== undefined) resolved = value;
|
|
137
135
|
}
|
|
138
|
-
return
|
|
136
|
+
return resolved;
|
|
139
137
|
};
|
|
140
138
|
|
|
141
139
|
const min =
|
|
@@ -157,7 +157,7 @@ function formatCandidateActions(
|
|
|
157
157
|
|
|
158
158
|
if (candidate) {
|
|
159
159
|
parts.push(
|
|
160
|
-
`Recommended range candidate: ${candidate.startId}..${candidate.endId} (${candidate.messageCount} messages, ~${candidate.estimatedTokens} tokens, ${candidate.reason}). Compress this before the next search/read/test if it is closed.`,
|
|
160
|
+
`Recommended range candidate: ${candidate.startId}..${candidate.endId} (${candidate.messageCount} messages, ~${candidate.estimatedTokens} tokens, ${candidate.reason}). Compress this before the next search/read/test/web lookup if it is closed.`,
|
|
161
161
|
);
|
|
162
162
|
if (candidate.includedBlockIds.length > 0) {
|
|
163
163
|
parts.push(
|
|
@@ -172,7 +172,7 @@ function formatCandidateActions(
|
|
|
172
172
|
parts.push(
|
|
173
173
|
`Recommended message candidates: ${listedMessages
|
|
174
174
|
.map((item) => `${item.messageId} (${item.priority}, ${item.role}, ~${item.estimatedTokens} tokens)`)
|
|
175
|
-
.join(", ")}. High-priority stale messages MUST be compressed once their full text is no longer needed. Batch multiple messages in one compress call when possible.`,
|
|
175
|
+
.join(", ")}. High-priority stale messages MUST be compressed once their full text is no longer needed; passing logs should become command + pass/fail + follow-up status only. Batch multiple messages in one compress call when possible.`,
|
|
176
176
|
);
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -180,7 +180,7 @@ function formatCandidateActions(
|
|
|
180
180
|
if (activeBlocks) parts.push(activeBlocks);
|
|
181
181
|
|
|
182
182
|
if (parts.length === 0) {
|
|
183
|
-
parts.push("No automatic candidate is certain; scan the older closed context now and compress any completed research, implementation, verification, CI-log inspection, or dead-end debugging slice before accumulating more tool output.");
|
|
183
|
+
parts.push("No automatic candidate is certain; scan the older closed context now and compress any completed research, implementation, config/doc edit, verification, CI-log inspection, or dead-end debugging slice before accumulating more tool output.");
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
return [`CONCRETE NEXT ACTION`, ...parts].join("\n");
|
|
@@ -19,11 +19,61 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
19
19
|
"dcp": {
|
|
20
20
|
"enabled": true,
|
|
21
21
|
"manualMode": { "enabled": false, "automaticStrategies": true },
|
|
22
|
+
"modelOverrides": {
|
|
23
|
+
"openai-codex/gpt-5.5": {
|
|
24
|
+
"compress": {
|
|
25
|
+
"minContextPercent": "28%",
|
|
26
|
+
"maxContextPercent": "48%"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"openai-codex/gpt-5.4-mini": {
|
|
30
|
+
"compress": {
|
|
31
|
+
"minContextPercent": "20%",
|
|
32
|
+
"maxContextPercent": "38%"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"zai/*": {
|
|
36
|
+
"compress": {
|
|
37
|
+
"minContextPercent": "16%",
|
|
38
|
+
"maxContextPercent": "30%"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"antigravity/*sonnet*": {
|
|
42
|
+
"compress": {
|
|
43
|
+
"minContextPercent": "22%",
|
|
44
|
+
"maxContextPercent": "40%"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"antigravity/gemini-3.1-pro*": {
|
|
48
|
+
"compress": {
|
|
49
|
+
"minContextPercent": "24%",
|
|
50
|
+
"maxContextPercent": "42%"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"antigravity/gemini-3-flash*": {
|
|
54
|
+
"compress": {
|
|
55
|
+
"minContextPercent": "18%",
|
|
56
|
+
"maxContextPercent": "34%"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"antigravity/gemini-2.5-flash*": {
|
|
60
|
+
"compress": {
|
|
61
|
+
"minContextPercent": "18%",
|
|
62
|
+
"maxContextPercent": "32%"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"antigravity/antigravity-claude-opus-4-6-thinking": {
|
|
66
|
+
"compress": {
|
|
67
|
+
"minContextPercent": "26%",
|
|
68
|
+
"maxContextPercent": "44%"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
22
72
|
"compress": {
|
|
23
73
|
"minContextPercent": "20%",
|
|
24
74
|
"maxContextPercent": "55%",
|
|
25
75
|
"nudgeFrequency": 1,
|
|
26
|
-
"iterationNudgeThreshold":
|
|
76
|
+
"iterationNudgeThreshold": 4,
|
|
27
77
|
"autoCandidates": { "minContextPercent": 0.2 },
|
|
28
78
|
"messageMode": { "minContextPercent": 0.2 }
|
|
29
79
|
}
|
|
@@ -5,6 +5,7 @@ import type { Api, AssistantMessage, ImageContent, Model, TextContent } from "@e
|
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
|
|
7
7
|
import { loadPiToolsSuiteConfig } from "../config.js";
|
|
8
|
+
import { ignoreStaleExtensionContextError } from "../context-usage.js";
|
|
8
9
|
|
|
9
10
|
type ExtensionAPI = any;
|
|
10
11
|
|
|
@@ -189,20 +190,24 @@ export default function glmCodingDiscipline(pi: ExtensionAPI) {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
function syncLookupToolAvailability(modelRef: string | undefined, cwd?: string): void {
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
try {
|
|
194
|
+
const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : undefined;
|
|
195
|
+
if (!Array.isArray(activeTools)) return;
|
|
194
196
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
const lookupEnabled = Boolean(lookupModelFromConfig(cwd));
|
|
198
|
+
const shouldExposeLookup = lookupEnabled && isGlmModel(modelRef);
|
|
199
|
+
const hasLookup = activeTools.includes(LOOKUP_TOOL_NAME);
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
if (shouldExposeLookup === hasLookup) return;
|
|
202
|
+
if (typeof pi.setActiveTools !== "function") return;
|
|
201
203
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
const nextTools = shouldExposeLookup
|
|
205
|
+
? [...activeTools, LOOKUP_TOOL_NAME]
|
|
206
|
+
: activeTools.filter((tool: unknown) => tool !== LOOKUP_TOOL_NAME);
|
|
207
|
+
pi.setActiveTools([...new Set(nextTools)]);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
ignoreStaleExtensionContextError(error);
|
|
210
|
+
}
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
maybeRegisterLookupTool(process.cwd());
|
|
@@ -40,6 +40,14 @@ const MANAGED_TOOLS = new Set([...CLAUDE_ALIAS_TOOLS, ...CODEX_ALIAS_TOOLS, ...B
|
|
|
40
40
|
const REPO_DISCOVERY_TOOL_NAME_SET = new Set(REPO_DISCOVERY_TOOL_NAMES);
|
|
41
41
|
const MAX_BUILTIN_DEFINITIONS = 64;
|
|
42
42
|
|
|
43
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
44
|
+
return error instanceof Error && /ctx is stale|stale ctx|stale after session replacement|stale after.*reload/i.test(error.message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ignoreStaleExtensionContextError(error: unknown): void {
|
|
48
|
+
if (!isStaleExtensionContextError(error)) throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
type ShellAliasInput = {
|
|
44
52
|
command?: string;
|
|
45
53
|
description?: string;
|
|
@@ -391,18 +399,22 @@ function sameTools(left: string[], right: string[]): boolean {
|
|
|
391
399
|
}
|
|
392
400
|
|
|
393
401
|
function applyToolProfile(pi: ExtensionAPI, model: unknown, baseTools: string[]): void {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
try {
|
|
403
|
+
const targetTools = toolsForProfile(detectModelProfile(model));
|
|
404
|
+
const preserveSelection = shouldPreserveSelection();
|
|
405
|
+
const active = pi.getActiveTools();
|
|
406
|
+
const repoTools = activeRepoDiscoveryTools(active, baseTools);
|
|
407
|
+
const preserved = active.filter((tool) => !MANAGED_TOOLS.has(tool) && !REPO_DISCOVERY_TOOL_NAME_SET.has(tool));
|
|
408
|
+
const baseWithoutRepoTools = baseTools.filter((tool) => !REPO_DISCOVERY_TOOL_NAME_SET.has(tool));
|
|
409
|
+
const selectedTargetTools = preserveSelection
|
|
410
|
+
? selectSuitableToolsForModel(model, active.filter((tool) => MANAGED_TOOLS.has(tool)))
|
|
411
|
+
: targetTools;
|
|
412
|
+
const next = selectedTargetTools ? [...repoTools, ...preserved, ...selectedTargetTools] : [...repoTools, ...preserved, ...baseWithoutRepoTools];
|
|
413
|
+
const nextTools = [...new Set(next)];
|
|
414
|
+
if (!sameTools(active, nextTools)) pi.setActiveTools(nextTools);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
ignoreStaleExtensionContextError(error);
|
|
417
|
+
}
|
|
406
418
|
}
|
|
407
419
|
|
|
408
420
|
function shouldPreserveSelection(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
@@ -3,6 +3,7 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
|
|
5
5
|
import { ensurePiToolsSuiteUserConfig, getPiToolsSuiteUserConfigPath } from "../config";
|
|
6
|
+
import { ignoreStaleExtensionContextError } from "../context-usage";
|
|
6
7
|
import { publishStartupSection } from "../startup-section";
|
|
7
8
|
|
|
8
9
|
type PromptCommand = {
|
|
@@ -166,12 +167,20 @@ async function runPromptCommand(pi: ExtensionAPI, ctx: ExtensionCommandContext,
|
|
|
166
167
|
if (!prompt) return notify(ctx, `/${name} has an empty prompt.`, "error");
|
|
167
168
|
|
|
168
169
|
await ctx.waitForIdle();
|
|
169
|
-
|
|
170
|
+
try {
|
|
171
|
+
pi.sendUserMessage(prompt);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
ignoreStaleExtensionContextError(error);
|
|
174
|
+
}
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
async function reloadAfterConfigChange(ctx: ExtensionCommandContext, message: string): Promise<void> {
|
|
173
178
|
notify(ctx, `${message}\nReloading commands from ${getConfigPath()}…`);
|
|
174
|
-
|
|
179
|
+
try {
|
|
180
|
+
await ctx.reload();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
ignoreStaleExtensionContextError(error);
|
|
183
|
+
}
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
async function selectCommand(ctx: ExtensionContext, title: string, commands: Record<string, PromptCommand>): Promise<string | undefined> {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
37
37
|
import { loadTelegramMirrorConfig } from "../config.js";
|
|
38
|
+
import { ignoreStaleExtensionContextError, isAgentBusyRaceError } from "../context-usage.js";
|
|
38
39
|
import { TelegramBot } from "./bot.js";
|
|
39
40
|
import { captureAbortableContext, registerPixEventHandlers, type PixMirrorHooks, type RendererSink } from "./events.js";
|
|
40
41
|
import { TurnRenderer, type RendererEvent } from "./renderer.js";
|
|
@@ -104,11 +105,32 @@ export default function telegramMirror(pi: ExtensionAPI): void {
|
|
|
104
105
|
console.error(`[telegram-mirror] ${message}`);
|
|
105
106
|
};
|
|
106
107
|
|
|
108
|
+
function staleSafe<T>(callback: () => T, fallback?: T): T | undefined {
|
|
109
|
+
try {
|
|
110
|
+
return callback();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
ignoreStaleExtensionContextError(error);
|
|
113
|
+
return fallback;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sendUserMessageSafely(text: string): void {
|
|
118
|
+
try {
|
|
119
|
+
pi.sendUserMessage(text);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (isAgentBusyRaceError(error)) {
|
|
122
|
+
pi.sendUserMessage(text, { deliverAs: "followUp" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
107
129
|
// Dispatch the leader uses to execute commands on its own pi session.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
const localDispatch: LocalDispatch = {
|
|
131
|
+
sendUserMessage(text) {
|
|
132
|
+
staleSafe(() => sendUserMessageSafely(text));
|
|
133
|
+
},
|
|
112
134
|
currentDialog() {
|
|
113
135
|
return mirrorCtx?.currentDialog();
|
|
114
136
|
},
|
|
@@ -119,8 +141,12 @@ export default function telegramMirror(pi: ExtensionAPI): void {
|
|
|
119
141
|
mirrorCtx?.compact();
|
|
120
142
|
},
|
|
121
143
|
status() {
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
const ctx = mirrorCtx;
|
|
145
|
+
if (!ctx) return undefined;
|
|
146
|
+
return {
|
|
147
|
+
idle: staleSafe(() => ctx.isIdle(), true) ?? true,
|
|
148
|
+
hasPending: staleSafe(() => ctx.hasPendingMessages(), false) ?? false,
|
|
149
|
+
};
|
|
124
150
|
},
|
|
125
151
|
};
|
|
126
152
|
|
|
@@ -336,7 +362,7 @@ export default function telegramMirror(pi: ExtensionAPI): void {
|
|
|
336
362
|
try {
|
|
337
363
|
switch (command) {
|
|
338
364
|
case "sendUserMessage":
|
|
339
|
-
|
|
365
|
+
sendUserMessageSafely(((args as { text?: string } | undefined)?.text ?? ""));
|
|
340
366
|
break;
|
|
341
367
|
case "abort":
|
|
342
368
|
mirrorCtx?.abort();
|
|
@@ -477,22 +503,30 @@ export default function telegramMirror(pi: ExtensionAPI): void {
|
|
|
477
503
|
function refreshCtx(ctx: ExtensionContext | undefined): void {
|
|
478
504
|
if (!ctx) return;
|
|
479
505
|
if (!mirrorCtx) {
|
|
480
|
-
|
|
481
|
-
abort: () =>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
506
|
+
mirrorCtx = makeCtx({
|
|
507
|
+
abort: () => {
|
|
508
|
+
staleSafe(() => ctx.abort());
|
|
509
|
+
},
|
|
510
|
+
isIdle: () => staleSafe(() => ctx.isIdle(), true) ?? true,
|
|
511
|
+
hasPendingMessages: () => staleSafe(() => ctx.hasPendingMessages(), false) ?? false,
|
|
512
|
+
compact: () => {
|
|
513
|
+
staleSafe(() => ctx.compact());
|
|
514
|
+
},
|
|
485
515
|
currentDialog: () => currentDialogFromContext(ctx),
|
|
486
516
|
});
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
const m = mirrorCtx as Mutable<MirrorContext>;
|
|
490
|
-
m.abort = () => ctx.abort();
|
|
491
|
-
m.isIdle = () => ctx.isIdle();
|
|
492
|
-
m.hasPendingMessages = () => ctx.hasPendingMessages();
|
|
493
|
-
m.compact = () => ctx.compact();
|
|
494
|
-
m.currentDialog = () => currentDialogFromContext(ctx);
|
|
517
|
+
return;
|
|
495
518
|
}
|
|
519
|
+
const m = mirrorCtx as Mutable<MirrorContext>;
|
|
520
|
+
m.abort = () => {
|
|
521
|
+
staleSafe(() => ctx.abort());
|
|
522
|
+
};
|
|
523
|
+
m.isIdle = () => staleSafe(() => ctx.isIdle(), true) ?? true;
|
|
524
|
+
m.hasPendingMessages = () => staleSafe(() => ctx.hasPendingMessages(), false) ?? false;
|
|
525
|
+
m.compact = () => {
|
|
526
|
+
staleSafe(() => ctx.compact());
|
|
527
|
+
};
|
|
528
|
+
m.currentDialog = () => currentDialogFromContext(ctx);
|
|
529
|
+
}
|
|
496
530
|
|
|
497
531
|
function refreshSelfInfo(ctx: ExtensionContext | undefined): void {
|
|
498
532
|
const snapshot = sessionSnapshot(ctx);
|
|
@@ -611,13 +645,18 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
611
645
|
|
|
612
646
|
function sessionSnapshot(ctx: ExtensionContext | undefined): SessionSnapshot | undefined {
|
|
613
647
|
if (!ctx) return undefined;
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
648
|
+
try {
|
|
649
|
+
const manager = ctx.sessionManager;
|
|
650
|
+
return {
|
|
651
|
+
cwd: manager.getCwd?.() ?? ctx.cwd,
|
|
652
|
+
...(manager.getSessionId?.() ? { sessionId: manager.getSessionId() } : {}),
|
|
653
|
+
...(manager.getSessionFile?.() ? { sessionFile: manager.getSessionFile() } : {}),
|
|
654
|
+
...(manager.getSessionName?.() ? { sessionName: manager.getSessionName() } : {}),
|
|
655
|
+
};
|
|
656
|
+
} catch (error) {
|
|
657
|
+
ignoreStaleExtensionContextError(error);
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
621
660
|
}
|
|
622
661
|
|
|
623
662
|
function notify(ctx: { hasUI?: boolean; ui?: { notify?: (message: string, type?: "info" | "warning" | "error") => void } }, message: string, type: "info" | "warning" | "error" = "info"): void {
|