File size: 5,306 Bytes
5f43c7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 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;
}