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.
Files changed (67) hide show
  1. package/dist/app/app.js +8 -6
  2. package/dist/app/constants.d.ts +1 -0
  3. package/dist/app/constants.js +1 -0
  4. package/dist/app/input/voice-controller.js +16 -12
  5. package/dist/app/popup/popup-menu-controller.d.ts +1 -5
  6. package/dist/app/popup/popup-menu-controller.js +7 -8
  7. package/dist/app/process.js +7 -0
  8. package/dist/app/rendering/conversation-entry-renderer.js +17 -16
  9. package/dist/app/rendering/conversation-viewport.js +4 -35
  10. package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
  11. package/dist/app/rendering/editor-layout-renderer.js +25 -16
  12. package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
  13. package/dist/app/rendering/popup-menu-renderer.js +24 -34
  14. package/dist/app/rendering/render-controller.d.ts +2 -0
  15. package/dist/app/rendering/render-controller.js +26 -25
  16. package/dist/app/rendering/render-text.js +2 -2
  17. package/dist/app/rendering/status-line-renderer.js +1 -1
  18. package/dist/app/rendering/tab-line-renderer.js +3 -3
  19. package/dist/app/runtime.js +29 -3
  20. package/dist/app/screen/file-link-opener.d.ts +2 -0
  21. package/dist/app/screen/file-link-opener.js +84 -17
  22. package/dist/app/screen/mouse-controller.d.ts +0 -2
  23. package/dist/app/screen/mouse-controller.js +6 -12
  24. package/dist/app/screen/screen-styler.js +1 -1
  25. package/dist/app/session/lazy-session-manager.d.ts +1 -1
  26. package/dist/app/session/lazy-session-manager.js +64 -52
  27. package/dist/app/session/queued-message-controller.d.ts +6 -0
  28. package/dist/app/session/queued-message-controller.js +9 -1
  29. package/dist/app/session/queued-message-entries.d.ts +8 -0
  30. package/dist/app/session/queued-message-entries.js +41 -0
  31. package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
  32. package/dist/app/session/session-lifecycle-controller.js +45 -11
  33. package/dist/app/session/tabs-controller.d.ts +11 -1
  34. package/dist/app/session/tabs-controller.js +197 -30
  35. package/dist/app/terminal/terminal-controller.d.ts +2 -0
  36. package/dist/app/terminal/terminal-controller.js +7 -5
  37. package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
  38. package/dist/schemas/pi-tools-suite-schema.js +3 -0
  39. package/dist/theme.d.ts +3 -0
  40. package/dist/theme.js +8 -2
  41. package/extensions/session-title/config.ts +3 -3
  42. package/extensions/session-title/index.ts +60 -5
  43. package/external/pi-tools-suite/README.md +3 -2
  44. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
  45. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
  46. package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
  47. package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
  49. package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
  50. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
  51. package/external/pi-tools-suite/src/config.ts +13 -0
  52. package/external/pi-tools-suite/src/dcp/state.ts +9 -4
  53. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -1
  54. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +580 -0
  55. package/external/pi-tools-suite/src/index.ts +1 -0
  56. package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
  57. package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
  58. package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
  59. package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
  60. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +1 -0
  61. package/external/pi-tools-suite/src/todo/index.ts +81 -4
  62. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
  63. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  64. package/package.json +3 -14
  65. package/schemas/pi-tools-suite.json +19 -0
  66. package/apps/desktop-tauri/README.md +0 -103
  67. 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
- userMessageBackground: "#1e1e1e",
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
- userMessageBackground: "#ffffff",
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: 1,
30
- generationAttempts: 2,
31
- retryDelayMs: 2500,
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) return undefined;
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 (!requestController.signal.aborted && pendingGeneration?.sessionId === currentSessionId && shouldGeneratePendingTitle(ctx)) {
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`. Built-in defaults treat GLM refs such as `zai/glm*`, `glm*`, and `*/glm*` as text-only; set the array to `[]` to disable the masks.
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
- subagentOverlay.update();
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
- liveRun.delete(task.id);
287
- if (liveRun.size === 0) liveAgents.delete(runDir);
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
- liveRun.delete(task.id);
295
- if (liveRun.size === 0) liveAgents.delete(runDir);
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::JSON(sortedArgs)`
19
- * Two calls with the same name + identical args share the same fingerprint.
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>::<JSON of recursively key-sorted args>`
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
- return `${toolName}::${JSON.stringify(sorted)}`
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": 8000,
418
+ // "diagnosticsWaitMs": 20000,
419
+ // "pullDiagnostics": false,
420
+ // "waitForPublishDiagnostics": true,
417
421
  // "languageIdByExtension": {
418
422
  // ".rs": "rust"
419
423
  // }