pi-ui-extend 1.0.7 → 1.0.8

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.
@@ -809,6 +809,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
809
809
  await saveDcpState(ctx, state)
810
810
  }
811
811
 
812
+ const anchorsBeforeFinalization = state.nudgeAnchors.length
812
813
  if (state.manualMode) {
813
814
  state.nudgeAnchors = state.nudgeAnchors.filter((anchor) =>
814
815
  anchor.type === "context-strong" || anchor.type === "context-soft",
@@ -817,6 +818,9 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
817
818
  applyAnchoredNudges(prunedMessages, state, (anchor) =>
818
819
  appendConcreteNudgeGuidance(baseNudgeText(anchor.type), candidate, messageCandidates, state),
819
820
  )
821
+ if (state.nudgeAnchors.length !== anchorsBeforeFinalization) {
822
+ await saveDcpState(ctx, state)
823
+ }
820
824
 
821
825
  return finishContext("complete", prunedMessages, {
822
826
  candidate,
@@ -51,7 +51,6 @@ function isRealAnchorCandidate(msg: any): boolean {
51
51
  const role = msg?.role ?? "";
52
52
  if (role !== "user" && role !== "assistant") return false;
53
53
  const text = messageText(msg);
54
- if (text.includes("<dcp-system-reminder>")) return false;
55
54
  if (extractBlockId(text) !== undefined) return false;
56
55
  return true;
57
56
  }
@@ -94,6 +93,12 @@ function anchorMatchesMessage(anchor: DcpNudgeAnchor, msg: any, index: number):
94
93
  return msg?.timestamp === anchor.anchorTimestamp;
95
94
  }
96
95
 
96
+ function isNewerAnchor(candidate: DcpNudgeAnchor, current: DcpNudgeAnchor): boolean {
97
+ if (candidate.updatedAt !== current.updatedAt) return candidate.updatedAt > current.updatedAt;
98
+ if (candidate.createdAt !== current.createdAt) return candidate.createdAt > current.createdAt;
99
+ return candidate.id > current.id;
100
+ }
101
+
97
102
  function appendTextToMessage(msg: any, text: string): void {
98
103
  const suffix = `\n\n${text}`;
99
104
  if (typeof msg.content === "string") {
@@ -205,15 +210,27 @@ export function upsertNudgeAnchor(
205
210
  options: { contextPercent?: number } = {},
206
211
  ): { anchor: DcpNudgeAnchor | null; created: boolean; updated: boolean } {
207
212
  const target = findAnchorMessage(messages);
208
- if (!target) return { anchor: null, created: false, updated: false };
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
+ }
209
220
 
210
221
  const key = `${target.stableId}|${target.timestamp}`;
211
- const existing = state.nudgeAnchors.find(
212
- (anchor) => `${anchor.anchorStableId ?? ""}|${anchor.anchorTimestamp}` === key,
213
- );
222
+ let existing: DcpNudgeAnchor | null = null;
223
+ for (const anchor of state.nudgeAnchors) {
224
+ const anchorKey = `${anchor.anchorStableId ?? ""}|${anchor.anchorTimestamp}`;
225
+ if (anchorKey !== key) continue;
226
+ if (!existing || isNewerAnchor(anchor, existing)) existing = anchor;
227
+ }
214
228
 
215
229
  const now = Date.now();
216
230
  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.
233
+ state.nudgeAnchors = [existing];
217
234
  const shouldUpgrade = typePriority(type) > typePriority(existing.type);
218
235
  if (shouldUpgrade) existing.type = type;
219
236
  existing.updatedAt = now;
@@ -240,7 +257,9 @@ export function upsertNudgeAnchor(
240
257
  createdAt: now,
241
258
  updatedAt: now,
242
259
  };
243
- state.nudgeAnchors.push(anchor);
260
+ // A nudge follows the latest useful message. Replacing the previous anchor
261
+ // prevents one full reminder from accumulating per turn/assistant response.
262
+ state.nudgeAnchors = [anchor];
244
263
  state.lastNudge = {
245
264
  type,
246
265
  anchorId: anchor.id,
@@ -259,15 +278,19 @@ export function applyAnchoredNudges(
259
278
  ): void {
260
279
  if (state.nudgeAnchors.length === 0) return;
261
280
 
262
- const activeAnchors: DcpNudgeAnchor[] = [];
281
+ let selected: { anchor: DcpNudgeAnchor; index: number } | null = null;
263
282
  for (const anchor of state.nudgeAnchors) {
264
283
  const index = messages.findIndex((msg, messageIndex) => anchorMatchesMessage(anchor, msg, messageIndex));
265
284
  if (index === -1) continue;
266
- activeAnchors.push(anchor);
267
- appendTextToMessage(messages[index], render(anchor));
285
+ if (!selected || isNewerAnchor(anchor, selected.anchor)) {
286
+ selected = { anchor, index };
287
+ }
268
288
  }
269
289
 
270
- state.nudgeAnchors = activeAnchors;
290
+ // Defensive migration for persisted pre-singleton state: render only the
291
+ // newest valid anchor and discard every stale predecessor.
292
+ state.nudgeAnchors = selected ? [selected.anchor] : [];
293
+ if (selected) appendTextToMessage(messages[selected.index], render(selected.anchor));
271
294
  }
272
295
 
273
296
  export function clearDcpNudgeAnchors(state: DcpState): number {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
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": {