pi-ui-extend 0.1.56 → 0.1.59

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 (44) hide show
  1. package/dist/app/app.js +2 -0
  2. package/dist/app/commands/command-session-actions.js +3 -2
  3. package/dist/app/extensions/extension-actions-controller.js +1 -3
  4. package/dist/app/rendering/conversation-entry-renderer.d.ts +3 -0
  5. package/dist/app/rendering/conversation-entry-renderer.js +2 -0
  6. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  7. package/dist/app/rendering/conversation-viewport.js +4 -0
  8. package/dist/app/rendering/extension-entry-renderer.d.ts +6 -0
  9. package/dist/app/rendering/extension-entry-renderer.js +100 -0
  10. package/dist/app/rendering/tool-block-renderer.d.ts +5 -0
  11. package/dist/app/rendering/tool-block-renderer.js +1 -1
  12. package/dist/app/screen/mouse-controller.js +1 -1
  13. package/dist/app/session/lazy-session-manager.js +18 -1
  14. package/dist/app/session/pix-system-message.d.ts +1 -0
  15. package/dist/app/session/pix-system-message.js +4 -0
  16. package/dist/app/session/session-event-controller.js +7 -1
  17. package/dist/app/session/session-history.d.ts +3 -0
  18. package/dist/app/session/session-history.js +19 -2
  19. package/dist/app/session/session-search.js +2 -0
  20. package/dist/app/session/session-stats.d.ts +3 -0
  21. package/dist/app/session/session-stats.js +65 -0
  22. package/dist/app/types.d.ts +6 -1
  23. package/dist/bundled-extensions/terminal-bell/index.js +1 -37
  24. package/external/pi-tools-suite/README.md +17 -1
  25. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +102 -0
  26. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +1 -1
  27. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +0 -33
  28. package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +1 -1
  29. package/external/pi-tools-suite/src/codex-reasoning-fix/index.ts +13 -121
  30. package/external/pi-tools-suite/src/dcp/config.ts +27 -0
  31. package/external/pi-tools-suite/src/dcp/debug-log.ts +2 -1
  32. package/external/pi-tools-suite/src/dcp/index.ts +193 -20
  33. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +85 -0
  34. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +254 -0
  35. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -0
  36. package/external/pi-tools-suite/src/dcp/pruner-types.ts +30 -0
  37. package/external/pi-tools-suite/src/dcp/pruner.ts +8 -0
  38. package/external/pi-tools-suite/src/dcp/state.ts +21 -6
  39. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +14 -0
  40. package/external/pi-tools-suite/src/index.ts +4 -2
  41. package/external/pi-tools-suite/src/telegram-mirror/events.ts +9 -4
  42. package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
  43. package/external/pi-tools-suite/src/todo/index.ts +23 -27
  44. package/package.json +1 -1
@@ -17,6 +17,36 @@ export interface MessageCompressionCandidate {
17
17
  reason: string;
18
18
  }
19
19
 
20
+ export interface EmergencyCurrentTurnStats {
21
+ totalPairs: number;
22
+ totalPairTokens: number;
23
+ eligiblePairs: number;
24
+ eligibleTokens: number;
25
+ eligibleRecoverableTokens: number;
26
+ preservedPairs: number;
27
+ preservedTokens: number;
28
+ preservedRecentPairs: number;
29
+ preservedRecentTokens: number;
30
+ preservedUnseenPairs: number;
31
+ preservedUnseenTokens: number;
32
+ preservedProtectedPairs: number;
33
+ preservedProtectedTokens: number;
34
+ }
35
+
36
+ export interface EmergencyCurrentTurnOutput {
37
+ toolCallId: string;
38
+ messageId?: string;
39
+ toolName: string;
40
+ tokenEstimate: number;
41
+ recoverableTokens: number;
42
+ resultIndex: number;
43
+ }
44
+
45
+ export interface EmergencyCurrentTurnSelection {
46
+ eligible: EmergencyCurrentTurnOutput[];
47
+ stats: EmergencyCurrentTurnStats;
48
+ }
49
+
20
50
  export type DcpNudgeType = "context-strong" | "context-soft" | "turn" | "iteration";
