@threadcrumbs/common 0.1.4 → 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/NOTICES.md +96 -0
- package/README.md +9 -2
- package/lib/claude-transcript.mjs +37 -5
- package/lib/context-breakdown.mjs +25 -17
- package/lib/jsonl-follower.mjs +15 -1
- package/lib/opencode-store.mjs +60 -21
- package/lib/telemetry-fields.mjs +4 -0
- package/package.json +18 -3
- package/telemetry/README.md +18 -4
- package/telemetry/antigravity.mjs +2 -1
- package/telemetry/claude-code.mjs +14 -8
- package/telemetry/codex.mjs +34 -24
- package/telemetry/cursor.mjs +2 -1
- package/telemetry/hermes.mjs +2 -1
- package/ui/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/ui/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/ui/fonts/inter-tight-latin-ext-wght-normal.woff2 +0 -0
- package/ui/fonts/inter-tight-latin-wght-normal.woff2 +0 -0
- package/ui/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
- package/ui/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/ui/fonts.css +48 -0
- package/ui/gauge.css +75 -0
- package/ui/gauge.mjs +14 -0
- package/ui/html.mjs +1 -0
- package/ui/icons.mjs +98 -0
- package/ui/status.css +75 -0
- package/ui/status.mjs +36 -0
- package/ui/theme.css +104 -0
- package/ui/where.css +43 -0
- package/ui/where.mjs +24 -0
|
@@ -6,7 +6,8 @@ import { awaitedPeers, readLiveSessions, transcriptDetail, transcriptReader, tur
|
|
|
6
6
|
import { exists, listDirs, listFiles, num } from '../lib/fsutil.mjs'
|
|
7
7
|
import { contextBreakdown } from '../lib/context-breakdown.mjs'
|
|
8
8
|
import { recentReaders } from '../lib/jsonl-follower.mjs'
|
|
9
|
-
import {
|
|
9
|
+
import { nameKind } from '../lib/tool-kinds.mjs'
|
|
10
|
+
import { DETAIL_LIMITS, RECENT_MS, askOf, filePaths, number, planSummary, sampleList, text, titleFromPrompt, toolNow } from '../lib/telemetry-fields.mjs'
|
|
10
11
|
|
|
11
12
|
export { nextPlan } from '../lib/telemetry-fields.mjs'
|
|
12
13
|
|
|
@@ -27,10 +28,10 @@ export function claudeCode({
|
|
|
27
28
|
const projectsDir = path.join(home, 'projects')
|
|
28
29
|
const liveDir = path.join(home, 'sessions')
|
|
29
30
|
const catalogDir = path.join(home, 'cache', 'model-catalog')
|
|
30
|
-
const transcripts = transcriptReader({ detail: true })
|
|
31
|
+
const transcripts = transcriptReader({ detail: true, sizes: true })
|
|
31
32
|
const subagentFiles = transcriptReader()
|
|
32
33
|
const subagentDetails = transcriptReader({ detail: true, words: true })
|
|
33
|
-
const openedFor = recentReaders(() => transcriptReader({ words: true,
|
|
34
|
+
const openedFor = recentReaders(() => transcriptReader({ words: true, tail: true }), OPENED_KEPT)
|
|
34
35
|
const desktopRecords = desktopRecordReader()
|
|
35
36
|
const olderReaders = new Map()
|
|
36
37
|
const metas = new Map()
|
|
@@ -219,11 +220,11 @@ export function claudeCode({
|
|
|
219
220
|
const read = await openedFor(file).read(file, stat).catch(() => null)
|
|
220
221
|
const limit = contextLimit(limits, state.model)
|
|
221
222
|
const reserved = limit ? Math.min(RESERVED_TOKENS, limit) : RESERVED_TOKENS
|
|
222
|
-
return { words: read?.words ?? null, contextBreakdown: contextBreakdown(
|
|
223
|
+
return { words: read?.words ?? null, contextBreakdown: contextBreakdown(state.sizes, { total: state.context, limit, reserved }) }
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
function olderReader(file) {
|
|
226
|
-
const reader = olderReaders.get(file) || transcriptReader({ detail: true })
|
|
227
|
+
const reader = olderReaders.get(file) || transcriptReader({ detail: true, sizes: true })
|
|
227
228
|
olderReaders.delete(file)
|
|
228
229
|
olderReaders.set(file, reader)
|
|
229
230
|
while (olderReaders.size > OLDER_KEPT) olderReaders.delete(olderReaders.keys().next().value)
|
|
@@ -252,13 +253,14 @@ export function claudeCode({
|
|
|
252
253
|
}
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
async function sessionDetail(sessionKey, { subagent = null } = {}) {
|
|
256
|
+
async function sessionDetail(sessionKey, { subagent = null, filesOnly = false } = {}) {
|
|
256
257
|
const id = String(sessionKey).replace(/^claude-code:/, '')
|
|
257
258
|
const file = known.get(id)
|
|
258
259
|
const stat = file ? await fsp.stat(file).catch(() => null) : null
|
|
259
260
|
if (!stat) return null
|
|
260
261
|
if (subagent != null) return subagentDetail(file, id, String(subagent))
|
|
261
262
|
const state = await transcripts.read(file, stat)
|
|
263
|
+
if (filesOnly) return { id: `claude-code:${id}`, sessionId: id, files: filePaths(state.detail.files, state.cwd) }
|
|
262
264
|
const said = await opened(file, stat, state, await contextLimits())
|
|
263
265
|
return {
|
|
264
266
|
id: `claude-code:${id}`,
|
|
@@ -334,7 +336,10 @@ function statusOf({ running, state, calls, subagents, awaiting, lastAt, mark, un
|
|
|
334
336
|
const failedTurn = failed || Boolean(mark?.failed)
|
|
335
337
|
const quiet = () => (unread ? ['done', 'unread'] : ['idle', mark ? 'read' : 'quiet'])
|
|
336
338
|
if (running) {
|
|
337
|
-
if (running.status === 'waiting')
|
|
339
|
+
if (running.status === 'waiting') {
|
|
340
|
+
const asked = calls.filter((c) => (c.kind ?? nameKind(c.name)) !== 'agent')
|
|
341
|
+
return ['asking', asked.some((c) => c.questions) ? 'question' : asked.length ? 'approval' : 'waiting']
|
|
342
|
+
}
|
|
338
343
|
const busy = running.status ? running.status === 'busy' : midTurn
|
|
339
344
|
if (busy) return onlyAgents ? ['delegating', 'subagents'] : ['working', running.status ? 'process-busy' : 'turn-open']
|
|
340
345
|
if (subagents || awaiting.length) return ['delegating', subagents ? 'subagents' : 'awaiting']
|
|
@@ -364,7 +369,8 @@ function toSession({ id, state, stat, running, subagents, workingSubagents, awai
|
|
|
364
369
|
const unread = Boolean(mark) && (state.handedBackAt || mark.activeAt) > mark.focusedAt
|
|
365
370
|
const calls = [...state.open.values()].sort((a, b) => a.startedAt - b.startedAt)
|
|
366
371
|
const [status, statusReason] = statusOf({ running, state, calls, subagents, awaiting, lastAt: lastActivityAt, mark, unread, now })
|
|
367
|
-
const
|
|
372
|
+
const markFailed = Boolean(mark?.failed) && !turnEnding(state).failed
|
|
373
|
+
const errorText = (markFailed && mark.errorText) || state.detail?.errors.at(-1)?.message || mark?.errorText || ''
|
|
368
374
|
const midTurn = Boolean(state.ending && !state.ending.handedBack)
|
|
369
375
|
const turnOpen = midTurn && (status === 'working' || status === 'delegating' || status === 'asking')
|
|
370
376
|
const { projectPath, project, worktree } = projectOf(mark?.cwd || state.firstCwd || state.cwd, mark?.originCwd)
|
package/telemetry/codex.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
emptySample,
|
|
20
20
|
endCall,
|
|
21
21
|
fileList,
|
|
22
|
+
filePaths,
|
|
22
23
|
inputKind,
|
|
23
24
|
joinedText,
|
|
24
25
|
keepNewest,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
startCall,
|
|
35
36
|
text,
|
|
36
37
|
textList,
|
|
38
|
+
wordsFull,
|
|
37
39
|
titleFromPrompt,
|
|
38
40
|
toolInput,
|
|
39
41
|
toolNow,
|
|
@@ -43,6 +45,7 @@ import {
|
|
|
43
45
|
const ACTIVE_WINDOW_MS = 30 * 60 * 1000
|
|
44
46
|
const SUBAGENT_WINDOW_MS = 10 * 60 * 1000
|
|
45
47
|
const OPENED_KEPT = 3
|
|
48
|
+
const TAIL_BYTES = 512 * 1024
|
|
46
49
|
const COMPACT_SHARE = 0.9
|
|
47
50
|
const REPORTED_SHARE = 0.95
|
|
48
51
|
const ROLLOUT_FILE = /^rollout-.{19}-([0-9a-f-]{36})(?:_[0-9a-f-]{36})?\.jsonl$/i
|
|
@@ -143,6 +146,7 @@ function emptyRollout() {
|
|
|
143
146
|
fallbackPrompts: 0,
|
|
144
147
|
open: false,
|
|
145
148
|
turnStartedAt: 0,
|
|
149
|
+
turnId: '',
|
|
146
150
|
usageLimits: null,
|
|
147
151
|
failed: false,
|
|
148
152
|
calls: new Map(),
|
|
@@ -151,6 +155,8 @@ function emptyRollout() {
|
|
|
151
155
|
tokens: null,
|
|
152
156
|
context: null,
|
|
153
157
|
contextSample: emptySample(),
|
|
158
|
+
sizes: emptySizes(),
|
|
159
|
+
sent: emptySent(),
|
|
154
160
|
plan: null,
|
|
155
161
|
history: [],
|
|
156
162
|
contextParts: null,
|
|
@@ -166,13 +172,19 @@ function emptyRollout() {
|
|
|
166
172
|
|
|
167
173
|
const AGENTS_FILE = /^# AGENTS\.md instructions for ([^\n]+)/
|
|
168
174
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
const emptyWords = () => ({ lastAt: 0, thoughts: [], replies: [] })
|
|
176
|
+
|
|
177
|
+
function applyWords(state, record) {
|
|
178
|
+
const at = Date.parse(record.timestamp) || state.lastAt
|
|
179
|
+
state.lastAt = at
|
|
180
|
+
const item = record.payload
|
|
181
|
+
if (record.type !== 'response_item' || !item || typeof item !== 'object') return
|
|
182
|
+
const id = text(item.id) || null
|
|
183
|
+
if (item.type === 'reasoning') keepText(state.thoughts, 'thoughts', { id, at, text: joinedText(item.summary, 'summary_text', 'text') })
|
|
184
|
+
else if (item.type === 'message' && item.role === 'assistant') keepText(state.replies, 'replies', { id, at, text: joinedText(item.content, 'output_text', 'text') })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const emptySent = () => ({ base: 0, developer: [], other: 0, memory: new Map(), tools: new Map() })
|
|
176
188
|
|
|
177
189
|
function callName(item) {
|
|
178
190
|
if (item.type === 'local_shell_call') return 'shell'
|
|
@@ -186,14 +198,11 @@ function codexFixed(sent) {
|
|
|
186
198
|
fixed.system = developer + (sent.developer.includes(sent.base) ? 0 : sent.base) + sent.other
|
|
187
199
|
fixed.mcp = new Map(sent.tools)
|
|
188
200
|
fixed.memory = new Map(sent.memory)
|
|
201
|
+
fixed.recorded = new Set(['system', 'mcpTools', 'memory'])
|
|
189
202
|
return fixed
|
|
190
203
|
}
|
|
191
204
|
|
|
192
|
-
function
|
|
193
|
-
const at = Date.parse(record.timestamp) || state.lastAt
|
|
194
|
-
state.lastAt = at
|
|
195
|
-
const item = record.payload
|
|
196
|
-
if (!item || typeof item !== 'object') return
|
|
205
|
+
function applySizes(state, record, item, at) {
|
|
197
206
|
const { sizes, sent } = state
|
|
198
207
|
const before = sizes.first == null
|
|
199
208
|
if (record.type === 'session_meta') {
|
|
@@ -207,14 +216,10 @@ function applyOpened(state, record) {
|
|
|
207
216
|
} else if (record.type !== 'response_item') {
|
|
208
217
|
return
|
|
209
218
|
} else if (item.type === 'reasoning') {
|
|
210
|
-
|
|
211
|
-
keepText(state.thoughts, 'thoughts', { id: text(item.id) || null, at, text: said })
|
|
212
|
-
sizeReply(sizes, said.length)
|
|
219
|
+
sizeReply(sizes, joinedText(item.summary, 'summary_text', 'text').length)
|
|
213
220
|
return
|
|
214
221
|
} else if (item.type === 'message' && item.role === 'assistant') {
|
|
215
|
-
|
|
216
|
-
keepText(state.replies, 'replies', { id: text(item.id) || null, at, text: said })
|
|
217
|
-
sizeReply(sizes, said.length)
|
|
222
|
+
sizeReply(sizes, joinedText(item.content, 'output_text', 'text').length)
|
|
218
223
|
return
|
|
219
224
|
} else if (CALLS.has(item.type)) {
|
|
220
225
|
sizeCall(sizes, text(item.call_id), callName(item), (text(item.arguments) || text(item.input) || JSON.stringify(item.action ?? '')).length)
|
|
@@ -230,7 +235,7 @@ function applyOpened(state, record) {
|
|
|
230
235
|
} else if (item.type === 'message') {
|
|
231
236
|
const raw = contentText(item.content)
|
|
232
237
|
const agents = item.role === 'user' ? AGENTS_FILE.exec(raw) : null
|
|
233
|
-
if (agents) sent.memory.set(`${agents[1].trim().replace(/[
|
|
238
|
+
if (agents) sent.memory.set(`${agents[1].trim().replace(/[\\/]+$/, '')}/AGENTS.md`, raw.length)
|
|
234
239
|
else if (item.role === 'user' && promptText(raw)) {
|
|
235
240
|
sizePrompt(sizes, raw.length)
|
|
236
241
|
return
|
|
@@ -253,6 +258,7 @@ function applyRecord(state, record) {
|
|
|
253
258
|
}
|
|
254
259
|
const payload = record.payload
|
|
255
260
|
if (!payload || typeof payload !== 'object') return
|
|
261
|
+
applySizes(state, record, payload, at || state.lastAt)
|
|
256
262
|
if (record.type === 'session_meta') applyMeta(state, payload)
|
|
257
263
|
else if (record.type === 'turn_context') applySettings(state, payload.model, payload.effort, payload.cwd)
|
|
258
264
|
else if (record.type === 'response_item') applyItem(state, payload, at || state.lastAt)
|
|
@@ -367,13 +373,16 @@ function addLines(state, patch) {
|
|
|
367
373
|
function applyEvent(state, event, at) {
|
|
368
374
|
switch (event.type) {
|
|
369
375
|
case 'task_started':
|
|
370
|
-
case 'turn_started':
|
|
371
|
-
|
|
376
|
+
case 'turn_started': {
|
|
377
|
+
const turn = text(event.turn_id)
|
|
378
|
+
if (!state.open || (turn && turn !== state.turnId)) state.turnStartedAt = at
|
|
379
|
+
if (turn) state.turnId = turn
|
|
372
380
|
state.open = true
|
|
373
381
|
state.failed = false
|
|
374
382
|
state.calls.clear()
|
|
375
383
|
settleCalls(state.activity)
|
|
376
384
|
break
|
|
385
|
+
}
|
|
377
386
|
case 'task_complete':
|
|
378
387
|
case 'turn_complete':
|
|
379
388
|
state.open = false
|
|
@@ -468,7 +477,7 @@ export function codex({
|
|
|
468
477
|
const locksDir = path.join(home, 'thread-writer-locks')
|
|
469
478
|
const indexFile = path.join(home, 'session_index.jsonl')
|
|
470
479
|
const rollouts = jsonlFollower({ empty: emptyRollout, apply: applyRecord })
|
|
471
|
-
const openedFor = recentReaders(() => jsonlFollower({ empty:
|
|
480
|
+
const openedFor = recentReaders(() => jsonlFollower({ empty: emptyWords, apply: applyWords, tail: { bytes: TAIL_BYTES, enough: wordsFull } }), OPENED_KEPT)
|
|
472
481
|
let index = { mtime: -1, names: new Map() }
|
|
473
482
|
let known = new Map()
|
|
474
483
|
let childrenOf = new Map()
|
|
@@ -576,11 +585,12 @@ export function codex({
|
|
|
576
585
|
return state ? { file, stat, state } : null
|
|
577
586
|
}
|
|
578
587
|
|
|
579
|
-
async function sessionDetail(key) {
|
|
588
|
+
async function sessionDetail(key, { filesOnly = false } = {}) {
|
|
580
589
|
const id = String(key).replace(/^codex:/, '').toLowerCase()
|
|
581
590
|
const found = await readKnown(id)
|
|
582
591
|
if (!found || found.state.subagent || found.state.internal) return null
|
|
583
592
|
const { file, stat, state } = found
|
|
593
|
+
if (filesOnly) return { id: `codex:${id}`, sessionId: id, files: filePaths(state.files, state.cwd) }
|
|
584
594
|
const said = await openedFor(file).read(file, stat).catch(() => null)
|
|
585
595
|
const limit = state.context?.limit || null
|
|
586
596
|
const subagents = []
|
|
@@ -617,7 +627,7 @@ export function codex({
|
|
|
617
627
|
prompts: (state.typed ? state.promptList : state.fallbackPromptList).map((p) => ({ ...p })),
|
|
618
628
|
thoughts: textList(said?.thoughts),
|
|
619
629
|
replies: textList(said?.replies),
|
|
620
|
-
contextBreakdown: contextBreakdown(
|
|
630
|
+
contextBreakdown: contextBreakdown(state.sizes, {
|
|
621
631
|
total: state.context?.used,
|
|
622
632
|
limit,
|
|
623
633
|
reserved: limit ? Math.round(limit * (1 - COMPACT_SHARE / REPORTED_SHARE)) : null,
|
package/telemetry/cursor.mjs
CHANGED
|
@@ -198,9 +198,10 @@ export function cursor({
|
|
|
198
198
|
return list.sort((a, b) => a.startedAt - b.startedAt).slice(-DETAIL_LIMITS.subagents)
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
async function sessionDetail(key) {
|
|
201
|
+
async function sessionDetail(key, { filesOnly = false } = {}) {
|
|
202
202
|
const id = String(key).replace(/^cursor:/, '')
|
|
203
203
|
const entry = known.get(id)
|
|
204
|
+
if (filesOnly) return entry ? { id: `cursor:${id}`, sessionId: id, files: null } : null
|
|
204
205
|
const stat = entry ? await fsp.stat(entry.file).catch(() => null) : null
|
|
205
206
|
if (!stat) return null
|
|
206
207
|
const state = await follow(entry.file, stat)
|
package/telemetry/hermes.mjs
CHANGED
|
@@ -122,10 +122,11 @@ export function hermes({ home = resolveHermesHome(), now = Date.now } = {}) {
|
|
|
122
122
|
return sessions
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async function sessionDetail(key) {
|
|
125
|
+
async function sessionDetail(key, { filesOnly = false } = {}) {
|
|
126
126
|
const match = /^hermes:([^:]+):(.+)$/.exec(String(key))
|
|
127
127
|
if (!match) return null
|
|
128
128
|
const [, profile, rootId] = match
|
|
129
|
+
if (filesOnly) return { id: `hermes:${profile}:${rootId}`, sessionId: rootId, files: null }
|
|
129
130
|
const store = (await storeFiles()).find((s) => s.profile === profile)
|
|
130
131
|
if (!store) return null
|
|
131
132
|
const detail = await withReadOnly(store.file, (db) => detailFrom(db, rootId, now()), null)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/ui/fonts.css
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@font-face {
|
|
2
|
+
font-family: 'Inter Variable';
|
|
3
|
+
font-style: normal;
|
|
4
|
+
font-display: swap;
|
|
5
|
+
font-weight: 100 900;
|
|
6
|
+
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
|
7
|
+
src: url('./fonts/inter-latin-ext-wght-normal.woff2') format('woff2-variations');
|
|
8
|
+
}
|
|
9
|
+
@font-face {
|
|
10
|
+
font-family: 'Inter Variable';
|
|
11
|
+
font-style: normal;
|
|
12
|
+
font-display: swap;
|
|
13
|
+
font-weight: 100 900;
|
|
14
|
+
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
|
15
|
+
src: url('./fonts/inter-latin-wght-normal.woff2') format('woff2-variations');
|
|
16
|
+
}
|
|
17
|
+
@font-face {
|
|
18
|
+
font-family: 'Inter Tight Variable';
|
|
19
|
+
font-style: normal;
|
|
20
|
+
font-display: swap;
|
|
21
|
+
font-weight: 100 900;
|
|
22
|
+
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
|
23
|
+
src: url('./fonts/inter-tight-latin-ext-wght-normal.woff2') format('woff2-variations');
|
|
24
|
+
}
|
|
25
|
+
@font-face {
|
|
26
|
+
font-family: 'Inter Tight Variable';
|
|
27
|
+
font-style: normal;
|
|
28
|
+
font-display: swap;
|
|
29
|
+
font-weight: 100 900;
|
|
30
|
+
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
|
31
|
+
src: url('./fonts/inter-tight-latin-wght-normal.woff2') format('woff2-variations');
|
|
32
|
+
}
|
|
33
|
+
@font-face {
|
|
34
|
+
font-family: 'JetBrains Mono Variable';
|
|
35
|
+
font-style: normal;
|
|
36
|
+
font-display: swap;
|
|
37
|
+
font-weight: 100 800;
|
|
38
|
+
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
|
39
|
+
src: url('./fonts/jetbrains-mono-latin-ext-wght-normal.woff2') format('woff2-variations');
|
|
40
|
+
}
|
|
41
|
+
@font-face {
|
|
42
|
+
font-family: 'JetBrains Mono Variable';
|
|
43
|
+
font-style: normal;
|
|
44
|
+
font-display: swap;
|
|
45
|
+
font-weight: 100 800;
|
|
46
|
+
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
|
47
|
+
src: url('./fonts/jetbrains-mono-latin-wght-normal.woff2') format('woff2-variations');
|
|
48
|
+
}
|
package/ui/gauge.css
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.inst {
|
|
2
|
+
display: block;
|
|
3
|
+
flex: none;
|
|
4
|
+
overflow: visible;
|
|
5
|
+
}
|
|
6
|
+
.i-dial {
|
|
7
|
+
fill: var(--dial);
|
|
8
|
+
stroke: var(--line);
|
|
9
|
+
stroke-width: 1;
|
|
10
|
+
}
|
|
11
|
+
.i-mini .i-dial {
|
|
12
|
+
stroke-width: 2.4;
|
|
13
|
+
}
|
|
14
|
+
.i-hub {
|
|
15
|
+
fill: var(--hub);
|
|
16
|
+
}
|
|
17
|
+
.i-track,
|
|
18
|
+
.i-fill,
|
|
19
|
+
.i-red {
|
|
20
|
+
fill: none;
|
|
21
|
+
stroke-width: 6;
|
|
22
|
+
stroke-linecap: round;
|
|
23
|
+
}
|
|
24
|
+
.i-track {
|
|
25
|
+
stroke: var(--dial-track);
|
|
26
|
+
}
|
|
27
|
+
.i-fill {
|
|
28
|
+
stroke: var(--inst);
|
|
29
|
+
}
|
|
30
|
+
.i-red {
|
|
31
|
+
stroke: var(--err);
|
|
32
|
+
opacity: 0.8;
|
|
33
|
+
}
|
|
34
|
+
.i-mini .i-track,
|
|
35
|
+
.i-mini .i-fill,
|
|
36
|
+
.i-mini .i-red {
|
|
37
|
+
stroke-width: 10;
|
|
38
|
+
}
|
|
39
|
+
.inst[data-level='high'] .i-fill {
|
|
40
|
+
stroke: var(--warn);
|
|
41
|
+
}
|
|
42
|
+
.inst[data-level='full'] .i-fill {
|
|
43
|
+
stroke: var(--err);
|
|
44
|
+
}
|
|
45
|
+
.i-needle {
|
|
46
|
+
stroke: var(--needle);
|
|
47
|
+
stroke-width: 3.2;
|
|
48
|
+
stroke-linecap: round;
|
|
49
|
+
filter: drop-shadow(0 0 3px var(--needle-glow));
|
|
50
|
+
}
|
|
51
|
+
.i-mini .i-needle {
|
|
52
|
+
stroke-width: 6;
|
|
53
|
+
filter: none;
|
|
54
|
+
}
|
|
55
|
+
.i-label {
|
|
56
|
+
fill: var(--text);
|
|
57
|
+
font: 600 12px var(--cond);
|
|
58
|
+
}
|
|
59
|
+
.i-wrap {
|
|
60
|
+
position: relative;
|
|
61
|
+
}
|
|
62
|
+
.i-wrap::after {
|
|
63
|
+
content: '';
|
|
64
|
+
position: absolute;
|
|
65
|
+
top: calc(-1 * var(--hit-t, 4px));
|
|
66
|
+
right: calc(-1 * var(--hit-r, 4px));
|
|
67
|
+
bottom: calc(-1 * var(--hit-b, 4px));
|
|
68
|
+
left: calc(-1 * var(--hit-l, 4px));
|
|
69
|
+
}
|
|
70
|
+
.i-wrap:has(+ .i-wrap) {
|
|
71
|
+
--hit-r: 2px;
|
|
72
|
+
}
|
|
73
|
+
.i-wrap + .i-wrap {
|
|
74
|
+
--hit-l: 2px;
|
|
75
|
+
}
|
package/ui/gauge.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const ARC = 'M-19.8,19.8 A28,28 0 1 1 19.8,19.8'
|
|
2
|
+
const fx = (n) => n.toFixed(2)
|
|
3
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v))
|
|
4
|
+
|
|
5
|
+
export const levelOf = (pct) => (pct == null ? 'unknown' : pct >= 90 ? 'full' : pct >= 70 ? 'high' : 'ok')
|
|
6
|
+
export const levelClass = (spec) => (spec.level === 'full' ? 'x' : spec.level === 'high' ? 'h' : 'f')
|
|
7
|
+
|
|
8
|
+
export function contextGauge(spec, size = 18) {
|
|
9
|
+
const mini = size <= 30
|
|
10
|
+
const p = clamp(spec.pct ?? 0, 0, 100)
|
|
11
|
+
const level = spec.level || levelOf(spec.pct ?? null)
|
|
12
|
+
const body = `<path d="${ARC}" class="i-track" pathLength="100"/><path d="${ARC}" class="i-red" pathLength="100" stroke-dasharray="10 100" stroke-dashoffset="-90"/><path d="${ARC}" class="i-fill" pathLength="100" stroke-dasharray="${fx(p)} 100"/><line x1="0" y1="0" x2="${mini ? 19 : 21}" y2="0" class="i-needle" transform="rotate(${fx(135 + (270 * p) / 100)})"/>${mini ? '' : `<text x="0" y="28" class="i-label" text-anchor="middle">${Math.round(p)}%</text>`}`
|
|
13
|
+
return `<svg class="inst${mini ? ' i-mini' : ''}" data-k="gauge" data-level="${level}" width="${size}" height="${size}" viewBox="-36 -36 72 72" aria-hidden="true"><circle r="35" class="i-dial"/>${body}<circle r="${mini ? 6 : 4.5}" class="i-hub"/></svg>`
|
|
14
|
+
}
|
package/ui/html.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c])
|
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
|
+
}
|