@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/NOTICES.md +96 -0
- package/README.md +9 -2
- package/lib/claude-transcript.mjs +41 -5
- package/lib/context-breakdown.mjs +25 -17
- package/lib/jsonl-follower.mjs +15 -1
- package/lib/opencode-store.mjs +89 -23
- package/lib/telemetry-fields.mjs +38 -0
- package/package.json +18 -3
- package/telemetry/README.md +19 -4
- package/telemetry/antigravity.mjs +3 -1
- package/telemetry/claude-code.mjs +15 -8
- package/telemetry/codex.mjs +40 -24
- package/telemetry/cursor.mjs +3 -1
- package/telemetry/hermes.mjs +3 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threadcrumbs/common",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Code the Threadcrumbs apps share, starting with the readers for each coding-agent harness.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -25,7 +25,16 @@
|
|
|
25
25
|
"./license": "./license/index.mjs",
|
|
26
26
|
"./license/store": "./license/store.mjs",
|
|
27
27
|
"./orgs": "./orgs/index.mjs",
|
|
28
|
-
"./lib/*": "./lib/*.mjs"
|
|
28
|
+
"./lib/*": "./lib/*.mjs",
|
|
29
|
+
"./ui/status": "./ui/status.mjs",
|
|
30
|
+
"./ui/status.css": "./ui/status.css",
|
|
31
|
+
"./ui/icons": "./ui/icons.mjs",
|
|
32
|
+
"./ui/where": "./ui/where.mjs",
|
|
33
|
+
"./ui/where.css": "./ui/where.css",
|
|
34
|
+
"./ui/gauge": "./ui/gauge.mjs",
|
|
35
|
+
"./ui/gauge.css": "./ui/gauge.css",
|
|
36
|
+
"./ui/theme.css": "./ui/theme.css",
|
|
37
|
+
"./ui/fonts.css": "./ui/fonts.css"
|
|
29
38
|
},
|
|
30
39
|
"files": [
|
|
31
40
|
"NOTICES.md",
|
|
@@ -34,9 +43,15 @@
|
|
|
34
43
|
"license/",
|
|
35
44
|
"lib/",
|
|
36
45
|
"orgs/",
|
|
37
|
-
"telemetry/"
|
|
46
|
+
"telemetry/",
|
|
47
|
+
"ui/"
|
|
38
48
|
],
|
|
39
49
|
"scripts": {
|
|
40
50
|
"test": "node --test \"test/**/*.test.mjs\""
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@fontsource-variable/inter": "^5.3.0",
|
|
54
|
+
"@fontsource-variable/inter-tight": "^5.3.0",
|
|
55
|
+
"@fontsource-variable/jetbrains-mono": "^5.3.0"
|
|
41
56
|
}
|
|
42
57
|
}
|
package/telemetry/README.md
CHANGED
|
@@ -59,6 +59,7 @@ adapter in `../harnesses/`, so a session has the same id in both apps. Add the h
|
|
|
59
59
|
| `tool` | `{ name, startedAt, target, kind }` or `null` | The tool running right now, only while working or delegating. `target` is its short file path, command, or search, as in `Detail.activity`, or `''` when the harness doesn't record one. `kind` is one of the [tool kinds](#tool-kinds) |
|
|
60
60
|
| `tokens` | `{ input, output, cacheRead, cacheWrite }` or `null` | Totals for the session's own thread. `input` leaves out cache reads and writes |
|
|
61
61
|
| `context` | `{ used, limit }` or `null` | Tokens in the window after the last reply; `limit` is `0` when unknown |
|
|
62
|
+
| `contextHistory` | number[] or `null` | The context after each reply, oldest first, for a sparkline: at most 24 whole numbers, evenly spaced across the session and always ending at `context.used`. A reply written twice counts once. Each reader keeps a running sample as it reads, never the whole history; OpenCode 2.x reads only the replies newer than the last one it saw, once the session changes, leaves the newest for the next read while it has no `time.completed`, and starts over if replies were removed. `null` for Cursor, Antigravity, and Hermes, whose session scan sees no per-reply counts, and before the first reply |
|
|
62
63
|
| `costUsd` | number or `null` | Spend as the harness records it; `null` when it doesn't |
|
|
63
64
|
| `lines` | `{ added, removed }` or `null` | Lines changed, as the harness records it |
|
|
64
65
|
| `plan` | `{ total, done, current }` or `null` | Plan progress, if the agent keeps one |
|
|
@@ -157,6 +158,15 @@ Again, a `null` field means the harness doesn't record it. The shared helpers fo
|
|
|
157
158
|
(`startCall`, `endCall`, `toolTarget`, `keepNewest`, `fileList`, and the limits) are in
|
|
158
159
|
`../lib/telemetry-fields.mjs`.
|
|
159
160
|
|
|
161
|
+
`sessionDetail(id, { filesOnly: true })` returns only `{ id, sessionId, files }`, with each file
|
|
162
|
+
as `{ path }`, from what the session scan already holds. It's for finding other threads that
|
|
163
|
+
changed the same files, so it never opens the readers that serve an open thread.
|
|
164
|
+
|
|
165
|
+
The readers that serve an open thread (thoughts and replies for Claude Code, Codex, Cursor, and
|
|
166
|
+
Antigravity) are kept for the last three sessions opened. Claude Code's and Codex's read the
|
|
167
|
+
transcript from its end first, in growing steps, until they have the newest 50 replies, and then
|
|
168
|
+
follow it forwards.
|
|
169
|
+
|
|
160
170
|
### Context breakdown
|
|
161
171
|
|
|
162
172
|
`contextBreakdown` splits the context into parts, so an app can show what fills it.
|
|
@@ -169,8 +179,8 @@ Again, a `null` field means the harness doesn't record it. The shared helpers fo
|
|
|
169
179
|
them aren't read, so an app should call it the default. Free space is `limit - total -
|
|
170
180
|
reserved`.
|
|
171
181
|
- **`parts`** are `[{ id, tokens, items }]` in this order: `messages`, `system`,
|
|
172
|
-
`builtInTools`, `mcpTools`, `skills`, `memory`. A part
|
|
173
|
-
out, and its tokens stay in `system`.
|
|
182
|
+
`builtInTools`, `mcpTools`, `skills`, `memory`. A part the harness records is sent even at 0
|
|
183
|
+
tokens; one it doesn't record is left out, and its tokens stay in `system`.
|
|
174
184
|
- `messages` items: `{ id: 'prompts' }`, `{ id: 'replies' }`, `{ id: 'summary' }` (from the
|
|
175
185
|
last compaction), and `{ id: 'tool', name }` for each tool's calls and results.
|
|
176
186
|
- `mcpTools` items: `{ name, tools }` per server, `tools` a count or `null`.
|
|
@@ -182,6 +192,11 @@ Again, a `null` field means the harness doesn't record it. The shared helpers fo
|
|
|
182
192
|
- That gives a characters-per-token ratio for the text of each fixed part (system prompt,
|
|
183
193
|
tool definitions, MCP servers, skills, memory files), so each part is its text length at
|
|
184
194
|
that ratio. A part added later counts at the same ratio.
|
|
195
|
+
- The ratio is used only when the system prompt and tool definitions were recorded, as in
|
|
196
|
+
Claude Code's `api-request-shape`. Otherwise it would spread their unrecorded text over the
|
|
197
|
+
parts that were, so each recorded part is its text at 4 characters a token, and `system` is
|
|
198
|
+
the rest of the fixed count. That's always the case for Codex, whose built-in tool
|
|
199
|
+
definitions aren't written.
|
|
185
200
|
- `messages` is the rest of the total. Each request's growth over the one before is split
|
|
186
201
|
among the prompts, replies, and tool calls and results since then, by text length. After a
|
|
187
202
|
compaction, the next request's growth over the fixed parts is the summary.
|
|
@@ -199,8 +214,8 @@ Again, a `null` field means the harness doesn't record it. The shared helpers fo
|
|
|
199
214
|
- Codex: the rollout's base instructions and developer messages, its AGENTS.md messages, and
|
|
200
215
|
its `additional_tools` items. Its built-in tool definitions aren't written, so they're in
|
|
201
216
|
`system`.
|
|
202
|
-
- **
|
|
203
|
-
|
|
217
|
+
- **Its sizes are kept by the session scan,** which already reads every record, as numbers and
|
|
218
|
+
names only, never the text, so opening a thread doesn't read its transcript again.
|
|
204
219
|
|
|
205
220
|
### Tool kinds
|
|
206
221
|
|
|
@@ -233,6 +233,7 @@ export function antigravity({
|
|
|
233
233
|
tool: null,
|
|
234
234
|
tokens: null,
|
|
235
235
|
context: null,
|
|
236
|
+
contextHistory: null,
|
|
236
237
|
costUsd: null,
|
|
237
238
|
lines: null,
|
|
238
239
|
plan: planSummary(plan),
|
|
@@ -251,9 +252,10 @@ export function antigravity({
|
|
|
251
252
|
return sessions
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
async function sessionDetail(key) {
|
|
255
|
+
async function sessionDetail(key, { filesOnly = false } = {}) {
|
|
255
256
|
const id = String(key).replace(/^antigravity:/, '')
|
|
256
257
|
if (!known.has(id)) return null
|
|
258
|
+
if (filesOnly) return { id: `antigravity:${id}`, sessionId: id, files: null }
|
|
257
259
|
const sessionDir = path.join(brainDir, id)
|
|
258
260
|
const found = await transcriptOf(sessionDir)
|
|
259
261
|
const state = (found && (await transcripts.read(found.file, found.stat).catch(() => null))) || emptyTranscript()
|
|
@@ -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)
|
|
@@ -388,6 +394,7 @@ function toSession({ id, state, stat, running, subagents, workingSubagents, awai
|
|
|
388
394
|
tool: status === 'working' || status === 'delegating' ? currentTool(state.open) : null,
|
|
389
395
|
tokens: { ...state.tokens },
|
|
390
396
|
context: state.context ? { used: state.context, limit } : null,
|
|
397
|
+
contextHistory: sampleList(state.contextSample, state.context),
|
|
391
398
|
costUsd: state.unpriced || totals.unpriced ? (state.cost ? state.cost.usd : null) : state.spend + totals.spend,
|
|
392
399
|
lines: { added: state.lines.added + totals.added, removed: state.lines.removed + totals.removed },
|
|
393
400
|
plan: planSummary(state.plan),
|
package/telemetry/codex.mjs
CHANGED
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
clipBlock,
|
|
17
17
|
diffHunk,
|
|
18
18
|
emptyActivity,
|
|
19
|
+
emptySample,
|
|
19
20
|
endCall,
|
|
20
21
|
fileList,
|
|
22
|
+
filePaths,
|
|
21
23
|
inputKind,
|
|
22
24
|
joinedText,
|
|
23
25
|
keepNewest,
|
|
@@ -27,10 +29,13 @@ import {
|
|
|
27
29
|
planItems,
|
|
28
30
|
planSummary,
|
|
29
31
|
relativePath,
|
|
32
|
+
sampleList,
|
|
33
|
+
samplePoint,
|
|
30
34
|
settleCalls,
|
|
31
35
|
startCall,
|
|
32
36
|
text,
|
|
33
37
|
textList,
|
|
38
|
+
wordsFull,
|
|
34
39
|
titleFromPrompt,
|
|
35
40
|
toolInput,
|
|
36
41
|
toolNow,
|
|
@@ -40,6 +45,7 @@ import {
|
|
|
40
45
|
const ACTIVE_WINDOW_MS = 30 * 60 * 1000
|
|
41
46
|
const SUBAGENT_WINDOW_MS = 10 * 60 * 1000
|
|
42
47
|
const OPENED_KEPT = 3
|
|
48
|
+
const TAIL_BYTES = 512 * 1024
|
|
43
49
|
const COMPACT_SHARE = 0.9
|
|
44
50
|
const REPORTED_SHARE = 0.95
|
|
45
51
|
const ROLLOUT_FILE = /^rollout-.{19}-([0-9a-f-]{36})(?:_[0-9a-f-]{36})?\.jsonl$/i
|
|
@@ -140,6 +146,7 @@ function emptyRollout() {
|
|
|
140
146
|
fallbackPrompts: 0,
|
|
141
147
|
open: false,
|
|
142
148
|
turnStartedAt: 0,
|
|
149
|
+
turnId: '',
|
|
143
150
|
usageLimits: null,
|
|
144
151
|
failed: false,
|
|
145
152
|
calls: new Map(),
|
|
@@ -147,6 +154,9 @@ function emptyRollout() {
|
|
|
147
154
|
lines: { added: 0, removed: 0 },
|
|
148
155
|
tokens: null,
|
|
149
156
|
context: null,
|
|
157
|
+
contextSample: emptySample(),
|
|
158
|
+
sizes: emptySizes(),
|
|
159
|
+
sent: emptySent(),
|
|
150
160
|
plan: null,
|
|
151
161
|
history: [],
|
|
152
162
|
contextParts: null,
|
|
@@ -162,13 +172,19 @@ function emptyRollout() {
|
|
|
162
172
|
|
|
163
173
|
const AGENTS_FILE = /^# AGENTS\.md instructions for ([^\n]+)/
|
|
164
174
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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() })
|
|
172
188
|
|
|
173
189
|
function callName(item) {
|
|
174
190
|
if (item.type === 'local_shell_call') return 'shell'
|
|
@@ -182,14 +198,11 @@ function codexFixed(sent) {
|
|
|
182
198
|
fixed.system = developer + (sent.developer.includes(sent.base) ? 0 : sent.base) + sent.other
|
|
183
199
|
fixed.mcp = new Map(sent.tools)
|
|
184
200
|
fixed.memory = new Map(sent.memory)
|
|
201
|
+
fixed.recorded = new Set(['system', 'mcpTools', 'memory'])
|
|
185
202
|
return fixed
|
|
186
203
|
}
|
|
187
204
|
|
|
188
|
-
function
|
|
189
|
-
const at = Date.parse(record.timestamp) || state.lastAt
|
|
190
|
-
state.lastAt = at
|
|
191
|
-
const item = record.payload
|
|
192
|
-
if (!item || typeof item !== 'object') return
|
|
205
|
+
function applySizes(state, record, item, at) {
|
|
193
206
|
const { sizes, sent } = state
|
|
194
207
|
const before = sizes.first == null
|
|
195
208
|
if (record.type === 'session_meta') {
|
|
@@ -203,14 +216,10 @@ function applyOpened(state, record) {
|
|
|
203
216
|
} else if (record.type !== 'response_item') {
|
|
204
217
|
return
|
|
205
218
|
} else if (item.type === 'reasoning') {
|
|
206
|
-
|
|
207
|
-
keepText(state.thoughts, 'thoughts', { id: text(item.id) || null, at, text: said })
|
|
208
|
-
sizeReply(sizes, said.length)
|
|
219
|
+
sizeReply(sizes, joinedText(item.summary, 'summary_text', 'text').length)
|
|
209
220
|
return
|
|
210
221
|
} else if (item.type === 'message' && item.role === 'assistant') {
|
|
211
|
-
|
|
212
|
-
keepText(state.replies, 'replies', { id: text(item.id) || null, at, text: said })
|
|
213
|
-
sizeReply(sizes, said.length)
|
|
222
|
+
sizeReply(sizes, joinedText(item.content, 'output_text', 'text').length)
|
|
214
223
|
return
|
|
215
224
|
} else if (CALLS.has(item.type)) {
|
|
216
225
|
sizeCall(sizes, text(item.call_id), callName(item), (text(item.arguments) || text(item.input) || JSON.stringify(item.action ?? '')).length)
|
|
@@ -226,7 +235,7 @@ function applyOpened(state, record) {
|
|
|
226
235
|
} else if (item.type === 'message') {
|
|
227
236
|
const raw = contentText(item.content)
|
|
228
237
|
const agents = item.role === 'user' ? AGENTS_FILE.exec(raw) : null
|
|
229
|
-
if (agents) sent.memory.set(`${agents[1].trim().replace(/[
|
|
238
|
+
if (agents) sent.memory.set(`${agents[1].trim().replace(/[\\/]+$/, '')}/AGENTS.md`, raw.length)
|
|
230
239
|
else if (item.role === 'user' && promptText(raw)) {
|
|
231
240
|
sizePrompt(sizes, raw.length)
|
|
232
241
|
return
|
|
@@ -249,6 +258,7 @@ function applyRecord(state, record) {
|
|
|
249
258
|
}
|
|
250
259
|
const payload = record.payload
|
|
251
260
|
if (!payload || typeof payload !== 'object') return
|
|
261
|
+
applySizes(state, record, payload, at || state.lastAt)
|
|
252
262
|
if (record.type === 'session_meta') applyMeta(state, payload)
|
|
253
263
|
else if (record.type === 'turn_context') applySettings(state, payload.model, payload.effort, payload.cwd)
|
|
254
264
|
else if (record.type === 'response_item') applyItem(state, payload, at || state.lastAt)
|
|
@@ -363,13 +373,16 @@ function addLines(state, patch) {
|
|
|
363
373
|
function applyEvent(state, event, at) {
|
|
364
374
|
switch (event.type) {
|
|
365
375
|
case 'task_started':
|
|
366
|
-
case 'turn_started':
|
|
367
|
-
|
|
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
|
|
368
380
|
state.open = true
|
|
369
381
|
state.failed = false
|
|
370
382
|
state.calls.clear()
|
|
371
383
|
settleCalls(state.activity)
|
|
372
384
|
break
|
|
385
|
+
}
|
|
373
386
|
case 'task_complete':
|
|
374
387
|
case 'turn_complete':
|
|
375
388
|
state.open = false
|
|
@@ -451,6 +464,7 @@ function applyTokens(state, info, at) {
|
|
|
451
464
|
const sum = number(total?.total_tokens)
|
|
452
465
|
if (sum === state.lastTotal) return
|
|
453
466
|
state.lastTotal = sum
|
|
467
|
+
samplePoint(state.contextSample, last.total_tokens)
|
|
454
468
|
keepNewest(state.history, { at, context: number(last.total_tokens), output: number(last.output_tokens) }, DETAIL_LIMITS.history)
|
|
455
469
|
}
|
|
456
470
|
|
|
@@ -463,7 +477,7 @@ export function codex({
|
|
|
463
477
|
const locksDir = path.join(home, 'thread-writer-locks')
|
|
464
478
|
const indexFile = path.join(home, 'session_index.jsonl')
|
|
465
479
|
const rollouts = jsonlFollower({ empty: emptyRollout, apply: applyRecord })
|
|
466
|
-
const openedFor = recentReaders(() => jsonlFollower({ empty:
|
|
480
|
+
const openedFor = recentReaders(() => jsonlFollower({ empty: emptyWords, apply: applyWords, tail: { bytes: TAIL_BYTES, enough: wordsFull } }), OPENED_KEPT)
|
|
467
481
|
let index = { mtime: -1, names: new Map() }
|
|
468
482
|
let known = new Map()
|
|
469
483
|
let childrenOf = new Map()
|
|
@@ -571,11 +585,12 @@ export function codex({
|
|
|
571
585
|
return state ? { file, stat, state } : null
|
|
572
586
|
}
|
|
573
587
|
|
|
574
|
-
async function sessionDetail(key) {
|
|
588
|
+
async function sessionDetail(key, { filesOnly = false } = {}) {
|
|
575
589
|
const id = String(key).replace(/^codex:/, '').toLowerCase()
|
|
576
590
|
const found = await readKnown(id)
|
|
577
591
|
if (!found || found.state.subagent || found.state.internal) return null
|
|
578
592
|
const { file, stat, state } = found
|
|
593
|
+
if (filesOnly) return { id: `codex:${id}`, sessionId: id, files: filePaths(state.files, state.cwd) }
|
|
579
594
|
const said = await openedFor(file).read(file, stat).catch(() => null)
|
|
580
595
|
const limit = state.context?.limit || null
|
|
581
596
|
const subagents = []
|
|
@@ -612,7 +627,7 @@ export function codex({
|
|
|
612
627
|
prompts: (state.typed ? state.promptList : state.fallbackPromptList).map((p) => ({ ...p })),
|
|
613
628
|
thoughts: textList(said?.thoughts),
|
|
614
629
|
replies: textList(said?.replies),
|
|
615
|
-
contextBreakdown: contextBreakdown(
|
|
630
|
+
contextBreakdown: contextBreakdown(state.sizes, {
|
|
616
631
|
total: state.context?.used,
|
|
617
632
|
limit,
|
|
618
633
|
reserved: limit ? Math.round(limit * (1 - COMPACT_SHARE / REPORTED_SHARE)) : null,
|
|
@@ -668,6 +683,7 @@ function toSession({ id, state, names, live, turnLive, fresh, locking, unread, s
|
|
|
668
683
|
tool,
|
|
669
684
|
tokens: state.tokens ? { ...state.tokens } : null,
|
|
670
685
|
context: state.context && state.context.used ? { ...state.context } : null,
|
|
686
|
+
contextHistory: sampleList(state.contextSample, state.context?.used),
|
|
671
687
|
costUsd: null,
|
|
672
688
|
lines: { ...state.lines },
|
|
673
689
|
plan: planSummary(state.plan),
|
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)
|
|
@@ -273,6 +274,7 @@ export function cursor({
|
|
|
273
274
|
tool: open && state.tool ? { ...toolNow(state.tool), target: clip(relativePath(state.tool.target, projectPath)) } : null,
|
|
274
275
|
tokens: null,
|
|
275
276
|
context: null,
|
|
277
|
+
contextHistory: null,
|
|
276
278
|
costUsd: null,
|
|
277
279
|
lines: null,
|
|
278
280
|
plan: planSummary(state.plan),
|
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)
|
|
@@ -423,6 +424,7 @@ function toSession({ db, profile, tip, root, chain, ids, open, errands, workingS
|
|
|
423
424
|
? { input: tokens[0] || 0, output: tokens[1] || 0, cacheRead: tokens[2] || 0, cacheWrite: tokens[3] || 0 }
|
|
424
425
|
: null,
|
|
425
426
|
context: used ? { used, limit: contextLimit(lengths, text(tip.model), tip.billing_base_url) } : null,
|
|
427
|
+
contextHistory: null,
|
|
426
428
|
costUsd: costs.some((c) => c != null) ? costs.reduce((total, c) => total + number(c), 0) : null,
|
|
427
429
|
lines: null,
|
|
428
430
|
plan: planSummary(latestPlan(db, ids, messages)),
|
|
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])
|