// Tactical Grey palette — the ONLY palette. Orange is reserved for cost-heat and // proven indirect dataflow; never decoration. No Elastic blue for structure. // (Source of truth: docs/UI-SPEC.md "Skin (Tactical Grey)".) import { FileText, Terminal, FilePen, Search, Cpu, Globe, Box, MessageSquare, Rocket, Database, HelpCircle, SlidersHorizontal, GitCommit, Bell, Server, } from "lucide-react"; export const C = { bg: "#2b2927", panel: "#363432", card: "#46433f", elevated: "#504d4a", black: "#1b1a19", header: "#211f1e", orange: "#F06A17", orangeHi: "#FF7B28", orangeMut: "rgba(240,106,23,0.13)", orangeBd: "rgba(240,106,23,0.42)", text: "#f4f1ee", text2: "#b3afaa", muted: "#7e7a76", border: "#565350", borderSoft: "#403e3c", cyan: "#2dd4bf", amber: "#fbbf24", red: "#f87171", blue: "#7aa2d6", }; export const FD = "'Chakra Petch',sans-serif"; export const FB = "'IBM Plex Sans',sans-serif"; export const FM = "'JetBrains Mono',monospace"; // Severity colour logic (legend): orange = heaviest cost · amber = has a fixable // tip · cyan = clean/efficient. This is the entity classification for Mode A. export const SEV = { heavy: { color: C.orange, label: "Heavy turn" }, tip: { color: C.amber, label: "Has-tip" }, clean: { color: C.cyan, label: "Clean" }, }; export function turnSeverity(t) { if (t.heavy) return "heavy"; if (t.guide) return "tip"; return "clean"; } export const fmt = (n) => n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e3 ? Math.round(n / 1e3) + "k" : "" + n; // Real session timestamp (Shripal: tell sessions apart). Accepts an ISO string or // epoch-ms/sec number; returns e.g. "Jun 04, 21:30" (local), or "" when absent. export function fmtWhen(v) { if (!v && v !== 0) return ""; let d; if (typeof v === "number") d = new Date(v < 1e12 ? v * 1000 : v); else d = new Date(v); if (isNaN(d.getTime())) return ""; const M = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const pad = (n) => String(n).padStart(2, "0"); return `${M[d.getMonth()]} ${pad(d.getDate())}, ${pad(d.getHours())}:${pad(d.getMinutes())}`; } // Map a tool name to an icon + a coarse "type" bucket used for the legend tallies. // MCP names are prefixed mcp____; treat all as the MCP bucket. export function toolBucket(name) { if (/^mcp__/i.test(name) || /hugging|notion|gmail|drive|calendar/i.test(name)) return "MCP"; if (name === "Read") return "Read"; if (name === "Bash") return "Bash"; if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(name)) return "Edit"; if (name === "Task" || /^Task/.test(name)) return "Task"; return "Other"; } // Legend tool-type eye -> bucket. Returns whether a tool of this name is // currently shown given the legend `vis` map. "Other" has no toggle (always on). const _BUCKET_KEY = { Read: "tRead", Bash: "tBash", Edit: "tEdit", MCP: "tMCP", Task: "tTask" }; export function toolTypeVisible(vis, name) { const key = _BUCKET_KEY[toolBucket(name)]; return key ? vis[key] !== false : true; } // A journey node is "tool-dimmed" when it uses TOGGLEABLE tool types but none of // them are currently visible — so e.g. leaving only MCP on dims every non-MCP // turn. "Other"-bucket tools (ToolSearch, AskUserQuestion, …) have no eye, so a // turn made only of those is never dimmed (toggles don't apply to it). export function turnToolDimmed(vis, turn) { let toggleable = 0; let anyVisible = false; for (const tl of turn.tools || []) { const key = _BUCKET_KEY[toolBucket(tl.name)]; if (!key) continue; // Other — unaffected by tool-type toggles toggleable += 1; if (vis[key] !== false) anyVisible = true; } return toggleable > 0 && !anyVisible; } export const toolIcon = (name) => /^mcp__/i.test(name) || /hugging|notion|gmail|drive|calendar/i.test(name) ? Globe : name === "Read" ? FileText : name === "Bash" ? Terminal : ["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(name) ? FilePen : ["Grep", "Glob"].includes(name) ? Search : name === "Task" || /^Task/.test(name) ? Cpu : Box; // Deterministic intent guess from the prompt text — used ONLY for the node glyph. // This is cosmetic (icon choice), not a finding; it never asserts causation. export function intentOf(turn) { if (turn.origin === "system") return "task"; const p = (turn.prompt || "").toLowerCase(); if (/\bdeploy|railway|ship|production|build\b/.test(p)) return "deploy"; if (/\bpsql|postgres|surreal|migrat|database|\bdb\b|role|schema\b/.test(p)) return "db"; if (/\bmodel|embed|llama|onnx|nomic|opus|temperature\b/.test(p)) return "model"; if (/\bcommit|git \b/.test(p)) return "commit"; if (/\bconfig|env|variable|disable|enable\b/.test(p)) return "config"; if (/\?$|why |can'?t |how |what /.test(p)) return "ask"; return "msg"; } export const intentIcon = (k) => ({ deploy: Rocket, db: Database, model: Cpu, ask: HelpCircle, config: SlidersHorizontal, commit: GitCommit, task: Bell, server: Server, msg: MessageSquare, }[k] || MessageSquare); // First sentence / clipped prompt for compact rows. export function shortPrompt(turn, n = 68) { const p = (turn.prompt || "").replace(/\s+/g, " ").trim(); return p.length > n ? p.slice(0, n) : p; }