@lamemind/loom-deck 0.69.0 → 0.71.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/dist/actions.js +55 -0
- package/dist/cli.js +39 -13
- package/dist/deck-model.js +113 -31
- package/dist/doc-tree.js +468 -0
- package/dist/doc-views.js +117 -0
- package/dist/frame.js +51 -23
- package/dist/hooks.js +55 -19
- package/dist/inbox.js +10 -1
- package/dist/input-modes.js +38 -1
- package/dist/input.js +90 -21
- package/dist/model.js +11 -0
- package/dist/mouse.js +18 -8
- package/dist/overlays/file-sheet.js +101 -0
- package/dist/pane-header.js +42 -0
- package/dist/sane-note.js +4 -4
- package/dist/session-meta.js +45 -27
- package/dist/spawn-catalog.js +40 -4
- package/dist/ui/doc-pane.js +85 -0
- package/dist/ui/file-sheet-screen.js +58 -0
- package/dist/ui/header-line.js +19 -0
- package/dist/ui/inbox-pane.js +83 -0
- package/dist/ui/panes.js +13 -74
- package/dist/ui/preview.js +25 -1
- package/dist/ui/screens.js +16 -4
- package/dist/viewport.js +41 -18
- package/package.json +1 -1
- package/scripts/deck-run +6 -5
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Il DETAIL di un file: testo scorrevole più la riga che dichiara cosa partirà
|
|
2
|
+
// su `⏎`.
|
|
3
|
+
//
|
|
4
|
+
// T134 nasce come `useInboxOverlay`, un hook per la sola coda inbox. T160/P11 lo
|
|
5
|
+
// rende PARAMETRICO su cosa sta guardando, e lo instanzia due volte: una per il
|
|
6
|
+
// file inbox, una per il bersaglio doc (file o cartella). Le due istanze restano
|
|
7
|
+
// due voci distinte nei tre cataloghi dei modi (`CAPTURING_MODES`,
|
|
8
|
+
// `SCROLLING_MODES`, `MODAL_HEIGHT`), perché sono due schermate diverse anche se
|
|
9
|
+
// condividono la meccanica: `esc` da una torna al pane inbox, dall'altra al pane
|
|
10
|
+
// doc, e un modo solo per entrambe obbligherebbe a ricordare da dove si era
|
|
11
|
+
// entrati.
|
|
12
|
+
//
|
|
13
|
+
// Cosa è parametrico e cosa no. Parametrico: l'OGGETTO fotografato (`T`), il
|
|
14
|
+
// testo, il prompt e la callback. Non parametrico: lo scorrimento, la capienza,
|
|
15
|
+
// la chiusura prima dello spawn — cioè tutto ciò che è la meccanica dello sheet,
|
|
16
|
+
// ed è la parte che non aveva senso duplicare.
|
|
17
|
+
//
|
|
18
|
+
// L'oggetto è FOTOGRAFATO all'apertura e non è una vista sulla selezione
|
|
19
|
+
// corrente: l'overlay copre la lista, quindi il bersaglio dell'azione deve
|
|
20
|
+
// restare quello che si è scelto anche se un tick dello scan spostasse la
|
|
21
|
+
// selezione sotto.
|
|
22
|
+
//
|
|
23
|
+
// Lo spawn NON sta qui: arriva come callback, per lo stesso confine di
|
|
24
|
+
// `useSheetOverlay` e `useSearchOverlay` — un hook di overlay non esce dal deck.
|
|
25
|
+
import { useMemo, useState } from 'react';
|
|
26
|
+
import { fileSheetCapacity, pageStep } from '../viewport.js';
|
|
27
|
+
import { wrapWithOffsets } from '../width.js';
|
|
28
|
+
import { parseMarkdown } from '../markdown.js';
|
|
29
|
+
export function useFileSheet(deps) {
|
|
30
|
+
const { mode, rows, columns, setMode, setNote, onEnter } = deps;
|
|
31
|
+
const [sheet, setSheet] = useState(null);
|
|
32
|
+
const [top, setTop] = useState(0);
|
|
33
|
+
// Cornici da scalare: box esterno (2 bordi + 2 padding) + box testo (2 bordi
|
|
34
|
+
// + 2 padding) = 8, come il detail della task.
|
|
35
|
+
const width = Math.max(20, (columns || 80) - 8);
|
|
36
|
+
// Il markdown si rende PRIMA del wrap: `**foo**` occupa 3 colonne rese e 7
|
|
37
|
+
// grezze, quindi wrappare sui marker manderebbe a capo su un conteggio che il
|
|
38
|
+
// terminale non disegna. Memo separato dal wrap perché il parse dipende solo
|
|
39
|
+
// dal testo: un resize ri-wrappa, non ri-parsa.
|
|
40
|
+
const doc = useMemo(() => (sheet?.text ? parseMarkdown(sheet.text) : null), [sheet]);
|
|
41
|
+
const lines = useMemo(() => (doc ? wrapWithOffsets(doc.text, width) : []), [doc, width]);
|
|
42
|
+
const capacity = fileSheetCapacity(rows);
|
|
43
|
+
const maxTop = Math.max(0, lines.length - capacity);
|
|
44
|
+
const prompt = sheet?.prompt ?? '';
|
|
45
|
+
const hint = sheet?.hint ?? '';
|
|
46
|
+
function open(next) {
|
|
47
|
+
setSheet(next);
|
|
48
|
+
setTop(0);
|
|
49
|
+
setNote('');
|
|
50
|
+
setMode(mode);
|
|
51
|
+
}
|
|
52
|
+
function close() {
|
|
53
|
+
setMode('normal');
|
|
54
|
+
setSheet(null);
|
|
55
|
+
}
|
|
56
|
+
function scroll(delta) {
|
|
57
|
+
setTop((t) => Math.max(0, Math.min(maxTop, t + delta)));
|
|
58
|
+
}
|
|
59
|
+
function onKey(input, key) {
|
|
60
|
+
if (key.escape) {
|
|
61
|
+
close();
|
|
62
|
+
}
|
|
63
|
+
else if (key.return) {
|
|
64
|
+
// Si chiude PRIMA di spawnare, come lo sheet della task: la schermata ha
|
|
65
|
+
// finito il suo lavoro, e lasciarla aperta sopra una tab appena nata
|
|
66
|
+
// farebbe credere che ci sia dell'altro da decidere.
|
|
67
|
+
const s = sheet;
|
|
68
|
+
if (s && !s.prompt) {
|
|
69
|
+
// Bersaglio senza azione: la schermata resta aperta e lo dice. Chiuderla
|
|
70
|
+
// senza fare niente sarebbe indistinguibile da uno spawn riuscito.
|
|
71
|
+
setNote(s.hint || '⏎ → nessuna azione su questo bersaglio');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
close();
|
|
75
|
+
if (s)
|
|
76
|
+
onEnter(s.item, s.prompt);
|
|
77
|
+
}
|
|
78
|
+
else if (key.upArrow) {
|
|
79
|
+
scroll(-1);
|
|
80
|
+
}
|
|
81
|
+
else if (key.downArrow) {
|
|
82
|
+
scroll(1);
|
|
83
|
+
}
|
|
84
|
+
else if (key.pageUp) {
|
|
85
|
+
scroll(-pageStep(capacity));
|
|
86
|
+
}
|
|
87
|
+
else if (key.pageDown) {
|
|
88
|
+
scroll(pageStep(capacity));
|
|
89
|
+
}
|
|
90
|
+
else if (input === 'g') {
|
|
91
|
+
// `g`/`G` sugli estremi, come reader, detail e project status: anche
|
|
92
|
+
// questa schermata scorre testo, quindi eredita lo stesso alfabeto invece
|
|
93
|
+
// di inventarne un secondo.
|
|
94
|
+
setTop(0);
|
|
95
|
+
}
|
|
96
|
+
else if (input === 'G') {
|
|
97
|
+
setTop(maxTop);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { sheet, doc, lines, top, capacity, maxTop, prompt, hint, open, onKey, scroll };
|
|
101
|
+
}
|
package/dist/pane-header.js
CHANGED
|
@@ -13,6 +13,8 @@ import { cutParts } from './width.js';
|
|
|
13
13
|
import { paneTextWidth } from './layout.js';
|
|
14
14
|
import { SESSION_VIEWS, TASK_VIEWS, } from './pane-views.js';
|
|
15
15
|
import { INBOX_VIEWS } from './inbox-views.js';
|
|
16
|
+
import { DOC_VIEWS } from './doc-views.js';
|
|
17
|
+
import { WARN } from './glyphs.js';
|
|
16
18
|
function finish(parts, columns) {
|
|
17
19
|
const shown = cutParts(parts.map((p) => p.text), paneTextWidth(columns), parts.findIndex((p) => p.active));
|
|
18
20
|
return { parts, shown };
|
|
@@ -80,6 +82,46 @@ export function inboxHeaderParts(counts, active, above, below, columns) {
|
|
|
80
82
|
{ key: null, text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false },
|
|
81
83
|
], columns);
|
|
82
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* T160 — header del pane doc, quarto gemello: `Doc` nomina il pane e non si
|
|
87
|
+
* seleziona, poi le cinque viste del catalogo, poi l'avviso di misura parziale,
|
|
88
|
+
* poi `↑N`/`↓N`.
|
|
89
|
+
*
|
|
90
|
+
* L'attenuazione la decide il catalogo (`v.dim(counts)`) come per l'inbox: la
|
|
91
|
+
* voce `Tutti` conta ogni riga dell'albero ma si grigia su quante ne sono
|
|
92
|
+
* flaggate, perché è quello a dire se c'è qualcosa da lanciare.
|
|
93
|
+
*
|
|
94
|
+
* L'avviso `senza cartelle` copre il caso della cascata (P2): il deck esegue lo
|
|
95
|
+
* script dalla cache installata del plugin e può incontrare una versione che
|
|
96
|
+
* emette la sola tabella dei file. L'albero si costruisce lo stesso — le
|
|
97
|
+
* cartelle si deducono dai path — ma i flag di cartella non ci sono, e senza
|
|
98
|
+
* l'avviso un `REGROUP (0)` direbbe «nessuna cartella da riorganizzare» invece
|
|
99
|
+
* di «non l'ho potuto misurare». Sta PRIMA di `↑N`/`↓N` perché è
|
|
100
|
+
* un'informazione sulla misura, e i contatori di scroll sono i primi a cedere il
|
|
101
|
+
* posto su un terminale stretto.
|
|
102
|
+
*/
|
|
103
|
+
export function docHeaderParts(counts, active, above, below, columns, hasDirs) {
|
|
104
|
+
const views = DOC_VIEWS.map((v) => ({
|
|
105
|
+
key: v.id,
|
|
106
|
+
text: ` · ${v.label(counts)}`,
|
|
107
|
+
color: v.color,
|
|
108
|
+
dim: v.dim(counts),
|
|
109
|
+
active: v.id === active,
|
|
110
|
+
}));
|
|
111
|
+
return finish([
|
|
112
|
+
{ key: null, text: 'Doc', dim: false, active: false },
|
|
113
|
+
...views,
|
|
114
|
+
{
|
|
115
|
+
key: null,
|
|
116
|
+
text: hasDirs ? '' : ` · ${WARN} senza cartelle`,
|
|
117
|
+
color: 'yellow',
|
|
118
|
+
dim: false,
|
|
119
|
+
active: false,
|
|
120
|
+
},
|
|
121
|
+
{ key: null, text: above > 0 ? ` · ↑${above}` : '', dim: true, active: false },
|
|
122
|
+
{ key: null, text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false },
|
|
123
|
+
], columns);
|
|
124
|
+
}
|
|
83
125
|
/** Le parti COME SONO A SCHERMO — testo tagliato, chiave intatta — nella forma
|
|
84
126
|
* che `inlineRegions` (`mouse.ts`) misura per l'hit-test. */
|
|
85
127
|
export function headerItems(h) {
|
package/dist/sane-note.js
CHANGED
|
@@ -22,24 +22,24 @@
|
|
|
22
22
|
// che fallisce se una resta indietro.
|
|
23
23
|
/**
|
|
24
24
|
* L'alfabeto ammesso, negato: lettere, cifre, spazio, `_`, `-`, le accentate
|
|
25
|
-
* italiane e
|
|
25
|
+
* italiane e sette emoji.
|
|
26
26
|
*
|
|
27
27
|
* Whitelist e non blacklist, come in bash: il titolo finisce dentro apici
|
|
28
28
|
* singoli in `bash -lc "claude --name '…'"`, ed enumerare i caratteri pericolosi
|
|
29
29
|
* significa sbagliarne uno.
|
|
30
30
|
*
|
|
31
|
-
* Le
|
|
31
|
+
* Le sette emoji sono nominate una per una e NON come intervallo di code point,
|
|
32
32
|
* per due ragioni che valgono su entrambi i lati: un'emoji arriva nella tab solo
|
|
33
33
|
* se il deck la mostra identica in lista, cioè se `sanitize` (`src/width.ts`) la
|
|
34
34
|
* lascia passare invece di renderla `·`, e quel verdetto si verifica un glifo
|
|
35
35
|
* alla volta; e l'ordine di un intervallo dentro un bracket expression segue la
|
|
36
36
|
* collazione della locale, non i code point.
|
|
37
37
|
*
|
|
38
|
-
* Il flag `u` non è cosmetico: senza, le
|
|
38
|
+
* Il flag `u` non è cosmetico: senza, le emoji astrali entrerebbero
|
|
39
39
|
* nella classe come coppie di surrogati sciolti, e la riduzione lascerebbe
|
|
40
40
|
* passare mezzo carattere di qualunque altra emoji che ne condivida il primo.
|
|
41
41
|
*/
|
|
42
|
-
const OUT_OF_ALPHABET = /[^A-Za-z0-9 _
|
|
42
|
+
const OUT_OF_ALPHABET = /[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁🧹📏🧭-]/gu;
|
|
43
43
|
/** Cap in CARATTERI (code point), non in byte: a byte un taglio a metà di una
|
|
44
44
|
* `à` lascerebbe UTF-8 rotto nel titolo. Largo di proposito — una tab che sfora
|
|
45
45
|
* tronca a destra da sola, e il limite serve solo a non spingere fuori vista la
|
package/dist/session-meta.js
CHANGED
|
@@ -8,14 +8,18 @@
|
|
|
8
8
|
// lì da chi ce l'ha in mano, cioè il deck, che a ogni tick materializza le
|
|
9
9
|
// `Session` complete di titolo e modello.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// Il giudizio è separato dalla scrittura: `sidecarGaps` decide e ritorna i
|
|
12
|
+
// record, `fillSidecarGaps` li scrive. La separazione è ciò che rende testabile
|
|
13
|
+
// la parte non ovvia — la convergenza: un derivato che non combacia mai col
|
|
14
|
+
// valore su disco produrrebbe un append a ogni tick del poll, per sempre, senza
|
|
15
|
+
// nessun errore a dirlo.
|
|
16
|
+
//
|
|
17
|
+
// Nessun import da ink/react: il chiamante è un hook, questo modulo no.
|
|
16
18
|
import { modelAlias } from './glyphs.js';
|
|
17
19
|
import { stripProjectCore, stripTaskId } from './session-list.js';
|
|
18
20
|
import { NO_TITLE } from './sessions.js';
|
|
21
|
+
import { NO_SPAWN } from './spawn.js';
|
|
22
|
+
import { appendSessionMeta } from './task-index.js';
|
|
19
23
|
/**
|
|
20
24
|
* Cap del titolo scritto nel sidecar, in CODE POINT.
|
|
21
25
|
*
|
|
@@ -62,28 +66,6 @@ export function sidecarTitle(s, core, taskId) {
|
|
|
62
66
|
return s.title === NO_TITLE ? '' : capTitle(s.title);
|
|
63
67
|
return capTitle(stripTaskId(stripProjectCore(s.title, core), taskId));
|
|
64
68
|
}
|
|
65
|
-
/**
|
|
66
|
-
* I buchi da riempire in questo giro: per ogni conversazione PINNATA, i campi
|
|
67
|
-
* derivati che nel sidecar mancano o non corrispondono più al valore corrente.
|
|
68
|
-
*
|
|
69
|
-
* Solo le pinnate, e non tutte le conversazioni del progetto, perché sono le
|
|
70
|
-
* sole che un lettore esterno mostra: il resto sarebbe churn su un file
|
|
71
|
-
* append-only, un record per conversazione per ogni deck che si apre.
|
|
72
|
-
*
|
|
73
|
-
* Tre cose che non producono un append, e nessuna delle tre è un errore:
|
|
74
|
-
* - una pinnata STALE (il transcript non c'è più) — nessuna `Session`, quindi
|
|
75
|
-
* nessun valore da cui derivare;
|
|
76
|
-
* - un titolo derivato VUOTO — è l'assenza di P1, e appenderlo scriverebbe la
|
|
77
|
-
* cancellazione di un campo che nessuno ha chiesto di cancellare;
|
|
78
|
-
* - un modello non riconducibile a un alias (`''` o fuori famiglia) — chi
|
|
79
|
-
* riprende cadrà sulla propria cascata, che è meglio di un alias inventato.
|
|
80
|
-
*
|
|
81
|
-
* La CONVERGENZA è l'invariante da tenere: il confronto avviene fra il valore
|
|
82
|
-
* derivato e il valore letto dal sidecar senza riscritture in mezzo (nessuna
|
|
83
|
-
* sanificazione in lettura, cap applicato da questo lato una volta sola),
|
|
84
|
-
* quindi un buco riempito resta riempito e il giro successivo non trova niente
|
|
85
|
-
* da fare.
|
|
86
|
-
*/
|
|
87
69
|
export function sidecarGaps(input) {
|
|
88
70
|
const { pinned, sessions, bindings, titles, models, core } = input;
|
|
89
71
|
if (pinned.size === 0)
|
|
@@ -106,3 +88,39 @@ export function sidecarGaps(input) {
|
|
|
106
88
|
}
|
|
107
89
|
return out;
|
|
108
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Scrive i buchi di questo giro. Ritorna quanti record ha appeso — zero è
|
|
93
|
+
* l'esito normale a regime, non un fallimento.
|
|
94
|
+
*
|
|
95
|
+
* `NO_SPAWN` frena anche questa scrittura, ed è lo stesso freno delle tab
|
|
96
|
+
* Ptyxis per la stessa ragione: il gate su pseudo-terminale avvia il DECK VERO
|
|
97
|
+
* con cwd la project root del cappello, quindi senza freno ogni run della suite
|
|
98
|
+
* appende record nel sidecar reale di chi la lancia. Il valore scritto sarebbe
|
|
99
|
+
* pure corretto — è quello che il deck scriverebbe comunque — ma è una
|
|
100
|
+
* scrittura fuori dalla sandbox del test, che è precisamente ciò che il freno
|
|
101
|
+
* esiste per impedire.
|
|
102
|
+
*
|
|
103
|
+
* Il freno sta QUI e non nel chiamante perché questa è l'unica sede della
|
|
104
|
+
* scrittura: nel chiamante sarebbe una guardia che il prossimo scrittore non
|
|
105
|
+
* vede.
|
|
106
|
+
*
|
|
107
|
+
* Un sidecar non scrivibile non è un errore da mostrare: il giro dopo riprova,
|
|
108
|
+
* e nel frattempo il deck funziona identico — questi campi li legge un altro
|
|
109
|
+
* processo, non lui.
|
|
110
|
+
*/
|
|
111
|
+
export function fillSidecarGaps(projectRoot, input) {
|
|
112
|
+
if (NO_SPAWN)
|
|
113
|
+
return 0;
|
|
114
|
+
let scritti = 0;
|
|
115
|
+
try {
|
|
116
|
+
for (const gap of sidecarGaps(input)) {
|
|
117
|
+
appendSessionMeta(projectRoot, gap.sessionId, gap);
|
|
118
|
+
scritti++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// sidecar non scrivibile: si riprova al giro dopo, niente a schermo — è
|
|
123
|
+
// manutenzione di un campo di servizio, non un'azione che l'utente ha chiesto.
|
|
124
|
+
}
|
|
125
|
+
return scritti;
|
|
126
|
+
}
|
package/dist/spawn-catalog.js
CHANGED
|
@@ -110,7 +110,7 @@ const UNWRAP_PROMPT = 'lancia md-wrap modo apply su {path} e backup in cartella
|
|
|
110
110
|
'verifica risultato e fai rapporto. se le modifiche sono tutte chiaramente safe, ' +
|
|
111
111
|
'puoi committare direttamente';
|
|
112
112
|
/**
|
|
113
|
-
* Le
|
|
113
|
+
* Le DODICI azioni configurabili, nell'ordine in cui la pagina le elenca.
|
|
114
114
|
*
|
|
115
115
|
* Il perimetro è quello di D1: entra una riga se ha almeno una cella editabile.
|
|
116
116
|
* Ne restano fuori resume e fork — lì il modello non è una preferenza ma
|
|
@@ -123,9 +123,10 @@ const UNWRAP_PROMPT = 'lancia md-wrap modo apply su {path} e backup in cartella
|
|
|
123
123
|
* T156 — le emoji dei titoli sono ASTRALI (code point ≥ U+10000) per necessità,
|
|
124
124
|
* non per gusto: `sanitize` (`src/width.ts`) sostituisce con `·` ogni glifo su
|
|
125
125
|
* cui le due contabilità di larghezza divergono, e le emoji del BMP a
|
|
126
|
-
* presentazione testo divergono sempre. Le stesse
|
|
127
|
-
* `_sane_note` in `deck-run` e nel suo gemello
|
|
128
|
-
* cambiata qui e non là sparisce fra la tabella e
|
|
126
|
+
* presentazione testo divergono sempre. Le stesse SETTE (T160 aggiunge `🧭`)
|
|
127
|
+
* stanno nella whitelist di `_sane_note` in `deck-run` e nel suo gemello
|
|
128
|
+
* `src/sane-note.ts`: un'emoji cambiata qui e non là sparisce fra la tabella e
|
|
129
|
+
* la tab, senza errore.
|
|
129
130
|
*/
|
|
130
131
|
export const SPAWN_ACTIONS = [
|
|
131
132
|
{
|
|
@@ -258,6 +259,35 @@ export const SPAWN_ACTIONS = [
|
|
|
258
259
|
holes: ['path'],
|
|
259
260
|
fixed: {},
|
|
260
261
|
},
|
|
262
|
+
{
|
|
263
|
+
// T160 — il rebalance della topologia doc su un bersaglio (file o cartella).
|
|
264
|
+
//
|
|
265
|
+
// La cella MODELLO è fissa, e la ragione mostrata al posto del valore non è
|
|
266
|
+
// una scorciatoia: `rebalance-doc` dichiara `model: sonnet` nel frontmatter,
|
|
267
|
+
// e quel campo ri-timbra il modello della sessione per la durata della
|
|
268
|
+
// skill, qualunque `--model` sia stato passato allo spawn. Una cella
|
|
269
|
+
// editabile qui accetterebbe un valore che non sopravvive all'invocazione —
|
|
270
|
+
// cioè mentirebbe senza produrre nessun errore. Lo stesso vale per le altre
|
|
271
|
+
// tre skill doc (`drain-notions`, `derive-notions`, `align-doc`), che la
|
|
272
|
+
// riga `drain` non ha ancora recepito.
|
|
273
|
+
//
|
|
274
|
+
// DUE buchi per lo stesso dato, e servono entrambi: `{target}` è il path
|
|
275
|
+
// VERO, che la skill risolve contro la colonna `PATH` della misura, e
|
|
276
|
+
// `{words}` lo stesso path reso a parole per il titolo. L'alfabeto di
|
|
277
|
+
// `_sane_note` non ha né `/` né `.`, quindi un `{target}` in un template di
|
|
278
|
+
// titolo uscirebbe con le parole saldate (`runtimereferencedoc-systemmd`) e
|
|
279
|
+
// la tab mostrerebbe un nome diverso da quello che il deck legge dal
|
|
280
|
+
// sidecar.
|
|
281
|
+
id: 'rebalance',
|
|
282
|
+
label: 'rebalance della doc',
|
|
283
|
+
surface: 'pane doc ⏎⏎',
|
|
284
|
+
kind: null,
|
|
285
|
+
title: '🧭 {words}',
|
|
286
|
+
model: 'sonnet',
|
|
287
|
+
prompt: '/loom-works:rebalance-doc {target}',
|
|
288
|
+
holes: ['target', 'words'],
|
|
289
|
+
fixed: { model: 'lo impone il frontmatter della skill' },
|
|
290
|
+
},
|
|
261
291
|
{
|
|
262
292
|
// T121 — il recap di progetto, headless (`claude -p`): nessuna tab, quindi
|
|
263
293
|
// nessun titolo da configurare.
|
|
@@ -289,6 +319,12 @@ export const EXAMPLE_HOLES = {
|
|
|
289
319
|
slug: 'esempio di task',
|
|
290
320
|
file: 'T42-nozioni',
|
|
291
321
|
path: 'runtime reference',
|
|
322
|
+
// T160 — i due buchi della riga `rebalance`, che portano lo stesso dato in due
|
|
323
|
+
// alfabeti: il path vero per il prompt della skill, le parole per il titolo
|
|
324
|
+
// della tab. L'esempio li mostra entrambi nella forma in cui arrivano davvero,
|
|
325
|
+
// o chi scrive un template li scambierebbe senza accorgersene.
|
|
326
|
+
target: 'runtime/reference/doc-system',
|
|
327
|
+
words: 'runtime reference doc-system',
|
|
292
328
|
};
|
|
293
329
|
/**
|
|
294
330
|
* I valori sono quelli che i FORNITORI dei buchi producono davvero, non testo
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// T160 — il pane dell'albero doc, che in modo doc occupa lo slot SINISTRO.
|
|
3
|
+
//
|
|
4
|
+
// Risponde a una domanda sola, ed è la ragione della sua forma: **vale la pena
|
|
5
|
+
// lanciare `rebalance-doc` adesso, e su cosa?** Da qui la colonna dei flag in
|
|
6
|
+
// testa, dove si scorre con l'occhio, e il nome come unica cella elastica.
|
|
7
|
+
//
|
|
8
|
+
// Le CIFRE non stanno in riga (P6): char, lunghezza del TLDR e conteggio file
|
|
9
|
+
// vivono nel blocco preview, insieme ai nomi interi dei flag. Su un pane largo la
|
|
10
|
+
// metà del terminale ogni colonna fissa la paga il nome, che è l'unica cosa con
|
|
11
|
+
// cui si riconosce un file — la stessa scelta già fatta per il pane inbox.
|
|
12
|
+
//
|
|
13
|
+
// Presentazionale puro: riceve la vista già selezionata e le larghezze già
|
|
14
|
+
// calcolate, non le deriva.
|
|
15
|
+
import { Box, Text } from 'ink';
|
|
16
|
+
import { cut } from '../width.js';
|
|
17
|
+
import { paneTextWidth } from '../layout.js';
|
|
18
|
+
import { CARET, CARET_OFF } from '../glyphs.js';
|
|
19
|
+
import { DOC_FLAG_W, flagCell } from '../doc-tree.js';
|
|
20
|
+
import { docView } from '../doc-views.js';
|
|
21
|
+
import { docHeaderParts } from '../pane-header.js';
|
|
22
|
+
import { HeaderLine } from './header-line.js';
|
|
23
|
+
/**
|
|
24
|
+
* Rientro di un livello dell'albero: UNA colonna, non due.
|
|
25
|
+
*
|
|
26
|
+
* L'albero della doc arriva a quattro livelli (`runtime/reference/loom-deck/
|
|
27
|
+
* sessions/`), e a due colonne per livello il rientro mangerebbe otto colonne
|
|
28
|
+
* della cella nome proprio sulle righe più profonde — che su un pane a 80
|
|
29
|
+
* colonne sono la metà di ciò che resta. Una colonna basta a vedere la
|
|
30
|
+
* gerarchia, perché le cartelle stanno comunque prima dei loro file.
|
|
31
|
+
*/
|
|
32
|
+
const INDENT_W = 1;
|
|
33
|
+
/** I flag che chiamano un'operazione di `rebalance-doc`: la riga che ne porta
|
|
34
|
+
* almeno uno è quella per cui il pane esiste, e si legge accesa. */
|
|
35
|
+
const ACTIONABLE = new Set(['SPLIT', 'MERGE?', 'REGROUP']);
|
|
36
|
+
/** Quelli che chiamano `write-tldr`: un difetto vero, ma di un'altra skill. */
|
|
37
|
+
const TLDR_FLAGS = new Set(['TLDR>CAP', 'NOTLDR', 'TLDR-ORFANA']);
|
|
38
|
+
/** Il colore della cella flag, dal flag più urgente che la riga porta. Un colore
|
|
39
|
+
* solo per cella e non uno per short: la cella è un `Text` unico, e stilarne i
|
|
40
|
+
* pezzi costerebbe tre nodi annidati per riga su una lista lunga quanto la doc. */
|
|
41
|
+
function flagColor(row) {
|
|
42
|
+
if (row.flags.some((f) => ACTIONABLE.has(f)))
|
|
43
|
+
return 'yellow';
|
|
44
|
+
if (row.flags.some((f) => TLDR_FLAGS.has(f)))
|
|
45
|
+
return 'cyan';
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Header del pane doc: le parti e il loro taglio vengono da `pane-header.ts`, lo
|
|
50
|
+
* stesso modulo da cui `frame.ts` ricava le colonne cliccabili.
|
|
51
|
+
*/
|
|
52
|
+
export function DocHeader({ counts, active, above, below, focused, columns, hasDirs, }) {
|
|
53
|
+
const { parts, shown } = docHeaderParts(counts, active, above, below, columns, hasDirs);
|
|
54
|
+
return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
|
|
55
|
+
}
|
|
56
|
+
export function DocPane({ rows, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, scanning, hasDirs, }) {
|
|
57
|
+
const width = paneTextWidth(columns);
|
|
58
|
+
// Cella elastica per sottrazione dalle colonne fisse, come nel pane inbox e
|
|
59
|
+
// nella lista sessioni. Pavimento 0 e non un minimo di cortesia: è un tetto,
|
|
60
|
+
// non una preferenza, e alzarlo sopra lo spazio reale fa uscire la riga dal
|
|
61
|
+
// pane mangiandone il bordo (invariante ③ di width.ts).
|
|
62
|
+
const nameW = Math.max(0, width - (2 /* caret */ + DOC_FLAG_W + 1 /* gutter */));
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(DocHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns, hasDirs: hasDirs }), paneCount === 0 ? (
|
|
64
|
+
// Quattro vuoti diversi, e dirlo è il punto. Ai tre del pane inbox se ne
|
|
65
|
+
// aggiunge uno che solo questo scan ha: «sto misurando». Quattro secondi
|
|
66
|
+
// di wall clock sono abbastanza da vedersi, e un pane vuoto per quattro
|
|
67
|
+
// secondi si legge come una doc senza file.
|
|
68
|
+
_jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(scanning
|
|
69
|
+
? 'misura in corso…'
|
|
70
|
+
: !scanned
|
|
71
|
+
? 'albero non ancora misurato'
|
|
72
|
+
: !ok
|
|
73
|
+
? 'misura della doc fallita: plugin assente o script in errore'
|
|
74
|
+
: docView(activeView).empty, width) })) : (rows.map((r) => {
|
|
75
|
+
const sel = r.path === selectedPath;
|
|
76
|
+
const indent = ' '.repeat(r.depth * INDENT_W);
|
|
77
|
+
// La barra finale distingue una cartella da un file senza spendere una
|
|
78
|
+
// colonna di marcatore: è la convenzione di `ls -F`, e su una riga che
|
|
79
|
+
// porta già una colonna di flag un glifo in più sarebbe la terza cosa
|
|
80
|
+
// da leggere prima del nome.
|
|
81
|
+
const label = r.kind === 'dir' ? `${r.name}/` : r.name;
|
|
82
|
+
const text = cut(indent + label, nameW);
|
|
83
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: flagColor(r), dimColor: r.flags.length === 0, children: flagCell(r.flags) }), ' ', _jsx(Text, { bold: r.kind === 'dir', dimColor: r.kind === 'file' && r.flags.length === 0, children: text })] }, r.path));
|
|
84
|
+
}))] }));
|
|
85
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// T134/T160 — il detail di un FILE: testo scorrevole più la riga che dichiara
|
|
3
|
+
// cosa partirà su `⏎`.
|
|
4
|
+
//
|
|
5
|
+
// Una schermata per le due istanze di `useFileSheet` (coda inbox e bersaglio
|
|
6
|
+
// doc). Quello che cambia fra loro è la TESTATA — tre celle di testo, che i due
|
|
7
|
+
// `…SheetHead` qui sotto compongono — e non la struttura: stesse righe fisse,
|
|
8
|
+
// stessa cornice, stesso `FILE_SHEET_CHROME` nel budget d'altezza. Due
|
|
9
|
+
// componenti avrebbero duplicato la cornice e il conteggio delle righe fisse,
|
|
10
|
+
// che è esattamente ciò che fa sfondare `rows` quando una delle due copie
|
|
11
|
+
// guadagna una riga e l'altra no.
|
|
12
|
+
//
|
|
13
|
+
// Riusa `DetailLine` del detail della task invece di ri-renderizzare il markdown
|
|
14
|
+
// per conto proprio: sono lo stesso costrutto — un documento reso in span
|
|
15
|
+
// tipizzati dentro un box — e due rese dello stesso markdown divergerebbero al
|
|
16
|
+
// primo stile aggiunto. L'evidenziazione della ricerca non serve qui, quindi le
|
|
17
|
+
// occorrenze arrivano vuote e il secondo taglio di `DetailLine` non gira nemmeno.
|
|
18
|
+
import { Box, Text } from 'ink';
|
|
19
|
+
import { cut } from '../width.js';
|
|
20
|
+
import { DetailLine } from './detail-screen.js';
|
|
21
|
+
import { INBOX_MARK, WARN, fmtDateTime, fmtSize } from '../glyphs.js';
|
|
22
|
+
import { NATURA_SHORT, inboxMark } from '../inbox.js';
|
|
23
|
+
import { flagNames } from '../doc-tree.js';
|
|
24
|
+
export function inboxSheetHead(file) {
|
|
25
|
+
const mark = inboxMark(file);
|
|
26
|
+
return {
|
|
27
|
+
tag: NATURA_SHORT[file.natura],
|
|
28
|
+
name: file.basename,
|
|
29
|
+
meta: `${INBOX_MARK[mark] ? `${INBOX_MARK[mark]} ` : ''}${mark}` +
|
|
30
|
+
`${file.branch ? ` ⟨${file.branch}⟩` : ''}` +
|
|
31
|
+
`${file.cappello ? ` · ${file.cappello}` : ''} · ${file.nozioni} nozioni · ` +
|
|
32
|
+
`${file.aperte} aperte · ${fmtSize(file.chars)} · ${fmtDateTime(file.created * 1000)}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* T160 — la testata del bersaglio doc.
|
|
37
|
+
*
|
|
38
|
+
* La meta cambia di natura fra file e cartella e non è un ramo di cortesia: il
|
|
39
|
+
* `char` di un file è la sua lunghezza, quello di una cartella la somma dei
|
|
40
|
+
* suoi, e le due si confrontano con due soglie diverse. Mostrarle sotto la
|
|
41
|
+
* stessa etichetta le farebbe leggere come la stessa misura.
|
|
42
|
+
*/
|
|
43
|
+
export function docSheetHead(row) {
|
|
44
|
+
const flags = flagNames(row.flags);
|
|
45
|
+
return {
|
|
46
|
+
tag: row.kind === 'dir' ? 'dir' : 'file',
|
|
47
|
+
name: row.path,
|
|
48
|
+
meta: (row.kind === 'dir'
|
|
49
|
+
? `${row.files} file · ${fmtSize(row.chars)} in cartella`
|
|
50
|
+
: `${fmtSize(row.chars)} · TLDR ${row.tldr || 'assente'}`) +
|
|
51
|
+
` · ${flags.length > 0 ? flags.join(' · ') : 'nessun flag'}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function FileSheetScreen({ head, missing, lines, spans, top, total, capacity, prompt, hint, columns, }) {
|
|
55
|
+
const last = Math.min(total, top + capacity);
|
|
56
|
+
const width = Math.max(20, (columns || 80) - 4);
|
|
57
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: head.tag }), " ", cut(head.name, Math.max(10, width - 8))] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [head.meta, missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193/PgUp/PgDn" }), " testo \u00B7 ", _jsx(Text, { color: "yellow", children: "g/G" }), " estremi \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " apre la sessione \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " chiude"] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " non leggibile \u00B7 l'azione resta attiva (la skill risolve il bersaglio per nome)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: [], current: -1 }, top + i)))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "\u23CE \u203A " }), _jsx(Text, { color: prompt ? 'green' : 'yellow', children: cut(prompt || hint || 'nessuna azione su questo bersaglio', Math.max(10, width - 4)) })] }) })] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// La resa di un header di pane già tagliato: ogni parte col suo stile, l'attiva
|
|
3
|
+
// in video inverso.
|
|
4
|
+
//
|
|
5
|
+
// T160 · D1 — esce da `ui/panes.tsx` insieme ai pane che ne uscivano, perché è
|
|
6
|
+
// ciò che tutti e quattro condividono. Il vincolo di `coding-standards`
|
|
7
|
+
// §Come si splitta un file ricresciuto è che nessun modulo estratto importi il
|
|
8
|
+
// file da cui è stato estratto: lasciando `HeaderLine` in `panes.tsx`, il pane
|
|
9
|
+
// inbox e il pane doc avrebbero dovuto importarlo da lì — cioè la dipendenza
|
|
10
|
+
// circolare che il vincolo esiste per vietare. Sta quindi in un file proprio, e
|
|
11
|
+
// la dipendenza va in un verso solo: i quattro pane importano questo, questo non
|
|
12
|
+
// importa nessuno di loro.
|
|
13
|
+
//
|
|
14
|
+
// Il video inverso e non un colore (T100/D5): costa 0 colonne e non entra in
|
|
15
|
+
// gara con la semantica di colore già occupata dalle voci del catalogo.
|
|
16
|
+
import { Text } from 'ink';
|
|
17
|
+
export function HeaderLine({ parts, shown, focused, }) {
|
|
18
|
+
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: parts.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, inverse: seg.active, children: shown[i] }, i)) : null) }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// T134 — il pane della coda inbox, che in modo doc occupa lo slot DESTRO.
|
|
3
|
+
//
|
|
4
|
+
// T160 · D1 — estratto da `ui/panes.tsx` insieme al pane doc, per la ragione
|
|
5
|
+
// scritta in `coding-standards` §Un criterio verificato una volta non è un
|
|
6
|
+
// custode: quel file era già stato splittato e stava ricrescendo. L'asse dello
|
|
7
|
+
// split è per PANE, non per livello — ogni pane porta il proprio header, la
|
|
8
|
+
// propria riga e la propria nota di vuoto, e sono le tre cose che si toccano
|
|
9
|
+
// insieme. Ciò che i quattro condividono (`HeaderLine`) sta in
|
|
10
|
+
// `ui/header-line.tsx`, e nessuno dei moduli estratti importa `ui/panes.tsx`.
|
|
11
|
+
//
|
|
12
|
+
// Presentazionale puro: riceve la vista già selezionata e le larghezze già
|
|
13
|
+
// calcolate, non le deriva.
|
|
14
|
+
import { Box, Text } from 'ink';
|
|
15
|
+
import { cut, pad, sanitize, termWidth } from '../width.js';
|
|
16
|
+
import { paneTextWidth } from '../layout.js';
|
|
17
|
+
import { CARET, CARET_OFF, INBOX_MARK, INBOX_MARK_W, relTime } from '../glyphs.js';
|
|
18
|
+
import { NATURA_SHORT, NATURA_W, inboxMark } from '../inbox.js';
|
|
19
|
+
import { inboxView } from '../inbox-views.js';
|
|
20
|
+
import { inboxHeaderParts } from '../pane-header.js';
|
|
21
|
+
import { HeaderLine } from './header-line.js';
|
|
22
|
+
/**
|
|
23
|
+
* Header del pane inbox: le parti e il loro taglio vengono da `pane-header.ts`,
|
|
24
|
+
* lo stesso modulo da cui `frame.ts` ricava le colonne cliccabili — una fonte
|
|
25
|
+
* sola, non due conti paralleli che divergono alla prima voce che cambia testo.
|
|
26
|
+
*/
|
|
27
|
+
export function InboxHeader({ counts, active, above, below, focused, columns, }) {
|
|
28
|
+
const { parts, shown } = inboxHeaderParts(counts, active, above, below, columns);
|
|
29
|
+
return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* T134 — il pane inbox, montato nello slot destro dal modo doc (D6).
|
|
33
|
+
*
|
|
34
|
+
* Mostra SEMPRE TUTTO, senza filtro sulla task selezionata a sinistra (D7
|
|
35
|
+
* preflight, che supersede la clausola di D6 congelate): niente righe meta,
|
|
36
|
+
* nessun trattamento speciale per il cappello vuoto o per un cappello che nomina
|
|
37
|
+
* una task non più in `tasks.md`. Ne discende che la colonna `CAPPELLO` di
|
|
38
|
+
* `doc-metrics` non serve al filtro — resta nel blocco preview, dove dice
|
|
39
|
+
* qualcosa senza costare una colonna su ogni riga.
|
|
40
|
+
*
|
|
41
|
+
* La riga porta marcatori, branch e nome (D8). Non le cifre (nozioni, aperte,
|
|
42
|
+
* char): stanno nella preview, e su un pane largo la metà del terminale ogni
|
|
43
|
+
* colonna fissa la paga la cella elastica del nome — che è l'unica cosa con cui
|
|
44
|
+
* si riconosce un file.
|
|
45
|
+
*/
|
|
46
|
+
export function InboxPane({ files, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, }) {
|
|
47
|
+
const width = paneTextWidth(columns);
|
|
48
|
+
// La cella elastica si calcola per SOTTRAZIONE dalle colonne fisse, come il
|
|
49
|
+
// titolo della lista sessioni: pavimento 0 e non un minimo di cortesia — è un
|
|
50
|
+
// tetto, non una preferenza, e alzarlo sopra lo spazio reale fa uscire la riga
|
|
51
|
+
// dal pane mangiandone il bordo (invariante ③ di width.ts).
|
|
52
|
+
const ageW = 3;
|
|
53
|
+
const nameW = Math.max(0, width -
|
|
54
|
+
(2 /* caret */ +
|
|
55
|
+
NATURA_W +
|
|
56
|
+
1 /* gutter */ +
|
|
57
|
+
INBOX_MARK_W +
|
|
58
|
+
1 /* gutter */ +
|
|
59
|
+
1 /* gutter prima della data */ +
|
|
60
|
+
ageW));
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(InboxHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), paneCount === 0 ? (
|
|
62
|
+
// Tre vuoti diversi, e dirlo è il punto: «non ho ancora misurato» non è
|
|
63
|
+
// «ho misurato e non c'è niente», e «la misura si è rotta» non è nessuno
|
|
64
|
+
// dei due (D2 preflight). Una lista vuota che non dice quale dei tre è
|
|
65
|
+
// si legge come un pane rotto.
|
|
66
|
+
_jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(!scanned
|
|
67
|
+
? 'coda non ancora misurata'
|
|
68
|
+
: !ok
|
|
69
|
+
? 'misura della coda fallita: plugin assente o script in errore'
|
|
70
|
+
: inboxView(activeView).empty, width) })) : (files.map((f) => {
|
|
71
|
+
const sel = f.path === selectedPath;
|
|
72
|
+
const mark = inboxMark(f);
|
|
73
|
+
// Il branch sta DENTRO la cella del nome, non in una colonna propria:
|
|
74
|
+
// è raro, e una colonna dedicata costerebbe spazio vuoto su ogni riga
|
|
75
|
+
// non-branchata di un pane largo la metà del terminale. Stessa scelta
|
|
76
|
+
// del marcatore di fork nella lista sessioni.
|
|
77
|
+
const branchMark = f.branch ? `⟨${f.branch}⟩ ` : '';
|
|
78
|
+
const inner = Math.max(0, nameW - termWidth(sanitize(branchMark)));
|
|
79
|
+
const name = cut(f.basename.replace(/\.md$/, ''), inner);
|
|
80
|
+
const age = relTime(f.created * 1000);
|
|
81
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: mark === 'broken' ? 'red' : mark === 'queued' ? 'green' : undefined, dimColor: mark === 'held' || mark === 'branched', children: pad(NATURA_SHORT[f.natura], NATURA_W) }), ' ', _jsx(Text, { color: mark === 'branched' ? 'yellow' : undefined, children: pad(INBOX_MARK[mark], INBOX_MARK_W) }), ' ', branchMark ? _jsx(Text, { color: "yellow", children: sanitize(branchMark) }) : null, _jsx(Text, { dimColor: mark !== 'queued', children: name }), ' '.repeat(Math.max(0, inner - termWidth(name))), ' ', _jsx(Text, { dimColor: true, children: pad(age, ageW, 'right') })] }, f.path));
|
|
82
|
+
}))] }));
|
|
83
|
+
}
|