@threadcrumbs/common 0.1.4 → 0.1.6
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 +14 -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 +26 -3
- package/telemetry/README.md +20 -4
- package/telemetry/antigravity.mjs +2 -1
- package/telemetry/claude-code.mjs +17 -10
- 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/format.mjs +60 -0
- package/ui/gauge.css +75 -0
- package/ui/gauge.mjs +14 -0
- package/ui/html.mjs +12 -0
- package/ui/icons.mjs +105 -0
- package/ui/pictures.css +100 -0
- package/ui/pictures.mjs +158 -0
- package/ui/rows.css +403 -0
- package/ui/rows.mjs +172 -0
- package/ui/status.css +75 -0
- package/ui/status.mjs +36 -0
- package/ui/theme.css +104 -0
- package/ui/totals.css +96 -0
- package/ui/totals.mjs +94 -0
- package/ui/where.css +43 -0
- package/ui/where.mjs +24 -0
package/ui/rows.css
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
.term {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
padding: 0;
|
|
4
|
+
background: none;
|
|
5
|
+
border: 0;
|
|
6
|
+
color: var(--text3);
|
|
7
|
+
font: 400 var(--fs-4) / 17px var(--cond);
|
|
8
|
+
text-align: left;
|
|
9
|
+
text-decoration: underline dotted transparent;
|
|
10
|
+
text-underline-offset: 3px;
|
|
11
|
+
white-space: nowrap;
|
|
12
|
+
cursor: help;
|
|
13
|
+
}
|
|
14
|
+
.term:hover,
|
|
15
|
+
.term[aria-expanded='true'] {
|
|
16
|
+
color: var(--text);
|
|
17
|
+
text-decoration-color: currentColor;
|
|
18
|
+
}
|
|
19
|
+
.i-wrap {
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
flex: none;
|
|
22
|
+
border-radius: 50%;
|
|
23
|
+
cursor: help;
|
|
24
|
+
}
|
|
25
|
+
.i-wrap:empty {
|
|
26
|
+
display: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.grp-h {
|
|
30
|
+
position: sticky;
|
|
31
|
+
top: 0;
|
|
32
|
+
z-index: 2;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 6px;
|
|
36
|
+
padding: 9px 12px 5px var(--gl);
|
|
37
|
+
background: var(--bg);
|
|
38
|
+
color: var(--text2);
|
|
39
|
+
font: 600 var(--fs-4) / 1.24 var(--cond);
|
|
40
|
+
letter-spacing: 0.05em;
|
|
41
|
+
text-transform: uppercase;
|
|
42
|
+
}
|
|
43
|
+
.grp-h[data-kind='repo'] {
|
|
44
|
+
font-size: var(--fs-5);
|
|
45
|
+
letter-spacing: 0;
|
|
46
|
+
text-transform: none;
|
|
47
|
+
}
|
|
48
|
+
.grp-h[data-k='you'] .g-l {
|
|
49
|
+
color: var(--lamp);
|
|
50
|
+
}
|
|
51
|
+
.grp-h .ico {
|
|
52
|
+
color: var(--text3);
|
|
53
|
+
}
|
|
54
|
+
.grp-h .n {
|
|
55
|
+
color: var(--text3);
|
|
56
|
+
font-weight: 500;
|
|
57
|
+
letter-spacing: 0;
|
|
58
|
+
font-variant-numeric: tabular-nums;
|
|
59
|
+
}
|
|
60
|
+
.grp-h .hint {
|
|
61
|
+
margin-left: auto;
|
|
62
|
+
color: var(--text3);
|
|
63
|
+
font: 400 var(--fs-4) / 1.04 var(--cond);
|
|
64
|
+
letter-spacing: 0;
|
|
65
|
+
text-transform: none;
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
68
|
+
.grp-h .hint kbd {
|
|
69
|
+
display: inline-block;
|
|
70
|
+
margin-right: 3px;
|
|
71
|
+
padding: 2px 4px;
|
|
72
|
+
border: 1px solid var(--line);
|
|
73
|
+
border-radius: 3px;
|
|
74
|
+
font: 500 var(--fs-3) / 1.04 var(--mono);
|
|
75
|
+
}
|
|
76
|
+
@media (hover: none) {
|
|
77
|
+
.grp-h .hint {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
.grp-h .g-cols {
|
|
82
|
+
display: none;
|
|
83
|
+
letter-spacing: 0;
|
|
84
|
+
text-transform: none;
|
|
85
|
+
}
|
|
86
|
+
.grp-t {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
flex: 0 1 auto;
|
|
89
|
+
align-items: flex-start;
|
|
90
|
+
gap: 6px;
|
|
91
|
+
min-width: 0;
|
|
92
|
+
margin-left: -4px;
|
|
93
|
+
padding: 2px 6px 2px 0;
|
|
94
|
+
background: none;
|
|
95
|
+
border: 0;
|
|
96
|
+
border-radius: 5px;
|
|
97
|
+
color: inherit;
|
|
98
|
+
font: inherit;
|
|
99
|
+
letter-spacing: inherit;
|
|
100
|
+
text-align: left;
|
|
101
|
+
text-transform: inherit;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
}
|
|
104
|
+
.grp-t > svg {
|
|
105
|
+
flex: none;
|
|
106
|
+
margin-top: 2px;
|
|
107
|
+
color: var(--text3);
|
|
108
|
+
transition: transform 0.14s;
|
|
109
|
+
}
|
|
110
|
+
.grp-t[aria-expanded='true'] > svg {
|
|
111
|
+
transform: rotate(90deg);
|
|
112
|
+
}
|
|
113
|
+
.grp-t:hover .g-c:last-child > span {
|
|
114
|
+
text-decoration: underline;
|
|
115
|
+
text-decoration-color: var(--text3);
|
|
116
|
+
text-underline-offset: 3px;
|
|
117
|
+
}
|
|
118
|
+
.grp-t:focus-visible {
|
|
119
|
+
outline: 2px solid var(--accent-text);
|
|
120
|
+
outline-offset: 1px;
|
|
121
|
+
}
|
|
122
|
+
section[data-shut] .grp-h {
|
|
123
|
+
padding-bottom: 7px;
|
|
124
|
+
border-bottom: 1px solid var(--line2);
|
|
125
|
+
}
|
|
126
|
+
.g-flow {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-wrap: wrap;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 2px 8px;
|
|
131
|
+
min-width: 0;
|
|
132
|
+
}
|
|
133
|
+
.g-l {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 5px;
|
|
137
|
+
min-width: 0;
|
|
138
|
+
}
|
|
139
|
+
.g-flow > .g-l {
|
|
140
|
+
display: contents;
|
|
141
|
+
}
|
|
142
|
+
.g-c {
|
|
143
|
+
display: inline-flex;
|
|
144
|
+
flex: none;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 5px;
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
}
|
|
149
|
+
.g-tallies {
|
|
150
|
+
display: inline-flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
gap: 8px;
|
|
153
|
+
margin-left: 4px;
|
|
154
|
+
letter-spacing: 0;
|
|
155
|
+
text-transform: none;
|
|
156
|
+
}
|
|
157
|
+
.g-tally {
|
|
158
|
+
display: inline-flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 3px;
|
|
161
|
+
}
|
|
162
|
+
.g-tally .lamp,
|
|
163
|
+
.g-tally .lamp svg {
|
|
164
|
+
width: 13px;
|
|
165
|
+
height: 13px;
|
|
166
|
+
}
|
|
167
|
+
.g-tally b {
|
|
168
|
+
color: var(--text2);
|
|
169
|
+
font: 600 var(--fs-5) / 1.04 var(--cond);
|
|
170
|
+
}
|
|
171
|
+
.g-tally[data-st='asking'] b {
|
|
172
|
+
color: var(--lamp);
|
|
173
|
+
}
|
|
174
|
+
.g-tally[data-st='error'] b {
|
|
175
|
+
color: var(--err);
|
|
176
|
+
}
|
|
177
|
+
@media (pointer: coarse) {
|
|
178
|
+
.grp-t {
|
|
179
|
+
min-height: 36px;
|
|
180
|
+
padding-block: 6px;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.row {
|
|
185
|
+
position: relative;
|
|
186
|
+
}
|
|
187
|
+
.row-main {
|
|
188
|
+
display: grid;
|
|
189
|
+
grid-template-columns: 18px minmax(0, 1fr) auto;
|
|
190
|
+
column-gap: 7px;
|
|
191
|
+
row-gap: 1px;
|
|
192
|
+
width: 100%;
|
|
193
|
+
margin: 0;
|
|
194
|
+
padding: 8px 12px 10px var(--gl);
|
|
195
|
+
background: none;
|
|
196
|
+
border: 0;
|
|
197
|
+
border-radius: 0;
|
|
198
|
+
color: var(--text);
|
|
199
|
+
font: inherit;
|
|
200
|
+
text-align: left;
|
|
201
|
+
}
|
|
202
|
+
.row-main:hover {
|
|
203
|
+
background: var(--raise);
|
|
204
|
+
}
|
|
205
|
+
.row-main:focus-visible {
|
|
206
|
+
outline-offset: -2px;
|
|
207
|
+
border-radius: var(--r-3);
|
|
208
|
+
}
|
|
209
|
+
.row-main > .row-where {
|
|
210
|
+
grid-row: 1;
|
|
211
|
+
grid-column: 1 / -1;
|
|
212
|
+
}
|
|
213
|
+
.row-main > .lamp {
|
|
214
|
+
grid-row: 2;
|
|
215
|
+
grid-column: 1;
|
|
216
|
+
margin-top: 2px;
|
|
217
|
+
}
|
|
218
|
+
.row-main > .row-text {
|
|
219
|
+
grid-row: 2;
|
|
220
|
+
grid-column: 2;
|
|
221
|
+
}
|
|
222
|
+
.row-main > .nums {
|
|
223
|
+
grid-row: 2;
|
|
224
|
+
grid-column: 3;
|
|
225
|
+
}
|
|
226
|
+
.row-main > .row-why {
|
|
227
|
+
grid-row: 3;
|
|
228
|
+
grid-column: 2 / -1;
|
|
229
|
+
}
|
|
230
|
+
.row-main > .row-meta {
|
|
231
|
+
grid-row: 4;
|
|
232
|
+
grid-column: 2 / -1;
|
|
233
|
+
}
|
|
234
|
+
.row-main > .row-plan {
|
|
235
|
+
grid-row: 5;
|
|
236
|
+
grid-column: 2 / -1;
|
|
237
|
+
justify-self: start;
|
|
238
|
+
max-width: 100%;
|
|
239
|
+
}
|
|
240
|
+
.row-where {
|
|
241
|
+
display: flex;
|
|
242
|
+
align-items: center;
|
|
243
|
+
gap: 0 10px;
|
|
244
|
+
min-width: 0;
|
|
245
|
+
margin-bottom: 2px;
|
|
246
|
+
overflow: hidden;
|
|
247
|
+
color: var(--text3);
|
|
248
|
+
font: 400 var(--fs-4) / 1.43 var(--cond);
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
}
|
|
251
|
+
.row-where .w {
|
|
252
|
+
flex: 0 1 auto;
|
|
253
|
+
overflow: hidden;
|
|
254
|
+
}
|
|
255
|
+
.row-where .w > span {
|
|
256
|
+
overflow: hidden;
|
|
257
|
+
text-overflow: ellipsis;
|
|
258
|
+
white-space: nowrap;
|
|
259
|
+
}
|
|
260
|
+
.row-where .w-folder {
|
|
261
|
+
color: var(--text2);
|
|
262
|
+
font-weight: 400;
|
|
263
|
+
}
|
|
264
|
+
.row-where .ico,
|
|
265
|
+
.row-meta .ico {
|
|
266
|
+
width: 12px;
|
|
267
|
+
height: 12px;
|
|
268
|
+
}
|
|
269
|
+
.row-h {
|
|
270
|
+
flex: none;
|
|
271
|
+
width: auto;
|
|
272
|
+
height: 15px;
|
|
273
|
+
margin-left: auto;
|
|
274
|
+
padding: 0 3px;
|
|
275
|
+
border: 0;
|
|
276
|
+
color: var(--text3);
|
|
277
|
+
font: 500 var(--fs-2) / 16px var(--mono);
|
|
278
|
+
}
|
|
279
|
+
.row-text {
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
gap: 1px;
|
|
283
|
+
min-width: 0;
|
|
284
|
+
}
|
|
285
|
+
.row-title {
|
|
286
|
+
font: 500 var(--fs-8) / 1.36 var(--sans);
|
|
287
|
+
overflow-wrap: anywhere;
|
|
288
|
+
}
|
|
289
|
+
.row[data-st='idle'] .row-title {
|
|
290
|
+
color: var(--text2);
|
|
291
|
+
font-weight: 400;
|
|
292
|
+
}
|
|
293
|
+
.row-sub {
|
|
294
|
+
display: flex;
|
|
295
|
+
flex-wrap: nowrap;
|
|
296
|
+
align-items: baseline;
|
|
297
|
+
gap: 6px;
|
|
298
|
+
min-width: 0;
|
|
299
|
+
overflow: hidden;
|
|
300
|
+
white-space: nowrap;
|
|
301
|
+
}
|
|
302
|
+
.row-now {
|
|
303
|
+
display: inline-flex;
|
|
304
|
+
flex: none;
|
|
305
|
+
align-items: baseline;
|
|
306
|
+
gap: 6px;
|
|
307
|
+
color: var(--text2);
|
|
308
|
+
font: 400 var(--fs-5) / 1.43 var(--cond);
|
|
309
|
+
}
|
|
310
|
+
.now-h {
|
|
311
|
+
color: var(--text2);
|
|
312
|
+
font-weight: 400;
|
|
313
|
+
}
|
|
314
|
+
.row[data-st='asking'] .now-h,
|
|
315
|
+
.row[data-st='error'] .now-h {
|
|
316
|
+
color: var(--st);
|
|
317
|
+
font-weight: 500;
|
|
318
|
+
}
|
|
319
|
+
.now-v {
|
|
320
|
+
color: var(--text3);
|
|
321
|
+
font-weight: 400;
|
|
322
|
+
}
|
|
323
|
+
.now-d {
|
|
324
|
+
flex: 0 1 auto;
|
|
325
|
+
min-width: 0;
|
|
326
|
+
overflow: hidden;
|
|
327
|
+
color: var(--text2);
|
|
328
|
+
font: 400 var(--fs-4) / 1.43 var(--mono);
|
|
329
|
+
text-overflow: ellipsis;
|
|
330
|
+
}
|
|
331
|
+
.now-d.plain {
|
|
332
|
+
font: 400 var(--fs-5) / 1.43 var(--cond);
|
|
333
|
+
}
|
|
334
|
+
.now-d.rich {
|
|
335
|
+
display: inline-flex;
|
|
336
|
+
align-items: baseline;
|
|
337
|
+
gap: 6px;
|
|
338
|
+
font: 400 var(--fs-7) / 1.51 var(--cond);
|
|
339
|
+
}
|
|
340
|
+
.nd-tool {
|
|
341
|
+
flex: none;
|
|
342
|
+
color: var(--text);
|
|
343
|
+
font: 600 var(--fs-7) / 1.51 var(--cond);
|
|
344
|
+
}
|
|
345
|
+
.nd-dur {
|
|
346
|
+
flex: none;
|
|
347
|
+
color: var(--text3);
|
|
348
|
+
font: 400 var(--fs-6) / 1.51 var(--cond);
|
|
349
|
+
}
|
|
350
|
+
.nd-t {
|
|
351
|
+
min-width: 0;
|
|
352
|
+
overflow: hidden;
|
|
353
|
+
color: var(--text2);
|
|
354
|
+
font: 400 var(--fs-6) / 1.51 var(--mono);
|
|
355
|
+
text-overflow: ellipsis;
|
|
356
|
+
}
|
|
357
|
+
.row-why {
|
|
358
|
+
display: -webkit-box;
|
|
359
|
+
overflow: hidden;
|
|
360
|
+
color: var(--text);
|
|
361
|
+
font: 400 var(--fs-5) / 1.38 var(--cond);
|
|
362
|
+
overflow-wrap: anywhere;
|
|
363
|
+
white-space: normal;
|
|
364
|
+
-webkit-box-orient: vertical;
|
|
365
|
+
-webkit-line-clamp: 2;
|
|
366
|
+
}
|
|
367
|
+
.row-why.mono {
|
|
368
|
+
font: 400 var(--fs-4) / 1.43 var(--mono);
|
|
369
|
+
}
|
|
370
|
+
.row-meta {
|
|
371
|
+
display: none;
|
|
372
|
+
color: var(--text3);
|
|
373
|
+
font: 400 var(--fs-4) / 1.43 var(--cond);
|
|
374
|
+
overflow-wrap: anywhere;
|
|
375
|
+
}
|
|
376
|
+
.row-meta > span {
|
|
377
|
+
display: inline-flex;
|
|
378
|
+
align-items: center;
|
|
379
|
+
gap: 4px;
|
|
380
|
+
margin-right: 10px;
|
|
381
|
+
}
|
|
382
|
+
.nums {
|
|
383
|
+
display: grid;
|
|
384
|
+
justify-items: end;
|
|
385
|
+
gap: 1px;
|
|
386
|
+
min-width: 0;
|
|
387
|
+
font: 500 var(--fs-6) / 1.43 var(--cond);
|
|
388
|
+
font-variant-numeric: tabular-nums;
|
|
389
|
+
}
|
|
390
|
+
.nums .xtra {
|
|
391
|
+
display: none;
|
|
392
|
+
color: var(--text3);
|
|
393
|
+
}
|
|
394
|
+
.add {
|
|
395
|
+
color: var(--add);
|
|
396
|
+
}
|
|
397
|
+
.del {
|
|
398
|
+
color: var(--del);
|
|
399
|
+
}
|
|
400
|
+
.row-plan {
|
|
401
|
+
display: flex;
|
|
402
|
+
margin-top: 5px;
|
|
403
|
+
}
|
package/ui/rows.mjs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { ago, callDuration, compactNumber, duration, modelName, percent, toolLabel } from './format.mjs'
|
|
2
|
+
import { esc, instAttrs, joinNames, plural, termButton } from './html.mjs'
|
|
3
|
+
import { IC, ICO, effortBars, effortName, harnessMark } from './icons.mjs'
|
|
4
|
+
import { planSpec, planTicks } from './pictures.mjs'
|
|
5
|
+
import { URGENCY_ORDER, lampHTML, statusOf } from './status.mjs'
|
|
6
|
+
import { repoName, whereHTML } from './where.mjs'
|
|
7
|
+
|
|
8
|
+
export const turnOf = (s) => (['asking', 'error', 'done'].includes(s.status) ? 'you' : ['working', 'delegating'].includes(s.status) ? 'run' : 'quiet')
|
|
9
|
+
|
|
10
|
+
export const groupKey = (group, g) => `${group}:${g.key}`
|
|
11
|
+
|
|
12
|
+
export const roughly = (ms) => duration(ms).replace(/ \d+s$/, '')
|
|
13
|
+
const turnStart = (s) => s.turnStartedAt || s.lastActivityAt
|
|
14
|
+
|
|
15
|
+
export function shownPlan(s) {
|
|
16
|
+
const p = s?.plan
|
|
17
|
+
return p && s.status === 'done' && p.done < p.total ? { ...p, done: p.total, current: '' } : p
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function waitingOn(s) {
|
|
21
|
+
const names = []
|
|
22
|
+
if (s.subagents) names.push(plural(s.subagents, 'subagent', 'subagents'))
|
|
23
|
+
const peers = s.awaiting || []
|
|
24
|
+
const name = (p) => p.title || 'another session'
|
|
25
|
+
if (peers.length > 2) names.push(name(peers[0]), plural(peers.length - 1, 'other session', 'other sessions'))
|
|
26
|
+
else names.push(...peers.map(name))
|
|
27
|
+
return names.length ? `Waiting on ${joinNames(names)}` : 'Waiting on agents'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function nowParts(s, now) {
|
|
31
|
+
switch (s.status) {
|
|
32
|
+
case 'working':
|
|
33
|
+
return { h: 'Working', v: roughly(now - turnStart(s)) }
|
|
34
|
+
case 'delegating':
|
|
35
|
+
return { h: waitingOn(s), v: roughly(now - turnStart(s)) }
|
|
36
|
+
case 'asking':
|
|
37
|
+
return { h: 'Waiting for your answer', v: duration(now - s.lastActivityAt) }
|
|
38
|
+
case 'done':
|
|
39
|
+
return { h: 'Finished', v: ago(now - s.lastActivityAt) }
|
|
40
|
+
case 'error':
|
|
41
|
+
return { h: 'Stopped on an API error', v: ago(now - s.lastActivityAt) }
|
|
42
|
+
default:
|
|
43
|
+
return { h: 'Last active', v: ago(now - s.lastActivityAt) }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function nowText(s, now) {
|
|
48
|
+
const p = nowParts(s, now)
|
|
49
|
+
return p.v ? `${p.h} ${p.v}` : p.h
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function nowHTML(s, now) {
|
|
53
|
+
const p = nowParts(s, now)
|
|
54
|
+
return `<span class="now-h">${esc(p.h)}</span>${p.v ? `<span class="now-v">${esc(p.v)}</span>` : ''}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function rowDetail(s, now) {
|
|
58
|
+
if (s.status === 'working') {
|
|
59
|
+
if (!s.tool) return { cls: 'rich', html: '<span class="nd-tool">Thinking</span>' }
|
|
60
|
+
const since = Number.isFinite(s.tool.startedAt) ? s.tool.startedAt : now
|
|
61
|
+
return {
|
|
62
|
+
cls: 'rich',
|
|
63
|
+
html: `<span class="nd-tool">${esc(toolLabel(s.tool.name))}</span><span class="nd-dur num" data-since="${since}">${esc(callDuration(now - since))}</span>${s.tool.target ? `<span class="nd-t">${esc(s.tool.target)}</span>` : ''}`,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (s.status === 'delegating') {
|
|
67
|
+
const names = (s.workingSubagents || []).map((a) => a.name).filter(Boolean)
|
|
68
|
+
return names.length ? { cls: 'plain', html: esc(names.join(', ')) } : null
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function errorShort(text) {
|
|
74
|
+
const m = /^(API Error:\s*\d+)\s*(\{.*\})\s*$/s.exec(text || '')
|
|
75
|
+
if (m) {
|
|
76
|
+
try {
|
|
77
|
+
const body = JSON.parse(m[2])
|
|
78
|
+
if (body?.error?.message) return `${m[1]} ${body.error.message}`
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
return text || ''
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function askLine(s) {
|
|
85
|
+
if ((s.status !== 'asking' && s.status !== 'error') || !s.ask) return null
|
|
86
|
+
const a = s.ask
|
|
87
|
+
if (a.type === 'question') {
|
|
88
|
+
const q = a.questions?.[0]
|
|
89
|
+
const text = q?.text || q?.header
|
|
90
|
+
return text ? { kind: 'question', text } : null
|
|
91
|
+
}
|
|
92
|
+
if (a.type === 'approval') {
|
|
93
|
+
if (a.target) return { kind: 'approval', text: a.target, mono: true }
|
|
94
|
+
return a.tool ? { kind: 'approval', text: toolLabel(a.tool), mono: false } : null
|
|
95
|
+
}
|
|
96
|
+
if (a.type === 'error') return { kind: 'error', text: errorShort(a.text) || 'The last turn failed.' }
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const linesHTML = (l) => (l ? `<span class="add">+${compactNumber(l.added)}</span> <span class="del">−${compactNumber(l.removed)}</span>` : '—')
|
|
101
|
+
|
|
102
|
+
export function rowLabel(s, harnessName = (id) => id) {
|
|
103
|
+
const p = shownPlan(s)
|
|
104
|
+
const pct = s.context ? percent(s.context.used, s.context.limit) : null
|
|
105
|
+
return [
|
|
106
|
+
s.org,
|
|
107
|
+
s.project,
|
|
108
|
+
statusOf(s.status).name,
|
|
109
|
+
s.title,
|
|
110
|
+
harnessName(s.harness),
|
|
111
|
+
s.gitBranch,
|
|
112
|
+
modelName(s.model),
|
|
113
|
+
s.effort && `${effortName(s.effort)} effort`,
|
|
114
|
+
pct != null && `Context ${Math.round(pct)}%`,
|
|
115
|
+
p?.total && `Plan ${p.done} of ${p.total}`,
|
|
116
|
+
]
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join(', ')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ticksHTML(s) {
|
|
122
|
+
const p = shownPlan(s)
|
|
123
|
+
if (!p?.total || s.status === 'idle') return ''
|
|
124
|
+
return `<span class="row-plan" data-term="plan" data-rowtip ${instAttrs(planSpec(p), `${p.done} of ${p.total} steps`, p.current || 'All steps done')}>${planTicks(p, s.status)}</span>`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function rowHTML(s, { sel = null, now = Date.now(), harnessName = (id) => id } = {}) {
|
|
128
|
+
const current = s.id === sel
|
|
129
|
+
const name = harnessName(s.harness)
|
|
130
|
+
const det = rowDetail(s, now)
|
|
131
|
+
const ask = askLine(s)
|
|
132
|
+
const meta = [
|
|
133
|
+
s.gitBranch && `<span>${ICO('branch')}${esc(s.gitBranch)}</span>`,
|
|
134
|
+
s.worktree && `<span>${ICO('worktree')}${esc(s.worktree)}</span>`,
|
|
135
|
+
s.model && `<span>${ICO('model')}${esc(modelName(s.model))}${effortBars(s.effort)}</span>`,
|
|
136
|
+
]
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.join('')
|
|
139
|
+
return `<li class="row" data-id="${esc(s.id)}" data-st="${esc(s.status)}" data-turn="${turnOf(s)}"${current ? ' data-current' : ''}>
|
|
140
|
+
<button type="button" class="row-main" data-act="select" data-key="${esc(s.id)}" data-peek="${esc(s.id)}" aria-label="${esc(rowLabel(s, harnessName))}"${current ? ' aria-current="true"' : ''}>
|
|
141
|
+
<span class="row-where">${whereHTML(s)}<span class="hmark row-h" title="${esc(name)}">${esc(harnessMark(s.harness, name))}</span></span>
|
|
142
|
+
${lampHTML(s.status)}
|
|
143
|
+
<span class="row-text"><span class="row-title">${esc(s.title)}</span><span class="row-sub"><span class="row-now num" data-now="${esc(s.id)}">${nowHTML(s, now)}</span>${det ? `<span class="now-d ${det.cls}">${det.html}</span>` : ''}</span></span>
|
|
144
|
+
<span class="nums"><span class="xtra">${esc(compactNumber(s.tokens?.output))}</span><span class="xtra">${linesHTML(s.lines)}</span></span>
|
|
145
|
+
${ask ? `<span class="row-why${ask.mono ? ' mono' : ''}">${esc(ask.text)}</span>` : ''}
|
|
146
|
+
<span class="row-meta">${meta}</span>
|
|
147
|
+
${ticksHTML(s)}
|
|
148
|
+
</button>
|
|
149
|
+
</li>`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function groupLabel(g) {
|
|
153
|
+
if (g.kind !== 'repo') return `<span class="g-l">${esc(g.label)}</span>`
|
|
154
|
+
const org = g.org ? `<span class="g-c">${ICO('org')}<span>${esc(g.org)}</span></span>` : ''
|
|
155
|
+
return `<span class="g-l">${org}<span class="g-c" title="${esc(g.path || g.label)}">${ICO('folder')}<span>${esc(repoName(g.label, g.org))}</span></span></span>`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function groupTally(g) {
|
|
159
|
+
if (g.kind === 'status') return ''
|
|
160
|
+
const counts = new Map()
|
|
161
|
+
for (const s of g.items) counts.set(s.status, (counts.get(s.status) || 0) + 1)
|
|
162
|
+
const parts = URGENCY_ORDER.filter((k) => counts.get(k)).map(
|
|
163
|
+
(k) => `<span class="g-tally" data-st="${k}" title="${esc(`${statusOf(k).name}: ${counts.get(k)}`)}">${lampHTML(k)}<b>${counts.get(k)}</b></span>`,
|
|
164
|
+
)
|
|
165
|
+
return parts.length ? `<span class="g-tallies">${parts.join('')}</span>` : ''
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function groupHeadHTML(g, first, { group = 'turn', shut = false } = {}) {
|
|
169
|
+
const key = groupKey(group, g)
|
|
170
|
+
const extra = first ? `<span class="hint"><kbd>N</kbd>next waiting</span><span class="g-cols">${termButton('output', 'Output')}${termButton('linesColumn', 'Lines')}</span>` : ''
|
|
171
|
+
return `<h2 class="grp-h" data-k="${esc(g.key)}" data-kind="${esc(g.kind)}"><button type="button" class="grp-t" data-act="grp-toggle" data-key="${esc(key)}" aria-expanded="${!shut}" aria-label="${esc(`${shut ? 'Show' : 'Hide'} ${g.label}, ${g.items.length}`)}">${IC.chev}<span class="g-flow">${groupLabel(g)}<span class="n">${g.items.length}</span>${shut ? groupTally(g) : ''}</span></button>${extra}</h2>`
|
|
172
|
+
}
|
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
|
+
}
|