21
51
 
22
52
  export interface NudgeThresholds {
@@ -12,10 +12,18 @@ import {
12
12
 
13
13
  export type {
14
14
  CompressionCandidate,
15
+ EmergencyCurrentTurnSelection,
16
+ EmergencyCurrentTurnStats,
15
17
  MessageCompressionCandidate,
16
18
  MessagePriority,
17
19
  NudgeThresholds,
18
20
  } from "./pruner-types.js";
21
+ export {
22
+ analyzeEmergencyCurrentTurn,
23
+ emergencyPressureState,
24
+ emergencyCurrentTurnMessageCandidates,
25
+ pruneEmergencyCurrentTurn,
26
+ } from "./pruner-emergency.js";
19
27
  export {
20
28
  estimateTokens,
21
29
  getActiveSummaryTokenEstimate,
@@ -145,6 +145,8 @@ export interface DcpState {
145
145
  prunedToolIds: Set<string>
146
146
  /** toolCallId → reason used for human-readable pruning placeholders/stats. */
147
147
  prunedToolReasons: Map<string, string>
148
+ /** Tool results included in at least one provider request. */
149
+ providerSeenToolIds: Set<string>
148
150
 
149
151
  // ── Compression ────────────────────────────────────────────────────────────
150
152
  /** All compression blocks (both active and soft-deleted) */
@@ -228,12 +230,10 @@ export interface DcpState {
228
230
  */
229
231
  lastContextWindow?: number
230
232
  /**
231
- * How many consecutive `context-strong` nudges have been emitted without a
232
- * subsequent successful `compress` (model- or DCP-initiated). When this
233
- * reaches `compress.autoCompress.patience` while context is above the
234
- * emergency threshold, the auto-compress fallback creates a block without
235
- * waiting for the model. Reset to 0 on any successful compression, when
236
- * pressure drops below the emergency threshold, or on a window change.
233
+ * How many consecutive emergency context reminders have been delivered
234
+ * without a successful compression or emergency prune. This advances even
235
+ * when no normal compression candidate exists so current-turn fallback
236
+ * patience cannot be reset by the exact condition it is designed to handle.
237
237
  */
238
238
  consecutiveIgnoredStrongNudges: number
239
239
  }
@@ -248,6 +248,7 @@ export function createState(): DcpState {
248
248
  toolCalls: new Map(),
249
249
  prunedToolIds: new Set(),
250
250
  prunedToolReasons: new Map(),
251
+ providerSeenToolIds: new Set(),
251
252
  compressionBlocks: [],
252
253
  nextBlockId: 1,
253
254
  messageIdSnapshot: new Map(),
@@ -279,6 +280,7 @@ export function resetState(state: DcpState): void {
279
280
  state.toolCalls.clear()
280
281
  state.prunedToolIds.clear()
281
282
  state.prunedToolReasons.clear()
283
+ state.providerSeenToolIds.clear()
282
284
  state.compressionBlocks = []
283
285
  state.nextBlockId = 1
284
286
  state.messageIdSnapshot.clear()
@@ -401,6 +403,8 @@ export interface SerializedDcpState {
401
403
  nextBlockId: number
402
404
  prunedToolIds: string[]
403
405
  prunedToolReasons: Array<[string, string]>
406
+ /** Tool result IDs known to have appeared in a provider request. */
407
+ providerSeenToolIds?: string[]
404
408
  /** Full tool records — present in legacy snapshots. */
405
409
  toolCalls?: ToolRecord[]
406
410
  /** Compact tool records — present in new compact snapshots. */
@@ -597,11 +601,16 @@ export function serializeState(state: DcpState): SerializedDcpState {
597
601
  seen.add(record.toolCallId)
598
602
  }
599
603
 
604
+ const persistedToolCallIds = new Set(compactToolCalls.map((record) => record.toolCallId))
605
+ const providerSeenToolIds = Array.from(state.providerSeenToolIds)
606
+ .filter((toolCallId) => persistedToolCallIds.has(toolCallId))
607
+
600
608
  return {
601
609
  compressionBlocks: state.compressionBlocks,
602
610
  nextBlockId: state.nextBlockId,
603
611
  prunedToolIds: Array.from(state.prunedToolIds),
604
612
  prunedToolReasons: Array.from(state.prunedToolReasons.entries()),
613
+ providerSeenToolIds,
605
614
  compactToolCalls,
606
615
  totalToolCallCount: allRecords.length,
607
616
  tokensSaved: state.tokensSaved,
@@ -661,6 +670,12 @@ export function restoreState(state: DcpState, data: unknown): void {
661
670
  )
662
671
  }
663
672
 
673
+ if (Array.isArray(saved.providerSeenToolIds)) {
674
+ state.providerSeenToolIds = new Set(
675
+ saved.providerSeenToolIds.filter((id): id is string => typeof id === "string"),
676
+ )
677
+ }
678
+
664
679
  if (Array.isArray(saved.compactToolCalls)) {
665
680
  // New compact format: restore CompactToolRecords as ToolRecords with
666
681
  // synthetic inputArgs derived from inputStringValues.
@@ -38,6 +38,20 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
38
38
  "debug": false,
39
39
  "debugLog": { "maxBytes": 5242880, "maxBackups": 3 },
40
40
  "manualMode": { "enabled": false, "automaticStrategies": true },
41
+ "strategies": {
42
+ "emergencyCurrentTurnPruning": {
43
+ // Disabling this turns off same-turn candidates and lossy pruning;
44
+ // non-destructive emergency reminders remain active.
45
+ "enabled": true,
46
+ "hardContextPercent": 0.82,
47
+ "targetContextPercent": 0.70,
48
+ "patience": 2,
49
+ "keepRecentToolPairs": 8,
50
+ "minOutputTokens": 500,
51
+ "maxSuggestions": 8,
52
+ "protectedTools": []
53
+ }
54
+ },
41
55
  "modelOverrides": {
42
56
  "openai-codex/gpt-5*": {
43
57
  "compress": {
@@ -9,8 +9,7 @@ type ExtensionModule = {
9
9
  default: ExtensionFactory;
10
10
  };
11
11
 
12
- const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule> }> = [
13
- { name: "codex-reasoning-fix", load: () => import("./codex-reasoning-fix/index") },
12
+ export const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule> }> = [
14
13
  { name: "coding-discipline", load: () => import("./coding-discipline/index") },
15
14
  { name: "ast-grep", load: () => import("./ast-grep/index") },
16
15
  { name: "async-subagents", load: () => import("./async-subagents/index") },
@@ -28,6 +27,9 @@ const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule> }> = [
28
27
  { name: "prompt-commands", load: () => import("./prompt-commands/index") },
29
28
  { name: "skill-installer", load: () => import("./skill-installer/index") },
30
29
  { name: "telegram-mirror", load: () => import("./telegram-mirror/index") },
30
+ // Keep this last: its before_provider_request handler is the final payload
31
+ // sanitizer after DCP and any other provider-payload modifiers.
32
+ { name: "codex-reasoning-fix", load: () => import("./codex-reasoning-fix/index") },
31
33
  ];
32
34
 
33
35
  export default async function piToolsSuite(pi: ExtensionAPI) {
@@ -18,7 +18,11 @@ export interface RendererSink {
18
18
  }
19
19
 
20
20
  export function registerPixEventHandlers(pi: ExtensionAPI, hooks: PixMirrorHooks): void {
21
+ let turnActive = false;
22
+
21
23
  pi.on("agent_start", (_event, ctx) => {
24
+ if (turnActive) return;
25
+ turnActive = true;
22
26
  hooks.getRenderer()?.push({ kind: "turn_start", instance: hooks.describeInstance(ctx as ExtensionContext | undefined) });
23
27
  });
24
28
 
@@ -33,16 +37,18 @@ export function registerPixEventHandlers(pi: ExtensionAPI, hooks: PixMirrorHooks
33
37
  // user-visible assistant answer, not internal reasoning/tools.
34
38
  });
35
39
 
36
- pi.on("agent_end", () => {
40
+ pi.on("agent_settled", () => {
41
+ if (!turnActive) return;
42
+ turnActive = false;
37
43
  hooks.getRenderer()?.push({ kind: "turn_end", reason: "end" });
38
- hooks.notifyAgentEnd();
44
+ hooks.notifyAgentSettled();
39
45
  });
40
46
  }
41
47
 
42
48
  export interface PixMirrorHooks {
43
49
  getRenderer(): RendererSink | undefined;
44
50
  describeInstance(ctx: ExtensionContext | undefined): RendererInstance | undefined;
45
- notifyAgentEnd(): void;
51
+ notifyAgentSettled(): void;
46
52
  }
47
53
 
48
54
  export function captureAbortableContext(ctx: ExtensionContext | undefined, hooks: ContextCapture): void {
@@ -59,4 +65,3 @@ export interface ContextCapture {
59
65
  capturePending(fn: () => boolean): void;
60
66
  captureCompact(fn: () => void): void;
61
67
  }
62
-
@@ -166,7 +166,7 @@ export default function telegramMirror(pi: ExtensionAPI): void {
166
166
  const hooks: PixMirrorHooks = {
167
167
  getRenderer: () => eventSink,
168
168
  describeInstance: (ctx) => describeInstance(ctx),
169
- notifyAgentEnd: () => undefined,
169
+ notifyAgentSettled: () => undefined,
170
170
  };
171
171
 
172
172
  registerPixEventHandlers(pi, hooks);
@@ -10,16 +10,6 @@ import { getState, replaceState } from "./state/store.js";
10
10
  import { activateTodoStateScope, DEFAULT_PROMPT_GUIDELINES, DEFAULT_PROMPT_SNIPPET, publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
11
11
  import type { Task, TaskMutationParams } from "./tool/types.js";
12
12
 
13
- /**
14
- * Renderer-relayed signal that the session is in an auto-retry cycle.
15
- * Payload: `{ active: boolean }`. The SDK does not forward retry state to
16
- * extensions, so the renderer emits this on the extension event bus. We use it
17
- * to suppress the auto-nudge on intermediate retry agent_end events: nudging
18
- * then races the pending retry continuation and surfaces a benign
19
- * "Agent is already processing" extension error in other tabs.
20
- */
21
- const RETRY_ACTIVE_EVENT = "pix:retry-active";
22
-
23
13
  type AgentMessageLike = { role?: unknown; stopReason?: unknown; content?: unknown };
24
14
 
25
15
  const TODO_NUDGE_LIMIT = 8;
@@ -144,10 +134,10 @@ export default function (pi: ExtensionAPI) {
144
134
  const pendingAskUserToolCallIds = new Set<string>();
145
135
  let suppressNextNudgeForThinkingSwitch = false;
146
136
  let inProgressAtAgentStart = new Set<number>();
147
- // True while the session is in an auto-retry cycle (relayed via the
148
- // extension event bus). Suppresses the auto-nudge on intermediate retry
149
- // agent_end events so it doesn't race the pending retry continuation.
150
- let retryActive = false;
137
+ // agent_end may be followed by an automatic retry, compaction, or queued
138
+ // continuation. Remember only whether its terminal assistant reply was a
139
+ // successful visible response; schedule a nudge later on agent_settled.
140
+ let settledNudgeEligible = false;
151
141
 
152
142
  function registerTodoToolWithCurrentPrompt(): void {
153
143
  const thinkingPrompt = todoThinkingEnabled ? buildThinkingPromptParts(currentModel) : {};
@@ -336,9 +326,6 @@ export default function (pi: ExtensionAPI) {
336
326
  const delayMs = attempt === 0 ? TODO_NUDGE_INITIAL_DELAY_MS : TODO_NUDGE_IDLE_RETRY_DELAY_MS;
337
327
  nudgeTimer = setTimeout(() => {
338
328
  nudgeTimer = undefined;
339
- // A retry-start signal may have arrived after the agent_end that
340
- // scheduled this nudge. Bail out so we don't nudge mid-retry.
341
- if (retryActive) return;
342
329
  try {
343
330
  activateTodoStateScope(ctx);
344
331
  if (!ctx.isIdle()) {
@@ -358,9 +345,8 @@ export default function (pi: ExtensionAPI) {
358
345
  if (nudge.signature === lastNudgedSignature) return;
359
346
  lastNudgedSignature = nudge.signature;
360
347
 
361
- // agent_end fires before Pi is fully back in idle dispatch. Sending as a
362
- // normal user message on the next idle tick reliably starts a fresh turn;
363
- // queueing followUp from inside agent_end can be too late to be drained.
348
+ // agent_settled means retries, compaction, and queued continuations are
349
+ // finished. Send on the next idle tick as a fresh turn.
364
350
  pi.sendUserMessage(nudge.message);
365
351
  } catch (err) {
366
352
  if (isAgentBusyRaceError(err)) {
@@ -413,12 +399,6 @@ export default function (pi: ExtensionAPI) {
413
399
  clearNudgeTimer();
414
400
  });
415
401
 
416
- pi.events.on(RETRY_ACTIVE_EVENT, (data: unknown) => {
417
- const active = data != null && typeof data === "object" && (data as { active?: unknown }).active === true;
418
- retryActive = active;
419
- if (active) clearNudgeTimer();
420
- });
421
-
422
402
  pi.on("model_select", async (event) => {
423
403
  currentModel = event.model;
424
404
  if (todoThinkingEnabled) registerTodoToolWithCurrentPrompt();
@@ -439,7 +419,7 @@ export default function (pi: ExtensionAPI) {
439
419
  pi.on("agent_start", async (_event, ctx) => {
440
420
  activateTodoStateScope(ctx);
441
421
  pendingAskUserToolCallIds.clear();
442
- retryActive = false;
422
+ settledNudgeEligible = false;
443
423
  inProgressAtAgentStart = new Set(selectVisibleTasks(getState()).filter((task) => task.status === "in_progress").map((task) => task.id));
444
424
  });
445
425
 
@@ -455,26 +435,42 @@ export default function (pi: ExtensionAPI) {
455
435
  pi.on("agent_end", async (event, ctx) => {
456
436
  activateTodoStateScope(ctx);
457
437
  const completedAssistantReply = hasCompletedAssistantReply((event as { messages?: readonly unknown[] } | undefined)?.messages);
438
+ settledNudgeEligible = completedAssistantReply;
458
439
 
459
440
  if (suppressNextNudgeForThinkingSwitch) {
460
441
  suppressNextNudgeForThinkingSwitch = false;
461
442
  if (!completedAssistantReply) {
443
+ settledNudgeEligible = false;
462
444
  clearNudgeTimer();
463
445
  return;
464
446
  }
465
447
  }
466
448
 
467
449
  if (pendingAskUserToolCallIds.size > 0) {
450
+ settledNudgeEligible = false;
468
451
  clearNudgeTimer();
469
452
  return;
470
453
  }
471
454
 
472
455
  if (completedAssistantReply && maybeRecoverCompletedCurrentTask((event as { messages?: readonly unknown[] } | undefined)?.messages, ctx)) {
456
+ settledNudgeEligible = false;
473
457
  lastNudgedSignature = undefined;
474
458
  clearNudgeTimer();
475
459
  return;
476
460
  }
461
+ });
477
462
 
463
+ pi.on("agent_settled", async (_event, ctx) => {
464
+ activateTodoStateScope(ctx);
465
+ if (!settledNudgeEligible) {
466
+ clearNudgeTimer();
467
+ return;
468
+ }
469
+ settledNudgeEligible = false;
470
+ if (pendingAskUserToolCallIds.size > 0) {
471
+ clearNudgeTimer();
472
+ return;
473
+ }
478
474
  const nudge = getUnfinishedTodoNudge();
479
475
  if (!nudge) {
480
476
  lastNudgedSignature = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "0.1.56",
3
+ "version": "0.1.59",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {