Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions app/L0/_all/mod/_core/spaces/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -596,11 +596,23 @@ function buildWidgetMetadataLines(widgetRecord) {

function getWidgetRendererReadLines(widgetRecord) {
const normalizedWidget = normalizeWidgetRecord(widgetRecord, widgetRecord);
return dedentMultilineText(normalizedWidget.rendererSource)
return getRendererSourceReadLines(normalizedWidget.rendererSource);
}

function getRendererSourceReadLines(rendererSource) {
return dedentMultilineText(typeof rendererSource === "string" ? rendererSource : "")
.replace(/\r\n?/gu, "\n")
.split("\n");
}

function countRendererSourceReadLines(rendererSource) {
if (typeof rendererSource !== "string" || !rendererSource.length) {
return 0;
}

return getRendererSourceReadLines(rendererSource).length;
}

function formatWidgetRecordForRead(widgetRecord) {
const rendererLines = getWidgetRendererReadLines(widgetRecord);
return [
Expand Down Expand Up @@ -956,15 +968,29 @@ function applyPatchedWidgetAttributes(widgetRecord, options = {}) {
);
}

function buildWidgetWriteResult(spaceRecord, widgetId) {
function buildWidgetWriteResult(spaceRecord, widgetId, priorRendererSource = null) {
const widgetRecord = spaceRecord?.widgets?.[widgetId];

return {
const result = {
space: spaceRecord,
widgetId,
widgetPath: buildSpaceWidgetFilePath(spaceRecord.id, widgetId),
widgetText: widgetRecord ? formatWidgetRecordForRead(widgetRecord) : ""
};

// Expose line counts (not full source strings) so consumers can report
// change magnitude in tool result status without parsing renderer text.
// Both counts use the same dedent + LF-normalize + split path that
// formatWidgetRecordForRead emits, so the numbers always match what the
// agent counts in the numbered renderer readback.
if (typeof priorRendererSource === "string") {
result.priorRendererLineCount = countRendererSourceReadLines(priorRendererSource);
}

if (typeof widgetRecord?.rendererSource === "string") {
result.nextRendererLineCount = countRendererSourceReadLines(widgetRecord.rendererSource);
}

return result;
}

function buildWidgetWriteResults(spaceRecord, widgetIds = []) {
Expand Down Expand Up @@ -2056,6 +2082,7 @@ export async function upsertWidget(options = {}) {
const currentSpace = cloneSpaceRecord(await readSpace(spaceId));
const widgetFallbackId = normalizeWidgetId(options.widgetId || options.id || options.name || options.title || "widget");
const existingWidget = currentSpace.widgets[widgetFallbackId] || null;
const priorRendererSource = typeof existingWidget?.rendererSource === "string" ? existingWidget.rendererSource : null;
const widgetRecord = validateWidgetRendererSourceForWrite(
previewWidgetRecord(options, {
...existingWidget,
Expand Down Expand Up @@ -2103,7 +2130,7 @@ export async function upsertWidget(options = {}) {
await runtime.api.fileWrite({ files });
clearRecentListedSpaceRecords();

return buildWidgetWriteResult(nextSpace, widgetId);
return buildWidgetWriteResult(nextSpace, widgetId, priorRendererSource);
}

export async function upsertWidgets(options = {}) {
Expand Down Expand Up @@ -2219,6 +2246,7 @@ export async function patchWidget(options = {}) {
throw new Error(`Cannot patch widget "${widgetId}": widget not found in space "${spaceId}".`);
}

const priorRendererSource = typeof currentWidget.rendererSource === "string" ? currentWidget.rendererSource : null;
const patchedRendererSource = applyWidgetPatchEdits(currentWidget, options.edits ?? options.lineEdits);
const nextWidget = validateWidgetRendererSourceForWrite(
applyPatchedWidgetAttributes(
Expand Down Expand Up @@ -2251,7 +2279,7 @@ export async function patchWidget(options = {}) {
await runtime.api.fileWrite({ files });
clearRecentListedSpaceRecords();

return buildWidgetWriteResult(nextSpace, widgetId);
return buildWidgetWriteResult(nextSpace, widgetId, priorRendererSource);
}

export async function removeWidget(options = {}) {
Expand Down
48 changes: 45 additions & 3 deletions app/L0/_all/mod/_core/spaces/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -1017,21 +1017,57 @@ function getWidgetOperationStatusVerb(operationLabel) {
}
}

// Render the change magnitude for a widget write. The line counts come from
// `buildWidgetWriteResult` in spaces/storage.js, which uses the same
// dedent + LF-normalize + split path that emits the numbered renderer
// readback the agent sees in `widgetText`. So the count printed here always
// matches the highest line index the agent counts in the readback.
function formatWidgetOperationChangeMagnitude({ priorLineCount, nextLineCount } = {}) {
const hasPrior = Number.isFinite(priorLineCount);
const hasNext = Number.isFinite(nextLineCount);

if (!hasPrior && !hasNext) {
return "";
}

const before = hasPrior ? Math.max(0, priorLineCount) : 0;
const after = hasNext ? Math.max(0, nextLineCount) : 0;

if (!hasPrior) {
return `${after} renderer line${after === 1 ? "" : "s"}`;
}

if (!hasNext || !after) {
return `0 renderer lines (was ${before})`;
}

const delta = after - before;

if (delta === 0) {
return `${after} renderer lines`;
}

const sign = delta > 0 ? "+" : "-";
return `${after} renderer lines (was ${before}, ${sign}${Math.abs(delta)})`;
}

function formatWidgetOperationStatusText(widgetId, operationLabel, widgetRender, options = {}) {
const check = cloneWidgetRenderCheck(widgetRender, widgetId);
const verb = getWidgetOperationStatusVerb(operationLabel);
const targetLabel = widgetId ? `Widget "${widgetId}"` : "Widget";
const suffix = options.transientUpdated ? "loaded to TRANSIENT." : "done.";
const magnitude = formatWidgetOperationChangeMagnitude(options);
const magnitudeFragment = magnitude ? `, ${magnitude}` : "";

if (check.status === "error") {
return `${targetLabel} ${verb}, render failed, ${suffix}`;
return `${targetLabel} ${verb}${magnitudeFragment}, render failed, ${suffix}`;
}

if (check.status === "ok") {
return `${targetLabel} ${verb}, rendered ok, ${suffix}`;
return `${targetLabel} ${verb}${magnitudeFragment}, rendered ok, ${suffix}`;
}

return `${targetLabel} ${verb}, not live-tested, ${suffix}`;
return `${targetLabel} ${verb}${magnitudeFragment}, not live-tested, ${suffix}`;
}

function extractWidgetIdFromWidgetText(widgetText) {
Expand Down Expand Up @@ -1271,7 +1307,13 @@ async function buildWidgetToolResult(
widgetText
})
: false;
const priorRendererLineCount =
Number.isFinite(nextResult.priorRendererLineCount) ? nextResult.priorRendererLineCount : null;
const nextRendererLineCount =
Number.isFinite(nextResult.nextRendererLineCount) ? nextResult.nextRendererLineCount : null;
const widgetStatusText = formatWidgetOperationStatusText(normalizedWidgetId, operationLabel, widgetRender, {
nextLineCount: nextRendererLineCount,
priorLineCount: priorRendererLineCount,
transientUpdated
});

Expand Down