@vibelord/lanes 1.3.0 → 1.5.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/package.json +1 -1
- package/payload/lanes/README.md +4 -3
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +790 -31
- package/payload/lanes/container/hooks/grok-cli-hooks.json +8 -0
- package/payload/lanes/container/hooks/grok-hooks.json +12 -0
- package/payload/lanes/container/hooks/kimi-hooks.toml +49 -0
- package/payload/lanes/container/lane +13 -7
- package/payload/lanes/container/lane-mcp +56 -17
- package/payload/lanes/container/lane-signal +86 -23
- package/payload/lanes/container/lane_core.py +346 -36
- package/payload/lanes/container/throttle-pulse +456 -0
- package/payload/lanes/docs/ARCHITECTURE.md +165 -14
- package/payload/lanes/docs/DESIGN.md +7 -0
- package/payload/lanes/docs/INSTALL.md +23 -3
- package/payload/lanes/docs/RUNBOOK.md +392 -4
- package/payload/lanes/docs/USERGUIDE.md +222 -23
- package/payload/lanes/libexec/agent_state.py +604 -12
- package/payload/lanes/libexec/economy_poll.py +6 -2
- package/payload/lanes/libexec/harness_grok.py +742 -0
- package/payload/lanes/libexec/lanes_lib.py +143 -20
- package/payload/lanes/libexec/sidebar.py +522 -14
- package/payload/lanes/libexec/statusd.py +434 -28
- package/payload/lanes/libexec/tokens.py +12 -1
- package/payload/lanes/libexec/update.py +78 -5
- package/payload/lanes/libexec/users.py +31 -3
- package/payload/lanes/site/site.example.conf +4 -0
- package/payload/manifest.json +4 -3
|
@@ -7,7 +7,7 @@ Fully usable from the keyboard (touch/mouse is a bonus):
|
|
|
7
7
|
↑/↓ j/k move · Enter/→/Space open · J/K open next/previous lane · / filter · Esc clear · g/G · r reload
|
|
8
8
|
2 / 3 open the lane under the cursor in the 2nd / 3rd viewport (wide view sessions) · x / × close (y, then Enter)
|
|
9
9
|
u unregister a session added with a (z within 10 s undoes it; a declared lane says which file to edit)
|
|
10
|
-
d shows the cursor lane's docs link in the footer (plan W10 W7)
|
|
10
|
+
d shows the cursor lane's docs link in the footer (plan W10 W7) · i folds / unfolds the fleet block (W11)
|
|
11
11
|
? shows every key in the body until the next key · Tab / Shift-Tab open the next / previous lane that needs you
|
|
12
12
|
The keyboard goes back to the lane after every action (cockpit v3 S3): the sidebar and the lane share one keyboard, so
|
|
13
13
|
an action that ends — a close, a rename, a register, a cancel, Esc, r, closing the key sheet, a click that opened
|
|
@@ -52,6 +52,13 @@ one tail cell by priority: stale `?` > starving ⚠ > downgraded ↓ > mostly pa
|
|
|
52
52
|
Docs (plan W10 W7): while the workspace's lanes have published documents the idle strip says ` ↑ uploads · ▤ docs N
|
|
53
53
|
(U)`, one OSC 8 link to the docs page; the cursor row's name links to its lane's documents, and `d` shows that link in
|
|
54
54
|
the footer. The counts and the origin come in status.json (statusd): no stat or fork of the sidebar's own.
|
|
55
|
+
Fleet block (W11, spec 2026-10-11 §3): pinned above the upload strip, from status.json's `fleet` and `throttle` objects
|
|
56
|
+
only — Throttle in one row, an availability bar per provider (Anthropic, OpenAI and, since W12, Moonshot's Kimi),
|
|
57
|
+
the host and its top containers; a pane of 25 rows or more draws a rule and six rows, 18–24 three, fewer none, and the
|
|
58
|
+
lane list scrolls in the rest. `i` folds it to one line in the owner's profile (`look.fleet_collapsed`); with
|
|
59
|
+
LANES_FLEET_DASHBOARD_URL in site.conf the host row is one OSC 8 link to the dashboard. While its unfolded Throttle
|
|
60
|
+
row draws the money and every part after it (the same facts), the strip row's economy line yields to it; a row that
|
|
61
|
+
dropped `spill` or `429` to fit keeps the strip.
|
|
55
62
|
Truthful under load: a lane statusd could not observe keeps its last glyph, dimmed, with a `?`; the footer line warns
|
|
56
63
|
when the host is under pressure or statusd has fallen behind (age of $STATE/health, measured with our own clock).
|
|
57
64
|
Never frozen by an action (cockpit v3 S9): a close, `P`'s write and a new shell run on one worker thread (Worker); the
|
|
@@ -301,6 +308,154 @@ def load_economy(doc):
|
|
|
301
308
|
lanes={k: v for k, v in lanes.items() if isinstance(v, dict)} if isinstance(lanes, dict) else {})
|
|
302
309
|
|
|
303
310
|
|
|
311
|
+
# ----------------------------------------------------------------- the fleet block (W11, spec 2026-10-11 §3)
|
|
312
|
+
# Pinned above the upload strip: Throttle in one line, an availability bar per provider, the host and its top
|
|
313
|
+
# containers — from status.json's `fleet` and `throttle` objects ONLY (statusd publishes both, spec §2.1/§2.2), so the
|
|
314
|
+
# block costs no stat, read or fork of the sidebar's own on a tick. A pane of FLEET_FULL rows or more draws a rule and
|
|
315
|
+
# six rows (W12 added the Kimi row: FLEET_FULL 24 -> 25, so the body a full block leaves keeps its W11 floor), one of
|
|
316
|
+
# FLEET_SHORT..FLEET_FULL − 1 rows three (Throttle, the worse provider, the host), a shorter one none; `i` folds it to
|
|
317
|
+
# one line (`look.fleet_collapsed` in the owner's profile). The lane list scrolls in the rest.
|
|
318
|
+
FLEET_SCHEMA = 1 # the `v` of both objects this reader understands (absent = 1; another is ignored)
|
|
319
|
+
FLEET_FULL, FLEET_SHORT = 25, 18
|
|
320
|
+
# Own-clock floors beneath the publishers' `stale` flags (DEF-1528's rule, as ECONOMY_STALE): statusd writes `fleet`
|
|
321
|
+
# every 10 s (a 12 s cycle at its slowest) and the pulse `throttle.at` at least every 60 s, which statusd flags past
|
|
322
|
+
# 60 s; a status.json that stopped changing (statusd frozen) still turns the block stale here.
|
|
323
|
+
FLEET_STALE_S, PULSE_STALE_S = 60, 90
|
|
324
|
+
BAR_CELLS = 10
|
|
325
|
+
# The pulse's pools, in row order, and the row's word: the vendor (W12, spec 2026-10-15 §6: Kimi Code's pool is
|
|
326
|
+
# `kimi`, its vendor Moonshot AI). No grok row until Throttle has an xAI pool (the 2026-10-14 xAI pool review's gates).
|
|
327
|
+
PROVIDERS = (('claude', 'anthropic'), ('codex', 'openai'), ('kimi', 'moonshot'))
|
|
328
|
+
# The pools whose overflow is money (fleet_money): Kimi has no paid overflow lane, so a kimi figure is never shown —
|
|
329
|
+
# the pulse records it as nulls (throttle-pulse MONEY_POOLS), and this reader would not show one if it did not.
|
|
330
|
+
MONEY_POOLS = ('claude', 'codex')
|
|
331
|
+
LABEL_CELLS = 9 # the label field of the Throttle and provider rows (`anthropic`), then one blank
|
|
332
|
+
# `429 +N`: how much the pools' upstream_429_total grew over the last SURGE_S seconds of pulse samples this sidebar
|
|
333
|
+
# saw — from the newest sample at or before that window's start; a counter that went down (a daemon restart) starts
|
|
334
|
+
# again. At most SURGE_SAMPLES are kept (the pulse writes at most every 10 s: 31 in five minutes).
|
|
335
|
+
SURGE_S, SURGE_SAMPLES = 300, 64
|
|
336
|
+
CPU_ATTENTION, MEM_ATTENTION = 0.95, 0.90
|
|
337
|
+
GIB = 1 << 30
|
|
338
|
+
SEVERITY = {'tone-urgent': 2, 'tone-attention': 1} # a row's worst part tones its lead mark; the fold's verdict
|
|
339
|
+
BAR_MARKS = ('bar-usable', 'bar-cooling', 'bar-benched', 'bar-dead')
|
|
340
|
+
# LANES_FLEET_DASHBOARD_URL, the operator's site.conf value (unset until an authenticated dashboard exists): the host
|
|
341
|
+
# row is then ONE OSC 8 link to it. Read at start and with the 30 s version recheck (recheck_version), never on a tick:
|
|
342
|
+
# the line as bin/lanes reads site.conf (SITE_LINE's characters, the last line wins), and only an https URL passes —
|
|
343
|
+
# it rides inside an OSC string, and a terminal needs an absolute URL.
|
|
344
|
+
SITE_CONF = os.path.join(os.path.expanduser('~'), '.config', 'lanes', 'site.conf')
|
|
345
|
+
SITE_CONF_MAX = 64 * 1024
|
|
346
|
+
DASHBOARD_LINE = re.compile(r'LANES_FLEET_DASHBOARD_URL=([A-Za-z0-9_.:@/-]*)')
|
|
347
|
+
DASHBOARD_URL = re.compile(r'https://[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?(?::[0-9]{1,5})?(?:/[A-Za-z0-9_.:@/-]*)?\Z',
|
|
348
|
+
re.ASCII)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def fleet_doc(value):
|
|
352
|
+
"""status.json's `fleet` / `throttle` object as the block reads it: a dict whose `v` is 1 (or absent), else {} —
|
|
353
|
+
an object of another version is ignored rather than half-read (load_economy's rule)."""
|
|
354
|
+
return value if isinstance(value, dict) and value.get('v', FLEET_SCHEMA) == FLEET_SCHEMA else {}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def tally(value):
|
|
358
|
+
"""A pool count: a non-negative int (a finite float is truncated), else 0."""
|
|
359
|
+
value = finite(value)
|
|
360
|
+
return max(0, int(value)) if value is not None else 0
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def half_up(numerator, denominator):
|
|
364
|
+
"""numerator / denominator (both ≥ 0, denominator > 0) rounded half up, in integers — never Python's half-to-even
|
|
365
|
+
(2.5 cells down, 3.5 up) and never a float that lands a hair under .5 (7 × 10 / 14 must give 5, not 4)."""
|
|
366
|
+
return (2 * numerator + denominator) // (2 * denominator)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def bar_segments(pool):
|
|
370
|
+
"""(usable, cooling, benched, dead) accounts of a pulse pool, exclusive and summing to its `configured`; None
|
|
371
|
+
without a configured count. The pulse's buckets overlap (spec §2.1: `cooling` is its remainder, `benched` the
|
|
372
|
+
model-benched accounts, usable or not), so the bar takes them in order, each capped by what is left: usable;
|
|
373
|
+
cooling; benched WITHIN cooling — a benched account the pool still counts usable reads usable, one it does not
|
|
374
|
+
reads benched instead of cooling; dead the rest (exhausted, auth failed, disabled, billing- or organization-blocked
|
|
375
|
+
or unhealthy: configured − usable − cooling)."""
|
|
376
|
+
configured = finite(pool.get('configured')) if isinstance(pool, dict) else None
|
|
377
|
+
if configured is None:
|
|
378
|
+
return None
|
|
379
|
+
configured = max(0, int(configured))
|
|
380
|
+
usable = min(tally(pool.get('usable')), configured)
|
|
381
|
+
cooling = min(tally(pool.get('cooling')), configured - usable)
|
|
382
|
+
benched = min(tally(pool.get('benched')), cooling)
|
|
383
|
+
return usable, cooling - benched, benched, configured - usable - cooling
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def bar_cells(segments):
|
|
387
|
+
"""Cells per segment (usable, cooling, benched, dead) of the BAR_CELLS-cell bar; they always sum to BAR_CELLS.
|
|
388
|
+
Usable is `usable × 10 / configured` rounded half up and held to 1..9 while some but not all accounts are usable:
|
|
389
|
+
the bar never shows none of a pool that serves, nor all of one that does not. The other three share what is left
|
|
390
|
+
by cumulative half-up rounding over the accounts that are not usable. Nothing configured is ten dead cells."""
|
|
391
|
+
usable, *rest = segments
|
|
392
|
+
configured = usable + sum(rest)
|
|
393
|
+
if not configured:
|
|
394
|
+
return (0, 0, 0, BAR_CELLS)
|
|
395
|
+
cells = half_up(usable * BAR_CELLS, configured)
|
|
396
|
+
if 0 < usable < configured:
|
|
397
|
+
cells = min(BAR_CELLS - 1, max(1, cells))
|
|
398
|
+
left, others = BAR_CELLS - cells, configured - usable
|
|
399
|
+
out, edge_before, seen = [cells], 0, 0
|
|
400
|
+
for value in rest:
|
|
401
|
+
seen += value
|
|
402
|
+
edge = half_up(seen * left, others) if others else 0
|
|
403
|
+
out.append(edge - edge_before)
|
|
404
|
+
edge_before = edge
|
|
405
|
+
return tuple(out)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def compact_gib(value):
|
|
409
|
+
"""Bytes as GiB in at most four cells: 0.4G, 8.4G, 14G, 123G (no trailing `.0`)."""
|
|
410
|
+
gib = max(0.0, float(value)) / GIB
|
|
411
|
+
return (f'{gib:.1f}' if gib < 9.95 else f'{gib:.0f}').removesuffix('.0') + 'G'
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def aged(doc, now, floor):
|
|
415
|
+
"""Whether a `fleet` / `throttle` object is a frozen picture: its publisher says so (`stale: true`), or its own `at`
|
|
416
|
+
is more than `floor` seconds behind `now` (OUR clock, DEF-1528). False for {} (nothing known, nothing claimed) and
|
|
417
|
+
without a numeric `at`."""
|
|
418
|
+
if not doc:
|
|
419
|
+
return False
|
|
420
|
+
at = number(doc.get('at'))
|
|
421
|
+
return doc.get('stale') is True or (at is not None and now - at > floor)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def finite(value):
|
|
425
|
+
"""number(value) when it is also finite (a NaN or an infinity in published JSON is no fact), else None."""
|
|
426
|
+
value = number(value)
|
|
427
|
+
return value if value is not None and -INF < value < INF else None
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def whole(value):
|
|
431
|
+
"""A finite value ≥ 0 rounded half up to an int (tenths first, so 37.25 is 37 and 2.5 is 3); held to 0..10⁹, so
|
|
432
|
+
no published figure can overflow a frame or a float."""
|
|
433
|
+
return half_up(round(min(max(value, 0.0), 1e9) * 10), 10)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def percent(fraction):
|
|
437
|
+
"""A finite 0–1 fraction as a whole percent (`53`), half up, never below 0."""
|
|
438
|
+
return whole(fraction * 100) if fraction < 1e7 else whole(1e9)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def short_name(value, cells):
|
|
442
|
+
"""A name from status.json as a row prints it: printable (control and format characters dropped — it is
|
|
443
|
+
published data, never an escape), at most `cells` cells; '' when it is no string."""
|
|
444
|
+
return clip(OSC_UNSAFE.sub('', value), cells)[0] if isinstance(value, str) else ''
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def site_dashboard(path=None):
|
|
448
|
+
"""LANES_FLEET_DASHBOARD_URL from the server's site.conf (read, never executed; at most SITE_CONF_MAX bytes): ''
|
|
449
|
+
when the file or the line is absent, unreadable, or the value is not an https URL of DNS-name characters."""
|
|
450
|
+
try:
|
|
451
|
+
with open(path or SITE_CONF, 'rb') as handle:
|
|
452
|
+
text = handle.read(SITE_CONF_MAX).decode('utf-8', 'replace')
|
|
453
|
+
except OSError:
|
|
454
|
+
return ''
|
|
455
|
+
found = [match.group(1) for match in map(DASHBOARD_LINE.fullmatch, text.splitlines()) if match]
|
|
456
|
+
return found[-1] if found and DASHBOARD_URL.match(found[-1]) else ''
|
|
457
|
+
|
|
458
|
+
|
|
304
459
|
# The six words statusd publishes (alignment contract A3) and the helper word for an `exec` lane. There is no
|
|
305
460
|
# seventh: statusd's zmx observer used to publish `idle`, which no other surface knew (DEF-823). Values are
|
|
306
461
|
# (unicode, ascii, tone) from the one token source — the tone is a role name from tokens.ROLES, never an SGR code:
|
|
@@ -424,6 +579,18 @@ def mtime(path):
|
|
|
424
579
|
return 0
|
|
425
580
|
|
|
426
581
|
|
|
582
|
+
def file_stamp(path):
|
|
583
|
+
"""(st_ino, st_mtime_ns, st_size) of `path`, or 0 when it cannot be stat()ed. For a file replaced by rename
|
|
584
|
+
(users.update -> atomic_write: a new temp file, created while the old one still exists, then renamed over it) every
|
|
585
|
+
write has a new inode, so two writes inside one timestamp tick of the file system still differ — an mtime alone
|
|
586
|
+
reads them as one (DEF-1972)."""
|
|
587
|
+
try:
|
|
588
|
+
info = os.stat(path)
|
|
589
|
+
except OSError:
|
|
590
|
+
return 0
|
|
591
|
+
return info.st_ino, info.st_mtime_ns, info.st_size
|
|
592
|
+
|
|
593
|
+
|
|
427
594
|
def cell_width(text):
|
|
428
595
|
if text.isascii(): # every ASCII character is one cell here (none combines, none is wide): the common case
|
|
429
596
|
return len(text)
|
|
@@ -489,7 +656,8 @@ BOX_ROWS = 1 # the upload strip (spec §4.6(d)); it was a four-row box
|
|
|
489
656
|
# config.kdl's shared block; lane chords such as Ctrl-Alt-m are no sidebar keys and stay off). 18 lines below the
|
|
490
657
|
# dynamic version row (cockpit v3 V2, LNS-84: ` lanes <current> · view <tree>`, key_sheet) — exactly the 19-row body
|
|
491
658
|
# of a 24-row pane with the strip row; R5's `p h` / `H o` (LNS-78) took the two rows S10 left, and V2's row the line
|
|
492
|
-
# `u z` now share. Plan W10 W7's `d` (the cursor lane's docs link) joins the strip's own keys, `+ U d`, for the same reason
|
|
659
|
+
# `u z` now share. Plan W10 W7's `d` (the cursor lane's docs link) joins the strip's own keys, `+ U d`, for the same reason,
|
|
660
|
+
# and W11's `i` (fold the fleet block) the reload and the sheet, `r i ?`.
|
|
493
661
|
# test_sidebar.DocumentedKeys holds the sheet, the table and handle() to the same keys.
|
|
494
662
|
KEY_SHEET = (('↑ ↓ j k', 'Up Dn j k', 'move'), ('⏎ → Space', 'Enter Spc', 'open the cursor lane'),
|
|
495
663
|
('J K', 'J K', 'open next / prev lane'), ('Tab S-Tab', 'Tab S-Tab', 'next / prev needing you'),
|
|
@@ -498,7 +666,7 @@ KEY_SHEET = (('↑ ↓ j k', 'Up Dn j k', 'move'), ('⏎ → Space', 'Enter Spc'
|
|
|
498
666
|
('+ U d', '+ U d', 'shell / upload / docs'), ('a', 'a', 'add to a workspace'),
|
|
499
667
|
('x', 'x', 'close (y, then Enter)'), ('u z', 'u z', 'unregister / undo 10 s'),
|
|
500
668
|
('R', 'R', 'rename'), ('P', 'P', 'register a session'), ('p h', 'p h', 'pin / hide lane'),
|
|
501
|
-
('H o', 'H o', 'hide tab / hidden lanes'), ('r ?', 'r ?', 'reload /
|
|
669
|
+
('H o', 'H o', 'hide tab / hidden lanes'), ('r i ?', 'r i ?', 'reload / fleet / keys'),
|
|
502
670
|
('Ctrl-Alt-l', 'Ctrl-Alt-l', 'focus the sidebar'))
|
|
503
671
|
PASTE_START, PASTE_END = '\033[200~', '\033[201~'
|
|
504
672
|
# A paste typed into a footer field (Sidebar.pasted): a run of line breaks / tabs is one space, never an Enter.
|
|
@@ -687,6 +855,14 @@ class Sidebar:
|
|
|
687
855
|
# document count (reload), the link of the footer message `d` set ('' = the message is no link; the message
|
|
688
856
|
# setter clears it) and the link the footer row draws this frame (footer_text sets it).
|
|
689
857
|
web_origin, docs_total, _message_link, footer_link = '', 0, '', ''
|
|
858
|
+
# The fleet block (W11; read-only defaults, every write assigns fresh values): status.json's `fleet` and `throttle`
|
|
859
|
+
# (take_fleet), `i`'s fold (the owner's profile look.fleet_collapsed; this process's own on a device nobody owns),
|
|
860
|
+
# the dashboard link (site.conf, fleet_url), the (pulse at, 429 total) samples `429 +N` is measured over, the
|
|
861
|
+
# block's place in the last frame (`block_top` its first 1-based row, 0 = not drawn; `block_rows` how many) and the
|
|
862
|
+
# stale verdicts poll_fleet last saw (fleet, throttle).
|
|
863
|
+
fleet_facts, throttle_facts, fleet_collapsed, fleet_url = {}, {}, False, ''
|
|
864
|
+
surge_samples, block_top, block_rows, fleet_frozen = (), 0, 0, (False, False)
|
|
865
|
+
block_says_money = False # fleet_block: the unfolded Throttle row drew the money and every part after it
|
|
690
866
|
|
|
691
867
|
def __init__(self, device, workspace, slot='main', viewports='1'):
|
|
692
868
|
self.device, self.workspace, self.slot = device, workspace, slot
|
|
@@ -764,6 +940,7 @@ class Sidebar:
|
|
|
764
940
|
self.current_version = (self.tree_version if self.current_path == str(L.REPO)
|
|
765
941
|
else shown_version(self.current_path))
|
|
766
942
|
self.version_at = time.monotonic()
|
|
943
|
+
self.fleet_url = site_dashboard() # W11: LANES_FLEET_DASHBOARD_URL, again with every version recheck
|
|
767
944
|
# A predecessor that swapped itself out left its cursor, scroll, seen marks and filter (cockpit v3 U5).
|
|
768
945
|
self.swappable, self.restore_id = False, None
|
|
769
946
|
self.restored = self.take_state()
|
|
@@ -815,6 +992,7 @@ class Sidebar:
|
|
|
815
992
|
self.zmx = (status.get('zmx') or {}).get(self.workspace, [])
|
|
816
993
|
origin = status.get('web_origin')
|
|
817
994
|
self.web_origin = origin if isinstance(origin, str) and WEB_ORIGIN.match(origin) else ''
|
|
995
|
+
self.take_fleet(status if isinstance(status, dict) else {}) # W11: the fleet block's facts
|
|
818
996
|
self.keep_moved_row()
|
|
819
997
|
if stamp[0] != self.stamp[0] or stamp[1] != self.stamp[1] or force:
|
|
820
998
|
self.fleet = self.fleet_counts()
|
|
@@ -880,6 +1058,9 @@ class Sidebar:
|
|
|
880
1058
|
if now - self.version_at < VERSION_RECHECK_S:
|
|
881
1059
|
return False
|
|
882
1060
|
self.version_at = now
|
|
1061
|
+
url = site_dashboard() # W11: the dashboard link follows site.conf within 30 s
|
|
1062
|
+
if url != self.fleet_url:
|
|
1063
|
+
self.fleet_url, self.dirty = url, True
|
|
883
1064
|
if self.current_path == str(L.REPO):
|
|
884
1065
|
return False
|
|
885
1066
|
version = shown_version(self.current_path)
|
|
@@ -999,8 +1180,9 @@ class Sidebar:
|
|
|
999
1180
|
return None, None
|
|
1000
1181
|
|
|
1001
1182
|
def profile_stamp(self):
|
|
1002
|
-
"""reload()'s seventh stamp field: the profile's mtime
|
|
1003
|
-
|
|
1183
|
+
"""reload()'s seventh stamp field: the profile's (inode, mtime, size) (`file_stamp`: two writes in one mtime
|
|
1184
|
+
tick are both seen, DEF-1972) — 0, with no stat at all, on a device nobody owns."""
|
|
1185
|
+
return file_stamp(self.profile_path) if self.profile_path else 0
|
|
1004
1186
|
|
|
1005
1187
|
def read_profile(self):
|
|
1006
1188
|
"""The person's profile as users.read normalizes it (None: no owner, no file yet, or a file this release does
|
|
@@ -1008,6 +1190,8 @@ class Sidebar:
|
|
|
1008
1190
|
profile = users.read(self.login, os.path.dirname(self.profile_path)) if self.profile_path else None
|
|
1009
1191
|
self.profile_look = dict(profile['look']) if profile else {}
|
|
1010
1192
|
self.take_arrangement(profile)
|
|
1193
|
+
if self.profile_path: # W11 `i`: the owner's fold; a device nobody owns keeps its own
|
|
1194
|
+
self.fleet_collapsed = users.look_flag(profile, 'fleet_collapsed')
|
|
1011
1195
|
return profile
|
|
1012
1196
|
|
|
1013
1197
|
def take_arrangement(self, profile):
|
|
@@ -1459,6 +1643,312 @@ class Sidebar:
|
|
|
1459
1643
|
lead_painted = f'{tone["ink-dim"]}{lead}{tone["reset"]}' if stale else lead
|
|
1460
1644
|
return lead_painted + sep_painted.join(painted), lead + sep_plain.join(plain)
|
|
1461
1645
|
|
|
1646
|
+
# ---------------------------------------------------------- the fleet block (W11, spec 2026-10-11 §3)
|
|
1647
|
+
def take_fleet(self, status):
|
|
1648
|
+
"""reload()'s status.json read: its `fleet` and `throttle` objects (fleet_doc) and, once per new pulse sample
|
|
1649
|
+
(`throttle.at` moved), the pools' summed `upstream_429_total` for `429 +N` (surge). Fresh values only."""
|
|
1650
|
+
self.fleet_facts = fleet_doc(status.get('fleet'))
|
|
1651
|
+
self.throttle_facts = fleet_doc(status.get('throttle'))
|
|
1652
|
+
at = finite(self.throttle_facts.get('at'))
|
|
1653
|
+
pools = self.throttle_facts.get('pools')
|
|
1654
|
+
totals = [finite(pool.get('upstream_429_total')) for pool in pools.values() if isinstance(pool, dict)] \
|
|
1655
|
+
if isinstance(pools, dict) else []
|
|
1656
|
+
totals = [total for total in totals if total is not None]
|
|
1657
|
+
if at is None or not totals:
|
|
1658
|
+
return
|
|
1659
|
+
total, samples = sum(totals), self.surge_samples
|
|
1660
|
+
if samples and at <= samples[-1][0]:
|
|
1661
|
+
return # the same pulse sample (statusd republished), or an older one
|
|
1662
|
+
if samples and total < samples[-1][1]:
|
|
1663
|
+
samples = () # the counter went down: a restarted daemon counts again
|
|
1664
|
+
samples += ((at, total),)
|
|
1665
|
+
start = at - SURGE_S
|
|
1666
|
+
keep = max((i for i, (when, _total) in enumerate(samples) if when <= start), default=0)
|
|
1667
|
+
self.surge_samples = samples[keep:][-SURGE_SAMPLES:]
|
|
1668
|
+
|
|
1669
|
+
def surge(self):
|
|
1670
|
+
"""`429 +N`'s N: the summed upstream_429_total's growth from the newest sample at or before SURGE_S seconds
|
|
1671
|
+
before the latest one (the oldest this sidebar has, while it has seen less than that) to the latest."""
|
|
1672
|
+
samples = self.surge_samples
|
|
1673
|
+
return samples[-1][1] - samples[0][1] if len(samples) > 1 else 0
|
|
1674
|
+
|
|
1675
|
+
def fleet_aged(self, now):
|
|
1676
|
+
"""(fleet stale, throttle stale): the publisher's flag, or the object's own `at` past FLEET_STALE_S /
|
|
1677
|
+
PULSE_STALE_S by our clock (aged)."""
|
|
1678
|
+
return aged(self.fleet_facts, now, FLEET_STALE_S), aged(self.throttle_facts, now, PULSE_STALE_S)
|
|
1679
|
+
|
|
1680
|
+
def poll_fleet(self):
|
|
1681
|
+
"""Repaint when either object crosses into (or out of) stale. Polled every tick like poll_economy(): a statusd
|
|
1682
|
+
that stopped never changes status.json's mtime again, reload()'s only trigger. In-memory only: no I/O."""
|
|
1683
|
+
frozen = self.fleet_aged(time.time())
|
|
1684
|
+
if frozen != self.fleet_frozen:
|
|
1685
|
+
self.fleet_frozen, self.dirty = frozen, True
|
|
1686
|
+
|
|
1687
|
+
def fleet_money(self):
|
|
1688
|
+
"""(text, role) of ONE pool's overflow money — `$8.5k/16k 53%` — or None (no token: every overflow figure is
|
|
1689
|
+
null). The pools' daily budgets are separate, non-fungible money, so the figure is never a sum: it is the pool
|
|
1690
|
+
nearest its cap — one at or over its alert level first, then the highest fraction spent, Anthropic on a tie (a
|
|
1691
|
+
pool with spend but no budget ranks last and shows `$120` alone). The attention tone when that pool's fraction
|
|
1692
|
+
reached its alert level (the ranking puts any such pool first)."""
|
|
1693
|
+
overflow = self.throttle_facts.get('overflow')
|
|
1694
|
+
overflow = overflow if isinstance(overflow, dict) else {}
|
|
1695
|
+
known = [] # (alarm, fraction or -1, spend, budget) in MONEY_POOLS order
|
|
1696
|
+
for key in MONEY_POOLS:
|
|
1697
|
+
pool = overflow.get(key) if isinstance(overflow.get(key), dict) else {}
|
|
1698
|
+
spent, cap = finite(pool.get('spend_today_usd')), finite(pool.get('budget_usd'))
|
|
1699
|
+
if spent is None:
|
|
1700
|
+
continue
|
|
1701
|
+
fraction, alert = finite(pool.get('fraction')), finite(pool.get('alert_level_pct'))
|
|
1702
|
+
if fraction is None and cap:
|
|
1703
|
+
fraction = spent / cap
|
|
1704
|
+
alarm = fraction is not None and alert is not None and fraction * 100 >= alert
|
|
1705
|
+
known.append((alarm, fraction if fraction is not None and cap else -1.0, spent, cap))
|
|
1706
|
+
if not known:
|
|
1707
|
+
return None
|
|
1708
|
+
alarm, fraction, spend, budget = max(known, key=lambda pool: pool[:2]) # max keeps the first on a tie
|
|
1709
|
+
text = '$' + compact_usd(spend)
|
|
1710
|
+
if budget and fraction >= 0:
|
|
1711
|
+
text += f'/{compact_usd(budget)} {percent(fraction)}%'
|
|
1712
|
+
return text, 'tone-attention' if alarm else 'ink-dim'
|
|
1713
|
+
|
|
1714
|
+
def throttle_shows_money(self):
|
|
1715
|
+
"""True when the block's Throttle row carries the overflow money (throttle_spec's tokened head): a pulse that
|
|
1716
|
+
is not `up: false` and whose token gave fleet_money() a figure. An untokened or down pulse shows `up …` /
|
|
1717
|
+
`down`, never the money — the strip row's economy line must not yield to it (spec 2026-10-11 §3)."""
|
|
1718
|
+
return bool(self.throttle_facts) and self.throttle_facts.get('up') is not False \
|
|
1719
|
+
and self.fleet_money() is not None
|
|
1720
|
+
|
|
1721
|
+
def throttle_spec(self, now):
|
|
1722
|
+
"""The Throttle row: (head after the lead mark, parts, worst role). Tokened: `$8.5k/16k 53% · 429 +4 · spill
|
|
1723
|
+
76%`; else `up · spill 76% · 429 +4`; `down · <error>` in the urgent tone when the daemon did not answer; `no
|
|
1724
|
+
pulse` before statusd has published one. Spill is the most-spilling pool's, shown above 0 %."""
|
|
1725
|
+
facts = self.throttle_facts
|
|
1726
|
+
if not facts:
|
|
1727
|
+
return 'throttle', [[('no pulse', 'ink-dim')]], 'ink-dim'
|
|
1728
|
+
if facts.get('up') is False:
|
|
1729
|
+
parts = [[('down', 'tone-urgent')]]
|
|
1730
|
+
error = short_name(facts.get('error'), 24)
|
|
1731
|
+
if error:
|
|
1732
|
+
parts.append([(error, 'ink-dim')])
|
|
1733
|
+
return 'throttle', parts, 'tone-urgent'
|
|
1734
|
+
pools = facts.get('pools') if isinstance(facts.get('pools'), dict) else {}
|
|
1735
|
+
spills = [finite(pool.get('spill_fraction')) for pool in pools.values() if isinstance(pool, dict)]
|
|
1736
|
+
spill = max((value for value in spills if value is not None), default=None)
|
|
1737
|
+
spill = [[(f'spill {percent(spill)}%', 'ink-dim')]] if spill is not None and percent(spill) > 0 else []
|
|
1738
|
+
surge = self.surge()
|
|
1739
|
+
surge = [[(f'429 +{int(surge)}', 'ink-dim')]] if surge > 0 else []
|
|
1740
|
+
money = self.fleet_money()
|
|
1741
|
+
if money:
|
|
1742
|
+
return 'throttle', [[money]] + surge + spill, money[1]
|
|
1743
|
+
return 'throttle', [[('up' if facts.get('up') is True else '?', 'ink-dim')]] + spill + surge, 'ink-dim'
|
|
1744
|
+
|
|
1745
|
+
def provider_spec(self, key, word, now):
|
|
1746
|
+
"""A provider's row: (head, parts, worst). The availability bar — usable/configured over BAR_CELLS cells, usable
|
|
1747
|
+
▰ cooling ▱ benched ▪ dead · each in its tone (bar_segments, bar_cells) — and, after one blank, `12/19` (the
|
|
1748
|
+
attention tone while none of a configured pool is usable; a pool with nothing configured is no alarm), then the
|
|
1749
|
+
tokened pulse's worst `wk N%` / `5h N%` utilization and the soonest reset. `no data` without the pool."""
|
|
1750
|
+
pools = self.throttle_facts.get('pools')
|
|
1751
|
+
pool = pools.get(key) if isinstance(pools, dict) else None
|
|
1752
|
+
segments = bar_segments(pool)
|
|
1753
|
+
if segments is None:
|
|
1754
|
+
return word, [[('no data', 'ink-dim')]], 'ink-dim'
|
|
1755
|
+
marks = self.glyphs['marks']
|
|
1756
|
+
bar = [(marks[name] * cells, MARKS[name][2]) for name, cells in zip(BAR_MARKS, bar_cells(segments)) if cells]
|
|
1757
|
+
usable, configured = segments[0], sum(segments)
|
|
1758
|
+
worst = 'tone-attention' if configured and not usable else 'ink-dim'
|
|
1759
|
+
parts = [bar + [(' ', None), (f'{usable}/{configured}', worst)]] # `▰▰▰▱▱ 12/19`: one part, one blank
|
|
1760
|
+
for field, prefix in (('worst_7d', 'wk'), ('worst_5h', '5h')):
|
|
1761
|
+
value = finite(pool.get(field))
|
|
1762
|
+
if value is not None:
|
|
1763
|
+
parts.append([(f'{prefix} {percent(value)}%', 'ink-dim')])
|
|
1764
|
+
reset = finite(pool.get('soonest_reset'))
|
|
1765
|
+
if reset is not None and reset > now:
|
|
1766
|
+
parts.append([(f'reset {age3(reset - now).strip()}', 'ink-dim')])
|
|
1767
|
+
return word, parts, worst
|
|
1768
|
+
|
|
1769
|
+
def worse_provider(self):
|
|
1770
|
+
"""(pool key, word) of the provider the three-row block shows, over every PROVIDERS pool: the one with no usable
|
|
1771
|
+
account, else the lower usable share; Anthropic, then OpenAI, on a tie. A pool with nothing configured ranks
|
|
1772
|
+
after every configured pool — it is no alarm (provider_spec), and a daemon without Kimi accounts reports its
|
|
1773
|
+
kimi pool so (DEF-2002) — and a pool the pulse does not report ranks last."""
|
|
1774
|
+
pools = self.throttle_facts.get('pools')
|
|
1775
|
+
pools = pools if isinstance(pools, dict) else {}
|
|
1776
|
+
|
|
1777
|
+
def share(provider):
|
|
1778
|
+
segments = bar_segments(pools.get(provider[0]))
|
|
1779
|
+
if segments is None:
|
|
1780
|
+
return 3.0
|
|
1781
|
+
configured = sum(segments)
|
|
1782
|
+
return segments[0] / configured if configured else 2.0
|
|
1783
|
+
return min(PROVIDERS, key=share)
|
|
1784
|
+
|
|
1785
|
+
def host_spec(self):
|
|
1786
|
+
"""The host row: (head, parts, worst) — `productbox cpu 42% · mem 61% · load 37 · psi 3%`; cpu at
|
|
1787
|
+
CPU_ATTENTION (one sample), memory at MEM_ATTENTION and any `memory.full` stall in the attention tone. psi is
|
|
1788
|
+
the highest avg10 statusd publishes."""
|
|
1789
|
+
facts = self.fleet_facts
|
|
1790
|
+
name = short_name(facts.get('host.name'), 12) or 'host'
|
|
1791
|
+
if not facts:
|
|
1792
|
+
return name, [[('no data', 'ink-dim')]], 'ink-dim'
|
|
1793
|
+
parts, worst = [], 'ink-dim'
|
|
1794
|
+
pressure = facts.get('system.pressure') if isinstance(facts.get('system.pressure'), dict) else {}
|
|
1795
|
+
stalls = [value for value in map(finite, pressure.values()) if value is not None]
|
|
1796
|
+
full = finite(pressure.get('memory.full.avg10'))
|
|
1797
|
+
load = finite(facts.get('system.cpu.load_average.1m'))
|
|
1798
|
+
for text, value, alarm in (
|
|
1799
|
+
('cpu', finite(facts.get('system.cpu.utilization')), lambda v: v >= CPU_ATTENTION),
|
|
1800
|
+
('mem', finite(facts.get('system.memory.utilization')), lambda v: v >= MEM_ATTENTION)):
|
|
1801
|
+
if value is not None:
|
|
1802
|
+
role = 'tone-attention' if alarm(value) else 'ink-dim'
|
|
1803
|
+
parts.append([(f'{text} {percent(value)}%', role)])
|
|
1804
|
+
worst = max(worst, role, key=lambda r: SEVERITY.get(r, 0))
|
|
1805
|
+
if load is not None:
|
|
1806
|
+
parts.append([(f'load {whole(load)}', 'ink-dim')])
|
|
1807
|
+
if stalls:
|
|
1808
|
+
role = 'tone-attention' if full is not None and full > 0 else 'ink-dim'
|
|
1809
|
+
parts.append([(f'psi {whole(max(stalls))}%', role)])
|
|
1810
|
+
worst = max(worst, role, key=lambda r: SEVERITY.get(r, 0))
|
|
1811
|
+
return name, parts, worst
|
|
1812
|
+
|
|
1813
|
+
def containers_spec(self):
|
|
1814
|
+
"""The containers row: (head, parts, worst) — `bw-tools 14G · worlds 8.4G · sakio 3.7G`, statusd's top three
|
|
1815
|
+
by memory in its order."""
|
|
1816
|
+
found = self.fleet_facts.get('containers')
|
|
1817
|
+
parts = []
|
|
1818
|
+
for container in found[:3] if isinstance(found, list) else ():
|
|
1819
|
+
if not isinstance(container, dict):
|
|
1820
|
+
continue
|
|
1821
|
+
name = short_name(container.get('container.name'), 14)
|
|
1822
|
+
used = finite(container.get('container.memory.usage.total'))
|
|
1823
|
+
if name and used is not None:
|
|
1824
|
+
parts.append([(f'{name} {compact_gib(used)}', 'ink-dim')])
|
|
1825
|
+
return '', parts or [[('no containers', 'ink-dim')]], 'ink-dim'
|
|
1826
|
+
|
|
1827
|
+
def paint(self, segments):
|
|
1828
|
+
"""[(text, role or None)] painted: one SGR and one reset per run of equal roles (a 16-colour ink-dim may be SGR 2,
|
|
1829
|
+
which only a reset ends), a None role written bare."""
|
|
1830
|
+
tone, out, run, role_of_run = self.tone, [], [], None
|
|
1831
|
+
for text, role in segments + [('', '\0')]:
|
|
1832
|
+
if role != role_of_run and run:
|
|
1833
|
+
joined = ''.join(run)
|
|
1834
|
+
out.append(joined if role_of_run is None else f'{tone[role_of_run]}{joined}{tone["reset"]}')
|
|
1835
|
+
run = []
|
|
1836
|
+
role_of_run = role
|
|
1837
|
+
run.append(text)
|
|
1838
|
+
return ''.join(out)
|
|
1839
|
+
|
|
1840
|
+
def fleet_row(self, mark, spec, width, stale, link=''):
|
|
1841
|
+
"""One block row from a spec (head, parts, worst): ` <mark> <head> ` then the parts joined by the `sep` mark,
|
|
1842
|
+
whole parts dropped from the end to fit `width` cells (economy_strip's rule). The lead mark takes the row's worst
|
|
1843
|
+
tone — of every part, a dropped one included, so a 36-column row still says something needs a look. A Throttle
|
|
1844
|
+
or provider head is a LABEL_CELLS field; the host's name is its own width. Stale: everything ink-dim. `link`:
|
|
1845
|
+
the whole row but its first blank one OSC 8 hyperlink (bytes, no cell)."""
|
|
1846
|
+
return self.fleet_layout(mark, spec, width, stale, link)[0]
|
|
1847
|
+
|
|
1848
|
+
def fleet_layout(self, mark, spec, width, stale, link=''):
|
|
1849
|
+
"""fleet_row's (painted row, whole): `whole` is True when every part of the spec fit — nothing was dropped
|
|
1850
|
+
from the end. render() keeps the strip row's economy line unless the Throttle row is whole (spec 2026-10-11
|
|
1851
|
+
§3: the strip yields only when the block shows the same facts)."""
|
|
1852
|
+
head, parts, worst = spec
|
|
1853
|
+
marks = self.glyphs['marks']
|
|
1854
|
+
label = head.ljust(LABEL_CELLS) if mark in ('fleet-throttle', 'fleet-provider') else head
|
|
1855
|
+
segments = [(marks[mark], 'ink-dim' if stale else worst if worst in SEVERITY else MARKS[mark][2]),
|
|
1856
|
+
(f' {label} ' if label else ' ', 'ink-dim')]
|
|
1857
|
+
if stale and mark == 'fleet-throttle':
|
|
1858
|
+
segments.append((f'{marks["stale"]} ', 'ink-dim'))
|
|
1859
|
+
used, sep, first = 1 + sum(cell_width(text) for text, _role in segments), f' {marks["sep"]} ', True
|
|
1860
|
+
drawn = 0
|
|
1861
|
+
for part in parts:
|
|
1862
|
+
cost = sum(cell_width(text) for text, _role in part) + (0 if first else cell_width(sep))
|
|
1863
|
+
if used + cost > width:
|
|
1864
|
+
break
|
|
1865
|
+
segments += ([] if first else [(sep, 'ink-dim')]) + part
|
|
1866
|
+
used, first, drawn = used + cost, False, drawn + 1
|
|
1867
|
+
if stale:
|
|
1868
|
+
segments = [(text, 'ink-dim') for text, _role in segments]
|
|
1869
|
+
return ' ' + osc8(link, self.paint(segments)), drawn == len(parts)
|
|
1870
|
+
|
|
1871
|
+
def fleet_rule(self, cols):
|
|
1872
|
+
"""` ── fleet ───…` to cols − 1: the rule ink-faint, the word ink-dim (divider_line's look)."""
|
|
1873
|
+
rule = self.glyphs['marks']['rule']
|
|
1874
|
+
return ' ' + self.paint([(rule * 2 + ' ', 'ink-faint'), ('fleet', 'ink-dim'),
|
|
1875
|
+
(' ' + rule * max(0, cols - 1 - 10), 'ink-faint')])
|
|
1876
|
+
|
|
1877
|
+
def fleet_folded(self, specs, width, stale):
|
|
1878
|
+
"""`i`'s one line: ` fleet ✓ · throttle 53% · cpu 42% · mem 61%` — the verdict is the worst tone of every row
|
|
1879
|
+
(✗ urgent, ⚠ attention, ✓ done), the stale mark while either object is stale; parts dropped from the end."""
|
|
1880
|
+
marks = self.glyphs['marks']
|
|
1881
|
+
worst = max((spec[2] for spec in specs), key=lambda role: SEVERITY.get(role, 0))
|
|
1882
|
+
verdicts = {'tone-urgent': ('fail', 'tone-urgent'), 'tone-attention': ('warning', 'tone-attention')}
|
|
1883
|
+
verdict = ('stale', 'ink-dim') if stale else verdicts.get(worst, ('ok', 'tone-done'))
|
|
1884
|
+
parts = []
|
|
1885
|
+
if self.throttle_facts: # the Throttle row's first part: the money's percent, up, down, ?
|
|
1886
|
+
text, role = specs[0][1][0][0]
|
|
1887
|
+
if text.startswith('$') and text.endswith('%'):
|
|
1888
|
+
text = text.rsplit(' ', 1)[1]
|
|
1889
|
+
parts.append([(f'throttle {text}', role)])
|
|
1890
|
+
parts += [part for part in specs[-1][1] if part[0][0][:4] in ('cpu ', 'mem ')] # the host's cpu and mem
|
|
1891
|
+
segments = [(' fleet ', 'ink-dim'), (marks[verdict[0]], verdict[1])]
|
|
1892
|
+
used, sep = cell_width(' fleet ' + marks[verdict[0]]), f' {marks["sep"]} '
|
|
1893
|
+
for part in parts:
|
|
1894
|
+
cost = cell_width(sep) + sum(cell_width(text) for text, _role in part)
|
|
1895
|
+
if used + cost > width:
|
|
1896
|
+
break
|
|
1897
|
+
segments += [(sep, 'ink-dim')] + part
|
|
1898
|
+
used += cost
|
|
1899
|
+
if stale:
|
|
1900
|
+
segments = [(text, 'ink-dim') for text, _role in segments]
|
|
1901
|
+
return self.paint(segments)
|
|
1902
|
+
|
|
1903
|
+
def fleet_block(self, cols, rows, now):
|
|
1904
|
+
"""The block's painted rows (render, render_mobile), [] when there is none: neither object published (a statusd
|
|
1905
|
+
before W11), a pane under FLEET_SHORT rows, or the key sheet up (it takes the rows back until the next key, so
|
|
1906
|
+
the whole sheet still fits a 24-row pane). Folded (`i`): one line. FLEET_FULL rows or more: the rule, Throttle,
|
|
1907
|
+
Anthropic, OpenAI, Moonshot (Kimi), the host, its containers; fewer: Throttle, the worse provider, the host.
|
|
1908
|
+
Each row at most cols − 1 cells (column 36 never written)."""
|
|
1909
|
+
self.block_says_money = False
|
|
1910
|
+
if self.showing_keys or rows < FLEET_SHORT or not (self.fleet_facts or self.throttle_facts):
|
|
1911
|
+
return []
|
|
1912
|
+
stale, width = any(self.fleet_aged(now)), max(1, cols - 1)
|
|
1913
|
+
throttle, host = self.throttle_spec(now), self.host_spec()
|
|
1914
|
+
link = self.fleet_url
|
|
1915
|
+
if self.fleet_collapsed:
|
|
1916
|
+
return [self.fleet_folded([throttle] + [self.provider_spec(k, w, now) for k, w in PROVIDERS] + [host],
|
|
1917
|
+
width, stale)]
|
|
1918
|
+
# the strip row's economy line yields only to a Throttle row that drew the money AND every part after it (429,
|
|
1919
|
+
# spill): a 36-column row that dropped `spill 76%` does not show the strip's facts, so the strip keeps them
|
|
1920
|
+
throttle_row, whole = self.fleet_layout('fleet-throttle', throttle, width, stale)
|
|
1921
|
+
self.block_says_money = whole and self.throttle_shows_money()
|
|
1922
|
+
if rows < FLEET_FULL:
|
|
1923
|
+
key, word = self.worse_provider()
|
|
1924
|
+
return [throttle_row,
|
|
1925
|
+
self.fleet_row('fleet-provider', self.provider_spec(key, word, now), width, stale),
|
|
1926
|
+
self.fleet_row('fleet-host', host, width, stale, link)]
|
|
1927
|
+
return [self.fleet_rule(cols), throttle_row] + [
|
|
1928
|
+
self.fleet_row('fleet-provider', self.provider_spec(key, word, now), width, stale) for key, word in PROVIDERS
|
|
1929
|
+
] + [self.fleet_row('fleet-host', host, width, stale, link),
|
|
1930
|
+
self.fleet_row('fleet-host', self.containers_spec(), width, stale)]
|
|
1931
|
+
|
|
1932
|
+
def toggle_fleet(self):
|
|
1933
|
+
"""`i`: fold the fleet block to its one line, or unfold it. On a device with an owner the fold is theirs
|
|
1934
|
+
(users.set_look_flag: `look.fleet_collapsed`, through the profile's lock for at most PROFILE_LOCK_S) and every
|
|
1935
|
+
device of theirs follows on its next tick; a write the lock refused changes nothing and says so, keeping the
|
|
1936
|
+
keyboard. On a device nobody owns it is this sidebar's own. Then the keyboard goes back to the lane, as `o`."""
|
|
1937
|
+
self.begin_action()
|
|
1938
|
+
self.dirty = True
|
|
1939
|
+
folded, g = not self.fleet_collapsed, self.glyphs['marks']
|
|
1940
|
+
if self.profile_path and not users.set_look_flag(self.login, self.device, 'fleet_collapsed', folded,
|
|
1941
|
+
directory=os.path.dirname(self.profile_path),
|
|
1942
|
+
timeout=PROFILE_LOCK_S):
|
|
1943
|
+
self.message = f'profile busy or unreadable {g["sep"]} try again'
|
|
1944
|
+
return
|
|
1945
|
+
self.fleet_collapsed = folded
|
|
1946
|
+
if not (self.fleet_facts or self.throttle_facts):
|
|
1947
|
+
self.message = f'fleet {"folded" if folded else "unfolded"} {g["sep"]} no fleet facts yet'
|
|
1948
|
+
else:
|
|
1949
|
+
self.message = f'fleet {"folded" if folded else "unfolded"} {g["sep"]} i {"unfolds" if folded else "folds"}'
|
|
1950
|
+
self.release_focus()
|
|
1951
|
+
|
|
1462
1952
|
def attention(self, entry):
|
|
1463
1953
|
"""The attention section key of a registry entry, or None (the remainder)."""
|
|
1464
1954
|
return self.agent_rows([entry])[0][3]
|
|
@@ -1859,7 +2349,7 @@ class Sidebar:
|
|
|
1859
2349
|
def begin_action(self):
|
|
1860
2350
|
"""An action begins (cockpit v3 S6): the last action's message goes, so the footer never shows what an EARLIER
|
|
1861
2351
|
action said beside what this one does. Called first by every verb — open (⏎ Space → J K 2 3, Tab, a click on a
|
|
1862
|
-
row), `x`/`×`, `R`, `P`, `a`/`+`, `/`, `U` (and a click on the strip), `+`, `r`, `?`, `u`, `z`, a sidebar-key
|
|
2352
|
+
row), `x`/`×`, `R`, `P`, `a`/`+`, `/`, `U` (and a click on the strip), `+`, `r`, `?`, `u`, `z`, `i`, a sidebar-key
|
|
1863
2353
|
chord. A cursor move, a typed letter, Esc, the wheel, a report begin nothing: a message outlives them for
|
|
1864
2354
|
MESSAGE_S. An undo offer (`u`, cockpit v3 S7) is not a message: only `z`, the next `u` or UNDO_S end it."""
|
|
1865
2355
|
self.message = ''
|
|
@@ -3163,14 +3653,21 @@ class Sidebar:
|
|
|
3163
3653
|
entries = self.follow() # the cursor re-resolved onto its lane, clamped (spec §4.5)
|
|
3164
3654
|
self.expire_theme() # no OSC 11 answer within the grace: dark it is (spec §2.4)
|
|
3165
3655
|
now = time.time()
|
|
3166
|
-
# Frame order (spec §4.6): header, body,
|
|
3167
|
-
# attention to show), rule, footer. `box_top` is the upload
|
|
3168
|
-
#
|
|
3656
|
+
# Frame order (spec §4.6, W11 §4.7): header, body, the fleet block (rows ≥ 18, fleet_block), upload strip
|
|
3657
|
+
# (rows ≥ 14), strip row (rows ≥ 18, economy or fleet attention to show), rule, footer. `box_top` is the upload
|
|
3658
|
+
# strip's 1-based row; a click at or below it arms, a click on the block (`block_top`, `block_rows`) opens
|
|
3659
|
+
# nothing. The strip row is the fleet economy line when economy.json knows a fleet fact (Task 12) and the block
|
|
3660
|
+
# does not show the same facts (block_says_money: the unfolded Throttle row drew the money and every part after
|
|
3661
|
+
# it; an untokened `up …`, a `down`, a row that dropped `spill` or `429` to fit, or the fold's bare percent do
|
|
3662
|
+
# not, and the strip keeps its place), else fleet attention.
|
|
3169
3663
|
self.box = rows >= 14 and not self.registration and not self.attach_only
|
|
3170
|
-
|
|
3664
|
+
block = self.fleet_block(cols, rows, now)
|
|
3665
|
+
economy_line = (self.economy_strip(cols - 2, now)[0] if self.economy and rows >= 18
|
|
3666
|
+
and not (block and self.block_says_money) else '')
|
|
3171
3667
|
self.strip_row = rows >= 18 and (bool(economy_line) or bool(self.fleet))
|
|
3172
|
-
body = max(1, rows - 3 - (BOX_ROWS if self.box else 0) - (1 if self.strip_row else 0))
|
|
3173
|
-
self.
|
|
3668
|
+
body = max(1, rows - 3 - (BOX_ROWS if self.box else 0) - (1 if self.strip_row else 0) - len(block))
|
|
3669
|
+
self.block_top, self.block_rows = (2 + body if block else 0), len(block)
|
|
3670
|
+
self.box_top = 2 + body + len(block)
|
|
3174
3671
|
rows, hidden_below = self.place_body(entries, body)
|
|
3175
3672
|
self.body_map = rows + ([next(i for i in range(self.top, len(entries)) if i not in rows)] if hidden_below else [])
|
|
3176
3673
|
self.more_row = 1 + body if hidden_below else 0 # 1-based screen row of `⋮ N more`, 0 = none
|
|
@@ -3250,6 +3747,7 @@ class Sidebar:
|
|
|
3250
3747
|
# legend rung itself says so, in the accent (keys_here, cockpit v3 S4): ` ▌ keys go to sidebar · Esc → lane` —
|
|
3251
3748
|
# no extra row, every rung above it unchanged.
|
|
3252
3749
|
self.legend_shown = self.keys_here()
|
|
3750
|
+
out.extend(f'{line}\033[K\r\n' for line in block) # W11: pinned above the upload strip
|
|
3253
3751
|
if self.box:
|
|
3254
3752
|
out.append(self.strip_line(cols) + '\033[K\r\n')
|
|
3255
3753
|
if self.strip_row: # spec §4.6(c): the fleet economy line first; without one, fleet attention
|
|
@@ -3268,7 +3766,9 @@ class Sidebar:
|
|
|
3268
3766
|
self.body_map = None # the phone's rows are `top + row` (three terminal rows each); place_body is the desktop's
|
|
3269
3767
|
self.box = False # the Mac file-drop helper is not available in a phone browser
|
|
3270
3768
|
self.entry_top, self.entry_height = 3, 3 if rows >= 8 else 1
|
|
3271
|
-
|
|
3769
|
+
# W11: the fleet block above the footer, as on the desktop (fleet_block: the same rows, the phone's width)
|
|
3770
|
+
block = self.fleet_block(cols, rows, time.time())
|
|
3771
|
+
body = max(1, (rows - 3 - len(block)) // self.entry_height)
|
|
3272
3772
|
self.box_top = self.entry_top + body * self.entry_height
|
|
3273
3773
|
self.top = max(0, min(self.top, self.cursor, len(entries) - body)) # bounded by the list, as in render()
|
|
3274
3774
|
if self.cursor >= self.top + body:
|
|
@@ -3370,8 +3870,10 @@ class Sidebar:
|
|
|
3370
3870
|
detail += sep + 'status delayed'
|
|
3371
3871
|
line(' ' + detail, tone['ink-dim'])
|
|
3372
3872
|
line(' ' + task if task else '', tone['ink-dim'])
|
|
3373
|
-
for _ in range(max(0, rows - (2 + body * self.entry_height + 1))):
|
|
3873
|
+
for _ in range(max(0, rows - (2 + body * self.entry_height + 1 + len(block)))):
|
|
3374
3874
|
line()
|
|
3875
|
+
self.block_top, self.block_rows = (rows - len(block) if block else 0), len(block)
|
|
3876
|
+
out.extend(f'{row}\033[K\r\n' for row in block)
|
|
3375
3877
|
# The footer the desktop draws (footer_text, cockpit v3 S6): every prompt, the filter, the banner, the message;
|
|
3376
3878
|
# only the picker's way out (`Tap Cancel`) and the idle hint (`Swipe to scroll · Tap to open`) are the phone's.
|
|
3377
3879
|
out.append(self.footer_row(cols, entries) + '\033[K\033[J')
|
|
@@ -3502,6 +4004,9 @@ class Sidebar:
|
|
|
3502
4004
|
elif button == 0 and pressed and self.box and y >= self.box_top:
|
|
3503
4005
|
self.begin_action()
|
|
3504
4006
|
self.armed = True # the click also gave this pane the focus: the next drop lands here
|
|
4007
|
+
elif (button == 0 and pressed and self.block_top
|
|
4008
|
+
and self.block_top <= y < self.block_top + self.block_rows):
|
|
4009
|
+
self.release_focus() # W11: the fleet block opens nothing (its link is the terminal's)
|
|
3505
4010
|
elif button == 0 and pressed:
|
|
3506
4011
|
# A click that opens nothing — a section header, `⋮ N more`, the header row, an empty row —
|
|
3507
4012
|
# ends there: the keyboard goes back to the lane (cockpit v3 S3). `×` and `+` open a prompt,
|
|
@@ -3637,6 +4142,8 @@ class Sidebar:
|
|
|
3637
4142
|
self.request_hide_workspace()
|
|
3638
4143
|
elif ch == 'o':
|
|
3639
4144
|
self.toggle_hidden()
|
|
4145
|
+
elif ch == 'i': # W11: fold / unfold the fleet block (the owner's profile)
|
|
4146
|
+
self.toggle_fleet()
|
|
3640
4147
|
elif ch == 'd': # plan W10 W7: the cursor lane's docs link in the footer
|
|
3641
4148
|
self.show_docs_link(entries[min(self.cursor, len(entries) - 1)] if entries else None)
|
|
3642
4149
|
elif ch == 'P' and entries:
|
|
@@ -3707,6 +4214,7 @@ class Sidebar:
|
|
|
3707
4214
|
self.poll_health()
|
|
3708
4215
|
self.poll_update()
|
|
3709
4216
|
self.poll_economy()
|
|
4217
|
+
self.poll_fleet() # W11: the fleet block's facts turning stale (no I/O)
|
|
3710
4218
|
# An upload in flight no longer forces a frame: poll_upload() dirties on progress and on the end.
|
|
3711
4219
|
if self.armed or (self.upload_note and time.time() - self.upload_note_at < 9):
|
|
3712
4220
|
self.dirty = True
|