Files
paperclip/packages/adapter-utils/src/log-redaction.ts
T
HenkDz 47f3cdc1bb fix(ui): external adapter selection, config field placement, and transcript parser freshness
- Fix external adapters (hermes, droid) not auto-selected when
  navigating with ?adapterType= param — was using a stale
  module-level Set built before async adapter registration
- Move SchemaConfigFields to render after thinking effort (same
  visual area as Claude's chrome toggle) instead of bottom of
  config section
- Extract SelectField into its own component to fix React hooks
  order violation when schema fields change between renders
- Add onAdapterChange() subscription in registry.ts so
  registerUIAdapter() notifies components when dynamic parsers
  load, fixing stale parser for old runs
- Add parserTick to both RunTranscriptView and
  useLiveRunTranscripts to force recomputation on parser change
2026-04-03 21:11:22 +01:00

98 lines
3.1 KiB
TypeScript

import type { TranscriptEntry } from "./types.js";
export const REDACTED_HOME_PATH_USER = "*";
export interface HomePathRedactionOptions {
enabled?: boolean;
}
function maskHomePathUserSegment(value: string) {
const trimmed = value.trim();
if (!trimmed) return REDACTED_HOME_PATH_USER;
return `${trimmed[0]}${"*".repeat(Math.max(1, Array.from(trimmed).length - 1))}`;
}
const HOME_PATH_PATTERNS = [
{
regex: /\/Users\/([^/\\\s]+)/g,
replace: (_match: string, user: string) => `/Users/${maskHomePathUserSegment(user)}`,
},
{
regex: /\/home\/([^/\\\s]+)/g,
replace: (_match: string, user: string) => `/home/${maskHomePathUserSegment(user)}`,
},
{
regex: /([A-Za-z]:\\Users\\)([^\\/\s]+)/g,
replace: (_match: string, prefix: string, user: string) => `${prefix}${maskHomePathUserSegment(user)}`,
},
] as const;
function isPlainObject(value: unknown): value is Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
export function redactHomePathUserSegments(text: string, opts?: HomePathRedactionOptions): string {
if (opts?.enabled === false) return text;
let result = text;
for (const pattern of HOME_PATH_PATTERNS) {
result = result.replace(pattern.regex, pattern.replace);
}
return result;
}
export function redactHomePathUserSegmentsInValue<T>(value: T, opts?: HomePathRedactionOptions): T {
if (typeof value === "string") {
return redactHomePathUserSegments(value, opts) as T;
}
if (Array.isArray(value)) {
return value.map((entry) => redactHomePathUserSegmentsInValue(entry, opts)) as T;
}
if (!isPlainObject(value)) {
return value;
}
const redacted: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(value)) {
redacted[key] = redactHomePathUserSegmentsInValue(entry, opts);
}
return redacted as T;
}
export function redactTranscriptEntryPaths(entry: TranscriptEntry, opts?: HomePathRedactionOptions): TranscriptEntry {
switch (entry.kind) {
case "assistant":
case "thinking":
case "user":
case "stderr":
case "system":
case "stdout":
case "diff":
return { ...entry, text: redactHomePathUserSegments(entry.text, opts) };
case "tool_call":
return {
...entry,
name: redactHomePathUserSegments(entry.name, opts),
input: redactHomePathUserSegmentsInValue(entry.input, opts),
};
case "tool_result":
return { ...entry, content: redactHomePathUserSegments(entry.content, opts) };
case "init":
return {
...entry,
model: redactHomePathUserSegments(entry.model, opts),
sessionId: redactHomePathUserSegments(entry.sessionId, opts),
};
case "result":
return {
...entry,
text: redactHomePathUserSegments(entry.text, opts),
subtype: redactHomePathUserSegments(entry.subtype, opts),
errors: entry.errors.map((error) => redactHomePathUserSegments(error, opts)),
};
default:
return entry;
}
}