@ucsandman/legcli 0.15.1 → 0.16.1
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/CHANGELOG.md +65 -0
- package/README.md +5 -3
- package/docs/ERRORS.md +39 -0
- package/docs/board-guide.md +33 -29
- package/docs/screenshots/board-400px.png +0 -0
- package/docs/screenshots/board-details-open.png +0 -0
- package/docs/screenshots/board-empty.png +0 -0
- package/docs/screenshots/board-handoff.png +0 -0
- package/docs/screenshots/capacity-drawer-1280.png +0 -0
- package/docs/screenshots/floor.png +0 -0
- package/docs/screenshots/new-card-dialog.png +0 -0
- package/docs/screenshots/settings-ladder-1280.png +0 -0
- package/docs/screenshots/terminals-1280.png +0 -0
- package/package.json +1 -1
- package/scripts/build-docs-site.mjs +3 -3
- package/scripts/npm-publish-gate.mjs +4 -0
- package/scripts/release-notes.mjs +51 -0
- package/src/attach.mjs +16 -7
- package/src/board/board.css +180 -55
- package/src/board/board.js +5 -2
- package/src/board/entry.js +4 -4
- package/src/board/favicon.svg +1 -1
- package/src/board/floor.html +2 -2
- package/src/board/floor.js +4 -1
- package/src/board/history.js +1 -1
- package/src/board/index.html +22 -8
- package/src/board/sessions.js +55 -14
- package/src/board/strip.js +5 -1
- package/src/server.mjs +1 -1
- package/src/usage-poll.mjs +4 -1
- package/src/usage.mjs +19 -1
package/src/board/board.css
CHANGED
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
|
|
136
136
|
/* type scale. Fixed rem, range 13 to 52. The old board ran 13 to 21, which
|
|
137
137
|
is why it read as a spreadsheet */
|
|
138
|
-
--t--1: 0.
|
|
138
|
+
--t--1: 0.875rem; /* 14px the row register and the scaffolding strip */
|
|
139
139
|
--t-0: 0.9375rem; /* 15px labels, meta, controls, the narrow floor */
|
|
140
140
|
--t-1: 1.0625rem; /* 17px prompts, panel heads, body default */
|
|
141
141
|
--t-2: 1.3125rem; /* 21px section heads, secondary readings */
|
|
@@ -191,13 +191,21 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
191
191
|
.banner { background: var(--e1); border-bottom: 1px solid var(--edge); color: var(--text-2); font-size: var(--t-0); padding: 10px clamp(20px, 4vw, 40px); padding-top: calc(10px + env(safe-area-inset-top, 0px)); }
|
|
192
192
|
|
|
193
193
|
.masthead { position: relative; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding-block: 28px 24px; }
|
|
194
|
-
.brand { font-size: var(--t-1); font-weight: var(--w-head); letter-spacing: -.01em; }
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
-
.
|
|
194
|
+
.brand { display: inline-flex; align-items: center; gap: 10px; font-size: var(--t-1); font-weight: var(--w-head); letter-spacing: -.01em; }
|
|
195
|
+
/* the mark: a tile with the L cut as hip, knee and foot. Inline SVG in the
|
|
196
|
+
markup, currentColor, so it is one shape on the board, the floor, the
|
|
197
|
+
favicon and the site */
|
|
198
|
+
.brand-glyph { display: inline-flex; width: 22px; height: 22px; color: var(--text); }
|
|
199
|
+
.brand-glyph svg { width: 22px; height: 22px; display: block; }
|
|
200
|
+
.masthead-right { display: flex; align-items: center; gap: 8px 24px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
|
|
201
|
+
/* connection and scheduler are one status cluster, read as one phrase */
|
|
202
|
+
.masthead-status { display: inline-flex; align-items: center; gap: 10px; color: var(--text-3); }
|
|
203
|
+
.masthead-status .sched-status:not(:empty)::before { content: ""; display: inline-block; width: 3px; height: 3px; border-radius: 999px; background: var(--line-strong); margin: 0 10px 3px 0; vertical-align: middle; }
|
|
204
|
+
/* the other page is a control, in the same clothes as every other quiet control */
|
|
205
|
+
.floor-link { display: inline-flex; align-items: center; min-height: 36px; padding: 0 14px; border: 1px solid var(--control-edge); border-radius: var(--r-control); background: var(--e3); color: var(--text); font-weight: var(--w-head); text-decoration: none; transition: background-color 150ms var(--ease), border-color 150ms var(--ease); }
|
|
206
|
+
.floor-link:hover { background: var(--e4); border-color: #7D828A; color: var(--text); }
|
|
207
|
+
|
|
208
|
+
.sse-status { display: inline-flex; align-items: center; gap: 8px; flex-direction: row-reverse; }
|
|
201
209
|
.sse-word { color: var(--text-3); }
|
|
202
210
|
.sse-rule { width: 8px; height: 8px; border-radius: 999px; background: var(--ok); flex: none; }
|
|
203
211
|
.sse-rule.is-connecting { background: var(--warn); }
|
|
@@ -224,24 +232,24 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
224
232
|
/* the strip itself never wraps: the disclosure keeps the strip's own line at
|
|
225
233
|
the right (A.2) and the tokens wrap inside their own box, so four logins
|
|
226
234
|
that need two rows of tokens cost no third line for one button. */
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
/* the tokens are a small aligned table, dot, name, track, figure, so four
|
|
236
|
+
logins read as four rows of one instrument and not a ragged sentence. The
|
|
237
|
+
name is text: identity is the dot and the fill, never the colour of a word */
|
|
238
|
+
.capacity-strip { display: flex; align-items: flex-start; gap: 12px 32px; flex-wrap: nowrap; min-height: 44px; font-size: var(--t-0); }
|
|
239
|
+
.cap-tokens { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px 40px; flex: 1 1 auto; min-width: 0; }
|
|
240
|
+
/* the name column is as wide as the longest login on the strip (strip.js sets
|
|
241
|
+
--cap-name on the container): a second claude login reads "claude/work",
|
|
242
|
+
and a fixed 7ch put the slash over the track */
|
|
243
|
+
.cap-token { display: grid; grid-template-columns: 8px var(--cap-name, 7ch) 120px minmax(0, 1fr); align-items: center; gap: 10px; min-height: 28px; }
|
|
230
244
|
.cap-name { font-weight: var(--w-head); color: var(--text-2); }
|
|
231
|
-
.cap-name.id-claude { color: var(--id-claude); }
|
|
232
|
-
.cap-name.id-codex { color: var(--id-codex); }
|
|
233
|
-
.cap-name.id-agy { color: var(--id-agy); }
|
|
234
|
-
.cap-name.id-grok { color: var(--id-grok); }
|
|
235
|
-
.cap-name.id-copilot { color: var(--id-copilot); }
|
|
236
|
-
.cap-name.id-fake { color: var(--id-fake); }
|
|
237
245
|
.cap-track { position: relative; flex: none; width: 120px; height: 6px; background: var(--track); border-radius: 999px; box-shadow: var(--sink); overflow: hidden; }
|
|
238
246
|
.cap-fill { position: absolute; inset: 0 auto 0 0; border-radius: 999px; background: var(--calm); }
|
|
239
|
-
.cap-figure { color: var(--text-
|
|
247
|
+
.cap-figure { color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
240
248
|
/* a login at its wall is the one place a word IS the failure, so it may carry
|
|
241
249
|
the colour; a percentage never does, the track carries that */
|
|
242
250
|
.cap-figure.is-out { color: var(--danger-text); }
|
|
243
|
-
.cap-figure--none { color: var(--text-3); }
|
|
244
|
-
.cap-open { margin-left: auto; flex: none; }
|
|
251
|
+
.cap-figure--none { color: var(--text-3); grid-column: 3 / -1; }
|
|
252
|
+
.cap-open { margin-left: auto; flex: none; margin-top: -4px; }
|
|
245
253
|
.capacity-drawer { margin-top: 16px; }
|
|
246
254
|
|
|
247
255
|
/* the model rail on a panel head inside the drawer: a measured percentage, or
|
|
@@ -297,12 +305,12 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
297
305
|
.reading-sub--lead { margin-top: 22px; }
|
|
298
306
|
|
|
299
307
|
/* ---- section heads ------------------------------------------------------ */
|
|
300
|
-
.section-head { padding-block:
|
|
308
|
+
.section-head { padding-block: 40px 14px; }
|
|
301
309
|
.section-head h2 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
|
|
302
|
-
.section-head .region-meta { margin-top: 8px; font-size: var(--t-0); color: var(--text-
|
|
310
|
+
.section-head .region-meta { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); }
|
|
303
311
|
.section-head--quiet { padding-block: 56px 16px; display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
|
|
304
312
|
.section-head--quiet .region-meta { margin-top: 0; }
|
|
305
|
-
.hoisted { margin-top:
|
|
313
|
+
.hoisted { margin-top: 4px; display: flex; flex-direction: column; gap: 4px; font-size: var(--t-0); }
|
|
306
314
|
.hoisted-note { color: var(--text-2); }
|
|
307
315
|
.hoisted-note.tone-warn { color: var(--warn-text); }
|
|
308
316
|
.hoisted-note.tone-danger { color: var(--danger-text); }
|
|
@@ -310,44 +318,59 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
310
318
|
|
|
311
319
|
/* ---- terminals: rows inside one panel, separated by the only rule left --- */
|
|
312
320
|
.terms { padding: 4px 0; }
|
|
313
|
-
.term { padding:
|
|
321
|
+
.term { padding: 22px 28px; transition: background-color 150ms var(--ease); }
|
|
314
322
|
.term + .term { border-top: 1px solid var(--line); }
|
|
315
|
-
.term
|
|
316
|
-
|
|
323
|
+
.term:hover { background: rgba(255,255,255,.015); }
|
|
324
|
+
/* a row whose state just changed is lit for a moment, then settles: the one
|
|
325
|
+
motion on the board, and it answers what moved while the reader was away */
|
|
326
|
+
.term.is-changed { animation: settle 1400ms var(--ease) 1; }
|
|
327
|
+
@keyframes settle { 0% { background-color: rgba(92,145,255,.14); } 100% { background-color: transparent; } }
|
|
328
|
+
.term-row { display: flex; align-items: flex-start; gap: 32px; }
|
|
317
329
|
.term-body { flex: 1 1 auto; min-width: 0; }
|
|
318
330
|
.term-row > .term-body { flex-basis: 24ch; }
|
|
319
331
|
.term-clock { flex: none; display: flex; align-items: baseline; justify-content: flex-end; gap: 14px; font-size: var(--t--1); color: var(--text-3); white-space: nowrap; }
|
|
320
|
-
.term-register { display: flex; align-items:
|
|
332
|
+
.term-register { display: flex; align-items: center; gap: 6px 14px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
|
|
321
333
|
.term-where { color: var(--text-2); }
|
|
322
334
|
/* the register's data tokens (A.4 rows 7 to 9). They are one line with the
|
|
323
335
|
status word, in the register's own muted tone: what changed and how long it
|
|
324
336
|
has been quiet are observations, not demands. The model token is the one
|
|
325
337
|
fact here a reader looks for by name, so it takes the identity colour and
|
|
326
338
|
the mono face the other machine strings on this page use. */
|
|
327
|
-
.term-dirty, .term-ahead, .term-quiet { color: var(--text-3); }
|
|
328
|
-
|
|
339
|
+
.term-dirty, .term-ahead, .term-quiet { color: var(--text-3); font-size: var(--t--1); }
|
|
340
|
+
/* the rung is a dot in the agent's colour beside its name in text, the same
|
|
341
|
+
grammar as the capacity strip; the chip-id class stays for the tests and the
|
|
342
|
+
dot, the colour no longer lands on the word */
|
|
343
|
+
.term-model { display: inline-flex; align-items: center; gap: 7px; color: var(--text-2); }
|
|
344
|
+
.term-model .dot { width: 7px; height: 7px; }
|
|
329
345
|
/* the files line and the row's capacity phrase share one line, the phrase at
|
|
330
346
|
the right (A.2). The phrase is per MODEL, which is a fact about this row;
|
|
331
347
|
the region head carries the share clause for the login. */
|
|
332
|
-
.term-meta { display: flex; align-items: baseline; justify-content:
|
|
333
|
-
.term-meta .files { flex:
|
|
348
|
+
.term-meta { display: flex; align-items: baseline; justify-content: flex-start; gap: 6px 24px; flex-wrap: wrap; margin-top: 6px; }
|
|
349
|
+
.term-meta .files { flex: 0 1 auto; min-width: 0; }
|
|
334
350
|
/* the phrase wraps rather than pushing the row sideways: with a forecast it is
|
|
335
351
|
`about 2h 40m of fable left, from 9 samples over 4h`, two and a half times
|
|
336
352
|
the percentage form, and a nowrap run that long overflows the 400px board. */
|
|
337
|
-
.term-capacity { margin-top:
|
|
353
|
+
.term-capacity { margin-top: 4px; flex: 0 1 auto; min-width: 0; display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: 10px; font-size: var(--t--1); color: var(--text-3); }
|
|
338
354
|
/* the climb is a link beside the figure that explains it, never a fifth button
|
|
339
355
|
in the 2x2 grid, which is the shipped shape */
|
|
340
356
|
.term-climb { font-size: var(--t--1); }
|
|
341
357
|
/* the keyboard ring: a row the keys act on, marked on the edge the eye scans
|
|
342
358
|
down rather than by a fill that would compete with is-urgent */
|
|
343
359
|
.term.is-focused { box-shadow: inset 3px 0 0 var(--focus); }
|
|
344
|
-
.term-when { color: var(--text-3);
|
|
360
|
+
.term-when { color: var(--text-3); text-align: right; }
|
|
345
361
|
.term-id { color: var(--text-3); min-width: 4ch; text-align: right; font-family: var(--mono); }
|
|
346
|
-
|
|
347
|
-
.term-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.term-actions .btn
|
|
362
|
+
/* the clock over the actions, both right-aligned, in one side column */
|
|
363
|
+
.term-side { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; }
|
|
364
|
+
.term-clock { font-family: var(--mono); gap: 10px; }
|
|
365
|
+
.term-actions { flex: none; display: flex; justify-content: flex-end; gap: 8px; }
|
|
366
|
+
.term-actions .btn { white-space: nowrap; }
|
|
367
|
+
/* a control that cannot run is not drawn: its reason is hoisted to the region
|
|
368
|
+
head (the blocker paragraph stays in the DOM for aria-describedby) */
|
|
369
|
+
.term-actions .btn:disabled { display: none; }
|
|
370
|
+
/* End and Remove are quiet at rest; the colour comes on hover, and the
|
|
371
|
+
confirm row is where the decision is made */
|
|
372
|
+
.term-actions .btn-danger { color: var(--text-2); }
|
|
373
|
+
.term-actions .btn-danger:hover { color: var(--danger-text); }
|
|
351
374
|
|
|
352
375
|
.state { display: inline-flex; align-items: center; gap: 8px; color: var(--text-2); font-weight: var(--w-head); }
|
|
353
376
|
.mark { width: 8px; height: 8px; border-radius: 999px; flex: none; background: var(--text-3); }
|
|
@@ -360,16 +383,22 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
360
383
|
}
|
|
361
384
|
@keyframes breathe { 0%,100% { opacity: 1 } 50% { opacity: .45 } }
|
|
362
385
|
|
|
363
|
-
.term-prompt { margin-top:
|
|
386
|
+
.term-prompt { margin-top: 10px; font-size: var(--t-1); line-height: 1.45; max-width: 62ch; color: var(--text); }
|
|
364
387
|
.term-prompt--empty { color: var(--text-3); }
|
|
365
|
-
|
|
388
|
+
/* two lines of the prompt, then the expansion: a pasted brief is read in
|
|
389
|
+
Details, not in the list */
|
|
390
|
+
.panel-prompt { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; width: 100%; text-align: left; background: none; border: 0; padding: 0; margin-top: 10px; font-size: var(--t-1); line-height: 1.45; max-width: 64ch; color: var(--text); cursor: pointer; }
|
|
391
|
+
.panel-prompt[aria-expanded="true"] { -webkit-line-clamp: unset; }
|
|
366
392
|
.panel-prompt:hover { color: var(--accent-hi); }
|
|
367
393
|
|
|
368
|
-
.sentence { margin-top:
|
|
394
|
+
.sentence { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); max-width: 64ch; }
|
|
369
395
|
.sentence.tone-warn { color: var(--warn-text); }
|
|
370
396
|
.sentence.tone-danger { color: var(--danger-text); }
|
|
371
397
|
.sentence.tone-ok { color: var(--ok-text); }
|
|
372
398
|
.sentence.tone-muted { color: var(--text-3); }
|
|
399
|
+
/* a warning sentence carries its colour only on the row that needs a person;
|
|
400
|
+
on a running row it is an observation, and an observation is not amber */
|
|
401
|
+
.term:not(.is-urgent) .sentence.tone-warn { color: var(--text-2); }
|
|
373
402
|
.files { margin-top: 10px; font-size: var(--t--1); color: var(--text-3); }
|
|
374
403
|
.file { color: var(--text-3); }
|
|
375
404
|
.file.is-overlap { color: var(--warn-text); }
|
|
@@ -440,11 +469,15 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
440
469
|
.drawer-msg-when { font-size: var(--t--1); color: var(--text-3); font-family: var(--mono); }
|
|
441
470
|
|
|
442
471
|
/* ---- ledger: on the ground, unpanelled --------------------------------- */
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
.ledger
|
|
446
|
-
.ledger-
|
|
447
|
-
.ledger-cell
|
|
472
|
+
/* four cells of one anatomy: a count, a line of meta, and the verb on one
|
|
473
|
+
baseline whatever the meta's length */
|
|
474
|
+
.ledger { margin-top: 48px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 32px 40px; padding-top: 32px; border-top: 1px solid var(--edge); }
|
|
475
|
+
.ledger-cell { display: flex; flex-direction: column; }
|
|
476
|
+
.ledger-cell h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; line-height: 1.2; }
|
|
477
|
+
.ledger-cell .region-meta { margin-top: 6px; font-size: var(--t-0); color: var(--text-3); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
|
|
478
|
+
.ledger-cell .region-meta:empty { display: none; }
|
|
479
|
+
.ledger-actions { margin-top: auto; padding-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
480
|
+
.ledger-cell .empty-line { margin-top: 6px; font-size: var(--t-0); color: var(--text-3); }
|
|
448
481
|
|
|
449
482
|
.list { margin-top: 20px; }
|
|
450
483
|
.list li { padding: 14px 0; font-size: var(--t-0); }
|
|
@@ -528,7 +561,9 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
528
561
|
.row-meta { font-size: var(--t-0); color: var(--text-3); display: flex; gap: 14px; flex-wrap: wrap; }
|
|
529
562
|
.row-actions { margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
530
563
|
/* the demoted-notes disclosure in a terminal row */
|
|
531
|
-
.also { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: var(--t
|
|
564
|
+
.also { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
|
|
565
|
+
.term .also { margin-top: 2px; min-height: 28px; font-weight: var(--w-text); text-decoration-color: var(--line-strong); }
|
|
566
|
+
.term .also:hover { color: var(--text-2); }
|
|
532
567
|
.status-word { font-weight: var(--w-head); color: var(--text-2); }
|
|
533
568
|
.elapsed { font-family: var(--mono); color: var(--text-3); }
|
|
534
569
|
.where { color: var(--text-2); }
|
|
@@ -671,10 +706,10 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
|
|
|
671
706
|
120px of instrument costs the percentage its own line, and the percentage
|
|
672
707
|
with its state word carries the whole fact on its own */
|
|
673
708
|
.capacity-strip { display: block; }
|
|
674
|
-
.cap-tokens {
|
|
675
|
-
.cap-token {
|
|
709
|
+
.cap-tokens { grid-template-columns: 1fr; }
|
|
710
|
+
.cap-token { grid-template-columns: 8px var(--cap-name, 7ch) minmax(0, 1fr); }
|
|
676
711
|
.cap-track { display: none; }
|
|
677
|
-
.cap-figure {
|
|
712
|
+
.cap-figure { grid-column: 3; text-align: right; }
|
|
678
713
|
.cap-open { margin-left: 0; margin-top: 12px; }
|
|
679
714
|
.panel { padding: 22px; border-radius: 14px; }
|
|
680
715
|
.logins-pair { grid-template-columns: 1fr; }
|
|
@@ -689,16 +724,26 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
|
|
|
689
724
|
code { font-size: 1em; }
|
|
690
725
|
.section-head { padding-block: 24px 16px; }
|
|
691
726
|
.section-head--quiet { padding-block: 24px 16px; }
|
|
692
|
-
.term { padding:
|
|
693
|
-
.term-row { flex-direction: column; align-items: stretch; gap:
|
|
694
|
-
|
|
727
|
+
.term { padding: 20px; }
|
|
728
|
+
.term-row { flex-direction: column; align-items: stretch; gap: 12px; }
|
|
729
|
+
/* the 24ch basis is a width in the row direction; stacked, it became 264px
|
|
730
|
+
of empty height under every row */
|
|
731
|
+
.term-row > .term-body { flex-basis: auto; }
|
|
732
|
+
/* the side column dissolves: the clock goes above the register and the
|
|
733
|
+
actions below the body, both in the row's own flow */
|
|
734
|
+
.term-side { display: contents; }
|
|
735
|
+
.term-clock { order: -1; width: auto; justify-content: flex-end; gap: 10px; }
|
|
736
|
+
.term-actions { order: 1; justify-content: flex-start; flex-wrap: wrap; }
|
|
695
737
|
.term-id { min-width: 0; }
|
|
696
|
-
.term-actions { width: auto; justify-content: flex-start; }
|
|
697
|
-
.term-actions .btn { flex: 1 1 auto; }
|
|
698
738
|
.term-register { gap: 10px 14px; }
|
|
699
739
|
.kv { grid-template-columns: 1fr; gap: 4px 0; }
|
|
700
740
|
.kv-val + .kv-key { margin-top: 12px; }
|
|
701
741
|
.ledger { grid-template-columns: 1fr; gap: 32px; margin-top: 40px; }
|
|
742
|
+
.region-settings .section-head--quiet .disclosure { margin-left: 0; }
|
|
743
|
+
#card-entry { padding: 18px 20px; }
|
|
744
|
+
#card-entry .entry-opts .btn-text { margin-left: 0; }
|
|
745
|
+
.region-floor.is-empty { grid-template-columns: 1fr; gap: 6px; }
|
|
746
|
+
.first-run { padding: 22px; }
|
|
702
747
|
.history-row { grid-template-columns: 1fr; }
|
|
703
748
|
.history-filters { grid-template-columns: 1fr; }
|
|
704
749
|
.line { grid-template-columns: 1fr auto; gap: 6px 14px; }
|
|
@@ -711,6 +756,7 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
|
|
|
711
756
|
|
|
712
757
|
@media (prefers-reduced-motion: reduce) {
|
|
713
758
|
.mark.state-active { animation: none; }
|
|
759
|
+
.term.is-changed { animation: none; }
|
|
714
760
|
* { transition: none !important; }
|
|
715
761
|
.btn:active { transform: none; }
|
|
716
762
|
}
|
|
@@ -772,6 +818,7 @@ dialog { max-height: calc(100vh - 40px); overflow-y: auto; }
|
|
|
772
818
|
rather than in the page's own ground. The board's copy is inside the
|
|
773
819
|
Background panel and inherits its surface; this one has none to inherit. */
|
|
774
820
|
.floor-entry { margin-top: 20px; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-well); padding: 20px 24px; }
|
|
821
|
+
.floor-entry .confirm-row { background: none; padding: 0; }
|
|
775
822
|
.floor-entry .confirm-row { margin-top: 0; }
|
|
776
823
|
.floor-entry .confirm-row input[type="text"] { flex: 1 1 320px; min-width: 0; }
|
|
777
824
|
.floor-entry .field-help { margin-top: 10px; }
|
|
@@ -798,3 +845,81 @@ dialog { max-height: calc(100vh - 40px); overflow-y: auto; }
|
|
|
798
845
|
a label with no control focuses nothing when it is clicked, which is the one
|
|
799
846
|
thing a label promises. Same face as the row labels beside it. */
|
|
800
847
|
.form-row .form-label { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
|
|
848
|
+
|
|
849
|
+
/* ---- 2026-09-19: the polish pass (.design/board-v5) --------------------- */
|
|
850
|
+
/* A disclosure says where it opens with a chevron drawn here, not a typed > */
|
|
851
|
+
.disclosure::after { content: ""; display: inline-block; width: 7px; height: 7px; margin-left: 10px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(45deg); transition: transform 150ms var(--ease); }
|
|
852
|
+
.disclosure[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }
|
|
853
|
+
|
|
854
|
+
/* The verdict names a row; the sentence is the way to that row. A button in
|
|
855
|
+
the h1's own clothes: no underline, the accent only on hover and focus. */
|
|
856
|
+
.verdict-link { font: inherit; letter-spacing: inherit; color: inherit; background: none; border: 0; padding: 0; text-align: left; cursor: pointer; text-wrap: inherit; }
|
|
857
|
+
.verdict-link:hover { color: var(--accent-hi); }
|
|
858
|
+
|
|
859
|
+
/* The background entry: on the board it is the terminals panel's own footer
|
|
860
|
+
(a sibling, joined to the panel above it); under the Background panel, and
|
|
861
|
+
on the floor, it is a well of its own. Its choices are quiet chips inside
|
|
862
|
+
the sentence, and More settings stays the one text link. */
|
|
863
|
+
#card-entry { margin-top: 12px; background: var(--e1); border: 1px solid var(--edge); border-top-color: var(--line); border-radius: var(--r-well); padding: 20px 28px 18px; box-shadow: var(--sink); }
|
|
864
|
+
/* entry.js parks the entry right after the Terminals section; when the panel
|
|
865
|
+
above it has rows the two join into one object, and an empty panel is not
|
|
866
|
+
drawn at all */
|
|
867
|
+
#session-grid:empty { display: none; }
|
|
868
|
+
.region-terminals:has(#session-grid:not(:empty)):has(+ #card-entry:not([hidden])) #session-grid { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
869
|
+
.region-terminals:has(#session-grid:not(:empty)) + #card-entry { border-radius: 0 0 var(--r-panel) var(--r-panel); margin-top: -1px; }
|
|
870
|
+
#background #card-entry { margin-top: 14px; }
|
|
871
|
+
#card-entry .confirm-row { margin: 0; background: none; padding: 0; gap: 12px; }
|
|
872
|
+
#card-entry .confirm-row > span:first-child { font-weight: var(--w-head); color: var(--text-2); }
|
|
873
|
+
#card-entry .confirm-row input[type="text"] { flex: 1 1 280px; min-width: 0; background: var(--e0); }
|
|
874
|
+
#card-entry .confirm-row .field-help { flex: 1 0 100%; margin: 0; }
|
|
875
|
+
#card-entry .entry-opts { margin-top: 12px; display: flex; align-items: center; gap: 8px 6px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
|
|
876
|
+
#card-entry .entry-opts .btn-sm { min-height: 30px; font-weight: var(--w-text); color: var(--text-2); background: var(--e2); border-color: var(--edge-hi); }
|
|
877
|
+
#card-entry .entry-opts .btn-sm:hover { color: var(--text); background: var(--e3); }
|
|
878
|
+
#card-entry .entry-opts .btn-text { min-height: 30px; }
|
|
879
|
+
#card-entry .entry-opts select { font-size: var(--t--1); min-height: 30px; padding: 4px 8px; }
|
|
880
|
+
|
|
881
|
+
/* First run. A board with no terminal is the first thing a new reader sees,
|
|
882
|
+
so it is one lit panel with the three commands and a Copy each, not a grey
|
|
883
|
+
sentence. sessions.js hides it the moment a terminal is live. */
|
|
884
|
+
.first-run { margin-top: 0; padding: 28px 32px; }
|
|
885
|
+
.first-run h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
|
|
886
|
+
.first-run p { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); max-width: 56ch; }
|
|
887
|
+
.first-run-cmds { margin-top: 20px; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
|
|
888
|
+
.first-run-cmd { display: flex; align-items: center; justify-content: space-between; gap: 12px; background: var(--e0); border: 1px solid var(--edge); border-radius: var(--r-control); padding: 10px 10px 10px 16px; }
|
|
889
|
+
.first-run-cmd code { font-size: var(--t-0); color: var(--text); }
|
|
890
|
+
.first-run-cmd .dot { margin-right: 10px; }
|
|
891
|
+
.first-run-note { margin-top: 16px; font-size: var(--t--1); color: var(--text-3); }
|
|
892
|
+
|
|
893
|
+
/* Settings is the last ledger row, not a stray sentence with a button */
|
|
894
|
+
.region-settings .section-head--quiet { margin-top: 40px; padding-block: 24px 0; border-top: 1px solid var(--edge); align-items: center; }
|
|
895
|
+
.region-settings .section-head--quiet h2 { font-size: var(--t-2); }
|
|
896
|
+
.region-settings .section-head--quiet .region-meta { flex: 1 1 auto; }
|
|
897
|
+
.region-settings .section-head--quiet .disclosure { margin-left: auto; }
|
|
898
|
+
|
|
899
|
+
/* The floor: an empty station is one line, heading and sentence side by side,
|
|
900
|
+
so seven empty stations are seven lines and not seven screens. A station
|
|
901
|
+
with rows keeps its full section. floor.js sets is-empty. */
|
|
902
|
+
.region-floor.is-empty { display: grid; grid-template-columns: 16ch minmax(0, 1fr); gap: 8px 24px; align-items: baseline; padding: 18px 0; margin-top: 0; border-top: 1px solid var(--edge); }
|
|
903
|
+
.region-floor.is-empty .section-head { padding: 0; display: flex; align-items: baseline; gap: 6px 12px; flex-wrap: wrap; }
|
|
904
|
+
.region-floor.is-empty .section-head h2 { font-size: var(--t-1); white-space: nowrap; }
|
|
905
|
+
.region-floor.is-empty .section-head--quiet .btn { margin-left: 4px; min-height: 30px; }
|
|
906
|
+
.region-floor.is-empty .region-meta:empty, .region-floor.is-empty .card-list:empty, .region-floor.is-empty .ftable { display: none; }
|
|
907
|
+
.region-floor.is-empty .empty-line { margin: 0; color: var(--text-3); }
|
|
908
|
+
.region-floor:not(.is-empty) + .region-floor.is-empty { margin-top: 24px; }
|
|
909
|
+
.masthead-left { font-size: var(--t-0); }
|
|
910
|
+
/* ---- end of the 2026-09-19 block ---------------------------------------- */
|
|
911
|
+
|
|
912
|
+
/* ---- 2026-09-19, second pass: what the rendered seed showed ------------- */
|
|
913
|
+
/* the rung word and the ladder's rung names are text: the chip-id class was
|
|
914
|
+
winning on order, and the identity dot beside each one already says who */
|
|
915
|
+
.term-model[class*="chip-id-"], .ladder-name[class*="chip-id-"] { color: var(--text-2); }
|
|
916
|
+
/* the cards cell's verb sits on the baseline the other cells share, whatever
|
|
917
|
+
order its markup arrives in */
|
|
918
|
+
.ledger-cell .ledger-actions { order: 9; }
|
|
919
|
+
/* a number field wears the same clothes as a text field */
|
|
920
|
+
input[type="number"] { font: inherit; font-size: var(--t-0); color: var(--text); background: var(--e0); border: 1px solid var(--control-edge); border-radius: var(--r-control); padding: 9px 12px; min-height: 36px; width: 7ch; }
|
|
921
|
+
/* a finished row has no Land, so its land blocker is read by nothing on screen */
|
|
922
|
+
#finished-list .blocker { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
923
|
+
/* the entry's help line sits after Start on the same row */
|
|
924
|
+
#card-entry .confirm-row .field-help { flex: 0 1 auto; }
|
|
925
|
+
/* ---- end of the second pass ----------------------------------------------- */
|
package/src/board/board.js
CHANGED
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
// release lands on disk under a board that was started before it: the page
|
|
69
69
|
// then draws controls the process has no routes for (an empty agent select,
|
|
70
70
|
// no buckets). test/files-version.test.mjs pins this to package.json.
|
|
71
|
-
const FILES_VERSION = '0.
|
|
71
|
+
const FILES_VERSION = '0.16.1'
|
|
72
72
|
const TIMELINE_CAP = 12
|
|
73
73
|
// mirrors LOOPBACK in src/auth.mjs; state.bind is "<host>:<port>" and an IPv6
|
|
74
74
|
// host arrives bracketed
|
|
@@ -1744,7 +1744,10 @@
|
|
|
1744
1744
|
initSettings()
|
|
1745
1745
|
initNewCardDialog()
|
|
1746
1746
|
document.getElementById('new-card-btn').addEventListener('click', () => openNewCardDialog())
|
|
1747
|
-
|
|
1747
|
+
// first run: each command has a Copy, the same clipboard path the drawer uses
|
|
1748
|
+
for (const b of document.querySelectorAll('#first-run [data-copy]')) {
|
|
1749
|
+
b.addEventListener('click', () => { copyToClipboard(b.getAttribute('data-copy')); b.textContent = 'Copied'; setTimeout(() => { b.textContent = 'Copy' }, 1400) })
|
|
1750
|
+
}
|
|
1748
1751
|
document.getElementById('drawer-close').addEventListener('click', () => collapseRow())
|
|
1749
1752
|
document.addEventListener('keydown', (e) => {
|
|
1750
1753
|
if (e.key !== 'Escape') return
|
package/src/board/entry.js
CHANGED
|
@@ -234,7 +234,7 @@
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
function nounButton(text, key) {
|
|
237
|
-
return el('button', { type: 'button', class: 'btn btn-
|
|
237
|
+
return el('button', { type: 'button', class: 'btn btn-secondary btn-sm entry-noun', onclick: () => { entryState.editing = key; renderEntryLine() } }, [text])
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
function renderEntryLine() {
|
|
@@ -262,7 +262,7 @@
|
|
|
262
262
|
// flex row with a gap on the raised surface, which is exactly this line.
|
|
263
263
|
box.appendChild(el('div', { class: 'confirm-row' }, [el('span', {}, ['Run in the background:']), task, start, reason]))
|
|
264
264
|
|
|
265
|
-
const line = el('p', { class: 'field-help' })
|
|
265
|
+
const line = el('p', { class: 'field-help entry-opts' })
|
|
266
266
|
line.appendChild(document.createTextNode('in '))
|
|
267
267
|
if (entryState.editing === 'repo') {
|
|
268
268
|
line.appendChild(nounSelect(knownRepos().map((r) => ({ value: r.path, label: r.name })), repo ? repo.path : '', (v) => { entryState.repo = knownRepos().find((r) => r.path === v) || null }))
|
|
@@ -297,13 +297,13 @@
|
|
|
297
297
|
// the sentence names the legs Start posts, models and all
|
|
298
298
|
line.appendChild(nounButton(ladderSentence(entryChain()), 'ladder'))
|
|
299
299
|
}
|
|
300
|
-
line.appendChild(document.createTextNode('
|
|
300
|
+
line.appendChild(document.createTextNode(' '))
|
|
301
301
|
if (entryState.editing === 'pipeline') {
|
|
302
302
|
line.appendChild(nounSelect(Object.keys(PIPELINE_WORDS).map((v) => ({ value: v, label: PIPELINE_WORDS[v] })), entryState.pipeline, (v) => { entryState.pipeline = v }))
|
|
303
303
|
} else {
|
|
304
304
|
line.appendChild(nounButton(PIPELINE_WORDS[entryState.pipeline] || 'build only', 'pipeline'))
|
|
305
305
|
}
|
|
306
|
-
line.appendChild(document.createTextNode('
|
|
306
|
+
line.appendChild(document.createTextNode(' '))
|
|
307
307
|
line.appendChild(el('button', { type: 'button', class: 'btn btn-text', onclick: () => onMoreSettings(entryState) }, ['More settings']))
|
|
308
308
|
box.appendChild(line)
|
|
309
309
|
}
|
package/src/board/favicon.svg
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#F3F4F7"/><path d="M8 5.5v8.5l4 4h5" stroke="#0E1012" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
|
package/src/board/floor.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
<header class="masthead wrap">
|
|
13
13
|
<div class="masthead-left">
|
|
14
|
-
<h1 class="brand"><span class="brand-glyph" aria-hidden="true"
|
|
14
|
+
<h1 class="brand"><span class="brand-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><rect width="24" height="24" rx="6" fill="currentColor"/><path d="M8 5.5v8.5l4 4h5" stroke="#0E1012" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span> Leg floor</h1>
|
|
15
15
|
<!-- the connection state sits second, immediately after the brand, the same
|
|
16
16
|
place it sits on the board: the two bars are read as one bar. -->
|
|
17
17
|
<div class="sse-status" role="status">
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
<section class="capacity" aria-label="Capacity by login">
|
|
44
44
|
<div class="capacity-strip">
|
|
45
45
|
<div class="cap-tokens" id="capacity-tokens"></div>
|
|
46
|
-
<button type="button" class="btn btn-secondary disclosure cap-open" id="capacity-toggle" aria-expanded="false" aria-controls="capacity-drawer">Capacity and models
|
|
46
|
+
<button type="button" class="btn btn-secondary disclosure cap-open" id="capacity-toggle" aria-expanded="false" aria-controls="capacity-drawer">Capacity and models</button>
|
|
47
47
|
</div>
|
|
48
48
|
<div class="drawer capacity-drawer" id="capacity-drawer" hidden>
|
|
49
49
|
<section class="logins" id="floor-accounts" aria-label="Logins"></section>
|
package/src/board/floor.js
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
// name. test/board-updates.test.mjs pins `state.stopped` in this file by
|
|
51
51
|
// source text, which is why the module object keeps the name.
|
|
52
52
|
// pinned to package.json by test/files-version.test.mjs; see board.js FILES_VERSION
|
|
53
|
-
const FILES_VERSION = '0.
|
|
53
|
+
const FILES_VERSION = '0.16.1'
|
|
54
54
|
const state = { es: null, retryMs: 1000, timers: [], stopped: false, sseRequest: 0, floorRequest: 0, trunkRequest: 0, headRequest: 0, cardsRequest: 0, lastReadingAt: null, bind: (typeof location !== 'undefined' && location.host) || '127.0.0.1:4747', pendingFloor: null, pendingCards: null, cards: new Map(), blockers: new Map(), doneOpen: false, ringId: null, scheduler: {}, trunkOff: false }
|
|
55
55
|
|
|
56
56
|
// What the entry row reads on every render: the terminals (for the repo it
|
|
@@ -443,6 +443,8 @@
|
|
|
443
443
|
const table = body.parentElement
|
|
444
444
|
const section = table && table.parentElement
|
|
445
445
|
if (section) { const old = section.querySelector('.empty-line'); if (old) old.remove() }
|
|
446
|
+
// an empty station is one line (board.css .region-floor.is-empty)
|
|
447
|
+
if (section && section.classList) section.classList.toggle('is-empty', !list.length)
|
|
446
448
|
const thead = table && table.querySelector('thead')
|
|
447
449
|
if (!list.length) {
|
|
448
450
|
if (thead) thead.hidden = true
|
|
@@ -747,6 +749,7 @@
|
|
|
747
749
|
const count = document.getElementById(station.count)
|
|
748
750
|
if (count) count.textContent = String(cards.length)
|
|
749
751
|
if (section && section.querySelector) { const old = section.querySelector('.empty-line'); if (old && old.remove) old.remove() }
|
|
752
|
+
if (section && section.classList) section.classList.toggle('is-empty', !cards.length)
|
|
750
753
|
if (!cards.length) {
|
|
751
754
|
box.textContent = ''
|
|
752
755
|
box.signature = ''
|
package/src/board/history.js
CHANGED
|
@@ -304,7 +304,7 @@
|
|
|
304
304
|
? `${byAgent}${filtered ? `; ${state.total} match the filters` : ''}${state.refreshedAt ? `; looked ${whenAgo(state.refreshedAt)}` : ''}`
|
|
305
305
|
: 'Leg looks in the Claude Code, Codex, Grok, Antigravity and Copilot homes on this machine, and its own sessions.'
|
|
306
306
|
const btn = el('button', { type: 'button', class: 'btn btn-secondary', 'aria-expanded': state.open ? 'true' : 'false', 'aria-controls': 'history-drawer', 'data-focus-key': 'history-toggle' },
|
|
307
|
-
[state.open ? 'Hide
|
|
307
|
+
[state.open ? 'Hide' : 'Browse'])
|
|
308
308
|
btn.addEventListener('click', () => { state.open = !state.open; if (state.open && state.worktrees === null) loadWorktrees(); render() })
|
|
309
309
|
slot.appendChild(btn)
|
|
310
310
|
panel.hidden = !state.open
|
package/src/board/index.html
CHANGED
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
<div class="wrap">
|
|
19
19
|
|
|
20
20
|
<header class="masthead">
|
|
21
|
-
<p class="brand"><span class="brand-glyph" aria-hidden="true"
|
|
21
|
+
<p class="brand"><span class="brand-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><rect width="24" height="24" rx="6" fill="currentColor"/><path d="M8 5.5v8.5l4 4h5" stroke="#0E1012" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span> Leg</p>
|
|
22
22
|
<div class="masthead-right">
|
|
23
|
-
<div class="
|
|
24
|
-
<
|
|
25
|
-
|
|
23
|
+
<div class="masthead-status">
|
|
24
|
+
<div class="sse-status" role="status">
|
|
25
|
+
<span class="sse-word" id="sse-text">connecting</span>
|
|
26
|
+
<span class="sse-rule is-connecting" id="sse-dot" aria-hidden="true"></span>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="sched-status" id="sched-status"></div>
|
|
26
29
|
</div>
|
|
27
|
-
<div class="sched-status" id="sched-status"></div>
|
|
28
30
|
<a class="floor-link" href="/floor">Floor</a>
|
|
29
31
|
</div>
|
|
30
32
|
</header>
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
<section class="capacity" aria-label="Capacity by login">
|
|
47
49
|
<div class="capacity-strip">
|
|
48
50
|
<div class="cap-tokens" id="capacity-tokens"></div>
|
|
49
|
-
<button type="button" class="btn btn-secondary disclosure cap-open" id="capacity-toggle" aria-expanded="false" aria-controls="capacity-drawer">Capacity and models
|
|
51
|
+
<button type="button" class="btn btn-secondary disclosure cap-open" id="capacity-toggle" aria-expanded="false" aria-controls="capacity-drawer">Capacity and models</button>
|
|
50
52
|
</div>
|
|
51
53
|
<div class="drawer capacity-drawer" id="capacity-drawer" hidden>
|
|
52
54
|
<section class="logins" id="accounts" aria-label="Logins"></section>
|
|
@@ -67,7 +69,19 @@
|
|
|
67
69
|
<span class="chip" id="whoami" hidden></span>
|
|
68
70
|
</div>
|
|
69
71
|
<div class="panel terms" id="session-grid"></div>
|
|
70
|
-
|
|
72
|
+
<!-- first run: the board with no terminal is the first screen a new reader
|
|
73
|
+
sees, so it is a lit panel with the commands and a Copy each. sessions.js
|
|
74
|
+
hides it the moment a terminal is live; board.js wires Copy. -->
|
|
75
|
+
<div class="panel panel--lit first-run" id="first-run" hidden>
|
|
76
|
+
<h3>No terminal is running</h3>
|
|
77
|
+
<p>Start one in any repo with the same command you use today, and Leg runs it with the board alongside. It appears here within a second of the agent's first turn.</p>
|
|
78
|
+
<div class="first-run-cmds">
|
|
79
|
+
<div class="first-run-cmd"><span><span class="dot id-claude" aria-hidden="true"></span><code>leg claude</code></span><button type="button" class="btn btn-secondary btn-sm" data-copy="leg claude" aria-label="Copy leg claude">Copy</button></div>
|
|
80
|
+
<div class="first-run-cmd"><span><span class="dot id-codex" aria-hidden="true"></span><code>leg codex</code></span><button type="button" class="btn btn-secondary btn-sm" data-copy="leg codex" aria-label="Copy leg codex">Copy</button></div>
|
|
81
|
+
<div class="first-run-cmd"><span><span class="dot id-agy" aria-hidden="true"></span><code>leg agy</code></span><button type="button" class="btn btn-secondary btn-sm" data-copy="leg agy" aria-label="Copy leg agy">Copy</button></div>
|
|
82
|
+
</div>
|
|
83
|
+
<p class="first-run-note">Usage per login, the handoff ladder and the context bundle all start with the first terminal.</p>
|
|
84
|
+
</div>
|
|
71
85
|
</section>
|
|
72
86
|
|
|
73
87
|
<!-- C.1: a card is a terminal you are not sitting at. Live cards are rows
|
|
@@ -118,7 +132,7 @@
|
|
|
118
132
|
<div class="ledger-actions">
|
|
119
133
|
<button type="button" class="btn btn-secondary" id="new-card-btn">New card</button>
|
|
120
134
|
</div>
|
|
121
|
-
<p class="empty-line" id="empty-state" hidden>A card runs an agent headless in its own worktree
|
|
135
|
+
<p class="empty-line" id="empty-state" hidden>A card runs an agent headless in its own worktree.</p>
|
|
122
136
|
</div>
|
|
123
137
|
|
|
124
138
|
</section>
|