pi-ui-extend 1.0.38 → 1.0.39

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 (40) hide show
  1. package/dist/app/extensions/extension-ui-controller.js +3 -0
  2. package/dist/app/rendering/extension-entry-renderer.js +2 -0
  3. package/dist/bundled-extensions/question/contract.d.ts +3 -0
  4. package/dist/bundled-extensions/question/contract.js +24 -0
  5. package/dist/bundled-extensions/question/desktop.d.ts +11 -0
  6. package/dist/bundled-extensions/question/desktop.js +143 -0
  7. package/dist/bundled-extensions/question/index.d.ts +1 -0
  8. package/dist/bundled-extensions/question/index.js +5 -1
  9. package/dist/bundled-extensions/question/render.js +6 -0
  10. package/dist/bundled-extensions/question/result.js +71 -9
  11. package/dist/bundled-extensions/question/tool-description.js +4 -3
  12. package/dist/bundled-extensions/question/tui.js +127 -12
  13. package/dist/bundled-extensions/question/types.d.ts +23 -2
  14. package/dist/tool-renderers/question.js +20 -1
  15. package/docs/desktop-markdown-media.md +77 -0
  16. package/docs/desktop-mvp.md +134 -0
  17. package/docs/desktop-task-manager.md +124 -0
  18. package/external/pi-tools-suite/package.json +3 -3
  19. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +8 -0
  20. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +41 -2
  21. package/external/pi-tools-suite/src/async-subagents/core/config.ts +12 -0
  22. package/external/pi-tools-suite/src/async-subagents/index.ts +6 -2
  23. package/external/pi-tools-suite/src/async-subagents/subagent-overlay.ts +1 -1
  24. package/external/pi-tools-suite/src/dcp/auto-compress.ts +1 -1
  25. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -51
  26. package/external/pi-tools-suite/src/dcp/debug-log.ts +6 -0
  27. package/external/pi-tools-suite/src/dcp/index.ts +27 -125
  28. package/external/pi-tools-suite/src/dcp/prompts.ts +2 -2
  29. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +2 -1
  30. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +31 -10
  31. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +6 -7
  32. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +153 -111
  33. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +28 -17
  34. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +61 -27
  35. package/external/pi-tools-suite/src/dcp/pruner.ts +21 -13
  36. package/external/pi-tools-suite/src/dcp/state.ts +59 -0
  37. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +4 -0
  38. package/external/pi-tools-suite/src/lib/rpc-session-state.ts +34 -0
  39. package/external/pi-tools-suite/src/todo/todo.ts +8 -3
  40. package/package.json +8 -4
@@ -7,7 +7,7 @@ import type {
7
7
  NudgeThresholds,
8
8
  } from "./pruner-types.js";
9
9
  import { extractBlockId, messageText } from "./pruner-metadata.js";
10
- import { stableMessageId } from "./pruner-message-ids.js";
10
+ import { stableMessageKeys } from "./pruner-message-ids.js";
11
11
 
12
12
  function coercePercentThreshold(value: number | string | undefined, fallback: number): number {
13
13
  if (typeof value === "number") return Number.isFinite(value) ? value : fallback;
@@ -56,6 +56,7 @@ function isRealAnchorCandidate(msg: any): boolean {
56
56
  }
57
57
 
58
58
  function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId: string; timestamp: number; role: string } | null {
59
+ const stableKeys = stableMessageKeys(messages);
59
60
  // Prefer the latest real user message: it gives the reminder direct user-like
60
61
  // salience without creating a new synthetic message at the end of context.
61
62
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -65,7 +66,7 @@ function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId
65
66
  return {
66
67
  msg,
67
68
  index: i,
68
- stableId: stableMessageId(msg, i),
69
+ stableId: stableKeys[i]!,
69
70
  timestamp: msg.timestamp,
70
71
  role: msg.role,
71
72
  };
@@ -79,7 +80,7 @@ function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId
79
80
  return {
80
81
  msg,
81
82
  index: i,
82
- stableId: stableMessageId(msg, i),
83
+ stableId: stableKeys[i]!,
83
84
  timestamp: msg.timestamp,
84
85
  role: msg.role,
85
86
  };
@@ -88,8 +89,8 @@ function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId
88
89
  return null;
89
90
  }
