`);
}
if (info) {
if (typeof info.recipeName === "string") {
details.push(html`
pattern:${info.recipeName}
`);
}
if (typeof info.moduleName === "string") {
details.push(html`
module:${info.moduleName}
`);
}
if (Array.isArray(info.reads) && info.reads.length > 0) {
details.push(html`
reads:${info.reads
.length} dependencies
`);
}
if (Array.isArray(info.writes) && info.writes.length > 0) {
details.push(html`
writes:${info.writes
.length} outputs
`);
}
}
if (eventData.error) {
details.push(html`
error:${eventData
.error}
`);
}
} else if (type === "cell.update") {
const change = (rest as Record).change;
if (isRecord(change)) {
if (isRecord(change.address)) {
if (change.address?.id) {
details.push(html`
cell:${change.address.id}
`);
}
if (change.address?.path) {
details.push(html`
path:${(change.address
.path as string[]).join(
"/",
)}
`);
}
if (change.address?.type) {
details.push(html`
type:${change.address.type}
`);
}
}
// Show a summary of the change
const hasBeforeAfter = change.before !== undefined ||
change.after !== undefined;
if (hasBeforeAfter) {
details.push(html`
`);
}
}
} else {
// Default rendering for other event types
for (const [key, value] of Object.entries(rest)) {
if (value !== undefined && value !== null) {
details.push(html`
${key}:${typeof value === "string"
? value
: typeof value === "boolean"
? value.toString()
: typeof value === "number"
? value.toString()
: this.safeJsonStringify(value, 100)}
`);
}
}
}
return details;
}
private formatValue(value: unknown): string {
if (value === null) return "null";
if (value === undefined) return "undefined";
if (typeof value === "string") return `"${value}"`;
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
// For objects/arrays, truncate JSON representation
const json = this.safeJsonStringify(value, 60);
return json;
}
/**
* Safely stringify a value with size limits to prevent context blowout.
* Truncates large strings, arrays, and objects.
*/
private safeJsonStringify(
value: unknown,
maxLength: number,
indent?: number,
): string {
const truncatedValue = this.truncateValue(value, 3); // Max depth 3
try {
const json = JSON.stringify(truncatedValue, null, indent);
if (json.length > maxLength) {
return json.slice(0, maxLength - 3) + "...";
}
return json;
} catch {
return "[Unable to serialize]";
}
}
/**
* Recursively truncate a value to prevent huge objects from being serialized.
* Replaces functions, large strings, large arrays, and deep objects with summaries.
*/
private truncateValue(value: unknown, maxDepth: number): unknown {
if (maxDepth <= 0) {
return "[...]";
}
if (value === null || value === undefined) {
return value;
}
if (typeof value === "function") {
// Serialize functions as objects with name + all enumerable properties
const fn = value as unknown as
& { name?: string }
& Record;
const result: Record = {
name: fn.name || "[anonymous]",
};
// Copy enumerable properties (actions often have metadata attached)
for (const key of Object.keys(fn)) {
result[key] = this.truncateValue(fn[key], maxDepth - 1);
}
return result;
}
if (typeof value === "string") {
if (value.length > 200) {
return value.slice(0, 197) + "...";
}
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return value;
}
if (Array.isArray(value)) {
if (value.length > 10) {
return [
...value.slice(0, 5).map((v) => this.truncateValue(v, maxDepth - 1)),
`[... ${value.length - 5} more items]`,
];
}
return value.map((v) => this.truncateValue(v, maxDepth - 1));
}
if (typeof value === "object") {
const obj = value as Record;
const keys = Object.keys(obj);
// Skip huge objects entirely (likely cell values or function metadata)
if (keys.length > 20) {
return `[Object with ${keys.length} keys]`;
}
const result: Record = {};
for (const key of keys) {
result[key] = this.truncateValue(obj[key], maxDepth - 1);
}
return result;
}
return String(value);
}
private getCellLabel(
watch: { label?: string; cellLink: { id: string } },
): string {
if (watch.label) return watch.label;
// Generate short ID from full ID
const id = watch.cellLink.id;
const shortId = id.split(":").pop()?.slice(-6) ?? "???";
return `#${shortId}`;
}
// ============================================================
// Logger stats methods
// ============================================================
private getLoggerRegistry(): Record {
const global = globalThis as unknown as {
commontools?: { logger?: Record };
};
return global.commontools?.logger ?? {};
}
private getLoggerBreakdown(): Record {
const global = globalThis as unknown as {
commontools?: {
getLoggerCountsBreakdown?: () => Record<
string,
LoggerBreakdown | number
>;
};
};
return global.commontools?.getLoggerCountsBreakdown?.() ?? { total: 0 };
}
private getBreakdownTotal(
breakdown: Record | null,
): number {
if (!breakdown) return 0;
const total = breakdown.total;
return typeof total === "number" ? total : 0;
}
private sampleLoggerCounts(): void {
this.loggerSample = this.getLoggerBreakdown();
}
private resetBaseline(): void {
this.loggerBaseline = this.getLoggerBreakdown();
this.sampleLoggerCounts();
}
private toggleLogger(name: string): void {
const registry = this.getLoggerRegistry();
const logger = registry[name];
if (logger) {
logger.disabled = !logger.disabled;
this.requestUpdate();
}
}
private setLoggerLevel(
name: string,
level: "debug" | "info" | "warn" | "error",
): void {
const registry = this.getLoggerRegistry();
const logger = registry[name];
if (logger) {
logger.level = level;
this.requestUpdate();
}
}
private toggleExpandLogger(name: string): void {
if (this.expandedLoggers.has(name)) {
this.expandedLoggers.delete(name);
} else {
this.expandedLoggers.add(name);
}
this.requestUpdate();
}
private getDelta(current: number, baseline: number | undefined): number {
return current - (baseline ?? 0);
}
private formatDelta(delta: number): string {
if (delta === 0) return "0";
return delta > 0 ? `+${delta}` : `${delta}`;
}
private renderLoggers(): TemplateResult {
const sample = this.loggerSample ?? this.getLoggerBreakdown();
const baseline = this.loggerBaseline;
const registry = this.getLoggerRegistry();
const loggerNames = Object.keys(sample).filter((k) => k !== "total");
if (loggerNames.length === 0) {
return html`