Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
// Human-friendly labels for the activity stream so users can follow what
// the model is doing without reading raw tool names. Pure functions — unit
// tested without React.
// Human duration: whole seconds under a minute, "Nm Ns" above. Floors so a
// fresh turn reads "0s" rather than rounding up mid-first-second.
export function formatDuration(ms) {
const total = Math.floor(ms / 1000);
if (total < 60) return `${total}s`;
const minutes = Math.floor(total / 60);
const seconds = total % 60;
return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
}
function titleize(name) {
const s = String(name || "").trim();
if (!s) return "Working";
return s
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
}
// The product skills (circuit-interfaces §3) mapped to what they mean for the
// user. Used both when a skill surfaces as its own tool name and when it runs
// through the generic `Skill` tool (input carries the skill name).
const SKILL_LABELS = Object.freeze({
circuitcode: "Designing the board",
"circuit-analysis": "Shaping the spec",
"parts-book": "Picking parts",
"board-viewer": "Opening the viewer",
});
function skillNameFromInput(input) {
const obj = input && typeof input === "object" ? input : {};
for (const key of ["skill", "name", "command"]) {
const value = obj[key];
if (typeof value === "string" && value.trim()) {
// A command like "circuitcode boards/main.tsx" still names the skill first.
return value.trim().split(/[\s/]+/)[0];
}
}
return "";
}
/**
* Map a raw tool name (+ its input) to a short human phrase.
* Falls back to a titleized tool name so nothing is ever hidden.
*
* @param {string} tool
* @param {unknown} [input]
* @returns {string}
*/
export function toolLabel(tool, input) {
const name = String(tool || "");
if (SKILL_LABELS[name]) return SKILL_LABELS[name];
switch (name) {
case "Skill":
return SKILL_LABELS[skillNameFromInput(input)] || "Running a skill";
case "Write":
return "Writing script";
case "Edit":
case "MultiEdit":
return "Editing script";
case "Read":
return "Reading files";
case "Grep":
return "Searching code";
case "Glob":
return "Finding files";
case "WebSearch":
return "Searching the web";
case "WebFetch":
return "Reading a page";
case "Task":
case "Agent":
return "Running a subtask";
case "ExitPlanMode":
return "Finalizing plan";
case "Bash": {
const cmd =
input && typeof input === "object" && "command" in input
? String(/** @type {{command?: unknown}} */ (input).command || "")
: "";
if (/scripts\/(circuit|check|review)|tscircuit|kicad|gerber/.test(cmd)) return "Building the board";
return "Running command";
}
default:
return titleize(name);
}
}
/**
* The specific target a tool acted on — the search pattern, the file, the
* command, the query — so a trace row reads "Searching code · «pattern»"
* instead of a bare verb. Returns "" when there's nothing meaningful to show.
* Full string (no truncation); callers truncate for display.
*
* @param {string} tool
* @param {unknown} [input]
* @returns {string}
*/
export function toolDetail(tool, input) {
const obj = input && typeof input === "object" ? /** @type {Record<string, unknown>} */ (input) : {};
const str = (v) => (typeof v === "string" ? v.trim() : "");
const basename = (p) => {
const s = str(p);
const parts = s.split(/[\\/]/);
return parts[parts.length - 1] || s;
};
switch (String(tool || "")) {
case "Read":
case "Write":
case "Edit":
case "MultiEdit":
return basename(obj.file_path);
case "Grep":
case "Glob":
return str(obj.pattern);
case "Bash":
return str(obj.command);
case "WebSearch":
return str(obj.query);
case "WebFetch":
return str(obj.url);
case "Task":
case "Agent":
return str(obj.description);
case "circuitcode":
case "circuit-analysis":
case "parts-book":
case "board-viewer":
case "Skill":
return str(obj.command) || str(obj.skill) || str(obj.name);
default: {
// Generic: surface the first non-empty string field so a new tool is
// never reduced to a bare verb.
for (const value of Object.values(obj)) {
const s = str(value);
if (s) return s;
}
return "";
}
}
}
/**
* The same detail, shortened for the row.
*
* Watched on a real build: eighteen consecutive activity rows all reading
* `Running command cd /Users/d/.clau…`. Every one was truncated at the same
* point, so the list carried no information at all — it was a wall of
* identical grey text in front of someone trying to work out whether anything
* was happening. The information was there; it was just behind a long absolute
* path and a shell preamble.
*
* Two rules, both lossless in the tooltip (which still carries the full
* command): collapse a long absolute path to `…/lastSegment`, and drop the
* leading `cd …` / `VAR=…;` preamble that every generated command starts with.
*/
export function toolDetailShort(tool, input) {
const full = toolDetail(tool, input);
if (String(tool || "") !== "Bash") return full;
return shortenCommand(full);
}
export function shortenCommand(command) {
let text = String(command || "").replace(/\s+/g, " ").trim();
// Any absolute path of three or more segments becomes …/<last segment>.
text = text.replace(/(?:\/[^\s/"';|&]+){3,}/g, (match) => `…/${match.split("/").pop()}`);
// Shell preamble: variable assignments and a leading `cd`, in any order.
for (let i = 0; i < 4; i += 1) {
const next = text
.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s*(?:;|&&)\s*/, "")
.replace(/^cd\s+\S+\s*(?:;|&&)\s*/, "");
if (next === text) break;
text = next;
}
return text;
}
/**
* Badge label for a turn's workflow phase.
* @param {"plan"|"implement"|string|undefined} phase
* @returns {string}
*/
export function phaseLabel(phase) {
if (phase === "plan") return "Planning";
if (phase === "implement") return "Building";
return "";
}
/**
* Roll a turn's tool-activity statuses into one summary status for the Activity
* disclosure header glyph. A live step outranks everything (the group still
* reads as working); otherwise a failure outranks a cancellation, which
* outranks all-clear. Empty/missing → "ok" (nothing to flag).
*
* @param {{status?: string}[]|undefined} activity
* @returns {"running"|"error"|"cancelled"|"ok"}
*/
export function aggregateActivityStatus(activity) {
return rollUpActivity(activity);
}
/**
* The same roll-up, phrased for the disclosure *header* rather than for a row.
*
* A tool step exiting non-zero is how an agent finds things out. Watched on a
* real run: the user typed "make it smaller" with no board yet, the model ran
* `ls` on a `boards/` directory that did not exist, got the expected non-zero,
* and answered perfectly — "this project is empty, tell me what you want and
* I'll spec it small from the start". The header above that answer was a red
* ✗. A first-timer reads red as "it broke", then reads a helpful paragraph,
* and the two do not agree.
*
* So a finished group with a failed step is `partial`, not `error`: amber, and
* still auto-expanded, so nothing is hidden — only the claim changes. A turn
* that genuinely failed renders its own error block; that is not this.
*/
export function headerActivityStatus(activity) {
const status = rollUpActivity(activity);
return status === "error" ? "partial" : status;
}
function rollUpActivity(activity) {
const list = Array.isArray(activity) ? activity : [];
let seenError = false;
let seenCancelled = false;
for (const block of list) {
if (block?.status === "running") return "running";
if (block?.status === "error") seenError = true;
else if (block?.status === "cancelled") seenCancelled = true;
}
if (seenError) return "error";
if (seenCancelled) return "cancelled";
return "ok";
}
/**
* Whether a segment's Activity disclosure should default to open: the active
* (live) group expands so progress is watchable, and any finished group with a
* failed tool expands so the error is never hidden behind a collapsed summary.
* A cancelled tool is not an error and collapses like a clean group.
*
* @param {{status?: string}[]|undefined} activity this segment's tool blocks
* @param {boolean} active whether this is the turn's live/active group
* @returns {boolean}
*/
export function activityDefaultsOpen(activity, active) {
if (active) return true;
return (Array.isArray(activity) ? activity : []).some((b) => b?.status === "error");
}
26e259c5cThis file is pinned to the selected release. Download the original to open it in your design tool.