diff --git a/app/L0/_all/mod/_core/spaces/storage.js b/app/L0/_all/mod/_core/spaces/storage.js index 853f8b45..661ef25d 100644 --- a/app/L0/_all/mod/_core/spaces/storage.js +++ b/app/L0/_all/mod/_core/spaces/storage.js @@ -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 [ @@ -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 = []) { @@ -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, @@ -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 = {}) { @@ -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( @@ -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 = {}) { diff --git a/app/L0/_all/mod/_core/spaces/store.js b/app/L0/_all/mod/_core/spaces/store.js index 84d61cfe..a4954193 100644 --- a/app/L0/_all/mod/_core/spaces/store.js +++ b/app/L0/_all/mod/_core/spaces/store.js @@ -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) { @@ -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 });