Spaces:
Running on Zero
Running on Zero
| // 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__<server>__<tool>; 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; | |
| } | |