openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
544 lines (543 loc) • 22.2 kB
JavaScript
import { r as readCompletedFileMutationDelta, t as resolveFileMutationToolName } from "./tool-mutation-names-DC4ihPXo.js";
import { C as resolveChannelStreamingPreviewToolProgress, E as resolveChannelStreamingSuppressDefaultToolProgressMessages, _ as resolveChannelProgressDraftMaxLines, c as isChannelProgressAttentionLine, d as mergeChannelProgressDraftLine, g as resolveChannelProgressDraftMaxLineChars, l as isChannelProgressDraftWorkToolName, n as buildChannelProgressDraftLineForEntry, o as formatChannelProgressDraftText, p as normalizeChannelProgressDraftLineIdentity, r as createChannelProgressDraftGate, w as resolveChannelStreamingProgressCommentary } from "./streaming-B_uLiLJZ.js";
import { a as sanitizeProgressStatusText, i as normalizeReasoningProgressLine, n as mergeReasoningProgressText, r as normalizeCommentaryProgressText, t as formatReasoningProgressDisplayLine } from "./progress-draft-status-text-DAFR4vkg.js";
import { t as settleProgressVisibilityCallbackResult } from "./progress-visibility-DVUJibF4.js";
//#region src/channels/progress-draft-diffstat.ts
const MAX_TRACKED_MUTATION_FILES = 256;
const MAX_PENDING_MUTATION_DIFFS = 64;
function createProgressDraftDiffStatTracker(params) {
let hasCommittedDiff = false;
let mutationFiles = /* @__PURE__ */ new Set();
let mutationOverflowFiles = 0;
let mutationAdded = 0;
let mutationRemoved = 0;
let pendingMutationDiffs = /* @__PURE__ */ new Map();
const reset = () => {
hasCommittedDiff = false;
mutationFiles = /* @__PURE__ */ new Set();
mutationOverflowFiles = 0;
mutationAdded = 0;
mutationRemoved = 0;
pendingMutationDiffs = /* @__PURE__ */ new Map();
};
const stageToolEvent = (payload) => {
if (!params.canStage()) return;
const toolCallId = payload.toolCallId?.trim();
if (payload.phase !== "start" || !toolCallId || !payload.name || !payload.args) return;
const kind = resolveFileMutationToolName(payload.name);
const delta = kind ? readCompletedFileMutationDelta(kind, payload.args) : void 0;
if (!delta) return;
if (!pendingMutationDiffs.has(toolCallId) && pendingMutationDiffs.size >= MAX_PENDING_MUTATION_DIFFS) return;
pendingMutationDiffs.set(toolCallId, delta);
};
const commitItemEvent = (payload) => {
const toolCallId = payload.toolCallId?.trim();
if (!toolCallId || payload.phase !== "end") return;
const delta = pendingMutationDiffs.get(toolCallId);
if (!delta) return;
pendingMutationDiffs.delete(toolCallId);
const status = payload.status?.trim().toLowerCase();
if (status === "failed" || status === "error") return;
hasCommittedDiff = true;
mutationAdded += delta.added;
mutationRemoved += delta.removed;
for (const file of delta.files) {
if (mutationFiles.has(file)) continue;
if (mutationFiles.size < MAX_TRACKED_MUTATION_FILES) {
mutationFiles.add(file);
continue;
}
mutationOverflowFiles += 1;
}
};
const resolve = () => hasCommittedDiff ? {
files: mutationFiles.size + mutationOverflowFiles,
added: mutationAdded,
removed: mutationRemoved
} : void 0;
return {
stageToolEvent,
commitItemEvent,
resolve,
reset
};
}
//#endregion
//#region src/channels/progress-draft-events.ts
function createChannelProgressDraftEventHandlers(params) {
const pushEvent = (input, detailMode) => {
const lineOptions = detailMode ? { detailMode } : void 0;
const line = params.buildLine ? params.buildLine(input, lineOptions) : buildChannelProgressDraftLineForEntry(params.entry, input, lineOptions);
return params.pushLine(line, input.event === "tool" ? { toolName: input.name?.trim() } : {});
};
return {
pushToolEvent: (payload) => {
const { detailMode, ...input } = payload;
params.onTool?.(payload);
return pushEvent({
event: "tool",
...input
}, detailMode);
},
pushItemEvent: (payload) => {
const { kind: itemKind, ...input } = payload;
params.onItem?.(payload);
return pushEvent({
event: "item",
...input,
itemKind
});
},
pushApprovalEvent: (payload) => {
return payload.phase === "requested" ? pushEvent({
event: "approval",
...payload
}) : Promise.resolve(false);
},
pushCommandOutputEvent: (payload) => {
return payload.phase === "end" ? pushEvent({
event: "command-output",
...payload
}) : Promise.resolve(false);
},
pushPatchEvent: (payload) => {
return payload.phase === "end" ? pushEvent({
event: "patch",
...payload
}) : Promise.resolve(false);
}
};
}
//#endregion
//#region src/channels/progress-draft-lines.ts
/**
* Removes a keyed structured progress line while preserving plain text draft lines.
* Returns the original array when no line is removed so renderers can use identity as a no-op signal.
*/
function removeChannelProgressDraftLine(lines, id) {
const lineId = id.trim();
if (!lineId) return lines;
const next = lines.filter((line) => typeof line !== "object" || line.id?.trim() !== lineId);
return next.length === lines.length ? lines : next;
}
//#endregion
//#region src/channels/progress-work-counter.ts
/**
* Per-turn work counters for live channel progress surfaces. These describe the
* turn while it runs; nothing here survives into the finished transcript.
*/
function createChannelProgressWorkCounter(params) {
const now = params?.now ?? Date.now;
let startedAt = now();
let toolCalls = 0;
return {
noteToolCall(toolName) {
if (isChannelProgressDraftWorkToolName(toolName)) toolCalls += 1;
},
reset() {
startedAt = now();
toolCalls = 0;
},
get toolCalls() {
return toolCalls;
},
get elapsedSeconds() {
return Math.max(1, Math.round((now() - startedAt) / 1e3));
}
};
}
//#endregion
//#region src/channels/progress-draft-compositor.ts
const PROGRESS_STATUS_PREAMBLE_FRESH_MS = 2e4;
/** Creates a stateful compositor for one streaming channel reply. */
function createChannelProgressDraftCompositor(params) {
const now = params.now ?? Date.now;
const setTimeoutFn = params.setTimeoutFn ?? setTimeout;
const clearTimeoutFn = params.clearTimeoutFn ?? clearTimeout;
const reasoningLinePrefix = params.reasoningLinePrefix ?? "";
const commentaryLinePrefix = params.commentaryLinePrefix ?? "";
const commentaryItalics = params.commentaryItalics ?? true;
const stripLaneItalics = (text) => text.split("\n").map((line) => line.replace(/^_(.*)_$/su, "$1")).join("\n");
const previewToolProgressEnabled = params.active && resolveChannelStreamingPreviewToolProgress(params.entry, params.mode !== "progress", params.mode);
const quietProgress = params.presentation === "summary" || !previewToolProgressEnabled;
const commentaryProgressEnabled = params.active && resolveChannelStreamingProgressCommentary(params.entry, false, params.mode);
const thinkingProgressEnabled = params.active && (params.reasoningGate ?? true);
const suppressDefaultToolProgressMessages = params.active && resolveChannelStreamingSuppressDefaultToolProgressMessages(params.entry, {
draftStreamActive: true,
mode: params.mode,
previewToolProgressEnabled
});
let progressSuppressed = false;
let lines = [];
let lastRenderedText = "";
let lastRenderedLines = lines;
let lastRenderedDiffStatKey = "";
let reasoningRawText = "";
let lastReasoningLine;
let lastIdLessCommentaryId;
let lastIdLessCommentaryBare = "";
let preambleText = "";
let preambleItemId;
let preambleAt;
let narrationText = "";
let planSteps;
let planExplanation = "";
let finalReplyStarted = false;
let finalReplyDelivered = false;
const diffStatTracker = createProgressDraftDiffStatTracker({ canStage: () => params.active && params.mode === "progress" && !progressSuppressed && !finalReplyStarted && !finalReplyDelivered });
let preambleExpiryTimer;
let lastStartRendered = false;
const mergeReasoningProgress = (text, options) => {
if (!text) return "";
reasoningRawText = mergeReasoningProgressText(reasoningRawText, text, { snapshot: options?.snapshot === true });
return normalizeReasoningProgressLine(reasoningRawText);
};
const clearPreambleExpiryTimer = () => {
if (preambleExpiryTimer !== void 0) {
clearTimeoutFn(preambleExpiryTimer);
preambleExpiryTimer = void 0;
}
};
const resolveStatusText = () => {
const preambleIsFresh = preambleAt !== void 0 && now() - preambleAt < 2e4;
const effectiveNarration = narrationText || planExplanation;
return preambleText && (preambleIsFresh || !effectiveNarration) ? preambleText : effectiveNarration;
};
const formatDraftText = (draftLines = lines, options) => {
const narration = resolveStatusText() || void 0;
const linesRenderedByChannel = params.rendersRollingLinesNatively === true && Boolean(narration || planSteps?.length);
return formatChannelProgressDraftText({
presentation: params.presentation,
entry: params.entry,
lines: linesRenderedByChannel ? [] : draftLines,
seed: params.seed,
formatLine: options?.formatted === false ? void 0 : params.formatLine,
narration,
plan: planSteps
});
};
const resolveDiffStat = diffStatTracker.resolve;
const getSnapshot = () => {
const statusHeadline = resolveStatusText();
const diffStat = resolveDiffStat();
return {
lines: lines.map((line) => typeof line === "string" ? line : { ...line }),
...statusHeadline ? { statusHeadline } : {},
...planSteps ? { plan: planSteps.map((entry) => ({ ...entry })) } : {},
...planExplanation ? { planExplanation } : {},
...diffStat ? { diffStat } : {}
};
};
const clearProgressState = (suppressed) => {
clearPreambleExpiryTimer();
progressSuppressed = suppressed;
lines = [];
lastRenderedText = "";
lastRenderedLines = lines;
lastRenderedDiffStatKey = "";
reasoningRawText = "";
lastReasoningLine = void 0;
lastIdLessCommentaryId = void 0;
lastIdLessCommentaryBare = "";
preambleText = "";
preambleItemId = void 0;
preambleAt = void 0;
narrationText = "";
planSteps = void 0;
planExplanation = "";
diffStatTracker.reset();
lastStartRendered = false;
};
const publish = async (options) => {
const text = formatDraftText();
const diffStatKey = JSON.stringify(resolveDiffStat() ?? null);
const structuredStateChanged = params.updateOnLineChange === true && (lines !== lastRenderedLines || diffStatKey !== lastRenderedDiffStatKey);
if (!text || text === lastRenderedText && !structuredStateChanged) return false;
if (!(await settleProgressVisibilityCallbackResult(params.update(text, {
...options,
lines: [...lines]
}))).visible) return false;
lastRenderedText = text;
lastRenderedLines = lines;
lastRenderedDiffStatKey = diffStatKey;
return true;
};
const render = async (options) => {
if (!params.active || params.mode !== "progress" || finalReplyStarted || finalReplyDelivered) return false;
return await publish(options);
};
const schedulePreambleExpiryRefresh = () => {
clearPreambleExpiryTimer();
if (!preambleText || !narrationText || preambleAt === void 0 || !gate.hasStarted || finalReplyStarted || finalReplyDelivered) return;
const remaining = PROGRESS_STATUS_PREAMBLE_FRESH_MS - (now() - preambleAt);
if (remaining <= 0) return;
preambleExpiryTimer = setTimeoutFn(() => {
preambleExpiryTimer = void 0;
render().catch((err) => {
console.warn(`[progress-draft] channel progress status refresh failed: ${String(err)}`);
});
}, remaining);
};
const gate = createChannelProgressDraftGate({
onStart: async () => {
lastStartRendered = await render({ flush: true });
schedulePreambleExpiryRefresh();
},
setTimeoutFn,
clearTimeoutFn
});
const startAndRender = async (options) => {
const alreadyStarted = gate.hasStarted;
if (!alreadyStarted) lastStartRendered = false;
await gate.startNow();
if (!gate.hasStarted) return false;
return alreadyStarted ? await render(options) : lastStartRendered;
};
const renderAfterRetraction = async () => {
if (!gate.hasStarted) return false;
const rendered = await render();
if (rendered || formatDraftText()) return rendered;
lastRenderedText = "";
await params.deleteCurrent?.();
return true;
};
/**
* Commentary line identity. An explicit item id owns its line. Without one,
* providers stream cumulative snapshots ("Checking" → "Checking the
* workspace"), so a snapshot that continues the open line reuses its id and
* updates in place; anything else starts a new line.
*/
const resolveCommentaryLineId = (commentary) => {
if (commentary.itemId) return `commentary:${commentary.itemId}`;
if (!commentary.normalized) return "";
if (Boolean(lastIdLessCommentaryBare) && (commentary.bareNormalized.startsWith(lastIdLessCommentaryBare) || lastIdLessCommentaryBare.startsWith(commentary.bareNormalized)) && lastIdLessCommentaryId) return lastIdLessCommentaryId;
return `commentary:${commentary.normalized}`;
};
const clearLine = async (lineId) => {
const nextLines = removeChannelProgressDraftLine(lines, lineId);
if (nextLines === lines) return false;
lines = nextLines;
if (!gate.hasStarted) return false;
if (formatDraftText()) return await render();
lastRenderedText = "";
await params.deleteCurrent?.();
return true;
};
const noteProgress = async (line, options) => {
if (!params.active || finalReplyStarted || finalReplyDelivered) return false;
if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return false;
if (params.isEmptyLine?.(line)) return false;
const normalized = normalizeChannelProgressDraftLineIdentity(line);
if (!normalized || progressSuppressed) return false;
if (params.mode !== "progress" && !previewToolProgressEnabled) return false;
const progressLine = typeof line === "object" && line !== void 0 ? line : normalized;
const needsAttention = isChannelProgressAttentionLine(progressLine);
const shouldStoreLine = !quietProgress || needsAttention;
const nextLines = shouldStoreLine ? mergeChannelProgressDraftLine(lines, progressLine, { maxLines: resolveChannelProgressDraftMaxLines(params.entry) }) : typeof line === "object" && line.id ? removeChannelProgressDraftLine(lines, line.id) : lines;
const lineChanged = nextLines !== lines;
const hasUnconfirmedRender = formatDraftText(nextLines) !== lastRenderedText;
const diffStatChanged = params.updateOnLineChange === true && JSON.stringify(resolveDiffStat() ?? null) !== lastRenderedDiffStatKey;
if (shouldStoreLine && !lineChanged && !hasUnconfirmedRender && !diffStatChanged) return false;
if (quietProgress || shouldStoreLine && lineChanged) {
reasoningRawText = "";
lastReasoningLine = void 0;
}
if (shouldStoreLine && params.tryNativeUpdate) {
const text = formatDraftText(nextLines, { formatted: false });
if (text && await params.tryNativeUpdate(text)) {
lines = nextLines;
lastRenderedText = text;
lastRenderedLines = lines;
return true;
}
}
lines = nextLines;
if (params.mode !== "progress") return shouldStoreLine ? await publish() : false;
if (options?.startImmediately || params.shouldStartNow?.(line) || needsAttention) {
const flush = options?.flush === true || needsAttention;
return await startAndRender(flush ? { flush: true } : void 0);
}
const alreadyStarted = gate.hasStarted;
const progressActive = await gate.noteWork();
if ((alreadyStarted || progressActive) && gate.hasStarted) return await render();
return false;
};
const progressEventHandlers = createChannelProgressDraftEventHandlers({
entry: params.entry,
pushLine: noteProgress,
onTool: diffStatTracker.stageToolEvent,
onItem: diffStatTracker.commitItemEvent,
...params.buildProgressEventLine ? { buildLine: params.buildProgressEventLine } : {}
});
return {
get previewToolProgressEnabled() {
return previewToolProgressEnabled;
},
get commentaryProgressEnabled() {
return commentaryProgressEnabled;
},
get suppressDefaultToolProgressMessages() {
return suppressDefaultToolProgressMessages;
},
get hasStarted() {
return gate.hasStarted;
},
get isVisible() {
return Boolean(lastRenderedText) && !finalReplyStarted && !finalReplyDelivered;
},
get hasStatusHeadline() {
return Boolean(resolveStatusText());
},
get hasPlanProgress() {
return Boolean(planSteps?.length);
},
getSnapshot,
markFinalReplyStarted() {
finalReplyStarted = true;
gate.cancel();
clearPreambleExpiryTimer();
},
markFinalReplyDelivered() {
finalReplyDelivered = true;
clearPreambleExpiryTimer();
},
beginNewTurn(options) {
if (options?.force !== true && !finalReplyStarted && !finalReplyDelivered) return false;
finalReplyStarted = false;
finalReplyDelivered = false;
gate.reset();
clearProgressState(false);
return true;
},
reset() {
clearProgressState(false);
},
resetReasoningProgress() {
reasoningRawText = "";
},
mergeReasoningProgress,
suppress() {
clearProgressState(true);
},
cancel() {
gate.cancel();
clearPreambleExpiryTimer();
},
start() {
return gate.startNow();
},
async noteActivity(options) {
if (!params.active || params.mode !== "progress" || progressSuppressed || finalReplyStarted || finalReplyDelivered) return false;
if (options?.startImmediately) return await startAndRender({ flush: true });
const alreadyStarted = gate.hasStarted;
const progressActive = await gate.noteWork();
if ((alreadyStarted || progressActive) && gate.hasStarted) return await render();
return false;
},
pushToolProgress: noteProgress,
...progressEventHandlers,
async pushApprovalEvent(payload) {
if (payload.phase === "resolved" && payload.approvalId) return await clearLine(`approval:${payload.approvalId}`);
return await progressEventHandlers.pushApprovalEvent(payload);
},
async pushPlanProgress(steps, options) {
if (!params.active || params.mode !== "progress" || progressSuppressed || finalReplyStarted || finalReplyDelivered) return false;
planSteps = steps && steps.length > 0 ? steps.map((entry) => ({ ...entry })) : void 0;
planExplanation = options?.explanation?.replace(/\s+/g, " ").trim() ?? "";
if (!planSteps && !planExplanation) return await renderAfterRetraction();
return await startAndRender();
},
async pushPreambleHeadline(text, options) {
if (!params.active || params.mode !== "progress" || progressSuppressed) return false;
if (commentaryProgressEnabled) return false;
if (finalReplyStarted || finalReplyDelivered) return false;
const itemId = options?.itemId?.trim() || void 0;
const normalized = sanitizeProgressStatusText(text ?? "").replace(/\s+/g, " ").trim();
if (!normalized) {
if (!itemId || itemId !== preambleItemId) return false;
preambleText = "";
preambleItemId = void 0;
preambleAt = void 0;
clearPreambleExpiryTimer();
return await renderAfterRetraction();
}
const isNewPreambleItem = Boolean(itemId && itemId !== preambleItemId);
if (isNewPreambleItem) preambleItemId = itemId;
else if (!itemId) preambleItemId = void 0;
if (normalized === preambleText && !isNewPreambleItem) return false;
preambleText = normalized;
preambleAt = now();
schedulePreambleExpiryRefresh();
return gate.hasStarted ? await render() : false;
},
async pushNarrationProgress(text) {
if (!params.active || params.mode !== "progress" || progressSuppressed) return false;
if (finalReplyStarted || finalReplyDelivered) return false;
const normalized = text?.replace(/\s+/g, " ").trim() ?? "";
if (normalized === narrationText) return false;
if (!normalized) {
narrationText = "";
clearPreambleExpiryTimer();
return await render();
}
narrationText = normalized;
schedulePreambleExpiryRefresh();
return gate.hasStarted ? await render() : false;
},
async pushReasoningProgress(text, options) {
if (!params.active || params.mode !== "progress" || !text || progressSuppressed || finalReplyStarted || finalReplyDelivered || !thinkingProgressEnabled) return false;
const normalized = mergeReasoningProgress(text, options);
if (!normalized) return false;
const compactLine = formatReasoningProgressDisplayLine(normalized, resolveChannelProgressDraftMaxLineChars(params.entry));
if (!compactLine) return false;
const displayLine = `${reasoningLinePrefix}${compactLine}`;
const priorIndex = lastReasoningLine === void 0 ? -1 : lines.lastIndexOf(lastReasoningLine);
if (params.presentation === "summary") lines = mergeChannelProgressDraftLine(lines, {
id: "reasoning",
kind: "item",
text: stripLaneItalics(compactLine),
label: "Reasoning",
prefix: false
}, { maxLines: resolveChannelProgressDraftMaxLines(params.entry) });
else if (priorIndex >= 0) {
lines = [...lines];
lines[priorIndex] = displayLine;
} else lines = mergeChannelProgressDraftLine(lines, displayLine, { maxLines: resolveChannelProgressDraftMaxLines(params.entry) });
lastReasoningLine = displayLine;
if (await gate.noteWork() && gate.hasStarted) return await render();
return false;
},
async pushCommentaryProgress(text, options) {
if (!params.active || params.mode !== "progress" || !commentaryProgressEnabled) return false;
if (finalReplyStarted || finalReplyDelivered) return false;
const itemId = options?.itemId?.trim();
if (!text && !itemId) return false;
const normalized = normalizeCommentaryProgressText(text ?? "");
const bareNormalized = stripLaneItalics(normalized);
const lineId = resolveCommentaryLineId({
itemId,
normalized,
bareNormalized
});
if (!normalized) {
if (lineId) await clearLine(lineId);
return false;
}
const line = {
id: lineId,
text: `${commentaryLinePrefix}${commentaryItalics ? normalized : bareNormalized}`,
kind: "item",
label: "Commentary",
prefix: false
};
lines = mergeChannelProgressDraftLine(lines, line, { maxLines: resolveChannelProgressDraftMaxLines(params.entry) });
if (!itemId) {
lastIdLessCommentaryId = lineId;
lastIdLessCommentaryBare = bareNormalized;
}
return await startAndRender();
}
};
}
//#endregion
export { createChannelProgressDraftCompositor as n, createChannelProgressWorkCounter as r, PROGRESS_STATUS_PREAMBLE_FRESH_MS as t };