pi-ui-extend 1.0.7 → 1.0.10
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/extensions/extension-ui-controller.js +11 -12
- package/dist/app/rendering/tool-block-renderer.js +55 -4
- package/dist/app/screen/screen-styler.js +1 -1
- package/dist/bundled-extensions/question/index.js +1 -1
- package/dist/bundled-extensions/question/tui.d.ts +1 -1
- package/dist/bundled-extensions/question/types.d.ts +1 -1
- package/dist/markdown-format.d.ts +2 -0
- package/dist/markdown-format.js +17 -2
- package/dist/syntax-highlight.d.ts +12 -0
- package/dist/syntax-highlight.js +20 -0
- package/external/pi-tools-suite/src/dcp/index.ts +4 -0
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +33 -10
- package/package.json +1 -1
|
@@ -394,16 +394,14 @@ export class ExtensionUiController {
|
|
|
394
394
|
if (!this.host.isRunning())
|
|
395
395
|
return undefined;
|
|
396
396
|
const scopeKey = this.normalizeScopeKey(options.scopeKey);
|
|
397
|
-
if (!this.isScopeActive(scopeKey))
|
|
398
|
-
return undefined;
|
|
399
397
|
if (this.activeCustomUis.has(scopeKey))
|
|
400
398
|
throw new Error("Another extension custom UI is already active.");
|
|
401
|
-
const savedInput = options.savedInput ?? this.host.getInput();
|
|
399
|
+
const savedInput = options.savedInput ?? (this.isScopeActive(scopeKey) ? this.host.getInput() : undefined);
|
|
402
400
|
return await new Promise((resolve, reject) => {
|
|
403
401
|
const active = {
|
|
404
402
|
key: CUSTOM_UI_WIDGET_KEY,
|
|
405
403
|
scopeKey,
|
|
406
|
-
savedInput,
|
|
404
|
+
...(savedInput === undefined ? {} : { savedInput }),
|
|
407
405
|
settled: false,
|
|
408
406
|
resolve: (value) => resolve(value),
|
|
409
407
|
reject,
|
|
@@ -414,20 +412,17 @@ export class ExtensionUiController {
|
|
|
414
412
|
const done = (value) => {
|
|
415
413
|
if (active.settled || this.activeCustomUis.get(scopeKey) !== active)
|
|
416
414
|
return;
|
|
417
|
-
this.finishActiveCustomUi(active,
|
|
415
|
+
this.finishActiveCustomUi(active, value, { resolve: true });
|
|
418
416
|
};
|
|
419
417
|
void (async () => {
|
|
420
418
|
try {
|
|
421
419
|
const component = await factory(this.widgetTuiHandle(scopeKey), this.createExtensionTheme(), {}, done);
|
|
422
|
-
if (active.settled || this.activeCustomUis.get(scopeKey) !== active
|
|
420
|
+
if (active.settled || this.activeCustomUis.get(scopeKey) !== active) {
|
|
423
421
|
component.dispose?.();
|
|
424
|
-
if (!active.settled && this.activeCustomUis.get(scopeKey) === active) {
|
|
425
|
-
this.finishActiveCustomUi(active, undefined, { resolve: true });
|
|
426
|
-
}
|
|
427
422
|
return;
|
|
428
423
|
}
|
|
429
424
|
active.component = component;
|
|
430
|
-
if (this.host.isRunning())
|
|
425
|
+
if (this.host.isRunning() && this.isScopeActive(scopeKey))
|
|
431
426
|
this.host.render();
|
|
432
427
|
}
|
|
433
428
|
catch (error) {
|
|
@@ -454,8 +449,9 @@ export class ExtensionUiController {
|
|
|
454
449
|
return;
|
|
455
450
|
active.settled = true;
|
|
456
451
|
this.activeCustomUis.delete(active.scopeKey);
|
|
457
|
-
if (this.isScopeActive(active.scopeKey) && this.host.getInput() !== active.savedInput)
|
|
452
|
+
if (this.isScopeActive(active.scopeKey) && active.savedInput !== undefined && this.host.getInput() !== active.savedInput) {
|
|
458
453
|
this.host.setInput(active.savedInput);
|
|
454
|
+
}
|
|
459
455
|
try {
|
|
460
456
|
active.component?.dispose?.();
|
|
461
457
|
}
|
|
@@ -479,7 +475,10 @@ export class ExtensionUiController {
|
|
|
479
475
|
const scopeKey = this.activeScopeKey();
|
|
480
476
|
if (!this.isScopeActive(scopeKey))
|
|
481
477
|
return undefined;
|
|
482
|
-
|
|
478
|
+
const active = this.activeCustomUis.get(scopeKey);
|
|
479
|
+
if (active && active.savedInput === undefined)
|
|
480
|
+
active.savedInput = this.host.getInput();
|
|
481
|
+
return active;
|
|
483
482
|
}
|
|
484
483
|
isScopeActive(scopeKey) {
|
|
485
484
|
return this.host.isExtensionUiScopeActive?.(scopeKey) ?? this.activeScopeKey() === scopeKey;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveColor } from "../../config.js";
|
|
2
|
+
import { renderMarkdownLine } from "../../markdown-format.js";
|
|
2
3
|
import { expandTabs, sliceByDisplayWidth, stringDisplayWidth, wrapDisplayLineByWords } from "../../terminal-width.js";
|
|
3
4
|
import { alertIconPrefixLength, hasToolLspDiagnosticsAfterMutation, lspDiagnosticSeverityForLine, sanitizeText, toolStatusIcon, toolStatusIconColor, wrapLine } from "./render-text.js";
|
|
4
5
|
import { APP_ICONS } from "../icons.js";
|
|
@@ -234,9 +235,16 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
|
|
|
234
235
|
const lspDiagnosticStyle = hasLspDiagnostics ? lspDiagnosticLineStyle(displayLine, colors) : undefined;
|
|
235
236
|
const bodyLineStyle = bodyLineStyleForLine(bodyLineStyles, absoluteRawLineIndex, colors);
|
|
236
237
|
const lineSyntaxHighlight = syntaxHighlightForLine(syntaxHighlight, absoluteRawLineIndex);
|
|
238
|
+
const markdownLine = lineSyntaxHighlight?.language === "markdown"
|
|
239
|
+
? renderMarkdownLine(displayLine, lineSyntaxHighlight.startColumn ?? 0)
|
|
240
|
+
: undefined;
|
|
241
|
+
const syntaxDisplayLine = markdownLine?.text ?? displayLine;
|
|
237
242
|
const wrappedLines = ansiLine && !diffStyle && !lspDiagnosticStyle && !bodyLineStyle && !lineSyntaxHighlight
|
|
238
243
|
? wrapAnsiStyledDisplayLine(ansiLine, bodyWidth)
|
|
239
|
-
: wrapBodyLine(
|
|
244
|
+
: wrapBodyLine(syntaxDisplayLine, bodyWidth).map((wrapped) => ({ text: wrapped, segments: [] }));
|
|
245
|
+
const wrappedSourceRanges = lineSyntaxHighlight && wrappedLines.length > 1
|
|
246
|
+
? sourceRangesForWrappedLines(syntaxDisplayLine, wrappedLines.map((line) => line.text))
|
|
247
|
+
: [];
|
|
240
248
|
for (const [wrapIndex, wrapped] of wrappedLines.entries()) {
|
|
241
249
|
const line = {
|
|
242
250
|
text: `${displayPrefix}${wrapped.text}`,
|
|
@@ -271,9 +279,33 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
|
|
|
271
279
|
}
|
|
272
280
|
else if (lineSyntaxHighlight) {
|
|
273
281
|
const rawStart = wrapIndex === 0 ? lineSyntaxHighlight.startColumn ?? 0 : 0;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
282
|
+
const sourceRange = wrappedSourceRanges[wrapIndex];
|
|
283
|
+
const markdownSegments = sourceRange
|
|
284
|
+
? markdownLine?.segments.flatMap((segment) => shiftStyledSegmentToRange(segment, sourceRange)) ?? []
|
|
285
|
+
: markdownLine?.segments ?? [];
|
|
286
|
+
line.syntaxHighlight = {
|
|
287
|
+
language: lineSyntaxHighlight.language,
|
|
288
|
+
start: Math.min(line.text.length, displayPrefix.length + rawStart),
|
|
289
|
+
...(sourceRange ? {
|
|
290
|
+
context: {
|
|
291
|
+
text: syntaxDisplayLine,
|
|
292
|
+
rangeStart: sourceRange.start,
|
|
293
|
+
rangeEnd: sourceRange.end,
|
|
294
|
+
renderStart: displayPrefix.length,
|
|
295
|
+
syntaxStart: lineSyntaxHighlight.startColumn ?? 0,
|
|
296
|
+
},
|
|
297
|
+
} : {}),
|
|
298
|
+
};
|
|
299
|
+
const syntaxSegments = [
|
|
300
|
+
...(gutterSegment ? [gutterSegment] : []),
|
|
301
|
+
...markdownSegments.map((segment) => ({
|
|
302
|
+
...segment,
|
|
303
|
+
start: segment.start + displayPrefix.length,
|
|
304
|
+
end: segment.end + displayPrefix.length,
|
|
305
|
+
})),
|
|
306
|
+
];
|
|
307
|
+
if (syntaxSegments.length > 0)
|
|
308
|
+
line.segments = syntaxSegments;
|
|
277
309
|
}
|
|
278
310
|
else if (wrapped.segments.length > 0) {
|
|
279
311
|
line.segments = [
|
|
@@ -302,6 +334,25 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
|
|
|
302
334
|
}
|
|
303
335
|
return lines;
|
|
304
336
|
}
|
|
337
|
+
function shiftStyledSegmentToRange(segment, range) {
|
|
338
|
+
const start = Math.max(segment.start, range.start);
|
|
339
|
+
const end = Math.min(segment.end, range.end);
|
|
340
|
+
if (end <= start)
|
|
341
|
+
return [];
|
|
342
|
+
return [{ ...segment, start: start - range.start, end: end - range.start }];
|
|
343
|
+
}
|
|
344
|
+
function sourceRangesForWrappedLines(text, wrappedLines) {
|
|
345
|
+
const ranges = [];
|
|
346
|
+
let cursor = 0;
|
|
347
|
+
for (const wrapped of wrappedLines) {
|
|
348
|
+
const found = text.indexOf(wrapped, cursor);
|
|
349
|
+
const start = found === -1 ? cursor : found;
|
|
350
|
+
const end = Math.min(text.length, start + wrapped.length);
|
|
351
|
+
ranges.push({ start, end });
|
|
352
|
+
cursor = end;
|
|
353
|
+
}
|
|
354
|
+
return ranges;
|
|
355
|
+
}
|
|
305
356
|
const ANSI_STANDARD_COLORS = ["#000000", "#cd3131", "#0dbc79", "#e5e510", "#2472c8", "#bc3fbc", "#11a8cd", "#e5e5e5"];
|
|
306
357
|
const ANSI_BRIGHT_COLORS = ["#666666", "#f14c4c", "#23d18b", "#f5f543", "#3b8eea", "#d670d6", "#29b8db", "#e5e5e5"];
|
|
307
358
|
function sanitizeToolBodyText(text, preserveAnsi) {
|
|
@@ -16,7 +16,7 @@ export class ScreenStyler {
|
|
|
16
16
|
...(line?.backgroundOverride === undefined ? {} : { background: line.backgroundOverride }),
|
|
17
17
|
};
|
|
18
18
|
const colors = this.host.theme.colors;
|
|
19
|
-
const markdownLine = line?.syntaxHighlight?.language === "markdown"
|
|
19
|
+
const markdownLine = line?.syntaxHighlight?.language === "markdown" && !line.syntaxHighlight.context
|
|
20
20
|
? renderMarkdownDisplayLine(line.text, width, line.syntaxHighlight.start)
|
|
21
21
|
: undefined;
|
|
22
22
|
const text = markdownLine?.text ?? line?.text ?? "";
|
|
@@ -18,7 +18,7 @@ export default function questionExtension(pi) {
|
|
|
18
18
|
if (!ctx.hasUI)
|
|
19
19
|
return createQuestionToolResult(createCanceledQuestionResult("ui_unavailable", questions), questions);
|
|
20
20
|
const selections = await runQuestionnaire(questions, ctx);
|
|
21
|
-
if (selections
|
|
21
|
+
if (selections == null)
|
|
22
22
|
return createQuestionToolResult(createCanceledQuestionResult("user_canceled"), questions);
|
|
23
23
|
return createQuestionToolResult(createSuccessfulQuestionResult(questions, selections), questions);
|
|
24
24
|
},
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { NormalizedQuestion, QuestionSelection, QuestionUiContext } from "./types.js";
|
|
2
|
-
export declare function runQuestionnaire(questions: NormalizedQuestion[], ctx: QuestionUiContext): Promise<QuestionSelection[] | null>;
|
|
2
|
+
export declare function runQuestionnaire(questions: NormalizedQuestion[], ctx: QuestionUiContext): Promise<QuestionSelection[] | null | undefined>;
|
|
@@ -61,7 +61,7 @@ export interface QuestionToolResult {
|
|
|
61
61
|
export interface QuestionUiContext {
|
|
62
62
|
hasUI?: boolean;
|
|
63
63
|
ui: {
|
|
64
|
-
custom<T>(factory: (tui: QuestionTui, theme: QuestionTheme, keybindings: unknown, done: (value: T) => void) => QuestionComponent): Promise<T>;
|
|
64
|
+
custom<T>(factory: (tui: QuestionTui, theme: QuestionTheme, keybindings: unknown, done: (value: T) => void) => QuestionComponent): Promise<T | undefined>;
|
|
65
65
|
setEditorText?(text: string): void;
|
|
66
66
|
getEditorText?(): string;
|
|
67
67
|
notify?(message: string, level: "info" | "warning" | "error"): void;
|
package/dist/markdown-format.js
CHANGED
|
@@ -84,13 +84,26 @@ export function renderMarkdownTextLines(text, width, start = 0, options = {}) {
|
|
|
84
84
|
const syntaxHighlight = markdownLineSyntaxHighlight(fence, Boolean(opensFence || closesFence), start);
|
|
85
85
|
const isHeadingLine = !fence && /^\s{0,3}#{1,6}\s/.test(rawLine);
|
|
86
86
|
const markdownLine = syntaxHighlight?.language === "markdown" || isHeadingLine ? renderMarkdownLine(rawLine) : undefined;
|
|
87
|
-
|
|
87
|
+
const logicalLine = markdownLine ?? { text: rawLine, segments: [] };
|
|
88
|
+
for (const wrapped of wrapRenderedMarkdownLine(logicalLine, width, options)) {
|
|
89
|
+
const wrappedSyntaxHighlight = syntaxHighlight && wrapped.sourceStart !== undefined && wrapped.sourceEnd !== undefined
|
|
90
|
+
? {
|
|
91
|
+
...syntaxHighlight,
|
|
92
|
+
context: {
|
|
93
|
+
text: logicalLine.text,
|
|
94
|
+
rangeStart: wrapped.sourceStart,
|
|
95
|
+
rangeEnd: wrapped.sourceEnd,
|
|
96
|
+
renderStart: start,
|
|
97
|
+
syntaxStart: 0,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
: syntaxHighlight;
|
|
88
101
|
lines.push({
|
|
89
102
|
text: wrapped.text,
|
|
90
103
|
...(wrapped.copyText === undefined ? {} : { copyText: wrapped.copyText }),
|
|
91
104
|
...(wrapped.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
|
|
92
105
|
...(wrapped.segments.length > 0 ? { segments: wrapped.segments } : {}),
|
|
93
|
-
...(
|
|
106
|
+
...(wrappedSyntaxHighlight ? { syntaxHighlight: wrappedSyntaxHighlight } : {}),
|
|
94
107
|
...(isHeadingLine ? { heading: true } : {}),
|
|
95
108
|
});
|
|
96
109
|
}
|
|
@@ -133,6 +146,8 @@ function wrapRenderedMarkdownLine(line, width, options) {
|
|
|
133
146
|
copyText: line.text.slice(range.start, ranges[index + 1]?.start ?? range.end),
|
|
134
147
|
...(index < ranges.length - 1 ? { continuesOnNextLine: true } : {}),
|
|
135
148
|
segments: line.segments.flatMap((segment) => shiftSegmentToRange(segment, range.start, range.end)),
|
|
149
|
+
sourceStart: range.start,
|
|
150
|
+
sourceEnd: range.end,
|
|
136
151
|
}));
|
|
137
152
|
}
|
|
138
153
|
function wrapDisplayLineByWordsWithRanges(text, width, options) {
|
|
@@ -3,6 +3,18 @@ export type SyntaxHighlightLanguage = "c" | "cpp" | "csharp" | "css" | "go" | "h
|
|
|
3
3
|
export type SyntaxLineHighlight = {
|
|
4
4
|
language: SyntaxHighlightLanguage;
|
|
5
5
|
start: number;
|
|
6
|
+
context?: SyntaxHighlightContext;
|
|
7
|
+
};
|
|
8
|
+
export type SyntaxHighlightContext = {
|
|
9
|
+
/** Complete logical line, before it was soft-wrapped for display. */
|
|
10
|
+
text: string;
|
|
11
|
+
/** Source range represented by this visual line. */
|
|
12
|
+
rangeStart: number;
|
|
13
|
+
rangeEnd: number;
|
|
14
|
+
/** Visual column where rangeStart is rendered. */
|
|
15
|
+
renderStart: number;
|
|
16
|
+
/** Source column where syntax parsing begins. */
|
|
17
|
+
syntaxStart: number;
|
|
6
18
|
};
|
|
7
19
|
export type ToolBodySyntaxHighlight = {
|
|
8
20
|
language: SyntaxHighlightLanguage;
|
package/dist/syntax-highlight.js
CHANGED
|
@@ -112,12 +112,32 @@ export function syntaxHighlightLanguageForMarkdownFence(info) {
|
|
|
112
112
|
return MARKDOWN_FENCE_LANGUAGES[token];
|
|
113
113
|
}
|
|
114
114
|
export function syntaxHighlightSegmentsForLine(text, highlight, colors) {
|
|
115
|
+
if (highlight.context)
|
|
116
|
+
return contextualSyntaxSegments(highlight.language, highlight.context, colors);
|
|
115
117
|
const start = Math.max(0, Math.min(text.length, highlight.start));
|
|
116
118
|
if (start >= text.length)
|
|
117
119
|
return [];
|
|
118
120
|
const segments = localSyntaxSegments(text.slice(start), highlight.language, colors);
|
|
119
121
|
return segments.map((segment) => ({ ...segment, start: segment.start + start, end: segment.end + start }));
|
|
120
122
|
}
|
|
123
|
+
function contextualSyntaxSegments(language, context, colors) {
|
|
124
|
+
const syntaxStart = Math.max(0, Math.min(context.text.length, context.syntaxStart));
|
|
125
|
+
const rangeStart = Math.max(0, Math.min(context.text.length, context.rangeStart));
|
|
126
|
+
const rangeEnd = Math.max(rangeStart, Math.min(context.text.length, context.rangeEnd));
|
|
127
|
+
const segments = localSyntaxSegments(context.text.slice(syntaxStart), language, colors)
|
|
128
|
+
.map((segment) => ({ ...segment, start: segment.start + syntaxStart, end: segment.end + syntaxStart }));
|
|
129
|
+
return segments.flatMap((segment) => {
|
|
130
|
+
const start = Math.max(segment.start, rangeStart);
|
|
131
|
+
const end = Math.min(segment.end, rangeEnd);
|
|
132
|
+
if (end <= start)
|
|
133
|
+
return [];
|
|
134
|
+
return [{
|
|
135
|
+
...segment,
|
|
136
|
+
start: context.renderStart + start - rangeStart,
|
|
137
|
+
end: context.renderStart + end - rangeStart,
|
|
138
|
+
}];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
121
141
|
function localSyntaxSegments(code, language, colors) {
|
|
122
142
|
switch (language) {
|
|
123
143
|
case "css":
|
|
@@ -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)
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
285
|
+
if (!selected || isNewerAnchor(anchor, selected.anchor)) {
|
|
286
|
+
selected = { anchor, index };
|
|
287
|
+
}
|
|
268
288
|
}
|
|
269
289
|
|
|
270
|
-
state
|
|
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