pi-ui-extend 1.0.5 → 1.0.7
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/dist/bundled-extensions/session-title/title-generation-compat.d.ts +2 -13
- package/dist/bundled-extensions/session-title/title-generation-compat.js +1 -10
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +8 -2
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +12 -8
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +4 -4
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +4 -4
- package/external/pi-tools-suite/src/coding-discipline/index.ts +98 -44
- package/external/pi-tools-suite/src/config.ts +22 -0
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +2 -2
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +6 -0
- package/external/pi-tools-suite/src/model-completion.ts +12 -18
- package/package.json +4 -4
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { SessionTitleConfig } from "./config.js";
|
|
3
|
-
type TitleModelRegistry =
|
|
4
|
-
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
5
|
-
getApiKeyAndHeaders(model: Model<Api>): Promise<{
|
|
6
|
-
ok: true;
|
|
7
|
-
apiKey?: string;
|
|
8
|
-
headers?: Record<string, string>;
|
|
9
|
-
env?: Record<string, string>;
|
|
10
|
-
} | {
|
|
11
|
-
ok: false;
|
|
12
|
-
error: string;
|
|
13
|
-
}>;
|
|
14
|
-
};
|
|
3
|
+
type TitleModelRegistry = Pick<ModelRegistry, "complete" | "find">;
|
|
15
4
|
/** Extension-side title generation through Pi's public ModelRegistry facade. */
|
|
16
5
|
export declare function generateSessionTitle(input: string, modelRegistry: TitleModelRegistry, config: SessionTitleConfig, modelRef: string, signal: AbortSignal, onWarning?: (message: string) => void): Promise<string | undefined>;
|
|
17
6
|
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { complete } from "@earendil-works/pi-ai/compat";
|
|
2
1
|
import { buildTitlePrompt, parseTitleModelRef, sanitizeSessionTitle, TITLE_SYSTEM_PROMPT, titleResponseText, } from "./title-generation.js";
|
|
3
2
|
/** Extension-side title generation through Pi's public ModelRegistry facade. */
|
|
4
3
|
export async function generateSessionTitle(input, modelRegistry, config, modelRef, signal, onWarning) {
|
|
@@ -12,12 +11,7 @@ export async function generateSessionTitle(input, modelRegistry, config, modelRe
|
|
|
12
11
|
onWarning?.(`Session-title model not found: ${modelRef}`);
|
|
13
12
|
return undefined;
|
|
14
13
|
}
|
|
15
|
-
const
|
|
16
|
-
if (auth.ok === false) {
|
|
17
|
-
onWarning?.(auth.error);
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
const response = await complete(model, {
|
|
14
|
+
const response = await modelRegistry.complete(model, {
|
|
21
15
|
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
22
16
|
messages: [
|
|
23
17
|
{
|
|
@@ -27,9 +21,6 @@ export async function generateSessionTitle(input, modelRegistry, config, modelRe
|
|
|
27
21
|
},
|
|
28
22
|
],
|
|
29
23
|
}, {
|
|
30
|
-
...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }),
|
|
31
|
-
...(auth.headers === undefined ? {} : { headers: auth.headers }),
|
|
32
|
-
...(auth.env === undefined ? {} : { env: auth.env }),
|
|
33
24
|
cacheRetention: "none",
|
|
34
25
|
maxRetries: config.maxRetries,
|
|
35
26
|
maxTokens: config.maxTokens,
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"vscode-languageserver-protocol": "^3.17.5"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@earendil-works/pi-ai": "0.
|
|
47
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
48
|
-
"@earendil-works/pi-tui": "0.
|
|
46
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
48
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
49
49
|
"typebox": "*"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
@@ -221,7 +221,11 @@ export async function addAntigravityAccount(
|
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
async function refreshAccountToken(
|
|
224
|
+
async function refreshAccountToken(
|
|
225
|
+
account: OpencodeAntigravityAccount,
|
|
226
|
+
oauthClient?: GoogleOAuthClientCredentials,
|
|
227
|
+
signal?: AbortSignal,
|
|
228
|
+
): Promise<RefreshedAntigravityAccount> {
|
|
225
229
|
const refreshToken = getAccountRefreshToken(account);
|
|
226
230
|
if (!refreshToken) throw new Error(`Missing refresh token for Antigravity account ${account.email ?? "<unknown>"}`);
|
|
227
231
|
const clientCredentials = getGoogleOAuthClientCredentials(account, oauthClient);
|
|
@@ -230,6 +234,7 @@ async function refreshAccountToken(account: OpencodeAntigravityAccount, oauthCli
|
|
|
230
234
|
const start = Date.now();
|
|
231
235
|
const response = await fetch("https://oauth2.googleapis.com/token", {
|
|
232
236
|
method: "POST",
|
|
237
|
+
signal,
|
|
233
238
|
headers: {
|
|
234
239
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
235
240
|
Accept: "*/*",
|
|
@@ -256,7 +261,7 @@ async function refreshAccountToken(account: OpencodeAntigravityAccount, oauthCli
|
|
|
256
261
|
};
|
|
257
262
|
}
|
|
258
263
|
|
|
259
|
-
export async function refreshAntigravityToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
264
|
+
export async function refreshAntigravityToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials> {
|
|
260
265
|
const credentialDetails = credentials as OAuthCredentials & PiAuthCredential;
|
|
261
266
|
const oauthClient = getGoogleOAuthClientCredentials(credentialDetails);
|
|
262
267
|
const storedAccounts = getStoredAccounts(credentialDetails);
|
|
@@ -272,6 +277,7 @@ export async function refreshAntigravityToken(credentials: OAuthCredentials): Pr
|
|
|
272
277
|
email: credentialDetails.email,
|
|
273
278
|
},
|
|
274
279
|
oauthClient,
|
|
280
|
+
signal,
|
|
275
281
|
);
|
|
276
282
|
return {
|
|
277
283
|
...refreshed.credentials,
|
|
@@ -68,17 +68,21 @@ async function sendAntigravityRequest(
|
|
|
68
68
|
const headerStyle = getModelHeaderStyle(model);
|
|
69
69
|
const endpoints = headerStyle === "gemini-cli" ? [ENDPOINT_PROD] : STREAM_ENDPOINTS;
|
|
70
70
|
for (const endpoint of endpoints) {
|
|
71
|
+
const headers = new Headers({
|
|
72
|
+
Authorization: `Bearer ${apiKey}`,
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
Accept: "text/event-stream",
|
|
75
|
+
...getAntigravityHeaders(headerStyle),
|
|
76
|
+
...requestHeaders,
|
|
77
|
+
});
|
|
78
|
+
for (const [name, value] of Object.entries(options?.headers ?? {})) {
|
|
79
|
+
if (value === null) headers.delete(name);
|
|
80
|
+
else headers.set(name, value);
|
|
81
|
+
}
|
|
71
82
|
response = await fetch(`${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
|
|
72
83
|
method: "POST",
|
|
73
84
|
signal: options?.signal,
|
|
74
|
-
headers
|
|
75
|
-
Authorization: `Bearer ${apiKey}`,
|
|
76
|
-
"Content-Type": "application/json",
|
|
77
|
-
Accept: "text/event-stream",
|
|
78
|
-
...getAntigravityHeaders(headerStyle),
|
|
79
|
-
...requestHeaders,
|
|
80
|
-
...(options?.headers ?? {}),
|
|
81
|
-
},
|
|
85
|
+
headers,
|
|
82
86
|
body: JSON.stringify(payload),
|
|
83
87
|
});
|
|
84
88
|
await options?.onResponse?.({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
1
|
+
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
2
|
import { completeWithModelRegistry, type ModelCompletionRegistry } from "../../model-completion.js";
|
|
3
3
|
import type { AgentTask } from "./types.js";
|
|
4
4
|
import {
|
|
@@ -14,7 +14,7 @@ export interface SubagentRoutingContext {
|
|
|
14
14
|
modelRegistry?: ModelCompletionRegistry & {
|
|
15
15
|
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
16
16
|
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
17
|
-
| { ok?: true; apiKey?: string; headers?:
|
|
17
|
+
| { ok?: true; apiKey?: string; headers?: ProviderHeaders; baseUrl?: string; env?: Record<string, string> }
|
|
18
18
|
| { ok: false; error: string }
|
|
19
19
|
>;
|
|
20
20
|
};
|
|
@@ -180,7 +180,7 @@ async function resolveRoutingModels(
|
|
|
180
180
|
interface RoutingCandidate {
|
|
181
181
|
model: Model<Api>;
|
|
182
182
|
apiKey?: string;
|
|
183
|
-
headers?:
|
|
183
|
+
headers?: ProviderHeaders;
|
|
184
184
|
env?: Record<string, string>;
|
|
185
185
|
}
|
|
186
186
|
|
|
@@ -189,7 +189,7 @@ type RoutingResponse = Awaited<ReturnType<typeof completeWithModelRegistry>>;
|
|
|
189
189
|
async function resolveModelRef(ctx: SubagentRoutingContext, modelRef: string): Promise<{
|
|
190
190
|
model: Model<Api>;
|
|
191
191
|
apiKey?: string;
|
|
192
|
-
headers?:
|
|
192
|
+
headers?: ProviderHeaders;
|
|
193
193
|
env?: Record<string, string>;
|
|
194
194
|
} | undefined> {
|
|
195
195
|
const parsed = parseModelRef(modelRef);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
1
|
+
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
2
|
import { completeWithModelRegistry, type ModelCompletionRegistry } from "../../model-completion.js";
|
|
3
3
|
import { currentModelRef, resolveSubagentRoutingConfig, type SubagentConfig } from "./config.js";
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ export interface UltraworkAutoContext {
|
|
|
9
9
|
modelRegistry?: ModelCompletionRegistry & {
|
|
10
10
|
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
11
11
|
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
12
|
-
| { ok?: true; apiKey?: string; headers?:
|
|
12
|
+
| { ok?: true; apiKey?: string; headers?: ProviderHeaders; baseUrl?: string; env?: Record<string, string> }
|
|
13
13
|
| { ok: false; error: string }
|
|
14
14
|
>;
|
|
15
15
|
};
|
|
@@ -131,7 +131,7 @@ function buildClassifierPrompt(userText: string): string {
|
|
|
131
131
|
async function resolveClassifierModel(ctx: UltraworkAutoContext, modelRef: string): Promise<{
|
|
132
132
|
model: Model<Api>;
|
|
133
133
|
apiKey?: string;
|
|
134
|
-
headers?:
|
|
134
|
+
headers?: ProviderHeaders;
|
|
135
135
|
env?: Record<string, string>;
|
|
136
136
|
} | undefined> {
|
|
137
137
|
const configured = await resolveModelRef(ctx, modelRef);
|
|
@@ -143,7 +143,7 @@ async function resolveClassifierModel(ctx: UltraworkAutoContext, modelRef: strin
|
|
|
143
143
|
async function resolveModelRef(ctx: UltraworkAutoContext, modelRef: string): Promise<{
|
|
144
144
|
model: Model<Api>;
|
|
145
145
|
apiKey?: string;
|
|
146
|
-
headers?:
|
|
146
|
+
headers?: ProviderHeaders;
|
|
147
147
|
env?: Record<string, string>;
|
|
148
148
|
} | undefined> {
|
|
149
149
|
const parsed = parseModelRef(modelRef);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { Api, AssistantMessage, ImageContent, Model, TextContent } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { Api, AssistantMessage, ImageContent, Model, ProviderHeaders, TextContent } from "@earendil-works/pi-ai";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
|
|
6
|
-
import { loadPiToolsSuiteConfig } from "../config.js";
|
|
6
|
+
import { loadPiToolsSuiteConfig, DEFAULT_CODING_DISCIPLINE_STRICTNESS, type CodingDisciplineStrictness } from "../config.js";
|
|
7
7
|
import { ignoreStaleExtensionContextError } from "../context-usage.js";
|
|
8
8
|
import { completeWithModelRegistry, type ModelCompletionRegistry } from "../model-completion.js";
|
|
9
9
|
|
|
@@ -28,7 +28,7 @@ type ResolvedLookupModel = {
|
|
|
28
28
|
modelRegistry: ModelCompletionRegistry;
|
|
29
29
|
model: Model<Api>;
|
|
30
30
|
apiKey?: string;
|
|
31
|
-
headers?:
|
|
31
|
+
headers?: ProviderHeaders;
|
|
32
32
|
env?: Record<string, string>;
|
|
33
33
|
};
|
|
34
34
|
|
|
@@ -43,6 +43,9 @@ const DEFAULT_LOOKUP_TIMEOUT_MS = 120_000;
|
|
|
43
43
|
const MAX_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
44
44
|
const SILENCE_REMINDER_MIN_VIOLATION_GAP = 3;
|
|
45
45
|
const SILENCE_REMINDER_MIN_MESSAGE_GAP = 12;
|
|
46
|
+
// When the visible history shrinks by more than this many messages (compaction or
|
|
47
|
+
// truncation), the chatter baseline is reset so it isn't measured against a stale peak.
|
|
48
|
+
const SILENCE_REMINDER_COMPACTION_MARGIN = 8;
|
|
46
49
|
const LOOKUP_TOOL_NAME = "lookup";
|
|
47
50
|
|
|
48
51
|
const LOOKUP_TOOL_PARAMS = Type.Object(
|
|
@@ -60,31 +63,46 @@ const LOOKUP_TOOL_PARAMS = Type.Object(
|
|
|
60
63
|
{ additionalProperties: false },
|
|
61
64
|
);
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
66
|
+
type DisciplinePromptOptions = { lookupEnabled?: boolean; strictness?: CodingDisciplineStrictness };
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Builds the tool-only contract lines. The working-state rule is strictness-aware:
|
|
70
|
+
* - "strict" — one tool call per turn, no assistant text at all (Opus-like);
|
|
71
|
+
* - "lenient" — independent (especially read-only) calls may be batched, and brief
|
|
72
|
+
* substantive reasoning text is acceptable when thinking is off.
|
|
73
|
+
*/
|
|
74
|
+
function buildDisciplineLines(strictness: CodingDisciplineStrictness): readonly string[] {
|
|
75
|
+
const isStrict = strictness === "strict";
|
|
76
|
+
return [
|
|
77
|
+
"TOOL-ONLY CODING AGENT CONTRACT.",
|
|
78
|
+
"",
|
|
79
|
+
"Follow this output-channel contract literally for coding tasks.",
|
|
80
|
+
isStrict
|
|
81
|
+
? "WORKING STATE: from the first assistant turn until complete, final, or blocked, emit exactly one tool call with empty text."
|
|
82
|
+
: "WORKING STATE: from the first assistant turn until complete, final, or blocked, emit tool calls with empty text.",
|
|
83
|
+
isStrict
|
|
84
|
+
? "No prose, markdown, acknowledgments, plans, progress, summaries, explanations, or commentary before/after tool calls."
|
|
85
|
+
: "Batch independent calls in one turn (especially read-only inspection: read/grep/glob/bash); make dependent calls sequentially. Avoid filler prose, acknowledgments, restatements, and narration of obvious next steps.",
|
|
86
|
+
"Keep reasoning, planning, hypotheses, and retry decisions internal when thinking is on; when thinking is off, brief substantive reasoning text is acceptable.",
|
|
87
|
+
"",
|
|
88
|
+
"After every tool result, choose exactly one transition:",
|
|
89
|
+
isStrict ? "- WORKING: exactly one next tool call with empty text;" : "- WORKING: one or more independent tool calls with empty text;",
|
|
90
|
+
"- FINAL: one final answer after completion or practical verification;",
|
|
91
|
+
"- BLOCKED: one concise question only when no safe/useful tool action can continue.",
|
|
92
|
+
"No transition permits commentary between tool calls.",
|
|
93
|
+
"",
|
|
94
|
+
"PRIORITY: this overrides default assistant friendliness and conversational behavior.",
|
|
95
|
+
"",
|
|
96
|
+
"Coding discipline (express only through tool choices, not prose):",
|
|
97
|
+
"- inspect before editing; do not invent APIs, files, commands, or behavior;",
|
|
98
|
+
"- make the smallest change that fully fixes the issue; follow nearby conventions;",
|
|
99
|
+
"- for bugs, prefer a failing repro first, then the minimal fix, then verify;",
|
|
100
|
+
"- high-risk changes (security, data/schema, public APIs, concurrency, irreversible) need a short spec first;",
|
|
101
|
+
"- handle edge cases, errors, cancellation, and async behavior; do not block UI/event loops;",
|
|
102
|
+
"- avoid duplicate state, duplicate prompts, and repeated side effects;",
|
|
103
|
+
"- write code, identifiers, comments, and commit messages in English.",
|
|
104
|
+
];
|
|
105
|
+
}
|
|
88
106
|
|
|
89
107
|
const LOOKUP_DISCIPLINE_LINES = [
|
|
90
108
|
"",
|
|
@@ -105,7 +123,7 @@ const FINAL_DISCIPLINE_LINES = [
|
|
|
105
123
|
const SILENCE_REMINDER_TEXT = [
|
|
106
124
|
"GLM silence reminder: remain in WORKING state.",
|
|
107
125
|
"Continue with tool-only discipline: inspect, verify, and act through tools only.",
|
|
108
|
-
"For the next step, emit
|
|
126
|
+
"For the next step, emit tool calls with no accompanying assistant text.",
|
|
109
127
|
"Do not acknowledge this reminder.",
|
|
110
128
|
].join("\n");
|
|
111
129
|
|
|
@@ -147,6 +165,7 @@ export default function codingDiscipline(pi: ExtensionAPI) {
|
|
|
147
165
|
let silenceViolationCount = 0;
|
|
148
166
|
let lastReminderViolationCount = 0;
|
|
149
167
|
let lastReminderMessageCount = -SILENCE_REMINDER_MIN_MESSAGE_GAP;
|
|
168
|
+
let peakMessageCount = 0;
|
|
150
169
|
|
|
151
170
|
function maybeRegisterLookupTool(cwd?: string): void {
|
|
152
171
|
if (lookupRegistered) return;
|
|
@@ -193,8 +212,10 @@ export default function codingDiscipline(pi: ExtensionAPI) {
|
|
|
193
212
|
pi.on("before_provider_request", async (event: { payload?: unknown }, ctx: unknown) => {
|
|
194
213
|
const modelRef = modelRefFromPayload(event.payload) ?? selectedModelRef ?? modelRefFromContext(ctx);
|
|
195
214
|
if (!isGlmModel(modelRef)) return undefined;
|
|
215
|
+
const cwd = contextCwd(ctx);
|
|
196
216
|
const injected = injectCodingDisciplineIntoPayload(event.payload, {
|
|
197
|
-
lookupEnabled: Boolean(lookupModelFromConfig(
|
|
217
|
+
lookupEnabled: Boolean(lookupModelFromConfig(cwd)),
|
|
218
|
+
strictness: codingDisciplineStrictnessFromConfig(cwd),
|
|
198
219
|
});
|
|
199
220
|
if (process.env.PI_DEBUG_PROMPT === "1") {
|
|
200
221
|
logFinalPrompt(injected, modelRef, contextCwd(ctx) ?? process.cwd());
|
|
@@ -270,10 +291,22 @@ export default function codingDiscipline(pi: ExtensionAPI) {
|
|
|
270
291
|
const modelRef = selectedModelRef ?? modelRefFromContext(ctx);
|
|
271
292
|
if (!isGlmModel(modelRef) || !Array.isArray(event.messages)) return undefined;
|
|
272
293
|
|
|
273
|
-
const
|
|
294
|
+
const messageCount = event.messages.length;
|
|
295
|
+
// Compaction/truncation prunes the visible history. Reset the stale chatter
|
|
296
|
+
// baseline so the post-compaction turn isn't measured against a pre-compaction
|
|
297
|
+
// violation peak (otherwise the model could chatter freely until the count
|
|
298
|
+
// climbed back above the old peak, or get nagged on a freshly small count).
|
|
299
|
+
if (messageCount + SILENCE_REMINDER_COMPACTION_MARGIN < peakMessageCount) {
|
|
300
|
+
silenceViolationCount = 0;
|
|
301
|
+
lastReminderViolationCount = 0;
|
|
302
|
+
lastReminderMessageCount = messageCount - SILENCE_REMINDER_MIN_MESSAGE_GAP;
|
|
303
|
+
}
|
|
304
|
+
peakMessageCount = Math.max(peakMessageCount, messageCount);
|
|
305
|
+
|
|
306
|
+
const strictness = codingDisciplineStrictnessFromConfig(contextCwd(ctx));
|
|
307
|
+
const violationCount = countAssistantToolChatter(event.messages, strictness);
|
|
274
308
|
if (violationCount <= silenceViolationCount) return undefined;
|
|
275
309
|
|
|
276
|
-
const messageCount = event.messages.length;
|
|
277
310
|
const violationGap = violationCount - lastReminderViolationCount;
|
|
278
311
|
const messageGap = messageCount - lastReminderMessageCount;
|
|
279
312
|
silenceViolationCount = violationCount;
|
|
@@ -286,7 +319,7 @@ export default function codingDiscipline(pi: ExtensionAPI) {
|
|
|
286
319
|
});
|
|
287
320
|
}
|
|
288
321
|
|
|
289
|
-
export function prependCodingDisciplinePrompt(systemPrompt: string, options:
|
|
322
|
+
export function prependCodingDisciplinePrompt(systemPrompt: string, options: DisciplinePromptOptions = {}): string {
|
|
290
323
|
const deduped = systemPrompt
|
|
291
324
|
.replace(LEGACY_SILENT_PROMPT_BLOCK_PATTERN, "")
|
|
292
325
|
.replace(DISCIPLINE_PROMPT_BLOCK_PATTERN, "")
|
|
@@ -296,10 +329,11 @@ export function prependCodingDisciplinePrompt(systemPrompt: string, options: { l
|
|
|
296
329
|
return deduped ? `${prompt}\n\n${deduped}` : prompt;
|
|
297
330
|
}
|
|
298
331
|
|
|
299
|
-
export function buildCodingDisciplinePrompt(options:
|
|
332
|
+
export function buildCodingDisciplinePrompt(options: DisciplinePromptOptions = {}): string {
|
|
333
|
+
const strictness = options.strictness ?? DEFAULT_CODING_DISCIPLINE_STRICTNESS;
|
|
300
334
|
return [
|
|
301
335
|
DISCIPLINE_PROMPT_MARKER_START,
|
|
302
|
-
...
|
|
336
|
+
...buildDisciplineLines(strictness),
|
|
303
337
|
...(options.lookupEnabled ? LOOKUP_DISCIPLINE_LINES : []),
|
|
304
338
|
...FINAL_DISCIPLINE_LINES,
|
|
305
339
|
DISCIPLINE_PROMPT_MARKER_END,
|
|
@@ -311,7 +345,7 @@ export function isGlmModel(modelRef: string | undefined): boolean {
|
|
|
311
345
|
return /(?:^|[/:_.-])glm(?:$|[/:_.-]|\d)/i.test(modelRef);
|
|
312
346
|
}
|
|
313
347
|
|
|
314
|
-
export function injectCodingDisciplineIntoPayload(payload: unknown, options:
|
|
348
|
+
export function injectCodingDisciplineIntoPayload(payload: unknown, options: DisciplinePromptOptions = {}): unknown {
|
|
315
349
|
if (!isRecord(payload)) return payload;
|
|
316
350
|
|
|
317
351
|
if (typeof payload.instructions === "string") {
|
|
@@ -555,7 +589,7 @@ function createLookupTool() {
|
|
|
555
589
|
};
|
|
556
590
|
}
|
|
557
591
|
|
|
558
|
-
function injectIntoMessages(messages: unknown[], options:
|
|
592
|
+
function injectIntoMessages(messages: unknown[], options: DisciplinePromptOptions): unknown[] {
|
|
559
593
|
const next = [...messages];
|
|
560
594
|
const index = next.findIndex(isInstructionMessage);
|
|
561
595
|
if (index === -1) return [{ role: "system", content: buildCodingDisciplinePrompt(options) }, ...next];
|
|
@@ -570,7 +604,7 @@ function injectIntoMessages(messages: unknown[], options: { lookupEnabled?: bool
|
|
|
570
604
|
return next;
|
|
571
605
|
}
|
|
572
606
|
|
|
573
|
-
function injectIntoMessageContent(content: unknown, options:
|
|
607
|
+
function injectIntoMessageContent(content: unknown, options: DisciplinePromptOptions): unknown {
|
|
574
608
|
if (typeof content === "string") return prependCodingDisciplinePrompt(content, options);
|
|
575
609
|
if (!Array.isArray(content)) return undefined;
|
|
576
610
|
|
|
@@ -588,21 +622,34 @@ function isInstructionMessage(message: unknown): boolean {
|
|
|
588
622
|
return message.role === "system" || message.role === "developer";
|
|
589
623
|
}
|
|
590
624
|
|
|
591
|
-
function countAssistantToolChatter(messages: readonly unknown[]): number {
|
|
625
|
+
function countAssistantToolChatter(messages: readonly unknown[], strictness: CodingDisciplineStrictness): number {
|
|
592
626
|
let count = 0;
|
|
593
627
|
for (const message of messages) {
|
|
594
|
-
if (!isAssistantToolChatter(message)) continue;
|
|
628
|
+
if (!isAssistantToolChatter(message, strictness)) continue;
|
|
595
629
|
count++;
|
|
596
630
|
}
|
|
597
631
|
return count;
|
|
598
632
|
}
|
|
599
633
|
|
|
600
|
-
|
|
634
|
+
/**
|
|
635
|
+
* Detects assistant chatter (text alongside a tool call).
|
|
636
|
+
* - "strict" — any such text counts as chatter.
|
|
637
|
+
* - "lenient" — text counts only when a thinking/reasoning block already captured the
|
|
638
|
+
* reasoning; without a thinking block, the visible text is the model's only reasoning
|
|
639
|
+
* channel and must not be suppressed.
|
|
640
|
+
*/
|
|
641
|
+
function isAssistantToolChatter(message: unknown, strictness: CodingDisciplineStrictness): boolean {
|
|
601
642
|
if (!isRecord(message) || message.role !== "assistant") return false;
|
|
602
643
|
if (!Array.isArray(message.content)) return false;
|
|
603
644
|
const hasToolCall = message.content.some((part) => isRecord(part) && part.type === "toolCall");
|
|
604
645
|
if (!hasToolCall) return false;
|
|
605
|
-
|
|
646
|
+
const hasText = message.content.some((part) => isRecord(part) && part.type === "text" && hasNonEmptyText(part.text));
|
|
647
|
+
if (!hasText) return false;
|
|
648
|
+
if (strictness === "strict") return true;
|
|
649
|
+
const hasThinking = message.content.some(
|
|
650
|
+
(part) => isRecord(part) && (part.type === "thinking" || part.type === "reasoning"),
|
|
651
|
+
);
|
|
652
|
+
return Boolean(hasThinking);
|
|
606
653
|
}
|
|
607
654
|
|
|
608
655
|
function hasNonEmptyText(value: unknown): boolean {
|
|
@@ -611,7 +658,9 @@ function hasNonEmptyText(value: unknown): boolean {
|
|
|
611
658
|
|
|
612
659
|
function createSilenceReminderMessage() {
|
|
613
660
|
return {
|
|
614
|
-
|
|
661
|
+
// Inject as a developer/system-level nudge rather than impersonating the user,
|
|
662
|
+
// so it reads as an automated reminder, not a user instruction.
|
|
663
|
+
role: "developer" as const,
|
|
615
664
|
content: [{ type: "text" as const, text: SILENCE_REMINDER_TEXT }],
|
|
616
665
|
timestamp: Date.now(),
|
|
617
666
|
};
|
|
@@ -621,6 +670,10 @@ function lookupModelFromConfig(cwd?: string): string | undefined {
|
|
|
621
670
|
return loadPiToolsSuiteConfig(["coding-discipline"], { cwd: cwd ?? process.cwd() }).lookupModel;
|
|
622
671
|
}
|
|
623
672
|
|
|
673
|
+
function codingDisciplineStrictnessFromConfig(cwd?: string): CodingDisciplineStrictness {
|
|
674
|
+
return loadPiToolsSuiteConfig(["coding-discipline"], { cwd: cwd ?? process.cwd() }).codingDisciplineStrictness ?? DEFAULT_CODING_DISCIPLINE_STRICTNESS;
|
|
675
|
+
}
|
|
676
|
+
|
|
624
677
|
function buildLookupPrompt(params: LookupParams, recentContext: string, imageCount: number, warnings: string[]): string {
|
|
625
678
|
return [
|
|
626
679
|
"Lookup request from a blind GLM parent model.",
|
|
@@ -772,7 +825,8 @@ async function resolveLookupModel(ctx: unknown, modelRef: string): Promise<Resol
|
|
|
772
825
|
const auth = await registry.getApiKeyAndHeaders(model) as {
|
|
773
826
|
ok?: true;
|
|
774
827
|
apiKey?: string;
|
|
775
|
-
headers?:
|
|
828
|
+
headers?: ProviderHeaders;
|
|
829
|
+
baseUrl?: string;
|
|
776
830
|
env?: Record<string, string>;
|
|
777
831
|
} | { ok: false; error: string };
|
|
778
832
|
if (auth.ok === false) return undefined;
|
|
@@ -11,6 +11,14 @@ export interface PiToolsSuiteConfig {
|
|
|
11
11
|
todoThinking: boolean;
|
|
12
12
|
/** Vision-capable model used by the coding-discipline lookup tool; unset disables lookup. */
|
|
13
13
|
lookupModel?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Chatter-detector strictness for the coding-discipline module:
|
|
16
|
+
* "strict" — any assistant text alongside a tool call is chatter (Opus-like);
|
|
17
|
+
* "lenient" — text is only chatter when a thinking block already captured the
|
|
18
|
+
* reasoning; without thinking, visible text is the reasoning channel.
|
|
19
|
+
* Default: "lenient".
|
|
20
|
+
*/
|
|
21
|
+
codingDisciplineStrictness?: CodingDisciplineStrictness;
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
type MutableConfig = {
|
|
@@ -18,8 +26,13 @@ type MutableConfig = {
|
|
|
18
26
|
disabledModules: Set<string>;
|
|
19
27
|
todoThinking: boolean;
|
|
20
28
|
lookupModel: string | undefined;
|
|
29
|
+
codingDisciplineStrictness: CodingDisciplineStrictness;
|
|
21
30
|
};
|
|
22
31
|
|
|
32
|
+
export const CODING_DISCIPLINE_STRICTNESS_VALUES = ["strict", "lenient"] as const;
|
|
33
|
+
export type CodingDisciplineStrictness = (typeof CODING_DISCIPLINE_STRICTNESS_VALUES)[number];
|
|
34
|
+
export const DEFAULT_CODING_DISCIPLINE_STRICTNESS: CodingDisciplineStrictness = "lenient";
|
|
35
|
+
|
|
23
36
|
type Env = Record<string, string | undefined>;
|
|
24
37
|
|
|
25
38
|
const TRUE_VALUES = new Set(["1", "true", "on", "yes"]);
|
|
@@ -62,6 +75,10 @@ function normalizeLookupModel(raw: unknown): string | undefined {
|
|
|
62
75
|
return trimmed ? trimmed : undefined;
|
|
63
76
|
}
|
|
64
77
|
|
|
78
|
+
function normalizeCodingDisciplineStrictness(raw: unknown): CodingDisciplineStrictness {
|
|
79
|
+
return raw === "strict" ? "strict" : "lenient";
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
function boolFromEnv(value: string | undefined): boolean | undefined {
|
|
66
83
|
if (value === undefined) return undefined;
|
|
67
84
|
const normalized = value.trim().toLowerCase();
|
|
@@ -121,6 +138,9 @@ function mergeConfigLayer(config: MutableConfig, raw: Record<string, unknown>, k
|
|
|
121
138
|
if (typeof raw.enabled === "boolean") config.enabled = raw.enabled;
|
|
122
139
|
if (typeof raw.todoThinking === "boolean") config.todoThinking = raw.todoThinking;
|
|
123
140
|
if (Object.prototype.hasOwnProperty.call(raw, "lookupModel")) config.lookupModel = normalizeLookupModel(raw.lookupModel);
|
|
141
|
+
if (Object.prototype.hasOwnProperty.call(raw, "codingDisciplineStrictness")) {
|
|
142
|
+
config.codingDisciplineStrictness = normalizeCodingDisciplineStrictness(raw.codingDisciplineStrictness);
|
|
143
|
+
}
|
|
124
144
|
|
|
125
145
|
for (const key of DISABLED_LIST_KEYS) addDisabled(config, raw[key], knownModules);
|
|
126
146
|
for (const key of ENABLED_LIST_KEYS) removeDisabled(config, raw[key], knownModules);
|
|
@@ -178,6 +198,7 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
|
|
|
178
198
|
disabledModules: new Set([...DEFAULT_DISABLED_MODULES].filter((name) => knownModules.has(name))),
|
|
179
199
|
todoThinking: false,
|
|
180
200
|
lookupModel: undefined,
|
|
201
|
+
codingDisciplineStrictness: DEFAULT_CODING_DISCIPLINE_STRICTNESS,
|
|
181
202
|
};
|
|
182
203
|
const userConfigPath = getPiToolsSuiteUserConfigPath(options.homeDir);
|
|
183
204
|
|
|
@@ -197,5 +218,6 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
|
|
|
197
218
|
disabledModules: [...config.disabledModules].sort(),
|
|
198
219
|
todoThinking: config.todoThinking,
|
|
199
220
|
...(config.lookupModel ? { lookupModel: config.lookupModel } : {}),
|
|
221
|
+
codingDisciplineStrictness: config.codingDisciplineStrictness,
|
|
200
222
|
};
|
|
201
223
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// automatic fallback to the programmatic digest on any failure/timeout.
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
|
-
import type { Model, Api } from "@earendil-works/pi-ai"
|
|
16
|
+
import type { Model, Api, ProviderHeaders } from "@earendil-works/pi-ai"
|
|
17
17
|
import { completeWithModelRegistry, type ModelCompletionRegistry } from "../model-completion.js"
|
|
18
18
|
import type { DcpState } from "./state.js"
|
|
19
19
|
import type { DcpConfig } from "./config.js"
|
|
@@ -137,7 +137,7 @@ export interface ModelSummaryResult {
|
|
|
137
137
|
type ModelSummaryRegistry = ModelCompletionRegistry & {
|
|
138
138
|
find(provider: string, modelId: string): Model<Api> | undefined
|
|
139
139
|
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
140
|
-
| { ok: true; apiKey?: string; headers?:
|
|
140
|
+
| { ok: true; apiKey?: string; headers?: ProviderHeaders; baseUrl?: string; env?: Record<string, string> }
|
|
141
141
|
| { ok: false; error: string }
|
|
142
142
|
>
|
|
143
143
|
}
|
|
@@ -11,6 +11,12 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
11
11
|
// Vision-capable model used by the coding-discipline lookup tool for blind-model
|
|
12
12
|
// screenshot/image questions. Remove or set to null to disable lookup.
|
|
13
13
|
"lookupModel": "openai-codex/gpt-5.4-mini",
|
|
14
|
+
// coding-discipline working-state strictness.
|
|
15
|
+
// "lenient" (default): batch independent read-only tool calls; brief reasoning
|
|
16
|
+
// text is acceptable when thinking is off. Text only counts as chatter when a
|
|
17
|
+
// thinking/reasoning block already captured the reasoning.
|
|
18
|
+
// "strict": one tool call per turn, no assistant text at all (Opus-like).
|
|
19
|
+
"codingDisciplineStrictness": "lenient",
|
|
14
20
|
"terminalBell": { "sound": true },
|
|
15
21
|
// comment-checker: nudges the agent to remove AI-slop code comments it just
|
|
16
22
|
// added via write/edit/apply_patch. Net-new comments are classified and a
|
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Api,
|
|
3
3
|
AssistantMessage,
|
|
4
|
-
AssistantMessageEventStream,
|
|
5
4
|
Context,
|
|
6
5
|
Model,
|
|
6
|
+
ModelsApiStreamOptions,
|
|
7
7
|
SimpleStreamOptions,
|
|
8
8
|
} from "@earendil-works/pi-ai";
|
|
9
9
|
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
10
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
|
|
11
|
-
type
|
|
12
|
-
streamSimple?: (
|
|
13
|
-
model: Model<Api>,
|
|
14
|
-
context: Context,
|
|
15
|
-
options?: SimpleStreamOptions,
|
|
16
|
-
) => AssistantMessageEventStream;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type ModelCompletionRegistry = {
|
|
20
|
-
getRegisteredProviderConfig?(providerId: string): RegisteredProviderConfig | undefined;
|
|
21
|
-
};
|
|
12
|
+
export type ModelCompletionRegistry = Partial<Pick<ModelRegistry, "complete" | "getRegisteredProviderConfig">>;
|
|
22
13
|
|
|
23
14
|
/**
|
|
24
|
-
* Complete through
|
|
25
|
-
*
|
|
26
|
-
*
|
|
15
|
+
* Complete through Pi's model runtime when available so custom providers and
|
|
16
|
+
* resolved auth endpoints are preserved. The older stream/compat branches are
|
|
17
|
+
* retained for narrow test doubles that do not expose ModelRegistry.complete().
|
|
27
18
|
*/
|
|
28
19
|
export async function completeWithModelRegistry(
|
|
29
20
|
modelRegistry: ModelCompletionRegistry | undefined,
|
|
30
21
|
model: Model<Api>,
|
|
31
22
|
context: Context,
|
|
32
|
-
options?:
|
|
23
|
+
options?: ModelsApiStreamOptions<Api>,
|
|
33
24
|
): Promise<AssistantMessage> {
|
|
25
|
+
if (modelRegistry?.complete) return modelRegistry.complete(model, context, options);
|
|
26
|
+
|
|
34
27
|
const providerConfig = modelRegistry?.getRegisteredProviderConfig?.(model.provider);
|
|
35
|
-
|
|
36
|
-
return
|
|
28
|
+
const simpleOptions = options as SimpleStreamOptions | undefined;
|
|
29
|
+
if (providerConfig?.streamSimple) return providerConfig.streamSimple(model, context, simpleOptions).result();
|
|
30
|
+
return completeSimple(model, context, simpleOptions);
|
|
37
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ui-extend",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Pix: a workspace-first terminal UI for Pi with tabs, readable tool activity, voice input, and bundled agent tools.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"repository": {
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
"prepublishOnly": "npm run check && npm run build:pix && npm run generate-schemas"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@earendil-works/pi-ai": "0.
|
|
79
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
80
|
-
"@earendil-works/pi-tui": "0.
|
|
78
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
79
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
80
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
81
81
|
"@mariozechner/clipboard": "^0.3.9",
|
|
82
82
|
"jsonc-parser": "3.3.1",
|
|
83
83
|
"typebox": "1.1.38",
|