143 lines
5.2 KiB
TypeScript
143 lines
5.2 KiB
TypeScript
/**
|
|
* Helpers that shape provisioning progress payloads before they are emitted
|
|
* to the renderer over IPC.
|
|
*
|
|
* Rationale: the renderer only renders a small "tail" preview of CLI logs
|
|
* and assistant output in ProvisioningProgressBlock / CliLogsRichView. Sending
|
|
* the full accumulated history on every throttled progress tick (≈ every
|
|
* second under load) serialized a multi-megabyte string over IPC and forced
|
|
* Zustand to produce a new immutable state object — which triggered renderer
|
|
* V8 OOM crashes for users with long-running teams. These helpers keep the
|
|
* hot emission path bounded while leaving the full history in-process for
|
|
* diagnostics and completion-time reports.
|
|
*/
|
|
|
|
import type { TeamLaunchDiagnosticItem } from '@shared/types';
|
|
|
|
export const PROGRESS_LOG_TAIL_LINES = 200;
|
|
export const PROGRESS_OUTPUT_TAIL_PARTS = 20;
|
|
export const PROGRESS_TRACE_TAIL_LINES = 120;
|
|
export const PROGRESS_LAUNCH_DIAGNOSTICS_LIMIT = 20;
|
|
const PROGRESS_LAUNCH_DIAGNOSTIC_TEXT_LIMIT = 500;
|
|
const PROGRESS_TRACE_TEXT_LIMIT = 800;
|
|
const PROVIDER_API_KEY_FLAG_PATTERN =
|
|
/(--(?:openai|codex|anthropic)[-_]api[-_]key(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi;
|
|
const SECRET_FLAG_PATTERN =
|
|
/(--(?:api[-_]key|token|password|secret|authorization|auth[-_]token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi;
|
|
const SECRET_ENV_ASSIGNMENT_PATTERN =
|
|
/\b([A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD|AUTHORIZATION)[A-Z0-9_]*\s*=\s*)("[^"]*"|'[^']*'|\S+)/gi;
|
|
const AUTH_HEADER_PATTERN = /\b(Authorization\s*:\s*)(Bearer\s+)?("[^"]*"|'[^']*'|\S+)/gi;
|
|
|
|
/**
|
|
* Return the trailing `maxLines` of a line-buffered CLI log, joined with "\n"
|
|
* and trimmed. Returns `undefined` when the tail is empty so callers can
|
|
* skip emitting a noop update.
|
|
*/
|
|
export function buildProgressLogsTail(
|
|
lines: readonly string[],
|
|
maxLines: number = PROGRESS_LOG_TAIL_LINES
|
|
): string | undefined {
|
|
if (lines.length === 0) {
|
|
return undefined;
|
|
}
|
|
const effectiveMax = Math.max(1, maxLines);
|
|
const tail = lines.length > effectiveMax ? lines.slice(-effectiveMax) : lines;
|
|
const joined = tail.join('\n').trim();
|
|
return joined.length === 0 ? undefined : joined;
|
|
}
|
|
|
|
/**
|
|
* Return the trailing `maxParts` of assistant output parts joined with a
|
|
* blank line, matching the renderer's rendering contract. Returns `undefined`
|
|
* when no parts are available.
|
|
*/
|
|
export function buildProgressAssistantOutput(
|
|
parts: readonly string[],
|
|
maxParts: number = PROGRESS_OUTPUT_TAIL_PARTS
|
|
): string | undefined {
|
|
if (parts.length === 0) {
|
|
return undefined;
|
|
}
|
|
const effectiveMax = Math.max(1, maxParts);
|
|
const tail = parts.length > effectiveMax ? parts.slice(-effectiveMax) : parts;
|
|
const joined = tail.join('\n\n');
|
|
return joined.trim().length === 0 ? undefined : joined;
|
|
}
|
|
|
|
function boundRedactedText(
|
|
value: string | undefined,
|
|
limit: number,
|
|
whitespace: 'collapse' | 'preserve'
|
|
): string | undefined {
|
|
const prepared = whitespace === 'collapse' ? value?.replace(/\s+/g, ' ').trim() : value?.trim();
|
|
if (!prepared) {
|
|
return undefined;
|
|
}
|
|
const redacted = prepared
|
|
.replace(PROVIDER_API_KEY_FLAG_PATTERN, '$1[redacted]')
|
|
.replace(SECRET_FLAG_PATTERN, '$1[redacted]')
|
|
.replace(SECRET_ENV_ASSIGNMENT_PATTERN, '$1[redacted]')
|
|
.replace(AUTH_HEADER_PATTERN, '$1$2[redacted]')
|
|
.replace(/```/g, "'''");
|
|
return redacted.length > limit ? `${redacted.slice(0, limit - 3).trimEnd()}...` : redacted;
|
|
}
|
|
|
|
function boundDiagnosticText(value: string | undefined): string | undefined {
|
|
return boundRedactedText(value, PROGRESS_LAUNCH_DIAGNOSTIC_TEXT_LIMIT, 'collapse');
|
|
}
|
|
|
|
export function buildProgressTraceLine(input: {
|
|
timestamp: string;
|
|
state: string;
|
|
message: string;
|
|
detail?: string;
|
|
}): string {
|
|
const message = boundRedactedText(input.message, PROGRESS_TRACE_TEXT_LIMIT, 'collapse') ?? '';
|
|
const detail = boundRedactedText(input.detail, PROGRESS_TRACE_TEXT_LIMIT, 'collapse');
|
|
return detail
|
|
? `${input.timestamp} [${input.state}] ${message} - ${detail}`
|
|
: `${input.timestamp} [${input.state}] ${message}`;
|
|
}
|
|
|
|
export function buildProgressTraceTail(
|
|
lines: readonly string[],
|
|
maxLines: number = PROGRESS_TRACE_TAIL_LINES
|
|
): string | undefined {
|
|
return buildProgressLogsTail(lines, maxLines);
|
|
}
|
|
|
|
export function buildProgressLiveOutput(
|
|
traceLines: readonly string[],
|
|
assistantParts: readonly string[],
|
|
options?: {
|
|
maxTraceLines?: number;
|
|
maxAssistantParts?: number;
|
|
}
|
|
): string | undefined {
|
|
const trace = buildProgressTraceTail(traceLines, options?.maxTraceLines);
|
|
const assistant = buildProgressAssistantOutput(assistantParts, options?.maxAssistantParts);
|
|
if (!trace) {
|
|
return assistant;
|
|
}
|
|
const traceBlock = `**Launch trace**\n\n\`\`\`text\n${trace}\n\`\`\``;
|
|
if (!assistant) {
|
|
return traceBlock;
|
|
}
|
|
return `${traceBlock}\n\n**Runtime output**\n\n${assistant}`;
|
|
}
|
|
|
|
export function boundLaunchDiagnostics(
|
|
items: readonly TeamLaunchDiagnosticItem[] | undefined,
|
|
maxItems: number = PROGRESS_LAUNCH_DIAGNOSTICS_LIMIT
|
|
): TeamLaunchDiagnosticItem[] | undefined {
|
|
if (!items || items.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const bounded = items.slice(0, Math.max(1, maxItems)).map((item) => ({
|
|
...item,
|
|
label: boundDiagnosticText(item.label) ?? item.code,
|
|
detail: boundDiagnosticText(item.detail),
|
|
}));
|
|
return bounded.length > 0 ? bounded : undefined;
|
|
}
|