@ucsandman/legcli 0.15.0 → 0.16.0
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 +54 -0
- package/README.md +5 -3
- package/docs/DEVIATIONS.md +1 -0
- package/docs/ERRORS.md +39 -0
- package/docs/adapters.md +5 -2
- package/docs/board-guide.md +33 -29
- package/docs/cli-contracts.md +14 -6
- package/docs/faq.md +4 -3
- 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/npm-publish-gate.mjs +4 -0
- package/scripts/release-notes.mjs +51 -0
- package/src/board/board.css +177 -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 +1 -1
- package/src/hook.mjs +5 -4
- package/src/taps/claude-usage.mjs +5 -4
- package/src/taps/claude.mjs +34 -6
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,21 @@ 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
|
+
.cap-token { display: grid; grid-template-columns: 8px 7ch 120px minmax(0, 1fr); align-items: center; gap: 10px; min-height: 28px; }
|
|
230
241
|
.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
242
|
.cap-track { position: relative; flex: none; width: 120px; height: 6px; background: var(--track); border-radius: 999px; box-shadow: var(--sink); overflow: hidden; }
|
|
238
243
|
.cap-fill { position: absolute; inset: 0 auto 0 0; border-radius: 999px; background: var(--calm); }
|
|
239
|
-
.cap-figure { color: var(--text-
|
|
244
|
+
.cap-figure { color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
240
245
|
/* a login at its wall is the one place a word IS the failure, so it may carry
|
|
241
246
|
the colour; a percentage never does, the track carries that */
|
|
242
247
|
.cap-figure.is-out { color: var(--danger-text); }
|
|
243
|
-
.cap-figure--none { color: var(--text-3); }
|
|
244
|
-
.cap-open { margin-left: auto; flex: none; }
|
|
248
|
+
.cap-figure--none { color: var(--text-3); grid-column: 3 / -1; }
|
|
249
|
+
.cap-open { margin-left: auto; flex: none; margin-top: -4px; }
|
|
245
250
|
.capacity-drawer { margin-top: 16px; }
|
|
246
251
|
|
|
247
252
|
/* the model rail on a panel head inside the drawer: a measured percentage, or
|
|
@@ -297,12 +302,12 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
297
302
|
.reading-sub--lead { margin-top: 22px; }
|
|
298
303
|
|
|
299
304
|
/* ---- section heads ------------------------------------------------------ */
|
|
300
|
-
.section-head { padding-block:
|
|
305
|
+
.section-head { padding-block: 40px 14px; }
|
|
301
306
|
.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-
|
|
307
|
+
.section-head .region-meta { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); }
|
|
303
308
|
.section-head--quiet { padding-block: 56px 16px; display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
|
|
304
309
|
.section-head--quiet .region-meta { margin-top: 0; }
|
|
305
|
-
.hoisted { margin-top:
|
|
310
|
+
.hoisted { margin-top: 4px; display: flex; flex-direction: column; gap: 4px; font-size: var(--t-0); }
|
|
306
311
|
.hoisted-note { color: var(--text-2); }
|
|
307
312
|
.hoisted-note.tone-warn { color: var(--warn-text); }
|
|
308
313
|
.hoisted-note.tone-danger { color: var(--danger-text); }
|
|
@@ -310,44 +315,59 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
310
315
|
|
|
311
316
|
/* ---- terminals: rows inside one panel, separated by the only rule left --- */
|
|
312
317
|
.terms { padding: 4px 0; }
|
|
313
|
-
.term { padding:
|
|
318
|
+
.term { padding: 22px 28px; transition: background-color 150ms var(--ease); }
|
|
314
319
|
.term + .term { border-top: 1px solid var(--line); }
|
|
315
|
-
.term
|
|
316
|
-
|
|
320
|
+
.term:hover { background: rgba(255,255,255,.015); }
|
|
321
|
+
/* a row whose state just changed is lit for a moment, then settles: the one
|
|
322
|
+
motion on the board, and it answers what moved while the reader was away */
|
|
323
|
+
.term.is-changed { animation: settle 1400ms var(--ease) 1; }
|
|
324
|
+
@keyframes settle { 0% { background-color: rgba(92,145,255,.14); } 100% { background-color: transparent; } }
|
|
325
|
+
.term-row { display: flex; align-items: flex-start; gap: 32px; }
|
|
317
326
|
.term-body { flex: 1 1 auto; min-width: 0; }
|
|
318
327
|
.term-row > .term-body { flex-basis: 24ch; }
|
|
319
328
|
.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:
|
|
329
|
+
.term-register { display: flex; align-items: center; gap: 6px 14px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
|
|
321
330
|
.term-where { color: var(--text-2); }
|
|
322
331
|
/* the register's data tokens (A.4 rows 7 to 9). They are one line with the
|
|
323
332
|
status word, in the register's own muted tone: what changed and how long it
|
|
324
333
|
has been quiet are observations, not demands. The model token is the one
|
|
325
334
|
fact here a reader looks for by name, so it takes the identity colour and
|
|
326
335
|
the mono face the other machine strings on this page use. */
|
|
327
|
-
.term-dirty, .term-ahead, .term-quiet { color: var(--text-3); }
|
|
328
|
-
|
|
336
|
+
.term-dirty, .term-ahead, .term-quiet { color: var(--text-3); font-size: var(--t--1); }
|
|
337
|
+
/* the rung is a dot in the agent's colour beside its name in text, the same
|
|
338
|
+
grammar as the capacity strip; the chip-id class stays for the tests and the
|
|
339
|
+
dot, the colour no longer lands on the word */
|
|
340
|
+
.term-model { display: inline-flex; align-items: center; gap: 7px; color: var(--text-2); }
|
|
341
|
+
.term-model .dot { width: 7px; height: 7px; }
|
|
329
342
|
/* the files line and the row's capacity phrase share one line, the phrase at
|
|
330
343
|
the right (A.2). The phrase is per MODEL, which is a fact about this row;
|
|
331
344
|
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:
|
|
345
|
+
.term-meta { display: flex; align-items: baseline; justify-content: flex-start; gap: 6px 24px; flex-wrap: wrap; margin-top: 6px; }
|
|
346
|
+
.term-meta .files { flex: 0 1 auto; min-width: 0; }
|
|
334
347
|
/* the phrase wraps rather than pushing the row sideways: with a forecast it is
|
|
335
348
|
`about 2h 40m of fable left, from 9 samples over 4h`, two and a half times
|
|
336
349
|
the percentage form, and a nowrap run that long overflows the 400px board. */
|
|
337
|
-
.term-capacity { margin-top:
|
|
350
|
+
.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
351
|
/* the climb is a link beside the figure that explains it, never a fifth button
|
|
339
352
|
in the 2x2 grid, which is the shipped shape */
|
|
340
353
|
.term-climb { font-size: var(--t--1); }
|
|
341
354
|
/* the keyboard ring: a row the keys act on, marked on the edge the eye scans
|
|
342
355
|
down rather than by a fill that would compete with is-urgent */
|
|
343
356
|
.term.is-focused { box-shadow: inset 3px 0 0 var(--focus); }
|
|
344
|
-
.term-when { color: var(--text-3);
|
|
357
|
+
.term-when { color: var(--text-3); text-align: right; }
|
|
345
358
|
.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
|
|
359
|
+
/* the clock over the actions, both right-aligned, in one side column */
|
|
360
|
+
.term-side { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; }
|
|
361
|
+
.term-clock { font-family: var(--mono); gap: 10px; }
|
|
362
|
+
.term-actions { flex: none; display: flex; justify-content: flex-end; gap: 8px; }
|
|
363
|
+
.term-actions .btn { white-space: nowrap; }
|
|
364
|
+
/* a control that cannot run is not drawn: its reason is hoisted to the region
|
|
365
|
+
head (the blocker paragraph stays in the DOM for aria-describedby) */
|
|
366
|
+
.term-actions .btn:disabled { display: none; }
|
|
367
|
+
/* End and Remove are quiet at rest; the colour comes on hover, and the
|
|
368
|
+
confirm row is where the decision is made */
|
|
369
|
+
.term-actions .btn-danger { color: var(--text-2); }
|
|
370
|
+
.term-actions .btn-danger:hover { color: var(--danger-text); }
|
|
351
371
|
|
|
352
372
|
.state { display: inline-flex; align-items: center; gap: 8px; color: var(--text-2); font-weight: var(--w-head); }
|
|
353
373
|
.mark { width: 8px; height: 8px; border-radius: 999px; flex: none; background: var(--text-3); }
|
|
@@ -360,16 +380,22 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
360
380
|
}
|
|
361
381
|
@keyframes breathe { 0%,100% { opacity: 1 } 50% { opacity: .45 } }
|
|
362
382
|
|
|
363
|
-
.term-prompt { margin-top:
|
|
383
|
+
.term-prompt { margin-top: 10px; font-size: var(--t-1); line-height: 1.45; max-width: 62ch; color: var(--text); }
|
|
364
384
|
.term-prompt--empty { color: var(--text-3); }
|
|
365
|
-
|
|
385
|
+
/* two lines of the prompt, then the expansion: a pasted brief is read in
|
|
386
|
+
Details, not in the list */
|
|
387
|
+
.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; }
|
|
388
|
+
.panel-prompt[aria-expanded="true"] { -webkit-line-clamp: unset; }
|
|
366
389
|
.panel-prompt:hover { color: var(--accent-hi); }
|
|
367
390
|
|
|
368
|
-
.sentence { margin-top:
|
|
391
|
+
.sentence { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); max-width: 64ch; }
|
|
369
392
|
.sentence.tone-warn { color: var(--warn-text); }
|
|
370
393
|
.sentence.tone-danger { color: var(--danger-text); }
|
|
371
394
|
.sentence.tone-ok { color: var(--ok-text); }
|
|
372
395
|
.sentence.tone-muted { color: var(--text-3); }
|
|
396
|
+
/* a warning sentence carries its colour only on the row that needs a person;
|
|
397
|
+
on a running row it is an observation, and an observation is not amber */
|
|
398
|
+
.term:not(.is-urgent) .sentence.tone-warn { color: var(--text-2); }
|
|
373
399
|
.files { margin-top: 10px; font-size: var(--t--1); color: var(--text-3); }
|
|
374
400
|
.file { color: var(--text-3); }
|
|
375
401
|
.file.is-overlap { color: var(--warn-text); }
|
|
@@ -440,11 +466,15 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
440
466
|
.drawer-msg-when { font-size: var(--t--1); color: var(--text-3); font-family: var(--mono); }
|
|
441
467
|
|
|
442
468
|
/* ---- ledger: on the ground, unpanelled --------------------------------- */
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
.ledger
|
|
446
|
-
.ledger-
|
|
447
|
-
.ledger-cell
|
|
469
|
+
/* four cells of one anatomy: a count, a line of meta, and the verb on one
|
|
470
|
+
baseline whatever the meta's length */
|
|
471
|
+
.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); }
|
|
472
|
+
.ledger-cell { display: flex; flex-direction: column; }
|
|
473
|
+
.ledger-cell h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; line-height: 1.2; }
|
|
474
|
+
.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; }
|
|
475
|
+
.ledger-cell .region-meta:empty { display: none; }
|
|
476
|
+
.ledger-actions { margin-top: auto; padding-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
477
|
+
.ledger-cell .empty-line { margin-top: 6px; font-size: var(--t-0); color: var(--text-3); }
|
|
448
478
|
|
|
449
479
|
.list { margin-top: 20px; }
|
|
450
480
|
.list li { padding: 14px 0; font-size: var(--t-0); }
|
|
@@ -528,7 +558,9 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
528
558
|
.row-meta { font-size: var(--t-0); color: var(--text-3); display: flex; gap: 14px; flex-wrap: wrap; }
|
|
529
559
|
.row-actions { margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
530
560
|
/* the demoted-notes disclosure in a terminal row */
|
|
531
|
-
.also { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: var(--t
|
|
561
|
+
.also { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
|
|
562
|
+
.term .also { margin-top: 2px; min-height: 28px; font-weight: var(--w-text); text-decoration-color: var(--line-strong); }
|
|
563
|
+
.term .also:hover { color: var(--text-2); }
|
|
532
564
|
.status-word { font-weight: var(--w-head); color: var(--text-2); }
|
|
533
565
|
.elapsed { font-family: var(--mono); color: var(--text-3); }
|
|
534
566
|
.where { color: var(--text-2); }
|
|
@@ -671,10 +703,10 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
|
|
|
671
703
|
120px of instrument costs the percentage its own line, and the percentage
|
|
672
704
|
with its state word carries the whole fact on its own */
|
|
673
705
|
.capacity-strip { display: block; }
|
|
674
|
-
.cap-tokens {
|
|
675
|
-
.cap-token {
|
|
706
|
+
.cap-tokens { grid-template-columns: 1fr; }
|
|
707
|
+
.cap-token { grid-template-columns: 8px 7ch minmax(0, 1fr); }
|
|
676
708
|
.cap-track { display: none; }
|
|
677
|
-
.cap-figure {
|
|
709
|
+
.cap-figure { grid-column: 3; text-align: right; }
|
|
678
710
|
.cap-open { margin-left: 0; margin-top: 12px; }
|
|
679
711
|
.panel { padding: 22px; border-radius: 14px; }
|
|
680
712
|
.logins-pair { grid-template-columns: 1fr; }
|
|
@@ -689,16 +721,26 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
|
|
|
689
721
|
code { font-size: 1em; }
|
|
690
722
|
.section-head { padding-block: 24px 16px; }
|
|
691
723
|
.section-head--quiet { padding-block: 24px 16px; }
|
|
692
|
-
.term { padding:
|
|
693
|
-
.term-row { flex-direction: column; align-items: stretch; gap:
|
|
694
|
-
|
|
724
|
+
.term { padding: 20px; }
|
|
725
|
+
.term-row { flex-direction: column; align-items: stretch; gap: 12px; }
|
|
726
|
+
/* the 24ch basis is a width in the row direction; stacked, it became 264px
|
|
727
|
+
of empty height under every row */
|
|
728
|
+
.term-row > .term-body { flex-basis: auto; }
|
|
729
|
+
/* the side column dissolves: the clock goes above the register and the
|
|
730
|
+
actions below the body, both in the row's own flow */
|
|
731
|
+
.term-side { display: contents; }
|
|
732
|
+
.term-clock { order: -1; width: auto; justify-content: flex-end; gap: 10px; }
|
|
733
|
+
.term-actions { order: 1; justify-content: flex-start; flex-wrap: wrap; }
|
|
695
734
|
.term-id { min-width: 0; }
|
|
696
|
-
.term-actions { width: auto; justify-content: flex-start; }
|
|
697
|
-
.term-actions .btn { flex: 1 1 auto; }
|
|
698
735
|
.term-register { gap: 10px 14px; }
|
|
699
736
|
.kv { grid-template-columns: 1fr; gap: 4px 0; }
|
|
700
737
|
.kv-val + .kv-key { margin-top: 12px; }
|
|
701
738
|
.ledger { grid-template-columns: 1fr; gap: 32px; margin-top: 40px; }
|
|
739
|
+
.region-settings .section-head--quiet .disclosure { margin-left: 0; }
|
|
740
|
+
#card-entry { padding: 18px 20px; }
|
|
741
|
+
#card-entry .entry-opts .btn-text { margin-left: 0; }
|
|
742
|
+
.region-floor.is-empty { grid-template-columns: 1fr; gap: 6px; }
|
|
743
|
+
.first-run { padding: 22px; }
|
|
702
744
|
.history-row { grid-template-columns: 1fr; }
|
|
703
745
|
.history-filters { grid-template-columns: 1fr; }
|
|
704
746
|
.line { grid-template-columns: 1fr auto; gap: 6px 14px; }
|
|
@@ -711,6 +753,7 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
|
|
|
711
753
|
|
|
712
754
|
@media (prefers-reduced-motion: reduce) {
|
|
713
755
|
.mark.state-active { animation: none; }
|
|
756
|
+
.term.is-changed { animation: none; }
|
|
714
757
|
* { transition: none !important; }
|
|
715
758
|
.btn:active { transform: none; }
|
|
716
759
|
}
|
|
@@ -772,6 +815,7 @@ dialog { max-height: calc(100vh - 40px); overflow-y: auto; }
|
|
|
772
815
|
rather than in the page's own ground. The board's copy is inside the
|
|
773
816
|
Background panel and inherits its surface; this one has none to inherit. */
|
|
774
817
|
.floor-entry { margin-top: 20px; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-well); padding: 20px 24px; }
|
|
818
|
+
.floor-entry .confirm-row { background: none; padding: 0; }
|
|
775
819
|
.floor-entry .confirm-row { margin-top: 0; }
|
|
776
820
|
.floor-entry .confirm-row input[type="text"] { flex: 1 1 320px; min-width: 0; }
|
|
777
821
|
.floor-entry .field-help { margin-top: 10px; }
|
|
@@ -798,3 +842,81 @@ dialog { max-height: calc(100vh - 40px); overflow-y: auto; }
|
|
|
798
842
|
a label with no control focuses nothing when it is clicked, which is the one
|
|
799
843
|
thing a label promises. Same face as the row labels beside it. */
|
|
800
844
|
.form-row .form-label { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
|
|
845
|
+
|
|
846
|
+
/* ---- 2026-09-19: the polish pass (.design/board-v5) --------------------- */
|
|
847
|
+
/* A disclosure says where it opens with a chevron drawn here, not a typed > */
|
|
848
|
+
.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); }
|
|
849
|
+
.disclosure[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }
|
|
850
|
+
|
|
851
|
+
/* The verdict names a row; the sentence is the way to that row. A button in
|
|
852
|
+
the h1's own clothes: no underline, the accent only on hover and focus. */
|
|
853
|
+
.verdict-link { font: inherit; letter-spacing: inherit; color: inherit; background: none; border: 0; padding: 0; text-align: left; cursor: pointer; text-wrap: inherit; }
|
|
854
|
+
.verdict-link:hover { color: var(--accent-hi); }
|
|
855
|
+
|
|
856
|
+
/* The background entry: on the board it is the terminals panel's own footer
|
|
857
|
+
(a sibling, joined to the panel above it); under the Background panel, and
|
|
858
|
+
on the floor, it is a well of its own. Its choices are quiet chips inside
|
|
859
|
+
the sentence, and More settings stays the one text link. */
|
|
860
|
+
#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); }
|
|
861
|
+
/* entry.js parks the entry right after the Terminals section; when the panel
|
|
862
|
+
above it has rows the two join into one object, and an empty panel is not
|
|
863
|
+
drawn at all */
|
|
864
|
+
#session-grid:empty { display: none; }
|
|
865
|
+
.region-terminals:has(#session-grid:not(:empty)):has(+ #card-entry:not([hidden])) #session-grid { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
866
|
+
.region-terminals:has(#session-grid:not(:empty)) + #card-entry { border-radius: 0 0 var(--r-panel) var(--r-panel); margin-top: -1px; }
|
|
867
|
+
#background #card-entry { margin-top: 14px; }
|
|
868
|
+
#card-entry .confirm-row { margin: 0; background: none; padding: 0; gap: 12px; }
|
|
869
|
+
#card-entry .confirm-row > span:first-child { font-weight: var(--w-head); color: var(--text-2); }
|
|
870
|
+
#card-entry .confirm-row input[type="text"] { flex: 1 1 280px; min-width: 0; background: var(--e0); }
|
|
871
|
+
#card-entry .confirm-row .field-help { flex: 1 0 100%; margin: 0; }
|
|
872
|
+
#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); }
|
|
873
|
+
#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); }
|
|
874
|
+
#card-entry .entry-opts .btn-sm:hover { color: var(--text); background: var(--e3); }
|
|
875
|
+
#card-entry .entry-opts .btn-text { min-height: 30px; }
|
|
876
|
+
#card-entry .entry-opts select { font-size: var(--t--1); min-height: 30px; padding: 4px 8px; }
|
|
877
|
+
|
|
878
|
+
/* First run. A board with no terminal is the first thing a new reader sees,
|
|
879
|
+
so it is one lit panel with the three commands and a Copy each, not a grey
|
|
880
|
+
sentence. sessions.js hides it the moment a terminal is live. */
|
|
881
|
+
.first-run { margin-top: 0; padding: 28px 32px; }
|
|
882
|
+
.first-run h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
|
|
883
|
+
.first-run p { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); max-width: 56ch; }
|
|
884
|
+
.first-run-cmds { margin-top: 20px; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
|
|
885
|
+
.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; }
|
|
886
|
+
.first-run-cmd code { font-size: var(--t-0); color: var(--text); }
|
|
887
|
+
.first-run-cmd .dot { margin-right: 10px; }
|
|
888
|
+
.first-run-note { margin-top: 16px; font-size: var(--t--1); color: var(--text-3); }
|
|
889
|
+
|
|
890
|
+
/* Settings is the last ledger row, not a stray sentence with a button */
|
|
891
|
+
.region-settings .section-head--quiet { margin-top: 40px; padding-block: 24px 0; border-top: 1px solid var(--edge); align-items: center; }
|
|
892
|
+
.region-settings .section-head--quiet h2 { font-size: var(--t-2); }
|
|
893
|
+
.region-settings .section-head--quiet .region-meta { flex: 1 1 auto; }
|
|
894
|
+
.region-settings .section-head--quiet .disclosure { margin-left: auto; }
|
|
895
|
+
|
|
896
|
+
/* The floor: an empty station is one line, heading and sentence side by side,
|
|
897
|
+
so seven empty stations are seven lines and not seven screens. A station
|
|
898
|
+
with rows keeps its full section. floor.js sets is-empty. */
|
|
899
|
+
.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); }
|
|
900
|
+
.region-floor.is-empty .section-head { padding: 0; display: flex; align-items: baseline; gap: 6px 12px; flex-wrap: wrap; }
|
|
901
|
+
.region-floor.is-empty .section-head h2 { font-size: var(--t-1); white-space: nowrap; }
|
|
902
|
+
.region-floor.is-empty .section-head--quiet .btn { margin-left: 4px; min-height: 30px; }
|
|
903
|
+
.region-floor.is-empty .region-meta:empty, .region-floor.is-empty .card-list:empty, .region-floor.is-empty .ftable { display: none; }
|
|
904
|
+
.region-floor.is-empty .empty-line { margin: 0; color: var(--text-3); }
|
|
905
|
+
.region-floor:not(.is-empty) + .region-floor.is-empty { margin-top: 24px; }
|
|
906
|
+
.masthead-left { font-size: var(--t-0); }
|
|
907
|
+
/* ---- end of the 2026-09-19 block ---------------------------------------- */
|
|
908
|
+
|
|
909
|
+
/* ---- 2026-09-19, second pass: what the rendered seed showed ------------- */
|
|
910
|
+
/* the rung word and the ladder's rung names are text: the chip-id class was
|
|
911
|
+
winning on order, and the identity dot beside each one already says who */
|
|
912
|
+
.term-model[class*="chip-id-"], .ladder-name[class*="chip-id-"] { color: var(--text-2); }
|
|
913
|
+
/* the cards cell's verb sits on the baseline the other cells share, whatever
|
|
914
|
+
order its markup arrives in */
|
|
915
|
+
.ledger-cell .ledger-actions { order: 9; }
|
|
916
|
+
/* a number field wears the same clothes as a text field */
|
|
917
|
+
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; }
|
|
918
|
+
/* a finished row has no Land, so its land blocker is read by nothing on screen */
|
|
919
|
+
#finished-list .blocker { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
920
|
+
/* the entry's help line sits after Start on the same row */
|
|
921
|
+
#card-entry .confirm-row .field-help { flex: 0 1 auto; }
|
|
922
|
+
/* ---- 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.0'
|
|
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.0'
|
|
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>
|