@threadcrumbs/common 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ui/icons.mjs ADDED
@@ -0,0 +1,98 @@
1
+ import { mcpServer } from '../lib/tool-kinds.mjs'
2
+ import { esc } from './html.mjs'
3
+
4
+ export { mcpServer }
5
+
6
+ const LINE_ICONS = {
7
+ key: '<circle cx="8" cy="15.5" r="4.5"/><path d="M11.3 12.2 20 3.5M16.5 7l2.5 2.5M14 9.5l2 2"/>',
8
+ folder: '<path d="M3 7.4A1.4 1.4 0 0 1 4.4 6h4.2l2 2.5h7A1.4 1.4 0 0 1 19 9.9v7.7a1.4 1.4 0 0 1-1.4 1.4H4.4A1.4 1.4 0 0 1 3 17.6z"/>',
9
+ org: '<path d="M4.5 20V5.4A1.4 1.4 0 0 1 5.9 4h6.7A1.4 1.4 0 0 1 14 5.4V20"/><path d="M14 9h4.1a1.4 1.4 0 0 1 1.4 1.4V20"/><path d="M3 20h18"/><path d="M7.5 8h3M7.5 11.5h3M7.5 15h3M16.8 13h.01M16.8 16.5h.01"/>',
10
+ branch:
11
+ '<circle cx="7" cy="5.5" r="2.2"/><circle cx="7" cy="18.5" r="2.2"/><circle cx="17" cy="7.5" r="2.2"/><path d="M7 7.7v8.6"/><path d="M17 9.7c0 3.4-2.6 4.6-6 5.2-1.9.3-3.3.9-4 1.4"/>',
12
+ worktree: '<circle cx="6.5" cy="5" r="2.2"/><circle cx="17.5" cy="5" r="2.2"/><circle cx="12" cy="19" r="2.2"/><path d="M6.5 7.2v1.3a3 3 0 0 0 3 3h5a3 3 0 0 0 3-3V7.2"/><path d="M12 11.5v5.3"/>',
13
+ model: '<path d="M11 3.5l1.8 4.9 4.9 1.8-4.9 1.8L11 16.9l-1.8-4.9-4.9-1.8 4.9-1.8z"/><path d="M18 14.5l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8z"/>',
14
+ 'k-read': '<path d="M13.5 3.5H7.4A1.4 1.4 0 0 0 6 4.9v14.2a1.4 1.4 0 0 0 1.4 1.4h9.2a1.4 1.4 0 0 0 1.4-1.4V8z"/><path d="M13.5 3.5V8H18"/><path d="M9 12.5h6M9 16h4"/>',
15
+ 'k-search': '<circle cx="10.5" cy="10.5" r="6"/><path d="M15 15l5 5"/>',
16
+ 'k-edit': '<path d="M15.2 5.3a2 2 0 0 1 2.8 0l.7.7a2 2 0 0 1 0 2.8L8.5 19 4 20l1-4.5z"/><path d="M13.5 7l3.5 3.5"/>',
17
+ 'k-delete':
18
+ '<path d="M4.5 7h15"/><path d="M9.5 7V5.2a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V7"/><path d="M6.5 7l.8 11.8a1.5 1.5 0 0 0 1.5 1.4h6.4a1.5 1.5 0 0 0 1.5-1.4L17.5 7"/><path d="M10.2 11v5.5M13.8 11v5.5"/>',
19
+ 'k-move': '<path d="M12 20.5H7.4A1.4 1.4 0 0 1 6 19.1V4.9a1.4 1.4 0 0 1 1.4-1.4h6.1L18 8v3"/><path d="M13.5 3.5V8H18"/><path d="M11.5 16h9M17.5 13l3 3-3 3"/>',
20
+ 'k-execute': '<path d="M5 7l5 5-5 5"/><path d="M12.5 17H19"/>',
21
+ 'k-fetch': '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17"/><path d="M12 3.5c2.3 2.3 3.5 5.2 3.5 8.5s-1.2 6.2-3.5 8.5C9.7 18.2 8.5 15.3 8.5 12S9.7 5.8 12 3.5z"/>',
22
+ 'k-think': '<path d="M9.5 17.5h5M10.5 20.5h3"/><path d="M12 3.5a5.5 5.5 0 0 0-3.3 9.9c.6.5 1 1.2 1 2v.1h4.6v-.1c0-.8.4-1.5 1-2A5.5 5.5 0 0 0 12 3.5z"/>',
23
+ 'k-agent': '<path d="M2.5 12H9M6.5 9.2 9.3 12l-2.8 2.8"/><circle cx="15.5" cy="12" r="5.5" stroke-linecap="butt" stroke-dasharray="3.2 2.56"/><circle cx="15.5" cy="12" r="1.7" fill="currentColor" stroke="none"/>',
24
+ 'k-plan': '<path d="M3.5 7.2 5.8 9.5 10 5M3.5 15.7 5.8 18 10 13.5"/><path d="M13.5 7.5H20.5M13.5 16H20.5"/>',
25
+ 'k-communicate':
26
+ '<path d="M4 6.4A1.9 1.9 0 0 1 5.9 4.5h12.2A1.9 1.9 0 0 1 20 6.4v8.2a1.9 1.9 0 0 1-1.9 1.9H11.5L7 20v-3.5H5.9A1.9 1.9 0 0 1 4 14.6z"/><path d="M10.1 8.7a1.9 1.9 0 1 1 2.8 1.7c-.6.3-.9.7-.9 1.3"/><path d="M12 14h.01"/>',
27
+ 'k-switch_mode': '<rect x="2.5" y="7" width="19" height="10" rx="5"/><circle cx="16.5" cy="12" r="2.5"/>',
28
+ 'k-other': '<path d="M17.57 4.17A4.8 4.8 0 0 0 11.13 10.47L5.11 16.49a1.7 1.7 0 0 0 2.4 2.4L13.53 12.87A4.8 4.8 0 0 0 19.83 6.43L17.76 8.5 15.5 6.24z"/>',
29
+ 'k-mcp': '<path d="M9 3.5v4M15 3.5v4"/><path d="M6.5 7.5h11V10a5.5 5.5 0 0 1-11 0z"/><path d="M12 15.5v5"/>',
30
+ }
31
+
32
+ export const ICON_NAMES = Object.freeze(Object.keys(LINE_ICONS))
33
+
34
+ export const ICO = (name, size = 13) =>
35
+ `<svg class="ico" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${LINE_ICONS[name] || ''}</svg>`
36
+
37
+ export const EFFORT_LEVELS = { minimal: 1, low: 1, medium: 2, high: 3, xhigh: 4, max: 5 }
38
+ export const EFFORT_NAMES = { minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', max: 'Max' }
39
+ export const effortName = (effort) => EFFORT_NAMES[String(effort).toLowerCase()] || effort || ''
40
+
41
+ export function effortBars(effort) {
42
+ if (!effort) return ''
43
+ const level = EFFORT_LEVELS[String(effort).toLowerCase()] ?? 0
44
+ const bars = [1, 2, 3, 4, 5]
45
+ .map((n) => `<rect x="${n * 4 - 1.5}" y="${(20 - n * 3.2).toFixed(1)}" width="2.6" height="${(n * 3.2).toFixed(1)}" rx="1.1" class="${n <= level ? 'eb-on' : 'eb-off'}"/>`)
46
+ .join('')
47
+ const label = esc(`${effortName(effort)} effort`)
48
+ return `<svg class="ico eff" width="14" height="14" viewBox="0 0 24 24" role="img" aria-label="${label}"><title>${label}</title>${bars}</svg>`
49
+ }
50
+
51
+ export const HARNESS_MARKS = Object.freeze({
52
+ 'claude-code': 'CC',
53
+ codex: 'Cx',
54
+ cursor: 'Cu',
55
+ antigravity: 'AG',
56
+ hermes: 'H',
57
+ opencode: 'OC',
58
+ kilocode: 'K',
59
+ 'gemini-cli': 'Gm',
60
+ 'copilot-cli': 'Cp',
61
+ })
62
+ export const harnessMark = (id, name = id) => HARNESS_MARKS[id] || String(name || '?')[0].toUpperCase()
63
+
64
+ export const KINDS = Object.freeze({
65
+ read: { name: 'Read', tip: 'Reads a file' },
66
+ search: { name: 'Search', tip: 'Searches for text or lists files' },
67
+ edit: { name: 'Edit', tip: 'Creates or changes a file' },
68
+ delete: { name: 'Delete', tip: 'Deletes files' },
69
+ move: { name: 'Move', tip: 'Moves or renames files' },
70
+ execute: { name: 'Run', tip: 'Runs a command' },
71
+ fetch: { name: 'Web', tip: 'Fetches a page or searches the web' },
72
+ think: { name: 'Think', tip: 'Thinks or takes notes, without changing anything' },
73
+ agent: { name: 'Subagent', tip: 'Hands work to a subagent' },
74
+ plan: { name: 'Plan', tip: 'Updates the plan' },
75
+ communicate: { name: 'Ask', tip: 'Asks you something' },
76
+ switch_mode: { name: 'Mode', tip: "Changes the agent's mode" },
77
+ other: { name: 'Other', tip: 'A tool that has no kind yet' },
78
+ mcp: { name: 'MCP', tip: 'A tool from an MCP server' },
79
+ })
80
+
81
+ const COMMAND_TOOLS = /bash|shell|command|exec|terminal|powershell/i
82
+ export function kindOf(item, byName = null) {
83
+ const server = item?.server || mcpServer(item?.name)
84
+ if (server) return { kind: 'mcp', server, shell: false }
85
+ const kind = KINDS[item?.kind] ? item.kind : 'other'
86
+ const nameKind = byName?.get(item?.name) ?? (COMMAND_TOOLS.test(item?.name || '') ? 'execute' : null)
87
+ return { kind, server: null, shell: kind !== 'execute' && kind !== 'other' && nameKind === 'execute' }
88
+ }
89
+
90
+ export function kindTip({ kind, server, shell }) {
91
+ if (kind === 'mcp') return server ? `A tool from the ${server} MCP server` : KINDS.mcp.tip
92
+ const tip = (KINDS[kind] || KINDS.other).tip
93
+ return shell ? `${tip}, with a shell command` : tip
94
+ }
95
+
96
+ export const kindIconHTML = (k) => `<span class="tk" data-k="${esc(k.kind)}" data-tip="${esc(kindTip(k))}">${ICO(`k-${k.kind}`)}</span>`
97
+
98
+ export const toolKinds = (tools) => new Map((tools || []).map((t) => [t.name, t.kind]))
package/ui/status.css ADDED
@@ -0,0 +1,75 @@
1
+ [data-st='asking'] {
2
+ --st: var(--lamp);
3
+ --plate: var(--lamp-wash);
4
+ }
5
+ [data-st='working'] {
6
+ --st: var(--work);
7
+ --plate: var(--work-plate);
8
+ }
9
+ [data-st='delegating'] {
10
+ --st: var(--wait);
11
+ --plate: var(--wait-plate);
12
+ }
13
+ [data-st='done'] {
14
+ --st: var(--done);
15
+ --plate: var(--done-plate);
16
+ }
17
+ [data-st='error'] {
18
+ --st: var(--err);
19
+ --plate: var(--err-plate);
20
+ }
21
+ [data-st='idle'] {
22
+ --st: var(--idle);
23
+ --plate: var(--idle-plate);
24
+ }
25
+
26
+ .lamp {
27
+ display: inline-grid;
28
+ place-items: center;
29
+ width: 16px;
30
+ height: 16px;
31
+ flex: none;
32
+ color: var(--st, var(--idle));
33
+ }
34
+ .lamp svg {
35
+ width: 16px;
36
+ height: 16px;
37
+ overflow: visible;
38
+ }
39
+ .lamp .spin {
40
+ animation: tc-spin 1.5s linear infinite;
41
+ transform-origin: 8px 8px;
42
+ }
43
+ .lamp .spin-slow {
44
+ animation: tc-spin 5s linear infinite;
45
+ transform-origin: 8px 8px;
46
+ }
47
+ .lamp .blink {
48
+ animation: tc-blink 2s ease-in-out infinite;
49
+ }
50
+
51
+ @keyframes tc-spin {
52
+ to {
53
+ transform: rotate(360deg);
54
+ }
55
+ }
56
+ @keyframes tc-blink {
57
+ 0%,
58
+ 60% {
59
+ opacity: 1;
60
+ }
61
+ 80% {
62
+ opacity: 0.6;
63
+ }
64
+ 100% {
65
+ opacity: 1;
66
+ }
67
+ }
68
+
69
+ @media (prefers-reduced-motion: reduce) {
70
+ .lamp .spin,
71
+ .lamp .spin-slow,
72
+ .lamp .blink {
73
+ animation: none;
74
+ }
75
+ }
package/ui/status.mjs ADDED
@@ -0,0 +1,36 @@
1
+ export const STATUSES = [
2
+ { key: 'asking', name: 'Needs you', color: '--lamp', plate: '--lamp-wash' },
3
+ { key: 'working', name: 'Working', color: '--work', plate: '--work-plate' },
4
+ { key: 'delegating', name: 'Waiting on agents', color: '--wait', plate: '--wait-plate' },
5
+ { key: 'done', name: 'Done', color: '--done', plate: '--done-plate' },
6
+ { key: 'error', name: 'Error', color: '--err', plate: '--err-plate' },
7
+ { key: 'idle', name: 'Idle', color: '--idle', plate: '--idle-plate' },
8
+ ]
9
+
10
+ export const STATUS_KEYS = STATUSES.map((s) => s.key)
11
+
12
+ export const URGENCY_ORDER = ['asking', 'error', 'done', 'working', 'delegating', 'idle']
13
+
14
+ export const LIVE_STATUSES = new Set(['asking', 'working', 'delegating'])
15
+
16
+ export const statusOf = (key) => STATUSES.find((s) => s.key === key) || STATUSES.at(-1)
17
+
18
+ export const LAMP_ICONS = {
19
+ asking:
20
+ '<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><g class="blink"><path d="M8 .9 15.100 8 8 15.100.9 8Z" fill="var(--sign)" stroke="var(--sign)" stroke-width="1"/><path d="M6.300 6.300a1.750 1.750 0 1 1 2.700 1.500c-.7.4-1 .8-1 1.400" fill="none" stroke="var(--sign-ink)" stroke-width="1.400" stroke-linecap="round"/><circle cx="8" cy="11.300" r=".85" fill="var(--sign-ink)"/></g></svg>',
21
+ working:
22
+ '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="5.600" stroke="currentColor" stroke-opacity=".28" stroke-width="2"/><g class="spin"><path d="M8 2.400a5.600 5.600 0 0 1 5.600 5.600" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></g></svg>',
23
+ delegating:
24
+ '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><g class="spin-slow"><circle cx="8" cy="8" r="5.800" stroke="currentColor" stroke-width="1.800" stroke-dasharray="2.600 2.460"/></g><circle cx="8" cy="8" r="2.100" fill="currentColor"/></svg>',
25
+ done: '<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.600" fill="currentColor"/><path d="M4.900 8.200 7.100 10.400 11.100 5.800" fill="none" stroke="var(--knock)" stroke-width="1.700" stroke-linecap="round" stroke-linejoin="round"/></svg>',
26
+ error:
27
+ '<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.500 15.200 14.200H.8Z" fill="currentColor"/><path d="M8 6v4" stroke="var(--knock)" stroke-width="1.600" stroke-linecap="round"/><circle cx="8" cy="12" r=".9" fill="var(--knock)"/></svg>',
28
+ idle: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="5.200" stroke="currentColor" stroke-width="1.600"/></svg>',
29
+ pending:
30
+ '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="4.600" stroke="currentColor" stroke-width="1.500"/></svg>',
31
+ }
32
+
33
+ export const lampHTML = (key, cls = '') => {
34
+ const st = STATUS_KEYS.includes(key) || key === 'pending' ? key : 'idle'
35
+ return `<span class="lamp${cls ? ` ${cls}` : ''}" data-st="${st}">${LAMP_ICONS[st]}</span>`
36
+ }
package/ui/theme.css ADDED
@@ -0,0 +1,104 @@
1
+ :root {
2
+ --bg: #14161b;
3
+ --pane: #1b1e25;
4
+ --raise: #272a33;
5
+ --sunk: #111318;
6
+ --line: rgba(255, 255, 255, 0.17);
7
+ --line2: rgba(255, 255, 255, 0.1);
8
+ --text: #e6e4df;
9
+ --text2: #c6c5cc;
10
+ --text3: #9f9ea8;
11
+ --accent: #c96442;
12
+ --accent-text: #e3865f;
13
+ --on-accent: #ffffff;
14
+ --focus: #e3865f;
15
+ --primary: #252833;
16
+ --on-primary: #f2f0ec;
17
+
18
+ --lamp: #f4cf50;
19
+ --lamp-ink: #f4cf50;
20
+ --lamp-wash: #2c2510;
21
+ --sign: #f4cf50;
22
+ --sign-ink: #15191d;
23
+ --knock: #101117;
24
+ --work: #79c2f4;
25
+ --wait: #a784eb;
26
+ --done: #9be6bd;
27
+ --err: #f87d7c;
28
+ --idle: #81878d;
29
+ --warn: #ffb56e;
30
+ --work-plate: #12293a;
31
+ --wait-plate: #241f3d;
32
+ --done-plate: #14301f;
33
+ --err-plate: #3a1d1b;
34
+ --idle-plate: #1a1c25;
35
+ --err-wash: #3a1d1b;
36
+
37
+ --add: #9be6bd;
38
+ --del: #f87d7c;
39
+ --add-bg: #14301f;
40
+ --del-bg: #3a1d1b;
41
+
42
+ --c0: #2a2d38;
43
+ --c1: #5cacf7;
44
+ --c2: #3cd0bc;
45
+ --c3: #b9a6ff;
46
+ --c4: #ff8fc8;
47
+ --chart: #7ccbff;
48
+ --chart-fill: rgba(124, 203, 255, 0.13);
49
+ --inst: #b9b8c0;
50
+ --inst-soft: #b9b8c0;
51
+ --dial: #101727;
52
+ --dial-track: #2f4466;
53
+ --needle: #eef8ff;
54
+ --needle-glow: rgba(127, 207, 255, 0.55);
55
+ --hub: #e8ecf2;
56
+
57
+ --shadow: 0 12px 40px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.4);
58
+ --scrim: rgba(0, 0, 0, 0.55);
59
+
60
+ --fs-1: 12px;
61
+ --fs-2: 12.5px;
62
+ --fs-3: 13px;
63
+ --fs-4: 13.5px;
64
+ --fs-5: 14px;
65
+ --fs-6: 14.5px;
66
+ --fs-7: 15px;
67
+ --fs-8: 15.5px;
68
+ --fs-9: 16px;
69
+ --fs-10: 17px;
70
+ --fs-11: 21.5px;
71
+ --fs-12: 28px;
72
+ --lh-tight: 1.24;
73
+ --lh-ui: 1.42;
74
+ --lh-body: 1.5;
75
+
76
+ --sp-1: 2px;
77
+ --sp-2: 4px;
78
+ --sp-3: 6px;
79
+ --sp-4: 8px;
80
+ --sp-5: 10px;
81
+ --sp-6: 12px;
82
+ --sp-7: 16px;
83
+ --sp-8: 24px;
84
+ --gl: max(16px, env(safe-area-inset-left, 0px));
85
+ --gr: max(16px, env(safe-area-inset-right, 0px));
86
+
87
+ --r-1: 2px;
88
+ --r-2: 4px;
89
+ --r-3: 6px;
90
+ --r-4: 8px;
91
+ --r-round: 999px;
92
+
93
+ --sans: 'Inter Variable', system-ui, -apple-system, 'Segoe UI', sans-serif;
94
+ --cond: 'Inter Tight Variable', 'Inter Variable', system-ui, sans-serif;
95
+ --mono: 'JetBrains Mono Variable', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
96
+
97
+ font-size-adjust: 0.51;
98
+ color-scheme: dark;
99
+ }
100
+ :root *,
101
+ :root *::before,
102
+ :root *::after {
103
+ font-size-adjust: 0.51 !important;
104
+ }
package/ui/where.css ADDED
@@ -0,0 +1,43 @@
1
+ .w {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 5px;
5
+ min-width: 0;
6
+ }
7
+ .w > span {
8
+ min-width: 0;
9
+ overflow-wrap: anywhere;
10
+ }
11
+ .w .ico {
12
+ color: var(--text3);
13
+ }
14
+ .w-folder {
15
+ color: var(--text);
16
+ font-weight: 500;
17
+ }
18
+ .hmark {
19
+ display: inline-grid;
20
+ place-items: center;
21
+ width: 20px;
22
+ height: 18px;
23
+ border: 1px solid var(--line);
24
+ border-radius: var(--r-2);
25
+ color: var(--text2);
26
+ font: 600 var(--fs-1) / 1.04 var(--mono);
27
+ }
28
+ .eff .eb-on {
29
+ fill: var(--text2);
30
+ }
31
+ .eff .eb-off {
32
+ fill: var(--line);
33
+ }
34
+ .tk {
35
+ display: inline-flex;
36
+ vertical-align: -2px;
37
+ margin-right: 6px;
38
+ color: var(--text3);
39
+ }
40
+ .tk .ico {
41
+ width: 14px;
42
+ height: 14px;
43
+ }
package/ui/where.mjs ADDED
@@ -0,0 +1,24 @@
1
+ import { esc } from './html.mjs'
2
+ import { ICO } from './icons.mjs'
3
+
4
+ export function repoName(name, org) {
5
+ const scoped = /^@([^/]+)\/(.+)$/.exec(name || '')
6
+ return scoped && org && scoped[1].toLowerCase() === String(org).toLowerCase() ? scoped[2] : name
7
+ }
8
+
9
+ export function whereHTML(s) {
10
+ const org = s.org ? `<span class="w w-org" title="${esc(`Organization ${s.org}`)}">${ICO('org')}<span>${esc(s.org)}</span></span>` : ''
11
+ return `${org}<span class="w w-folder" title="${esc(s.projectPath || s.project || '')}">${ICO('folder')}<span>${esc(repoName(s.project, s.org) || 'unknown')}</span></span>`
12
+ }
13
+
14
+ export function whereFullHTML(s) {
15
+ const chip = (icon, text) => `<span class="w">${ICO(icon)}<span>${esc(text)}</span></span>`
16
+ return [
17
+ s.org && chip('org', s.org),
18
+ `<span class="w w-folder">${ICO('folder')}<span>${esc(repoName(s.project, s.org) || 'unknown')}</span></span>`,
19
+ s.worktree && chip('worktree', s.worktree),
20
+ s.gitBranch && chip('branch', s.gitBranch),
21
+ ]
22
+ .filter(Boolean)
23
+ .join('')
24
+ }