pi-ui-extend 0.1.18 → 0.1.19
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/app/app.js +8 -6
- package/dist/app/constants.d.ts +1 -0
- package/dist/app/constants.js +1 -0
- package/dist/app/input/voice-controller.js +16 -12
- package/dist/app/popup/popup-menu-controller.d.ts +1 -5
- package/dist/app/popup/popup-menu-controller.js +7 -8
- package/dist/app/process.js +7 -0
- package/dist/app/rendering/conversation-entry-renderer.js +17 -16
- package/dist/app/rendering/conversation-viewport.js +4 -35
- package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
- package/dist/app/rendering/editor-layout-renderer.js +25 -16
- package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
- package/dist/app/rendering/popup-menu-renderer.js +24 -34
- package/dist/app/rendering/render-controller.d.ts +2 -0
- package/dist/app/rendering/render-controller.js +26 -25
- package/dist/app/rendering/render-text.js +2 -2
- package/dist/app/rendering/status-line-renderer.js +1 -1
- package/dist/app/rendering/tab-line-renderer.js +3 -3
- package/dist/app/runtime.js +29 -3
- package/dist/app/screen/file-link-opener.d.ts +2 -0
- package/dist/app/screen/file-link-opener.js +84 -17
- package/dist/app/screen/mouse-controller.d.ts +0 -2
- package/dist/app/screen/mouse-controller.js +6 -12
- package/dist/app/screen/screen-styler.js +1 -1
- package/dist/app/session/lazy-session-manager.d.ts +1 -1
- package/dist/app/session/lazy-session-manager.js +64 -52
- package/dist/app/session/queued-message-controller.d.ts +6 -0
- package/dist/app/session/queued-message-controller.js +9 -1
- package/dist/app/session/queued-message-entries.d.ts +8 -0
- package/dist/app/session/queued-message-entries.js +41 -0
- package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
- package/dist/app/session/session-lifecycle-controller.js +45 -11
- package/dist/app/session/tabs-controller.d.ts +11 -1
- package/dist/app/session/tabs-controller.js +197 -30
- package/dist/app/terminal/terminal-controller.d.ts +2 -0
- package/dist/app/terminal/terminal-controller.js +7 -5
- package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
- package/dist/schemas/pi-tools-suite-schema.js +3 -0
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +8 -2
- package/extensions/session-title/config.ts +3 -3
- package/extensions/session-title/index.ts +60 -5
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
- package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
- package/external/pi-tools-suite/src/config.ts +13 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -4
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -1
- package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +580 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
- package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
- package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
- package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
- package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +1 -0
- package/external/pi-tools-suite/src/todo/index.ts +81 -4
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
- package/package.json +3 -14
- package/schemas/pi-tools-suite.json +19 -0
- package/apps/desktop-tauri/README.md +0 -103
- package/apps/desktop-tauri/bin/pix-desktop.mjs +0 -89
package/dist/theme.js
CHANGED
|
@@ -14,7 +14,10 @@ export const THEMES = {
|
|
|
14
14
|
inputForeground: "#f0f6fc",
|
|
15
15
|
inputBackground: "#090d13",
|
|
16
16
|
inputBorder: "#30363d",
|
|
17
|
-
|
|
17
|
+
inputBorderWidgetBackground: "#2a2f36",
|
|
18
|
+
tabBorder: "#7d8590",
|
|
19
|
+
assistantMessageBackground: "#161b22",
|
|
20
|
+
userMessageBackground: "#262224",
|
|
18
21
|
inputCursorBackground: "#7fb3c8",
|
|
19
22
|
popupForeground: "#e6edf3",
|
|
20
23
|
popupBackground: "#1e1e1e",
|
|
@@ -56,7 +59,10 @@ export const THEMES = {
|
|
|
56
59
|
inputForeground: "#0f172a",
|
|
57
60
|
inputBackground: "#f8fafc",
|
|
58
61
|
inputBorder: "#334155",
|
|
59
|
-
|
|
62
|
+
inputBorderWidgetBackground: "#f1f5f9",
|
|
63
|
+
tabBorder: "#64748b",
|
|
64
|
+
assistantMessageBackground: "#eef2f7",
|
|
65
|
+
userMessageBackground: "#f9f0ee",
|
|
60
66
|
inputCursorBackground: "#0284c7",
|
|
61
67
|
popupForeground: "#0f172a",
|
|
62
68
|
popupBackground: "#ffffff",
|
|
@@ -26,9 +26,9 @@ const DEFAULT_CONFIG: SessionTitleConfig = {
|
|
|
26
26
|
maxInputChars: 2000,
|
|
27
27
|
maxTitleChars: 80,
|
|
28
28
|
maxTokens: 32,
|
|
29
|
-
maxRetries:
|
|
30
|
-
generationAttempts:
|
|
31
|
-
retryDelayMs:
|
|
29
|
+
maxRetries: 2,
|
|
30
|
+
generationAttempts: 3,
|
|
31
|
+
retryDelayMs: 3000,
|
|
32
32
|
temperature: 0.2,
|
|
33
33
|
timeoutMs: 12_000,
|
|
34
34
|
terminalTitle: true,
|
|
@@ -70,6 +70,31 @@ export function firstUserMessageText(ctx: ExtensionContext): string | undefined
|
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export function fallbackSessionTitleFromInput(input: string, maxTitleChars: number): string | undefined {
|
|
74
|
+
const normalized = input
|
|
75
|
+
.replace(/[\t\r\n]+/gu, " ")
|
|
76
|
+
.replace(/\s+/gu, " ")
|
|
77
|
+
.trim()
|
|
78
|
+
.replace(/^[`"'«»“”()[\]{}<>.,:;!?~@#$%^&*_+=\\/|\-]+/gu, "")
|
|
79
|
+
.trim();
|
|
80
|
+
|
|
81
|
+
if (!normalized) return undefined;
|
|
82
|
+
|
|
83
|
+
const words = normalized.split(/\s+/u).filter(Boolean);
|
|
84
|
+
if (words.length === 0) return undefined;
|
|
85
|
+
|
|
86
|
+
const selected: string[] = [];
|
|
87
|
+
for (const word of words) {
|
|
88
|
+
const next = selected.length === 0 ? word : `${selected.join(" ")} ${word}`;
|
|
89
|
+
if (selected.length > 0 && next.length > maxTitleChars) break;
|
|
90
|
+
selected.push(word);
|
|
91
|
+
if (selected.length >= 8) break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const candidate = selected.join(" ");
|
|
95
|
+
return sanitizeSessionTitle(candidate || normalized, maxTitleChars);
|
|
96
|
+
}
|
|
97
|
+
|
|
73
98
|
function truncateInput(text: string, maxChars: number): string {
|
|
74
99
|
const trimmed = text.trim();
|
|
75
100
|
if (trimmed.length <= maxChars) return trimmed;
|
|
@@ -173,7 +198,9 @@ async function generateSessionTitle(
|
|
|
173
198
|
signal: AbortSignal,
|
|
174
199
|
): Promise<string | undefined> {
|
|
175
200
|
const resolved = await resolveTitleModel(ctx, config);
|
|
176
|
-
if (!resolved || signal.aborted)
|
|
201
|
+
if (!resolved || signal.aborted) {
|
|
202
|
+
return fallbackSessionTitleFromInput(input, config.maxTitleChars);
|
|
203
|
+
}
|
|
177
204
|
|
|
178
205
|
const response = await complete(
|
|
179
206
|
resolved.model,
|
|
@@ -288,6 +315,17 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
288
315
|
retryTimer.unref?.();
|
|
289
316
|
}
|
|
290
317
|
|
|
318
|
+
function applyFallbackSessionTitle(ctx: ExtensionContext, currentConfig: SessionTitleConfig, input: string, options: { force?: boolean } = {}): boolean {
|
|
319
|
+
const currentName = currentSessionName(ctx);
|
|
320
|
+
if (!options.force && currentName) return false;
|
|
321
|
+
const fallbackTitle = fallbackSessionTitleFromInput(input, currentConfig.maxTitleChars);
|
|
322
|
+
if (!fallbackTitle) return false;
|
|
323
|
+
pi.setSessionName(fallbackTitle);
|
|
324
|
+
refreshSessionUi(ctx, { force: true });
|
|
325
|
+
scheduleSessionUiRefresh(ctx);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
291
329
|
function startTitleGeneration(ctx: ExtensionContext, currentConfig: SessionTitleConfig): void {
|
|
292
330
|
if (!pendingGeneration) return;
|
|
293
331
|
if (controller) return;
|
|
@@ -319,19 +357,30 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
319
357
|
}
|
|
320
358
|
} finally {
|
|
321
359
|
if (controller === requestController) controller = undefined;
|
|
322
|
-
if (
|
|
360
|
+
if (requestController.signal.aborted || pendingGeneration?.sessionId !== currentSessionId) return;
|
|
361
|
+
if (shouldGeneratePendingTitle(ctx)) {
|
|
323
362
|
scheduleGenerationRetry(ctx, currentConfig);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (pendingGeneration && pendingGeneration.attempts >= currentConfig.generationAttempts) {
|
|
366
|
+
applyFallbackSessionTitle(ctx, currentConfig, generation.input, {
|
|
367
|
+
force: Boolean(generation.replaceSessionName),
|
|
368
|
+
});
|
|
369
|
+
pendingGeneration = undefined;
|
|
324
370
|
}
|
|
325
371
|
}
|
|
326
372
|
})();
|
|
327
373
|
}
|
|
328
374
|
|
|
329
375
|
function primeTitleGenerationFromExistingSession(ctx: ExtensionContext, currentConfig: SessionTitleConfig): void {
|
|
330
|
-
if (!currentConfig.enabled) return;
|
|
331
376
|
if (currentSessionName(ctx)) return;
|
|
332
377
|
|
|
333
378
|
const input = firstUserMessageText(ctx);
|
|
334
379
|
if (!input) return;
|
|
380
|
+
if (!currentConfig.enabled) {
|
|
381
|
+
applyFallbackSessionTitle(ctx, currentConfig, input);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
335
384
|
|
|
336
385
|
pendingGeneration = {
|
|
337
386
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -425,8 +474,6 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
425
474
|
config = currentConfig;
|
|
426
475
|
refreshSessionUi(ctx);
|
|
427
476
|
scheduleSessionUiRefresh(ctx);
|
|
428
|
-
|
|
429
|
-
if (!currentConfig.enabled) return { action: "continue" as const };
|
|
430
477
|
if (event.source === "extension") return { action: "continue" as const };
|
|
431
478
|
if (!event.text.trim()) return { action: "continue" as const };
|
|
432
479
|
if (event.text.trimStart().startsWith("/")) return { action: "continue" as const };
|
|
@@ -438,6 +485,14 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
438
485
|
forkTitleState = undefined;
|
|
439
486
|
return { action: "continue" as const };
|
|
440
487
|
}
|
|
488
|
+
if (!currentConfig.enabled) {
|
|
489
|
+
applyFallbackSessionTitle(ctx, currentConfig, activeForkTitleState
|
|
490
|
+
? buildForkTitleInput(activeForkTitleState.parentTitle, event.text)
|
|
491
|
+
: event.text,
|
|
492
|
+
{ force: Boolean(activeForkTitleState) });
|
|
493
|
+
forkTitleState = undefined;
|
|
494
|
+
return { action: "continue" as const };
|
|
495
|
+
}
|
|
441
496
|
|
|
442
497
|
if (!pendingGeneration || pendingGeneration.sessionId !== currentSessionId) {
|
|
443
498
|
const input = activeForkTitleState
|
|
@@ -4,6 +4,7 @@ Local all-in-one Pi extension package.
|
|
|
4
4
|
|
|
5
5
|
This package keeps shared Pi tools as ordinary source folders under `src/` and registers them through one entrypoint.
|
|
6
6
|
|
|
7
|
+
- `src/glm-coding-discipline` — injects a deduplicated silent-mode and quality-discipline block at the very top of the main-session per-turn system prompt for GLM-family models immediately before the LLM request; disabled for async sub-agents
|
|
7
8
|
- `src/ast-grep` — `ast_grep` / `ast_apply`
|
|
8
9
|
- `src/async-subagents` — `subagents` tool and sub-agent slash commands, including oh-my-openagent-style `/ultrawork` (`/ulw`) and `/hyperplan` orchestration prompts, plus config-defined sub-agent model/thinking/args presets selected via `/subagent-preset` from `asyncSubagents` in `~/.config/pi/pi-tools-suite.jsonc`; includes the `frontend` profile for Gemini-friendly UI/UX and visual frontend work plus the `vision` profile for screenshot/image description via `openai-codex/gpt-5.4-mini`; enforces a 30-minute per-agent execution timeout, project-wide `maxConcurrent` queueing, optional retry/backoff, and `result.json` structured metadata/chaining fields next to raw `result.md`; stores project-local run files and a registry under `.pi/subagents/` so result/status collection can recover after compaction or reload while the main session remains alive
|
|
9
10
|
- `src/lsp` — shared LSP diagnostics hook/library that enriches mutating tool results with diagnostics and shuts down language servers on session shutdown
|
|
@@ -18,7 +19,7 @@ This package keeps shared Pi tools as ordinary source folders under `src/` and r
|
|
|
18
19
|
|
|
19
20
|
`index.ts` is intentionally only a thin auto-discovery shim that re-exports `src/index.ts`. There is no `pi.extensions` manifest here, so local Pi auto-discovery loads the suite once via `~/.pi/agent/extensions/pi-tools-suite/index.ts` and does not double-register tools.
|
|
20
21
|
|
|
21
|
-
Registration order is preserved in `src/index.ts`: ast-grep, async-subagents, lsp, repo-discovery command/tool gate, antigravity-auth provider, todo, model-tools, usage, web-search, dcp, then prompt-commands. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. When `.indexer-cli` enables `repo_*`, those tools stay active ahead of overlapping lower-level aliases so the indexed discovery surface has priority.
|
|
22
|
+
Registration order is preserved in `src/index.ts`: glm-coding-discipline, ast-grep, async-subagents, lsp, repo-discovery command/tool gate, antigravity-auth provider, todo, model-tools, usage, web-search, dcp, then prompt-commands. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. When `.indexer-cli` enables `repo_*`, those tools stay active ahead of overlapping lower-level aliases so the indexed discovery surface has priority.
|
|
22
23
|
|
|
23
24
|
## Disabling modules
|
|
24
25
|
|
|
@@ -160,7 +161,7 @@ For an oh-my-openagent-style workflow, run `/ultrawork` or `/ulw` to ask the par
|
|
|
160
161
|
|
|
161
162
|
Async-subagents also injects a lightweight oh-my-openagent-style system-prompt strategy by model: non-GPT parents get `parallel-first`, an orchestration-first hint that favors ultrawork/subagents for broad work, while GPT-like parents get `deep-work`, a direct deep-worker hint that uses subagents only when clearly useful. Explicit custom system prompts (`--system-prompt`, `SYSTEM.md`, custom templates) are respected and skip this injection by default. Disable it with `PI_AGENT_STRATEGY=off`; force a strategy with `PI_AGENT_STRATEGY=parallel-first` or `PI_AGENT_STRATEGY=deep-work`; set `PI_AGENT_STRATEGY_WITH_CUSTOM_PROMPT=1` to append it even when a custom prompt is present.
|
|
162
163
|
|
|
163
|
-
When the parent model cannot inspect images, async-subagents adds vision-delegation guidance and can save current-turn image attachments under `.pi/subagents/attachments/` so a `vision` sub-agent can receive them as `imagePaths`. Dynamic provider capabilities can be missing or stale after switching models, so blind parent models can be configured with case-insensitive `*` masks under `asyncSubagents.vision.blindModelPatterns` in `~/.config/pi/pi-tools-suite.jsonc`.
|
|
164
|
+
When the parent model cannot inspect images, async-subagents adds vision-delegation guidance and can save current-turn image attachments under `.pi/subagents/attachments/` so a `vision` sub-agent can receive them as `imagePaths`. Dynamic provider capabilities can be missing or stale after switching models, so blind parent models can still be configured explicitly with case-insensitive `*` masks under `asyncSubagents.vision.blindModelPatterns` in `~/.config/pi/pi-tools-suite.jsonc`. GLM is no longer treated as blind by async-subagents by default; the main-session `glm-coding-discipline` lookup tool is the preferred path for GLM visual lookups.
|
|
164
165
|
|
|
165
166
|
When a task omits `subagentType`, async-subagents asks a lightweight router model to choose one configured type for each task from the task text/scope and the `types.<name>.description` metadata. Explicit task `subagentType` still wins. Keep type descriptions short, literal, and distinct because they are inserted into the router prompt for a small model. Router settings live under `asyncSubagents.routing` (`enabled`, `model`, `maxTaskChars`, `maxTokens`, `maxRetries`, `temperature`, `timeoutMs`, `debug`); the default router model is `zai/glm-4.5-air`. If the router is disabled, unavailable, aborted, or returns invalid JSON, omitted types fall back to `defaultType`.
|
|
166
167
|
|
|
@@ -221,6 +221,7 @@ export async function importOpencodeAntigravityAccount(options: {
|
|
|
221
221
|
access: "",
|
|
222
222
|
expires: 0,
|
|
223
223
|
email: selected.account.email,
|
|
224
|
+
...getGoogleOAuthClientCredentials(selected.account),
|
|
224
225
|
accounts: storage.accounts.filter((account) => account.enabled !== false && getAccountRefreshToken(account)),
|
|
225
226
|
activeIndex: selected.index,
|
|
226
227
|
};
|
|
@@ -203,6 +203,7 @@ export async function addAntigravityAccount(
|
|
|
203
203
|
access: shouldActivate ? credentials.access : existing?.access ?? "",
|
|
204
204
|
expires: shouldActivate ? credentials.expires : existing?.expires ?? 0,
|
|
205
205
|
email: shouldActivate ? account.email : existing?.email ?? activeAccount.email,
|
|
206
|
+
...getGoogleOAuthClientCredentials(existing, credentials, account),
|
|
206
207
|
accounts,
|
|
207
208
|
activeIndex,
|
|
208
209
|
};
|
|
@@ -158,9 +158,6 @@ export const DEFAULT_ROUTING_CONFIG: ResolvedSubagentRoutingConfig = {
|
|
|
158
158
|
const BUILTIN_CONFIG: SubagentConfig = {
|
|
159
159
|
maxConcurrent: DEFAULT_MAX_CONCURRENT,
|
|
160
160
|
routing: { ...DEFAULT_ROUTING_CONFIG },
|
|
161
|
-
vision: {
|
|
162
|
-
blindModelPatterns: ["zai/glm*", "glm*", "*/glm*"],
|
|
163
|
-
},
|
|
164
161
|
types: {
|
|
165
162
|
quick: {
|
|
166
163
|
description: "Use for tiny cheap tasks: answer a simple question, inspect one known file, or verify one fact. Not for broad repo search.",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AgentState } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns true if the agent status represents a terminal (no-longer-active) state.
|
|
5
|
+
*/
|
|
6
|
+
export function isTerminalAgentStatus(status: AgentState["status"]): boolean {
|
|
7
|
+
return status === "done" || status === "failed" || status === "stopped";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface AgentCompletionNotificationInput {
|
|
11
|
+
agentId: string;
|
|
12
|
+
runDir: string;
|
|
13
|
+
state: AgentState;
|
|
14
|
+
runAgents: AgentState[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds a custom notification for a completed sub-agent, including information
|
|
19
|
+
* about remaining active agents.
|
|
20
|
+
*/
|
|
21
|
+
export function buildAgentCompletionNotification(input: AgentCompletionNotificationInput) {
|
|
22
|
+
const { agentId, runDir, state, runAgents } = input;
|
|
23
|
+
|
|
24
|
+
const remainingActive = runAgents.filter(
|
|
25
|
+
(a) => a.id !== agentId && !isTerminalAgentStatus(a.status),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const statusLabel = (s: AgentState["status"]): string =>
|
|
29
|
+
s === "running" ? "in progress" : s;
|
|
30
|
+
|
|
31
|
+
const lines: string[] = [];
|
|
32
|
+
lines.push(
|
|
33
|
+
`Background sub-agent ${agentId} finished with status ${state.status}, exitCode=${state.exitCode ?? "n/a"}.`,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (remainingActive.length > 0) {
|
|
37
|
+
const remainingDesc = remainingActive
|
|
38
|
+
.map((a) => `${a.id} (${statusLabel(a.status)})`)
|
|
39
|
+
.join(", ");
|
|
40
|
+
lines.push(
|
|
41
|
+
`${remainingActive.length} other sub-agent${remainingActive.length > 1 ? "s" : ""} still active: ${remainingDesc}.`,
|
|
42
|
+
);
|
|
43
|
+
} else {
|
|
44
|
+
lines.push("All other sub-agents have finished.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
lines.push(
|
|
48
|
+
`To retrieve the result: subagents({ action: "result", agentId: "${agentId}", runDir: "${runDir}" })`,
|
|
49
|
+
);
|
|
50
|
+
lines.push("Do not poll for the remaining agents; you will receive a notification when each finishes.");
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
customType: "async-subagents-agent-completion",
|
|
54
|
+
display: true,
|
|
55
|
+
details: {
|
|
56
|
+
agentId,
|
|
57
|
+
runDir,
|
|
58
|
+
status: state.status,
|
|
59
|
+
exitCode: state.exitCode,
|
|
60
|
+
remainingAgentIds: remainingActive.map((a) => a.id),
|
|
61
|
+
},
|
|
62
|
+
content: lines.join("\n"),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -677,6 +677,7 @@ function subagentEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
|
677
677
|
PI_TERMINAL_BELL_DISABLED: "1",
|
|
678
678
|
PI_TOOLS_SUITE_DISABLED_MODULES: appendEnvList(env.PI_TOOLS_SUITE_DISABLED_MODULES, [
|
|
679
679
|
"async-subagents",
|
|
680
|
+
"glm-coding-discipline",
|
|
680
681
|
"question",
|
|
681
682
|
]),
|
|
682
683
|
};
|
|
@@ -24,15 +24,22 @@ import {
|
|
|
24
24
|
type BridgedImageAttachment,
|
|
25
25
|
type BridgeImageAttachmentsResult,
|
|
26
26
|
} from "./core/attachment-bridge.js";
|
|
27
|
+
|
|
27
28
|
import { appendUltraworkAutoHint, decideUltraworkAuto, isGptLikeModel, isUltraworkAutoEnvEnabled } from "./core/ultrawork-auto.js";
|
|
28
29
|
import { SubagentOverlay } from "./subagent-overlay.js";
|
|
29
30
|
import { registerSubagentsTool } from "./tools/subagents.js";
|
|
30
31
|
import type { LiveAgent, SubagentsLiveStateEvent } from "./types.js";
|
|
32
|
+
import type { AgentState } from "./core/types.js";
|
|
31
33
|
import { publishStartupSection } from "../startup-section.js";
|
|
32
34
|
|
|
35
|
+
function isTerminalAgentStatus(status: AgentState["status"]): boolean {
|
|
36
|
+
return status === "done" || status === "failed" || status === "stopped";
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
const SUBAGENTS_LIVE_COUNT_EVENT = "pi-tools-suite:async-subagents:live-count";
|
|
34
40
|
const SUBAGENTS_LIVE_STATE_EVENT = "pi-tools-suite:async-subagents:live-state";
|
|
35
41
|
const SESSION_SHUTDOWN_KILL_GRACE_MS = 500;
|
|
42
|
+
const COMPLETION_WATCH_INTERVAL_MS = 2_000;
|
|
36
43
|
|
|
37
44
|
interface ShutdownTarget {
|
|
38
45
|
runDir: string;
|
|
@@ -74,30 +81,65 @@ function agentMatchesSession(agent: LiveAgent, sessionFile: string | undefined):
|
|
|
74
81
|
return pathsEqual(sessionFile, agent.parentSession);
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
function isTerminalAgentStatus(status: ReturnType<typeof getRunState>["agents"][number]["status"]): boolean {
|
|
78
|
-
return status === "done" || status === "failed" || status === "stopped";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
84
|
export default function (pi: ExtensionAPI) {
|
|
82
85
|
const liveAgents = new Map<string, Map<string, LiveAgent>>();
|
|
83
86
|
const subagentOverlay = new SubagentOverlay(liveAgents);
|
|
84
87
|
let sawAutoUltraworkCandidate = false;
|
|
85
88
|
let currentSessionFile: string | undefined;
|
|
89
|
+
let completionWatchTimer: ReturnType<typeof setInterval> | undefined;
|
|
86
90
|
publishSubagentPresetsStartupSection();
|
|
87
91
|
|
|
88
92
|
function refreshSubagentOverlay(): void {
|
|
89
|
-
|
|
93
|
+
reconcileLiveAgentCompletions();
|
|
90
94
|
const liveState = createLiveStatePayload(liveAgents, currentSessionFile);
|
|
91
95
|
pi.events?.emit?.(SUBAGENTS_LIVE_COUNT_EVENT, { count: liveState.count });
|
|
92
96
|
pi.events?.emit?.(SUBAGENTS_LIVE_STATE_EVENT, liveState);
|
|
97
|
+
updateCompletionWatcher();
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const handleAgentCompletion: AgentCompletionHandler = ({ runDir, agentId }) => {
|
|
100
|
+
function removeLiveAgent(runDir: string, agentId: string): void {
|
|
98
101
|
const liveRun = liveAgents.get(runDir);
|
|
99
102
|
liveRun?.delete(agentId);
|
|
100
103
|
if (liveRun?.size === 0) liveAgents.delete(runDir);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function reconcileLiveAgentCompletions(): void {
|
|
107
|
+
for (const [runDir, liveRun] of [...liveAgents.entries()]) {
|
|
108
|
+
const states = new Map(
|
|
109
|
+
getRunState(runDir, [...liveRun.keys()], {
|
|
110
|
+
includeLineCounts: false,
|
|
111
|
+
checkRpcPromptFailure: false,
|
|
112
|
+
}).agents.map((agent) => [agent.id, agent]),
|
|
113
|
+
);
|
|
114
|
+
for (const agentId of [...liveRun.keys()]) {
|
|
115
|
+
const state = states.get(agentId);
|
|
116
|
+
if (!state) {
|
|
117
|
+
removeLiveAgent(runDir, agentId);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!isTerminalAgentStatus(state.status)) continue;
|
|
121
|
+
removeLiveAgent(runDir, agentId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hasLiveAgentsForCurrentSession(): boolean {
|
|
127
|
+
return createLiveStatePayload(liveAgents, currentSessionFile).count > 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function updateCompletionWatcher(): void {
|
|
131
|
+
if (hasLiveAgentsForCurrentSession()) {
|
|
132
|
+
if (completionWatchTimer) return;
|
|
133
|
+
completionWatchTimer = setInterval(refreshSubagentOverlay, COMPLETION_WATCH_INTERVAL_MS);
|
|
134
|
+
completionWatchTimer.unref?.();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!completionWatchTimer) return;
|
|
138
|
+
clearInterval(completionWatchTimer);
|
|
139
|
+
completionWatchTimer = undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const handleAgentCompletion: AgentCompletionHandler = () => {
|
|
101
143
|
refreshSubagentOverlay();
|
|
102
144
|
};
|
|
103
145
|
|
|
@@ -166,6 +208,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
166
208
|
|
|
167
209
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
168
210
|
subagentOverlay.dispose();
|
|
211
|
+
if (completionWatchTimer) {
|
|
212
|
+
clearInterval(completionWatchTimer);
|
|
213
|
+
completionWatchTimer = undefined;
|
|
214
|
+
}
|
|
169
215
|
if (event?.reason === "reload" || event?.reason === "fork") return;
|
|
170
216
|
try {
|
|
171
217
|
await cleanupProjectSubagentState(ctx.cwd, liveAgents);
|
|
@@ -283,16 +283,16 @@ export function registerSpawnTool(
|
|
|
283
283
|
onCancelled: () => {
|
|
284
284
|
stopAgents(runDir, [task.id], { signal: "SIGTERM" });
|
|
285
285
|
resolveCompleted();
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
const state = getAgentState(runDir, task.id, { includeLineCounts: false }) ?? { id: task.id, status: "stopped" as const };
|
|
287
|
+
handleAgentCompletion({ runDir, agentId: task.id, agentDir: path.join(runDir, task.id), exitCode: 0, state });
|
|
288
288
|
},
|
|
289
289
|
onLaunchError: (error) => {
|
|
290
290
|
const message = errorMessage(error);
|
|
291
291
|
launchErrors.push({ id: task.id, error: message });
|
|
292
292
|
writeLaunchFailure(runDir, task, message, resolved.maxResultBytes);
|
|
293
293
|
resolveCompleted();
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
const state = getAgentState(runDir, task.id, { includeLineCounts: false }) ?? { id: task.id, status: "failed" as const, exitCode: 1 };
|
|
295
|
+
handleAgentCompletion({ runDir, agentId: task.id, agentDir: path.join(runDir, task.id), exitCode: 1, state });
|
|
296
296
|
},
|
|
297
297
|
onUpdate: () => {
|
|
298
298
|
onLiveAgentsChange?.();
|
|
@@ -15,6 +15,8 @@ export interface PiToolsSuiteConfig {
|
|
|
15
15
|
enabled: boolean;
|
|
16
16
|
disabledModules: string[];
|
|
17
17
|
todoThinking: boolean;
|
|
18
|
+
/** Vision-capable model used by the GLM lookup tool; unset disables lookup. */
|
|
19
|
+
lookupModel?: string;
|
|
18
20
|
telegramMirror?: TelegramMirrorConfig;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -22,6 +24,7 @@ type MutableConfig = {
|
|
|
22
24
|
enabled: boolean;
|
|
23
25
|
disabledModules: Set<string>;
|
|
24
26
|
todoThinking: boolean;
|
|
27
|
+
lookupModel: string | undefined;
|
|
25
28
|
telegramMirror: TelegramMirrorConfig | undefined;
|
|
26
29
|
};
|
|
27
30
|
|
|
@@ -60,6 +63,13 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
60
63
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
function normalizeLookupModel(raw: unknown): string | undefined {
|
|
67
|
+
if (raw === null || raw === false) return undefined;
|
|
68
|
+
if (typeof raw !== "string") return undefined;
|
|
69
|
+
const trimmed = raw.trim();
|
|
70
|
+
return trimmed ? trimmed : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
function normalizeTelegramMirror(raw: unknown): TelegramMirrorConfig | undefined {
|
|
64
74
|
if (!isRecord(raw)) return undefined;
|
|
65
75
|
const botToken = typeof raw.botToken === "string" ? raw.botToken.trim() : "";
|
|
@@ -139,6 +149,7 @@ function removeDisabled(config: MutableConfig, value: unknown, knownModules: Rea
|
|
|
139
149
|
function mergeConfigLayer(config: MutableConfig, raw: Record<string, unknown>, knownModules: ReadonlySet<string>): MutableConfig {
|
|
140
150
|
if (typeof raw.enabled === "boolean") config.enabled = raw.enabled;
|
|
141
151
|
if (typeof raw.todoThinking === "boolean") config.todoThinking = raw.todoThinking;
|
|
152
|
+
if (Object.prototype.hasOwnProperty.call(raw, "lookupModel")) config.lookupModel = normalizeLookupModel(raw.lookupModel);
|
|
142
153
|
|
|
143
154
|
for (const key of DISABLED_LIST_KEYS) addDisabled(config, raw[key], knownModules);
|
|
144
155
|
for (const key of ENABLED_LIST_KEYS) removeDisabled(config, raw[key], knownModules);
|
|
@@ -198,6 +209,7 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
|
|
|
198
209
|
enabled: true,
|
|
199
210
|
disabledModules: new Set([...DEFAULT_DISABLED_MODULES].filter((name) => knownModules.has(name))),
|
|
200
211
|
todoThinking: false,
|
|
212
|
+
lookupModel: undefined,
|
|
201
213
|
telegramMirror: undefined,
|
|
202
214
|
};
|
|
203
215
|
const userConfigPath = getPiToolsSuiteUserConfigPath(options.homeDir);
|
|
@@ -217,6 +229,7 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
|
|
|
217
229
|
enabled: config.enabled,
|
|
218
230
|
disabledModules: [...config.disabledModules].sort(),
|
|
219
231
|
todoThinking: config.todoThinking,
|
|
232
|
+
...(config.lookupModel ? { lookupModel: config.lookupModel } : {}),
|
|
220
233
|
...(config.telegramMirror ? { telegramMirror: config.telegramMirror } : {}),
|
|
221
234
|
};
|
|
222
235
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Types
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
+
import { createHash } from "node:crypto"
|
|
5
6
|
import type { DcpNudgeType } from "./pruner-types.js"
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -15,8 +16,9 @@ export interface ToolRecord {
|
|
|
15
16
|
/** The arguments passed to the tool (from the corresponding ToolCall) */
|
|
16
17
|
inputArgs: Record<string, unknown>
|
|
17
18
|
/**
|
|
18
|
-
* Deduplication fingerprint: `toolName::
|
|
19
|
-
* Two calls with the same name +
|
|
19
|
+
* Deduplication fingerprint: `toolName::sha256:<hash>` where the hash is
|
|
20
|
+
* computed over recursively key-sorted args. Two calls with the same name +
|
|
21
|
+
* identical args share the same fingerprint without persisting full args.
|
|
20
22
|
*/
|
|
21
23
|
inputFingerprint: string
|
|
22
24
|
/** Whether the tool result was an error */
|
|
@@ -714,14 +716,17 @@ function sortObjectKeys(value: unknown): unknown {
|
|
|
714
716
|
* Two calls with the same `toolName` and semantically identical `args`
|
|
715
717
|
* (regardless of key ordering) will produce the same fingerprint.
|
|
716
718
|
*
|
|
717
|
-
* Format: `<toolName
|
|
719
|
+
* Format: `<toolName>::sha256:<hash of recursively key-sorted args>`
|
|
718
720
|
*/
|
|
719
721
|
export function createInputFingerprint(
|
|
720
722
|
toolName: string,
|
|
721
723
|
args: Record<string, unknown>,
|
|
722
724
|
): string {
|
|
723
725
|
const sorted = sortObjectKeys(args)
|
|
724
|
-
|
|
726
|
+
const hash = createHash("sha256")
|
|
727
|
+
.update(JSON.stringify(sorted))
|
|
728
|
+
.digest("hex")
|
|
729
|
+
return `${toolName}::sha256:${hash}`
|
|
725
730
|
}
|
|
726
731
|
|
|
727
732
|
// ---------------------------------------------------------------------------
|
|
@@ -8,6 +8,8 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
8
8
|
// When true, todo items may carry a per-task thinking level and the todo
|
|
9
9
|
// module will switch/restore Pi's thinking level as in-progress tasks change.
|
|
10
10
|
"todoThinking": false,
|
|
11
|
+
// Vision-capable model used by GLM's lookup tool. Remove or set to null to disable lookup.
|
|
12
|
+
"lookupModel": "openai-codex/gpt-5.4-mini",
|
|
11
13
|
"terminalBell": { "sound": true },
|
|
12
14
|
// "telegramMirror": {
|
|
13
15
|
// "enabled": true,
|
|
@@ -413,7 +415,9 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
413
415
|
// "bin": "rust-analyzer",
|
|
414
416
|
// "args": [],
|
|
415
417
|
// "startupTimeoutMs": 20000,
|
|
416
|
-
// "diagnosticsWaitMs":
|
|
418
|
+
// "diagnosticsWaitMs": 20000,
|
|
419
|
+
// "pullDiagnostics": false,
|
|
420
|
+
// "waitForPublishDiagnostics": true,
|
|
417
421
|
// "languageIdByExtension": {
|
|
418
422
|
// ".rs": "rust"
|
|
419
423
|
// }
|