90
91
 
91
- function anchorMatchesMessage(anchor: DcpNudgeAnchor, msg: any, index: number): boolean {
92
- if (anchor.anchorStableId && stableMessageId(msg, index) === anchor.anchorStableId) return true;
92
+ function anchorMatchesMessage(anchor: DcpNudgeAnchor, msg: any, stableKey: string): boolean {
93
+ if (anchor.anchorStableId && stableKey === anchor.anchorStableId) return true;
93
94
  return msg?.timestamp === anchor.anchorTimestamp;
94
95
  }
95
96
 
@@ -207,34 +208,33 @@ export function upsertNudgeAnchor(
207
208
  messages: any[],
208
209
  state: DcpState,
209
210
  type: DcpNudgeType,
210
- options: { contextPercent?: number } = {},
211
+ options: { contextPercent?: number; renderedReminder?: string } = {},
211
212
  ): { anchor: DcpNudgeAnchor | null; created: boolean; updated: boolean } {
212
- const target = findAnchorMessage(messages);
213
- if (!target) {
214
- // The caller will append one synthetic tail reminder. Drop persisted
215
- // anchors first so applyAnchoredNudges cannot render a second reminder.
216
- state.nudgeAnchors = [];
217
- state.lastNudge = undefined;
218
- return { anchor: null, created: false, updated: false };
219
- }
220
-
221
- const key = `${target.stableId}|${target.timestamp}`;
213
+ const stableKeys = stableMessageKeys(messages);
222
214
  let existing: DcpNudgeAnchor | null = null;
223
215
  for (const anchor of state.nudgeAnchors) {
224
- const anchorKey = `${anchor.anchorStableId ?? ""}|${anchor.anchorTimestamp}`;
225
- if (anchorKey !== key) continue;
216
+ const stillPresent = messages.some((message, index) =>
217
+ isRealAnchorCandidate(message) && anchorMatchesMessage(anchor, message, stableKeys[index]!),
218
+ );
219
+ if (!stillPresent) continue;
226
220
  if (!existing || isNewerAnchor(anchor, existing)) existing = anchor;
227
221
  }
228
222
 
229
223
  const now = Date.now();
230
224
  if (existing) {
231
- // Older sidecars may contain several anchors. Keep only the anchor for the
232
- // current target so every subsequent context pass has singleton state.
225
+ // Keep the original carrier and frozen text. Moving a reminder to the
226
+ // newest user message would remove bytes from an old provider-cache prefix.
233
227
  state.nudgeAnchors = [existing];
234
228
  const shouldUpgrade = typePriority(type) > typePriority(existing.type);
235
- if (shouldUpgrade) existing.type = type;
236
- existing.updatedAt = now;
237
- existing.contextPercent = options.contextPercent ?? existing.contextPercent;
229
+ if (shouldUpgrade) {
230
+ existing.type = type;
231
+ existing.contextPercent = options.contextPercent ?? existing.contextPercent;
232
+ existing.renderedReminder = options.renderedReminder ?? existing.renderedReminder;
233
+ existing.updatedAt = now;
234
+ } else if (!existing.renderedReminder && options.renderedReminder) {
235
+ // One-time migration for anchors persisted before frozen rendering.
236
+ existing.renderedReminder = options.renderedReminder;
237
+ }
238
238
  state.lastNudge = {
239
239
  type: existing.type,
240
240
  anchorId: existing.id,
@@ -246,6 +246,15 @@ export function upsertNudgeAnchor(
246
246
  return { anchor: existing, created: false, updated: shouldUpgrade };
247
247
  }
248
248
 
249
+ const target = findAnchorMessage(messages);
250
+ if (!target) {
251
+ // The caller will append one synthetic tail reminder. Drop persisted
252
+ // anchors first so applyAnchoredNudges cannot render a second reminder.
253
+ state.nudgeAnchors = [];
254
+ state.lastNudge = undefined;
255
+ return { anchor: null, created: false, updated: false };
256
+ }
257
+
249
258
  const anchor: DcpNudgeAnchor = {
250
259
  id: state.nextNudgeAnchorId++,
251
260
  type,
@@ -254,6 +263,7 @@ export function upsertNudgeAnchor(
254
263
  anchorRole: target.role,
255
264
  turnIndex: state.currentTurn,
256
265
  contextPercent: options.contextPercent,
266
+ renderedReminder: options.renderedReminder,
257
267
  createdAt: now,
258
268
  updatedAt: now,
259
269
  };
@@ -275,12 +285,16 @@ export function applyAnchoredNudges(
275
285
  messages: any[],
276
286
  state: DcpState,
277
287
  render: (anchor: DcpNudgeAnchor) => string,
278
- ): void {
279
- if (state.nudgeAnchors.length === 0) return;
288
+ ): { rendered: boolean; stateChanged: boolean } {
289
+ if (state.nudgeAnchors.length === 0) return { rendered: false, stateChanged: false };
280
290
 
291
+ const stableKeys = stableMessageKeys(messages);
292
+ const priorAnchors = state.nudgeAnchors;
281
293
  let selected: { anchor: DcpNudgeAnchor; index: number } | null = null;
282
294
  for (const anchor of state.nudgeAnchors) {
283
- const index = messages.findIndex((msg, messageIndex) => anchorMatchesMessage(anchor, msg, messageIndex));
295
+ const index = messages.findIndex((msg, messageIndex) =>
296
+ anchorMatchesMessage(anchor, msg, stableKeys[messageIndex]!),
297
+ );
284
298
  if (index === -1) continue;
285
299
  if (!selected || isNewerAnchor(anchor, selected.anchor)) {
286
300
  selected = { anchor, index };
@@ -290,7 +304,27 @@ export function applyAnchoredNudges(
290
304
  // Defensive migration for persisted pre-singleton state: render only the
291
305
  // newest valid anchor and discard every stale predecessor.
292
306
  state.nudgeAnchors = selected ? [selected.anchor] : [];
293
- if (selected) appendTextToMessage(messages[selected.index], render(selected.anchor));
307
+ const anchorSetChanged = state.nudgeAnchors.length !== priorAnchors.length ||
308
+ state.nudgeAnchors.some((anchor, index) => anchor !== priorAnchors[index]);
309
+ if (!selected) return { rendered: false, stateChanged: anchorSetChanged };
310
+
311
+ const materializedReminder = selected.anchor.renderedReminder === undefined;
312
+ const reminder = selected.anchor.renderedReminder ?? render(selected.anchor);
313
+ if (materializedReminder) selected.anchor.renderedReminder = reminder;
314
+ const anchorMessage = messages[selected.index];
315
+ if (anchorMessage?.role === "assistant") {
316
+ messages.splice(selected.index + 1, 0, {
317
+ role: "user",
318
+ content: reminder,
319
+ timestamp: selected.anchor.createdAt,
320
+ });
321
+ } else {
322
+ appendTextToMessage(anchorMessage, reminder);
323
+ }
324
+ return {
325
+ rendered: true,
326
+ stateChanged: anchorSetChanged || materializedReminder,
327
+ };
294
328
  }
295
329
 
296
330
  export function clearDcpNudgeAnchors(state: DcpState): number {
@@ -1,7 +1,7 @@
1
1
  import type { DcpConfig } from "./config.js";
2
2
  import type { DcpState } from "./state.js";
3
3
  import { applyCompressionBlocks, repairOrphanedToolPairs, syncCompressionBlocks } from "./pruner-compression-blocks.js";
4
- import { stripStaleDcpMetadataFromAssistantMessage } from "./pruner-metadata.js";
4
+ import { stripStaleDcpMetadataFromMessage } from "./pruner-metadata.js";
5
5
  import { injectMessageIds } from "./pruner-message-ids.js";
6
6
  import {
7
7
  applyAutoToolOutputPruning,
@@ -63,7 +63,7 @@ export function applyPruning(
63
63
  typeof contentBlock === "object" && contentBlock !== null ? { ...contentBlock } : contentBlock,
64
64
  );
65
65
  }
66
- return stripStaleDcpMetadataFromAssistantMessage(clone);
66
+ return stripStaleDcpMetadataFromMessage(clone);
67
67
  });
68
68
 
69
69
  // 1. Count user turns → update state.currentTurn. Do this before inserting
@@ -79,21 +79,29 @@ export function applyPruning(
79
79
  // expansion logic could not catch (e.g. multi-block interactions, pre-broken state).
80
80
  repairOrphanedToolPairs(msgs);
81
81
 
82
- // 3. Apply deduplication
83
- applyDeduplication(msgs, state, config);
84
-
85
- // 4. Apply error purging
86
- applyErrorPurging(msgs, state, config);
87
-
88
- // 5. Apply autonomous policy pruning for old/large/stale tool outputs
89
- applyAutoToolOutputPruning(msgs, state, config);
82
+ // 3-5. Discover new automatic pruning decisions only at stable checkpoints.
83
+ // Rewriting an old result after every same-turn duplicate breaks provider
84
+ // continuation repeatedly. A new user turn or compression block already
85
+ // establishes a natural history boundary where one prefix rebuild is
86
+ // acceptable. Branches that move the turn count backwards also reopen the
87
+ // checkpoint instead of suppressing pruning indefinitely.
88
+ const newestBlockId = Math.max(0, state.nextBlockId - 1);
89
+ const automaticPruneCheckpoint =
90
+ state.currentTurn !== state.lastAutomaticPruneTurn ||
91
+ newestBlockId !== state.lastAutomaticPruneBlockId;
92
+ if (automaticPruneCheckpoint) {
93
+ applyDeduplication(msgs, state, config);
94
+ applyErrorPurging(msgs, state, config);
95
+ applyAutoToolOutputPruning(msgs, state, config);
96
+ state.lastAutomaticPruneTurn = state.currentTurn;
97
+ state.lastAutomaticPruneBlockId = newestBlockId;
98
+ }
90
99
 
91
100
  // 6. Apply explicit tool output pruning (prunedToolIds)
92
101
  applyToolOutputPruning(msgs, state);
93
102
 
94
- // 7. Refresh message ID snapshots used by the compress tool. Markers are
95
- // never injected into provider-visible content; the before_provider_request
96
- // hook delivers the id map as a hidden control payload.
103
+ // 7. Refresh message ID snapshots and append stable distributed metadata to
104
+ // user/tool-result carriers. Assistant items remain byte-stable.
97
105
  injectMessageIds(msgs, state, { config });
98
106
 
99
107
  // 8. state.messageIdSnapshot/messageMetaSnapshot are already updated by injectMessageIds
@@ -119,6 +119,8 @@ export interface DcpNudgeAnchor {
119
119
  turnIndex: number
120
120
  /** Approximate context usage that triggered the reminder, as a 0-1 fraction. */
121
121
  contextPercent?: number
122
+ /** Frozen provider-visible reminder text for cache-stable reapplication. */
123
+ renderedReminder?: string
122
124
  /** Wall-clock creation time. */
123
125
  createdAt: number
124
126
  /** Wall-clock time of the latest re-application/update. */
@@ -166,6 +168,10 @@ export interface DcpState {
166
168
  messageIdSnapshot: Map<string, number>
167
169
  /** Extra metadata for the model-visible DCP message IDs in messageIdSnapshot. */
168
170
  messageMetaSnapshot: Map<string, MessageIdMeta>
171
+ /** Stable message identity → persistent model-visible mNNN assignment. */
172
+ messageIdsByStableId: Map<string, string>
173
+ /** Monotonic counter for persistent message IDs. */
174
+ nextMessageId: number
169
175
 
170
176
  // ── Turn tracking ──────────────────────────────────────────────────────────
171
177
  /**
@@ -174,6 +180,10 @@ export interface DcpState {
174
180
  * context array in the `context` event handler.
175
181
  */
176
182
  currentTurn: number
183
+ /** Last user-turn count at which automatic prune discovery ran. */
184
+ lastAutomaticPruneTurn: number
185
+ /** Highest created block ID at the last automatic-prune checkpoint. */
186
+ lastAutomaticPruneBlockId: number
177
187
 
178
188
  // ── Statistics ─────────────────────────────────────────────────────────────
179
189
  /** Running total of tokens estimated to have been saved by pruning/compression */
@@ -253,7 +263,11 @@ export function createState(): DcpState {
253
263
  nextBlockId: 1,
254
264
  messageIdSnapshot: new Map(),
255
265
  messageMetaSnapshot: new Map(),
266
+ messageIdsByStableId: new Map(),
267
+ nextMessageId: 1,
256
268
  currentTurn: 0,
269
+ lastAutomaticPruneTurn: -1,
270
+ lastAutomaticPruneBlockId: 0,
257
271
  tokensSaved: 0,
258
272
  totalPruneCount: 0,
259
273
  totalToolCallCount: 0,
@@ -285,7 +299,11 @@ export function resetState(state: DcpState): void {
285
299
  state.nextBlockId = 1
286
300
  state.messageIdSnapshot.clear()
287
301
  state.messageMetaSnapshot.clear()
302
+ state.messageIdsByStableId.clear()
303
+ state.nextMessageId = 1
288
304
  state.currentTurn = 0
305
+ state.lastAutomaticPruneTurn = -1
306
+ state.lastAutomaticPruneBlockId = 0
289
307
  state.tokensSaved = 0
290
308
  state.totalPruneCount = 0
291
309
  state.totalToolCallCount = 0
@@ -424,12 +442,18 @@ export interface SerializedDcpState {
424
442
  nudgeAnchors?: DcpNudgeAnchor[]
425
443
  nextNudgeAnchorId?: number
426
444
  lastNudge?: DcpLastNudge
445
+ /** Persistent stable-identity → mNNN assignments. */
446
+ messageIdsByStableId?: Array<[string, string]>
447
+ nextMessageId?: number
427
448
  /**
428
449
  * Persisted since v??. `context` events re-seed currentTurn from raw
429
450
  * messages, but keeping it across session restarts gives diagnostics and
430
451
  * telemetry a contiguous turn counter instead of resetting to 0.
431
452
  */
432
453
  currentTurn?: number
454
+ /** Automatic pruning runs only when one of these checkpoints changes. */
455
+ lastAutomaticPruneTurn?: number
456
+ lastAutomaticPruneBlockId?: number
433
457
  /**
434
458
  * Persisted since v?.?. Without persistence a pi process restart silently
435
459
  * reset the nudge cadence counter to 0, which could suppress the next
@@ -622,7 +646,11 @@ export function serializeState(state: DcpState): SerializedDcpState {
622
646
  nudgeAnchors: state.nudgeAnchors,
623
647
  nextNudgeAnchorId: state.nextNudgeAnchorId,
624
648
  lastNudge: state.lastNudge,
649
+ messageIdsByStableId: Array.from(state.messageIdsByStableId.entries()),
650
+ nextMessageId: state.nextMessageId,
625
651
  currentTurn: state.currentTurn,
652
+ lastAutomaticPruneTurn: state.lastAutomaticPruneTurn,
653
+ lastAutomaticPruneBlockId: state.lastAutomaticPruneBlockId,
626
654
  nudgeCounter: state.nudgeCounter,
627
655
  lastNudgeTurn: state.lastNudgeTurn,
628
656
  lastContextWindow: state.lastContextWindow,
@@ -776,6 +804,31 @@ export function restoreState(state: DcpState, data: unknown): void {
776
804
  state.lastNudge = saved.lastNudge
777
805
  }
778
806
 
807
+ if (Array.isArray(saved.messageIdsByStableId)) {
808
+ const usedIds = new Set<string>()
809
+ const usedStableKeys = new Set<string>()
810
+ state.messageIdsByStableId = new Map(
811
+ saved.messageIdsByStableId.filter(
812
+ (entry): entry is [string, string] => {
813
+ if (!Array.isArray(entry) || typeof entry[0] !== "string" || typeof entry[1] !== "string") return false
814
+ if (!entry[0] || !/^m\d+$/.test(entry[1]) || usedIds.has(entry[1]) || usedStableKeys.has(entry[0])) return false
815
+ usedIds.add(entry[1])
816
+ usedStableKeys.add(entry[0])
817
+ return true
818
+ },
819
+ ),
820
+ )
821
+ }
822
+ const highestRestoredMessageId = Math.max(
823
+ 0,
824
+ ...Array.from(state.messageIdsByStableId.values()).map((id) => Number.parseInt(id.slice(1), 10) || 0),
825
+ )
826
+ if (typeof saved.nextMessageId === "number" && Number.isFinite(saved.nextMessageId) && saved.nextMessageId > 0) {
827
+ state.nextMessageId = Math.max(Math.floor(saved.nextMessageId), highestRestoredMessageId + 1)
828
+ } else {
829
+ state.nextMessageId = highestRestoredMessageId + 1
830
+ }
831
+
779
832
  // nudgeCounter: clamp to a non-negative integer so a corrupted payload
780
833
  // cannot stall reminders by going negative. Default 0 keeps older sessions
781
834
  // behaving like a fresh cadence on first post-restart context event.
@@ -793,6 +846,12 @@ export function restoreState(state: DcpState, data: unknown): void {
793
846
  if (typeof saved.currentTurn === "number" && Number.isFinite(saved.currentTurn) && saved.currentTurn >= 0) {
794
847
  state.currentTurn = Math.floor(saved.currentTurn)
795
848
  }
849
+ if (typeof saved.lastAutomaticPruneTurn === "number" && Number.isFinite(saved.lastAutomaticPruneTurn)) {
850
+ state.lastAutomaticPruneTurn = Math.floor(saved.lastAutomaticPruneTurn)
851
+ }
852
+ if (typeof saved.lastAutomaticPruneBlockId === "number" && Number.isFinite(saved.lastAutomaticPruneBlockId) && saved.lastAutomaticPruneBlockId >= 0) {
853
+ state.lastAutomaticPruneBlockId = Math.floor(saved.lastAutomaticPruneBlockId)
854
+ }
796
855
  if (typeof saved.lastContextWindow === "number" && Number.isFinite(saved.lastContextWindow) && saved.lastContextWindow > 0) {
797
856
  state.lastContextWindow = saved.lastContextWindow
798
857
  }
@@ -329,6 +329,10 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
329
329
  "description": "Use when the sub-agent should make or plan code changes for a feature, bug fix, or refactor.",
330
330
  "model": "openai-codex/gpt-5.6-sol",
331
331
  "fallbackModels": ["zai/glm-5.3"],
332
+ "modelByParent": {
333
+ "openai-codex/gpt-5.6-luna*": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5.3"] },
334
+ "openai-codex/gpt-5.6-sol*": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5.3"] }
335
+ },
332
336
  "thinking": "high"
333
337
  },
334
338
  "deep": {
@@ -0,0 +1,34 @@
1
+ /** Reserved RPC widget key consumed by Pix ACP as a structured event envelope. */
2
+ export const RPC_SESSION_STATE_WIDGET_KEY = "pix.session-state";
3
+ export const RPC_SESSION_STATE_ENV = "PIX_ACP_SESSION_STATE_BRIDGE";
4
+
5
+ export interface RpcSessionStateContext {
6
+ readonly mode?: unknown;
7
+ readonly ui?: {
8
+ setWidget?: (key: string, lines: string[] | undefined) => void;
9
+ };
10
+ }
11
+
12
+ /**
13
+ * Publish structured extension state through pi's supported RPC UI protocol.
14
+ *
15
+ * TUI/print/json modes keep using their native event consumers. Serialization
16
+ * is best-effort so an optional external UI can never break the source tool.
17
+ */
18
+ export function publishRpcSessionState(
19
+ ctx: RpcSessionStateContext | undefined,
20
+ channel: string,
21
+ data: unknown,
22
+ ): void {
23
+ if (
24
+ process.env[RPC_SESSION_STATE_ENV] !== "1"
25
+ || ctx?.mode !== "rpc"
26
+ || !ctx.ui?.setWidget
27
+ || !channel.trim()
28
+ ) return;
29
+ try {
30
+ ctx.ui.setWidget(RPC_SESSION_STATE_WIDGET_KEY, [channel, JSON.stringify(data)]);
31
+ } catch {
32
+ // The event bus remains authoritative when RPC UI transport is absent.
33
+ }
34
+ }
@@ -42,6 +42,7 @@ import {
42
42
  } from "./tool/types.js";
43
43
  import { formatCommandTaskLine } from "./view/format.js";
44
44
  import { formatStatusLabel } from "./view/labels.js";
45
+ import { publishRpcSessionState, type RpcSessionStateContext } from "../lib/rpc-session-state.js";
45
46
 
46
47
  const SECTION_PENDING = "── Pending ──";
47
48
  const SECTION_IN_PROGRESS = "── In Progress ──";
@@ -96,7 +97,9 @@ interface TodoToolRegistrationOptions extends TodoToolHooks {
96
97
  parameters?: typeof TodoParamsSchema;
97
98
  }
98
99
 
99
- type TodoStateEventContext = { sessionManager?: { getSessionFile?: () => unknown; getSessionId?: () => unknown } };
100
+ type TodoStateEventContext = RpcSessionStateContext & {
101
+ sessionManager?: { getSessionFile?: () => unknown; getSessionId?: () => unknown };
102
+ };
100
103
  type TodoStateEventEmitter = { events?: { emit?: (channel: string, data: unknown) => void } };
101
104
  type TodoStateEntryWriter = { appendEntry<T = unknown>(customType: string, data?: T): void };
102
105
 
@@ -225,7 +228,7 @@ export function publishTodoState(
225
228
  const state = getState();
226
229
  const sessionFile = sessionFileFromContext(ctx);
227
230
  const sessionId = sessionIdFromContext(ctx);
228
- pi.events?.emit?.(TODO_STATE_EVENT, {
231
+ const snapshot = {
229
232
  version: 1,
230
233
  details: {
231
234
  action,
@@ -236,7 +239,9 @@ export function publishTodoState(
236
239
  ...(sessionFile ? { sessionFile } : {}),
237
240
  ...(sessionId ? { sessionId } : {}),
238
241
  checkedAt: Date.now(),
239
- });
242
+ };
243
+ pi.events?.emit?.(TODO_STATE_EVENT, snapshot);
244
+ publishRpcSessionState(ctx as TodoStateEventContext | undefined, TODO_STATE_EVENT, snapshot);
240
245
  }
241
246
 
242
247
  function appendTodoStateSnapshot(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "1.0.38",
3
+ "version": "1.0.39",
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": {
@@ -62,6 +62,9 @@
62
62
  "test:tools-suite": "npm --prefix external/pi-tools-suite test",
63
63
  "build:acp": "npm --prefix acp run build",
64
64
  "check:acp": "npm --prefix acp run check",
65
+ "dev:desktop": "npm --prefix desktop run dev",
66
+ "build:desktop": "npm --prefix desktop run build",
67
+ "check:desktop": "npm --prefix desktop run check",
65
68
  "test:browser-qa-e2e": "npm --prefix external/pi-tools-suite run test:browser-qa-e2e",
66
69
  "sync:pi-tools-suite": "node scripts/sync-pi-tools-suite.mjs",
67
70
  "sync:pi-tools-suite:check": "node scripts/sync-pi-tools-suite.mjs --check",
@@ -80,9 +83,10 @@
80
83
  "prepublishOnly": "npm run check && npm run build:pix && npm run generate-schemas"
81
84
  },
82
85
  "dependencies": {
83
- "@earendil-works/pi-ai": "0.84.4",
84
- "@earendil-works/pi-coding-agent": "0.84.4",
85
- "@earendil-works/pi-tui": "0.84.4",
86
+ "@earendil-works/pi-ai": "0.85.0",
87
+ "@earendil-works/pi-coding-agent": "0.85.0",
88
+ "@earendil-works/pi-server": "0.85.0",
89
+ "@earendil-works/pi-tui": "0.85.0",
86
90
  "@mariozechner/clipboard": "^0.3.9",
87
91
  "grok-mermaid": "0.2.2",
88
92
  "jsonc-parser": "3.3.1",