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
|
@@ -9,12 +9,13 @@ import {
|
|
|
9
9
|
getSubagentRegistryPath,
|
|
10
10
|
isBlindModelRef,
|
|
11
11
|
loadSubagentConfig,
|
|
12
|
-
|
|
12
|
+
listSubagentSessionRecords,
|
|
13
13
|
loadSubagentRegistry,
|
|
14
14
|
removeSubagentRunsFromRegistry,
|
|
15
15
|
stopAgents,
|
|
16
16
|
type AgentCompletionHandler,
|
|
17
17
|
type StopSignal,
|
|
18
|
+
type SubagentSessionRecord,
|
|
18
19
|
} from "./lib.js";
|
|
19
20
|
import { buildUltraworkPrompt, isUltraworkEnvEnabled, registerCommands } from "./commands.js";
|
|
20
21
|
import { agentStrategyPrompt, appendAgentStrategyPrompt } from "./core/agent-strategy.js";
|
|
@@ -46,6 +47,11 @@ interface ShutdownTarget {
|
|
|
46
47
|
agentIds?: string[];
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
interface ShutdownPlan {
|
|
51
|
+
targets: ShutdownTarget[];
|
|
52
|
+
runDirsToDelete: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
function createLiveStatePayload(
|
|
50
56
|
liveAgents: Map<string, Map<string, LiveAgent>>,
|
|
51
57
|
sessionFile: string | undefined,
|
|
@@ -81,6 +87,14 @@ function agentMatchesSession(agent: LiveAgent, sessionFile: string | undefined):
|
|
|
81
87
|
return pathsEqual(sessionFile, agent.parentSession);
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
91
|
+
return error instanceof Error && /ctx is stale|stale ctx|stale after session replacement|stale after.*reload/i.test(error.message);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ignoreStaleExtensionContextError(error: unknown): void {
|
|
95
|
+
if (!isStaleExtensionContextError(error)) throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
export default function (pi: ExtensionAPI) {
|
|
85
99
|
const liveAgents = new Map<string, Map<string, LiveAgent>>();
|
|
86
100
|
const subagentOverlay = new SubagentOverlay(liveAgents);
|
|
@@ -90,11 +104,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
90
104
|
publishSubagentPresetsStartupSection();
|
|
91
105
|
|
|
92
106
|
function refreshSubagentOverlay(): void {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
try {
|
|
108
|
+
reconcileLiveAgentCompletions();
|
|
109
|
+
const liveState = createLiveStatePayload(liveAgents, currentSessionFile);
|
|
110
|
+
pi.events?.emit?.(SUBAGENTS_LIVE_COUNT_EVENT, { count: liveState.count });
|
|
111
|
+
pi.events?.emit?.(SUBAGENTS_LIVE_STATE_EVENT, liveState);
|
|
112
|
+
updateCompletionWatcher();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
ignoreStaleExtensionContextError(error);
|
|
115
|
+
}
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
function removeLiveAgent(runDir: string, agentId: string): void {
|
|
@@ -147,10 +165,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
147
165
|
registerCommands(pi);
|
|
148
166
|
|
|
149
167
|
pi.on("session_start", async (_event, ctx) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
try {
|
|
169
|
+
sawAutoUltraworkCandidate = false;
|
|
170
|
+
currentSessionFile = sessionFileFromContext(ctx);
|
|
171
|
+
subagentOverlay.restoreRunningAgents(ctx.cwd, currentSessionFile);
|
|
172
|
+
refreshSubagentOverlay();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
ignoreStaleExtensionContextError(error);
|
|
175
|
+
}
|
|
154
176
|
});
|
|
155
177
|
|
|
156
178
|
pi.on("tool_execution_end", async (event) => {
|
|
@@ -207,18 +229,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
207
229
|
});
|
|
208
230
|
|
|
209
231
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
210
|
-
subagentOverlay.dispose();
|
|
211
|
-
if (completionWatchTimer) {
|
|
212
|
-
clearInterval(completionWatchTimer);
|
|
213
|
-
completionWatchTimer = undefined;
|
|
214
|
-
}
|
|
215
|
-
if (event?.reason === "reload" || event?.reason === "fork") return;
|
|
216
232
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
233
|
+
subagentOverlay.dispose();
|
|
234
|
+
if (completionWatchTimer) {
|
|
235
|
+
clearInterval(completionWatchTimer);
|
|
236
|
+
completionWatchTimer = undefined;
|
|
237
|
+
}
|
|
238
|
+
if (event?.reason === "reload" || event?.reason === "fork") return;
|
|
239
|
+
try {
|
|
240
|
+
const shutdownSessionFile = sessionFileFromContext(ctx) ?? currentSessionFile;
|
|
241
|
+
await cleanupProjectSubagentState(ctx.cwd, liveAgents, { parentSession: shutdownSessionFile });
|
|
242
|
+
liveAgents.clear();
|
|
243
|
+
refreshSubagentOverlay();
|
|
244
|
+
} catch {
|
|
245
|
+
// Shutdown cleanup is best-effort and must never block the main session from closing.
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
ignoreStaleExtensionContextError(error);
|
|
222
249
|
}
|
|
223
250
|
});
|
|
224
251
|
}
|
|
@@ -394,31 +421,100 @@ function selectedToolsInclude(event: unknown, toolName: string): boolean {
|
|
|
394
421
|
return !Array.isArray(selectedTools) || selectedTools.includes(toolName);
|
|
395
422
|
}
|
|
396
423
|
|
|
397
|
-
async function cleanupProjectSubagentState(
|
|
398
|
-
|
|
399
|
-
|
|
424
|
+
async function cleanupProjectSubagentState(
|
|
425
|
+
cwd: string,
|
|
426
|
+
liveAgents: Map<string, Map<string, LiveAgent>>,
|
|
427
|
+
options: { parentSession?: string } = {},
|
|
428
|
+
): Promise<void> {
|
|
429
|
+
const shutdownPlan = collectShutdownPlan(cwd, liveAgents, options.parentSession);
|
|
430
|
+
const signaled = signalShutdownTargets(shutdownPlan.targets, "SIGTERM");
|
|
400
431
|
if (signaled > 0) await sleep(SESSION_SHUTDOWN_KILL_GRACE_MS);
|
|
401
|
-
signalShutdownTargets(
|
|
432
|
+
signalShutdownTargets(shutdownPlan.targets, "SIGKILL");
|
|
402
433
|
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
removeSubagentRunsFromRegistry(cwd, runDirs);
|
|
434
|
+
for (const target of shutdownPlan.targets) stopRunBestEffort(target.runDir, target.agentIds, "SIGKILL");
|
|
435
|
+
deleteRunDirs(shutdownPlan.runDirsToDelete);
|
|
436
|
+
removeSubagentRunsFromRegistry(cwd, shutdownPlan.runDirsToDelete);
|
|
407
437
|
removeEmptySubagentState(cwd);
|
|
408
438
|
}
|
|
409
439
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
440
|
+
|
|
441
|
+
function collectShutdownPlan(
|
|
442
|
+
cwd: string,
|
|
443
|
+
liveAgents: Map<string, Map<string, LiveAgent>>,
|
|
444
|
+
parentSession: string | undefined,
|
|
445
|
+
): ShutdownPlan {
|
|
446
|
+
if (!parentSession) return collectLiveShutdownPlan(liveAgents);
|
|
447
|
+
|
|
448
|
+
const targets = new Map<string, Set<string>>();
|
|
449
|
+
const runDirsToDelete = new Set<string>();
|
|
450
|
+
const recordsByRun = groupRecordsByRun(listSubagentSessionRecords(cwd));
|
|
451
|
+
|
|
452
|
+
for (const [runDir, records] of recordsByRun) {
|
|
453
|
+
const matchingRecords = records.filter((record) => recordMatchesSession(record, parentSession));
|
|
454
|
+
if (matchingRecords.length === 0) continue;
|
|
455
|
+
mergeTargetIds(targets, runDir, matchingRecords.map((record) => record.agentId));
|
|
456
|
+
const liveRun = liveAgents.get(runDir);
|
|
457
|
+
if (records.every((record) => recordMatchesSession(record, parentSession)) && liveRunMatchesSession(liveRun, parentSession)) {
|
|
458
|
+
runDirsToDelete.add(runDir);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
413
462
|
for (const [runDir, liveRun] of liveAgents) {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
463
|
+
const matchingIds = [...liveRun.values()]
|
|
464
|
+
.filter((agent) => liveAgentMatchesSession(agent, parentSession))
|
|
465
|
+
.map((agent) => agent.agentId);
|
|
466
|
+
if (matchingIds.length === 0) continue;
|
|
467
|
+
mergeTargetIds(targets, runDir, matchingIds);
|
|
468
|
+
const records = recordsByRun.get(runDir) ?? [];
|
|
469
|
+
if (records.every((record) => recordMatchesSession(record, parentSession)) && liveRunMatchesSession(liveRun, parentSession)) {
|
|
470
|
+
runDirsToDelete.add(runDir);
|
|
471
|
+
}
|
|
417
472
|
}
|
|
418
|
-
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
targets: targetMapToTargets(targets),
|
|
476
|
+
runDirsToDelete: [...runDirsToDelete],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function collectLiveShutdownPlan(liveAgents: Map<string, Map<string, LiveAgent>>): ShutdownPlan {
|
|
481
|
+
const targets = [...liveAgents.entries()].map(([runDir, liveRun]) => ({
|
|
419
482
|
runDir,
|
|
420
|
-
|
|
483
|
+
agentIds: [...liveRun.keys()],
|
|
421
484
|
}));
|
|
485
|
+
return { targets, runDirsToDelete: targets.map((target) => target.runDir) };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function groupRecordsByRun(records: SubagentSessionRecord[]): Map<string, SubagentSessionRecord[]> {
|
|
489
|
+
const grouped = new Map<string, SubagentSessionRecord[]>();
|
|
490
|
+
for (const record of records) {
|
|
491
|
+
const existing = grouped.get(record.runDir) ?? [];
|
|
492
|
+
existing.push(record);
|
|
493
|
+
grouped.set(record.runDir, existing);
|
|
494
|
+
}
|
|
495
|
+
return grouped;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function recordMatchesSession(record: SubagentSessionRecord, sessionFile: string): boolean {
|
|
499
|
+
return Boolean(record.parentSession && pathsEqual(record.parentSession, sessionFile));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function liveAgentMatchesSession(agent: LiveAgent, sessionFile: string): boolean {
|
|
503
|
+
return Boolean(agent.parentSession && pathsEqual(agent.parentSession, sessionFile));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function liveRunMatchesSession(liveRun: Map<string, LiveAgent> | undefined, sessionFile: string): boolean {
|
|
507
|
+
return !liveRun || [...liveRun.values()].every((agent) => liveAgentMatchesSession(agent, sessionFile));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function mergeTargetIds(targets: Map<string, Set<string>>, runDir: string, agentIds: string[]): void {
|
|
511
|
+
const target = targets.get(runDir) ?? new Set<string>();
|
|
512
|
+
for (const agentId of agentIds) target.add(agentId);
|
|
513
|
+
targets.set(runDir, target);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function targetMapToTargets(targets: Map<string, Set<string>>): ShutdownTarget[] {
|
|
517
|
+
return [...targets.entries()].map(([runDir, agentIds]) => ({ runDir, agentIds: [...agentIds] }));
|
|
422
518
|
}
|
|
423
519
|
|
|
424
520
|
function signalShutdownTargets(targets: ShutdownTarget[], signal: StopSignal): number {
|
|
@@ -10,13 +10,18 @@ export interface ContextUsageProvider {
|
|
|
10
10
|
|
|
11
11
|
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
12
12
|
if (!(error instanceof Error)) return false
|
|
13
|
-
return /ctx is stale|stale after session replacement|stale after.*reload/i.test(error.message)
|
|
13
|
+
return /ctx is stale|stale ctx|stale after session replacement|stale after.*reload/i.test(error.message)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function ignoreStaleExtensionContextError(error: unknown): void {
|
|
17
17
|
if (!isStaleExtensionContextError(error)) throw error
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function isAgentBusyRaceError(error: unknown): boolean {
|
|
21
|
+
if (!(error instanceof Error)) return false
|
|
22
|
+
return /Agent is already processing(?: a prompt)?\.|Wait for completion before continuing|Specify streamingBehavior/i.test(error.message)
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
// Fork/newSession/switchSession/reload can invalidate a runner while late UI or
|
|
21
26
|
// context events from the old runner are still unwinding. In that race,
|
|
22
27
|
// ctx.getContextUsage() throws the Pi stale-ctx guard; treat it as unavailable
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"
|
|
2
2
|
import type { AutocompleteItem } from "@earendil-works/pi-tui"
|
|
3
3
|
import type { DcpState } from "./state.js"
|
|
4
|
-
import type
|
|
4
|
+
import { modelKeysFromContext, resolveModelConfig, type DcpConfig } from "./config.js"
|
|
5
5
|
import type { DcpNudgeType } from "./pruner-types.js"
|
|
6
6
|
import { isToolRecordProtected, markToolPruned } from "./pruner.js"
|
|
7
7
|
import { safeGetContextUsage } from "../context-usage.js"
|
|
@@ -540,6 +540,7 @@ export function registerCommands(
|
|
|
540
540
|
async handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
541
541
|
const parts = args.trim().split(/\s+/).filter(Boolean)
|
|
542
542
|
const sub = parts[0] ?? ""
|
|
543
|
+
const effectiveConfig = resolveModelConfig(config, modelKeysFromContext(ctx))
|
|
543
544
|
|
|
544
545
|
try {
|
|
545
546
|
switch (sub) {
|
|
@@ -559,7 +560,7 @@ export function registerCommands(
|
|
|
559
560
|
case "sweep": {
|
|
560
561
|
const rawN = parts[1] !== undefined ? parseInt(parts[1], 10) : 0
|
|
561
562
|
const n = isNaN(rawN) || rawN < 0 ? 0 : rawN
|
|
562
|
-
await handleSweep(ctx, state,
|
|
563
|
+
await handleSweep(ctx, state, effectiveConfig, n)
|
|
563
564
|
break
|
|
564
565
|
}
|
|
565
566
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Type } from "typebox"
|
|
6
6
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
7
7
|
import type { DcpState } from "./state.js"
|
|
8
|
-
import type
|
|
8
|
+
import { modelKeysFromContext, resolveModelConfig, type DcpConfig } from "./config.js"
|
|
9
9
|
import { clearDcpNudgeAnchors } from "./pruner.js"
|
|
10
10
|
import type { DcpCompressionVisualDetails } from "./ui.js"
|
|
11
11
|
import { normalizeDcpContextUsage } from "./ui.js"
|
|
@@ -149,6 +149,11 @@ export function registerCompressTool(
|
|
|
149
149
|
}),
|
|
150
150
|
|
|
151
151
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
152
|
+
const effectiveConfig = resolveModelConfig(config, modelKeysFromContext(ctx))
|
|
153
|
+
if (!effectiveConfig.enabled) {
|
|
154
|
+
throw new Error("DCP is disabled for the active model")
|
|
155
|
+
}
|
|
156
|
+
|
|
152
157
|
const newBlockIds: number[] = []
|
|
153
158
|
const ranges = Array.isArray(params.ranges) ? params.ranges : []
|
|
154
159
|
const messages = Array.isArray(params.messages) ? params.messages : []
|
|
@@ -215,7 +220,7 @@ export function registerCompressTool(
|
|
|
215
220
|
anchorMessageId: anchor.stableId,
|
|
216
221
|
createdByToolCallId: _toolCallId,
|
|
217
222
|
state,
|
|
218
|
-
config,
|
|
223
|
+
config: effectiveConfig,
|
|
219
224
|
mode: "range",
|
|
220
225
|
})
|
|
221
226
|
const block = created.block
|
|
@@ -260,7 +265,7 @@ export function registerCompressTool(
|
|
|
260
265
|
skippedMessageIssues.push({ kind: "non-finite", messageId })
|
|
261
266
|
continue
|
|
262
267
|
}
|
|
263
|
-
if (
|
|
268
|
+
if (effectiveConfig.compress.protectUserMessages && meta.role === "user") {
|
|
264
269
|
skippedMessageIssues.push({ kind: "protected-user", messageId })
|
|
265
270
|
continue
|
|
266
271
|
}
|
|
@@ -291,7 +296,7 @@ export function registerCompressTool(
|
|
|
291
296
|
anchorMessageId: anchor.stableId,
|
|
292
297
|
createdByToolCallId: _toolCallId,
|
|
293
298
|
state,
|
|
294
|
-
config,
|
|
299
|
+
config: effectiveConfig,
|
|
295
300
|
mode: "message",
|
|
296
301
|
validatePlaceholders: false,
|
|
297
302
|
expandPlaceholders: false,
|
|
@@ -25,7 +25,7 @@ export interface DcpConfig {
|
|
|
25
25
|
modelMinContextLimits?: Record<string, number | string> // same formats as modelMinContextPercent; checked before modelMinContextPercent
|
|
26
26
|
summaryBuffer: boolean
|
|
27
27
|
nudgeFrequency: number // inject nudge every N context events (default: 2)
|
|
28
|
-
iterationNudgeThreshold: number // nudge after N tool calls since last user msg (default:
|
|
28
|
+
iterationNudgeThreshold: number // nudge after N tool calls since last user msg (default: 4)
|
|
29
29
|
nudgeForce: "strong" | "soft"
|
|
30
30
|
protectedTools: string[] // these tool outputs always protected from pruning
|
|
31
31
|
protectTags: boolean
|
|
@@ -67,8 +67,17 @@ export interface DcpConfig {
|
|
|
67
67
|
}
|
|
68
68
|
protectedFilePatterns: string[]
|
|
69
69
|
pruneNotification: "off" | "minimal" | "detailed"
|
|
70
|
+
modelOverrides: Record<string, DcpConfigOverride>
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
export type DcpConfigOverride = DeepPartial<Omit<DcpConfig, "modelOverrides">>
|
|
74
|
+
|
|
75
|
+
type DeepPartial<T> = T extends Array<infer U>
|
|
76
|
+
? Array<DeepPartial<U>>
|
|
77
|
+
: T extends object
|
|
78
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
79
|
+
: T
|
|
80
|
+
|
|
72
81
|
// ---------------------------------------------------------------------------
|
|
73
82
|
// Defaults
|
|
74
83
|
// ---------------------------------------------------------------------------
|
|
@@ -87,7 +96,7 @@ const DEFAULT_CONFIG: DcpConfig = {
|
|
|
87
96
|
modelMinContextPercent: {},
|
|
88
97
|
summaryBuffer: true,
|
|
89
98
|
nudgeFrequency: 1,
|
|
90
|
-
iterationNudgeThreshold:
|
|
99
|
+
iterationNudgeThreshold: 4,
|
|
91
100
|
nudgeForce: "soft",
|
|
92
101
|
protectedTools: ["compress", "write", "edit"],
|
|
93
102
|
protectTags: false,
|
|
@@ -95,14 +104,14 @@ const DEFAULT_CONFIG: DcpConfig = {
|
|
|
95
104
|
autoCandidates: {
|
|
96
105
|
enabled: true,
|
|
97
106
|
minContextPercent: 0.20,
|
|
98
|
-
keepRecentTurns:
|
|
107
|
+
keepRecentTurns: 1,
|
|
99
108
|
minMessages: 6,
|
|
100
109
|
minTokens: 1500,
|
|
101
110
|
},
|
|
102
111
|
messageMode: {
|
|
103
112
|
enabled: true,
|
|
104
113
|
minContextPercent: 0.20,
|
|
105
|
-
keepRecentTurns:
|
|
114
|
+
keepRecentTurns: 1,
|
|
106
115
|
mediumTokens: 500,
|
|
107
116
|
highTokens: 5000,
|
|
108
117
|
maxSuggestions: 5,
|
|
@@ -120,13 +129,17 @@ const DEFAULT_CONFIG: DcpConfig = {
|
|
|
120
129
|
},
|
|
121
130
|
autoToolPruning: {
|
|
122
131
|
enabled: true,
|
|
123
|
-
maxOutputTokens:
|
|
124
|
-
keepRecentTurns:
|
|
132
|
+
maxOutputTokens: 1200,
|
|
133
|
+
keepRecentTurns: 1,
|
|
125
134
|
readLikeTools: [
|
|
126
135
|
"read",
|
|
136
|
+
"shell",
|
|
137
|
+
"bash",
|
|
127
138
|
"grep",
|
|
128
139
|
"find",
|
|
129
140
|
"ls",
|
|
141
|
+
"web_search",
|
|
142
|
+
"web_fetch",
|
|
130
143
|
"repo_architecture",
|
|
131
144
|
"repo_structure",
|
|
132
145
|
"repo_ast",
|
|
@@ -141,6 +154,7 @@ const DEFAULT_CONFIG: DcpConfig = {
|
|
|
141
154
|
},
|
|
142
155
|
protectedFilePatterns: [],
|
|
143
156
|
pruneNotification: "detailed",
|
|
157
|
+
modelOverrides: {},
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
// ---------------------------------------------------------------------------
|
|
@@ -226,6 +240,128 @@ function mergeSuiteDcpConfig(config: DcpConfig, filePath: string): DcpConfig {
|
|
|
226
240
|
return deepMerge(config, raw as Partial<DcpConfig>)
|
|
227
241
|
}
|
|
228
242
|
|
|
243
|
+
function normalizeModelKey(key: string | undefined): string | undefined {
|
|
244
|
+
if (typeof key !== "string") return undefined
|
|
245
|
+
const trimmed = key.trim()
|
|
246
|
+
return trimmed.length > 0 ? trimmed : undefined
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function escapeRegExp(text: string): string {
|
|
250
|
+
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&")
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function globToRegExp(pattern: string): RegExp {
|
|
254
|
+
let source = "^"
|
|
255
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
256
|
+
const char = pattern[i]!
|
|
257
|
+
if (char === "*") {
|
|
258
|
+
source += ".*"
|
|
259
|
+
} else if (char === "?") {
|
|
260
|
+
source += "."
|
|
261
|
+
} else {
|
|
262
|
+
source += escapeRegExp(char)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
source += "$"
|
|
266
|
+
return new RegExp(source)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isWildcardPattern(pattern: string): boolean {
|
|
270
|
+
return pattern.includes("*") || pattern.includes("?")
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function modelPatternMatches(pattern: string, candidate: string): boolean {
|
|
274
|
+
return globToRegExp(pattern).test(candidate)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function uniqueModelCandidates(modelKeys: Array<string | undefined>): string[] {
|
|
278
|
+
return modelKeys
|
|
279
|
+
.map((key) => normalizeModelKey(key))
|
|
280
|
+
.filter((key, index, array): key is string => typeof key === "string" && array.indexOf(key) === index)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function matchingModelEntries<T>(
|
|
284
|
+
record: Record<string, T> | undefined,
|
|
285
|
+
modelKeys: Array<string | undefined> = [],
|
|
286
|
+
): Array<[string, T]> {
|
|
287
|
+
if (!record || Object.keys(record).length === 0) return []
|
|
288
|
+
|
|
289
|
+
const candidates = uniqueModelCandidates(modelKeys)
|
|
290
|
+
if (candidates.length === 0) return []
|
|
291
|
+
|
|
292
|
+
const exactEntries = new Map<string, T>()
|
|
293
|
+
const wildcardEntries: Array<[string, T]> = []
|
|
294
|
+
|
|
295
|
+
for (const [rawKey, value] of Object.entries(record)) {
|
|
296
|
+
const key = normalizeModelKey(rawKey)
|
|
297
|
+
if (!key) continue
|
|
298
|
+
if (isWildcardPattern(key)) wildcardEntries.push([key, value])
|
|
299
|
+
else exactEntries.set(key, value)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const fullCandidates = candidates.filter((candidate) => candidate.includes("/"))
|
|
303
|
+
const bareCandidates = candidates.filter((candidate) => !candidate.includes("/"))
|
|
304
|
+
const matches: Array<[string, T]> = []
|
|
305
|
+
|
|
306
|
+
for (const candidate of bareCandidates) {
|
|
307
|
+
for (const entry of wildcardEntries) {
|
|
308
|
+
if (entry[0].includes("/")) continue
|
|
309
|
+
if (modelPatternMatches(entry[0], candidate)) matches.push(entry)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const candidate of bareCandidates) {
|
|
314
|
+
const value = exactEntries.get(candidate)
|
|
315
|
+
if (value !== undefined) matches.push([candidate, value])
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const candidate of fullCandidates) {
|
|
319
|
+
for (const entry of wildcardEntries) {
|
|
320
|
+
if (!entry[0].includes("/")) continue
|
|
321
|
+
if (modelPatternMatches(entry[0], candidate)) matches.push(entry)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const candidate of fullCandidates) {
|
|
326
|
+
const value = exactEntries.get(candidate)
|
|
327
|
+
if (value !== undefined) matches.push([candidate, value])
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return matches
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function modelKeysFromContext(ctx: unknown): string[] {
|
|
334
|
+
const ctxModel = (ctx as any)?.model
|
|
335
|
+
const provider = normalizeModelKey(
|
|
336
|
+
ctxModel?.provider ?? ctxModel?.providerId ?? ctxModel?.providerID,
|
|
337
|
+
)
|
|
338
|
+
const model = normalizeModelKey(
|
|
339
|
+
ctxModel?.id ?? ctxModel?.model ?? ctxModel?.modelId ?? ctxModel?.modelID,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return [provider && model ? `${provider}/${model}` : undefined, model].filter(
|
|
343
|
+
(key): key is string => typeof key === "string",
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function resolveModelConfig(
|
|
348
|
+
config: DcpConfig,
|
|
349
|
+
modelKeys: Array<string | undefined> = [],
|
|
350
|
+
): DcpConfig {
|
|
351
|
+
const overrides = config.modelOverrides
|
|
352
|
+
if (!overrides || Object.keys(overrides).length === 0) return config
|
|
353
|
+
|
|
354
|
+
const matches = matchingModelEntries(overrides, modelKeys)
|
|
355
|
+
if (matches.length === 0) return config
|
|
356
|
+
|
|
357
|
+
let resolved = deepMerge(config, {})
|
|
358
|
+
for (const [, override] of matches) {
|
|
359
|
+
resolved = deepMerge(resolved, override as Partial<DcpConfig>)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return resolved
|
|
363
|
+
}
|
|
364
|
+
|
|
229
365
|
// ---------------------------------------------------------------------------
|
|
230
366
|
// Public API
|
|
231
367
|
// ---------------------------------------------------------------------------
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
6
|
-
import { loadConfig } from "./config.js"
|
|
6
|
+
import { loadConfig, modelKeysFromContext, resolveModelConfig } from "./config.js"
|
|
7
7
|
import {
|
|
8
8
|
createState,
|
|
9
9
|
resetState,
|
|
@@ -102,8 +102,12 @@ function isDcpControlPlaneMessage(message: any): boolean {
|
|
|
102
102
|
export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
103
103
|
// ── 1. Load config ────────────────────────────────────────────────────────
|
|
104
104
|
const config = loadConfig()
|
|
105
|
+
const configForContext = (ctx: unknown) => resolveModelConfig(config, modelKeysFromContext(ctx))
|
|
106
|
+
const hasEnabledModelOverride = Object.values(config.modelOverrides).some(
|
|
107
|
+
(override) => override.enabled === true,
|
|
108
|
+
)
|
|
105
109
|
|
|
106
|
-
if (!config.enabled) return
|
|
110
|
+
if (!config.enabled && !hasEnabledModelOverride) return
|
|
107
111
|
|
|
108
112
|
// ── 2. Create state ───────────────────────────────────────────────────────
|
|
109
113
|
const state = createState()
|
|
@@ -175,6 +179,9 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
175
179
|
|
|
176
180
|
// ── 7. before_agent_start: inject system prompt ───────────────────────────
|
|
177
181
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
182
|
+
const effectiveConfig = configForContext(_ctx)
|
|
183
|
+
if (!effectiveConfig.enabled) return { systemPrompt: event.systemPrompt }
|
|
184
|
+
|
|
178
185
|
const promptAddition = state.manualMode
|
|
179
186
|
? MANUAL_MODE_SYSTEM_PROMPT
|
|
180
187
|
: SYSTEM_PROMPT
|
|
@@ -239,11 +246,15 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
239
246
|
|
|
240
247
|
// ── 10. context: apply pruning and inject nudges ──────────────────────────
|
|
241
248
|
pi.on("context", async (event, ctx) => {
|
|
249
|
+
const effectiveConfig = configForContext(ctx)
|
|
242
250
|
const contextMessages = event.messages.filter((message: any) =>
|
|
243
251
|
!isUserVisibleOnlyMessage(message) && !isDcpControlPlaneMessage(message)
|
|
244
252
|
)
|
|
253
|
+
if (!effectiveConfig.enabled) {
|
|
254
|
+
return { messages: contextMessages }
|
|
255
|
+
}
|
|
245
256
|
annotateMessagesWithBranchEntryIds(contextMessages, ctx)
|
|
246
|
-
let prunedMessages = applyPruning(contextMessages, state,
|
|
257
|
+
let prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
|
|
247
258
|
let candidate = null as ReturnType<typeof detectCompressionCandidate>
|
|
248
259
|
let messageCandidates = [] as ReturnType<typeof detectMessageCompressionCandidates>
|
|
249
260
|
|
|
@@ -273,11 +284,11 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
273
284
|
const ctxModel = (ctx as any).model
|
|
274
285
|
const provider = ctxModel?.provider ?? ctxModel?.providerId ?? ctxModel?.providerID
|
|
275
286
|
const model = ctxModel?.id ?? ctxModel?.model ?? ctxModel?.modelId ?? ctxModel?.modelID
|
|
276
|
-
const thresholds = resolveContextThresholds(
|
|
287
|
+
const thresholds = resolveContextThresholds(effectiveConfig, [
|
|
277
288
|
provider && model ? `${provider}/${model}` : undefined,
|
|
278
289
|
model,
|
|
279
290
|
], usage.contextWindow)
|
|
280
|
-
if (
|
|
291
|
+
if (effectiveConfig.compress.summaryBuffer) {
|
|
281
292
|
const summaryBonus = getActiveSummaryTokenEstimate(state) / usage.contextWindow
|
|
282
293
|
thresholds.maxContextPercent += Math.min(summaryBonus, SUMMARY_BUFFER_MAX_CONTEXT_BONUS)
|
|
283
294
|
}
|
|
@@ -292,7 +303,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
292
303
|
const nudgeType = getNudgeType(
|
|
293
304
|
contextPercent,
|
|
294
305
|
state,
|
|
295
|
-
|
|
306
|
+
effectiveConfig,
|
|
296
307
|
toolCallsSinceLastUser,
|
|
297
308
|
thresholds,
|
|
298
309
|
)
|
|
@@ -304,12 +315,13 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
304
315
|
candidate = detectCompressionCandidate(
|
|
305
316
|
prunedMessages,
|
|
306
317
|
state,
|
|
307
|
-
|
|
318
|
+
effectiveConfig,
|
|
308
319
|
contextPercent,
|
|
309
320
|
)
|
|
310
321
|
messageCandidates = detectMessageCompressionCandidates(
|
|
311
322
|
prunedMessages,
|
|
312
|
-
|
|
323
|
+
state,
|
|
324
|
+
effectiveConfig,
|
|
313
325
|
contextPercent,
|
|
314
326
|
)
|
|
315
327
|
|