@lamemind/loom-deck 0.31.0 → 0.33.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/README.md CHANGED
@@ -2,28 +2,19 @@
2
2
 
3
3
  Deck TUI (Ink) **per-progetto** della famiglia [loom](https://github.com/lamemind/loom-works).
4
4
 
5
- Legge il `tasks.md` del progetto e, con un tasto (poi un click), **spawna** una tab
6
- [Ptyxis](https://gitlab.gnome.org/chergert/ptyxis) che avvia una sessione Claude Code
7
- già bound alla task via `LOOM_TASK`.
5
+ Legge il `tasks.md` del progetto e, con un tasto (poi un click), **spawna** una tab [Ptyxis](https://gitlab.gnome.org/chergert/ptyxis) che avvia una sessione Claude Code già bound alla task via `LOOM_TASK`.
8
6
 
9
7
  ```
10
8
  ↑↓ scegli la task → ⏎ → tab CC di fianco → LOOM_TASK bound
11
9
  ```
12
10
 
13
- `⏎` apre il **detail** della task — task file scrollabile più una barra azioni —
14
- e da lì si sceglie con quale prompt entrare; `^K`/`^P`/`^R` restano acceleratori
15
- per chi lo sa già (vedi [I cinque modi di entrare in una
16
- task](#i-cinque-modi-di-entrare-in-una-task)).
11
+ `⏎` apre il **detail** della task — task file scrollabile più una barra azioni — e da lì si sceglie con quale prompt entrare; `^K`/`^P`/`^R` restano acceleratori per chi lo sa già (vedi [I cinque modi di entrare in una task](#i-cinque-modi-di-entrare-in-una-task)).
17
12
 
18
- Entrambi i prefissi del contratto loom entrano in lista: **`T`** (code task) e
19
- **`D`** (doc task). Il trattamento è identico ovunque tranne che su `^R`, dove il
20
- prompt dispatcha sulla skill di esecuzione della rispettiva famiglia.
13
+ Entrambi i prefissi del contratto loom entrano in lista: **`T`** (code task) e **`D`** (doc task). Il trattamento è identico ovunque tranne che su `^R`, dove il prompt dispatcha sulla skill di esecuzione della rispettiva famiglia.
21
14
 
22
15
  ## Ruolo nella famiglia loom
23
16
 
24
- `loom-deck` è un **client** con runtime proprio (TUI Ink) che **consuma** il contratto
25
- definito da `loom-works-plugin` (formato `tasks.md`, variabile `LOOM_TASK`) — non lo
26
- ridefinisce. Divisione dei ruoli con Compass:
17
+ `loom-deck` è un **client** con runtime proprio (TUI Ink) che **consuma** il contratto definito da `loom-works-plugin` (formato `tasks.md`, variabile `LOOM_TASK`) — non lo ridefinisce. Divisione dei ruoli con Compass:
27
18
 
28
19
  | | scope | ruolo | domanda |
29
20
  |---|---|---|---|
@@ -32,8 +23,7 @@ ridefinisce. Divisione dei ruoli con Compass:
32
23
 
33
24
  ## Architettura di processo
34
25
 
35
- Il deck è **UN processo Node**: spawna ma **non contiene** le sessioni CC — le possiede
36
- Ptyxis. Chiudere il deck non uccide le sessioni. La tab nasce nella window *attiva*
26
+ Il deck è **UN processo Node**: spawna ma **non contiene** le sessioni CC — le possiede Ptyxis. Chiudere il deck non uccide le sessioni. La tab nasce nella window *attiva*
37
27
  (quella col focus = il deck) → desktop isolation "gratis".
38
28
 
39
29
  ## Stato
@@ -50,13 +40,13 @@ Bootstrap + spike ① + **TUI ③** funzionante (legge `tasks.md`, `⏎` apre il
50
40
 
51
41
  ## Standard shortcut
52
42
 
53
- Regola unica, senza eccezioni — pensata per reggere l'aggiunta di nuove azioni
54
- senza collisioni:
43
+ Regola unica, senza eccezioni — pensata per reggere l'aggiunta di nuove azioni senza collisioni:
55
44
 
56
45
  | Tasto | Semantica | Note |
57
46
  |---|---|---|
58
47
  | `↑` `↓` | naviga nella lista | |
59
- | `←` `→` `tab` | cambia pane | |
48
+ | `tab` | cambia pane | l'unico tasto che sposta il focus |
49
+ | `←` `→` | cambia **vista** del pane a fuoco | ciclico; il catalogo è l'header stesso |
60
50
  | `⏎` | azione primaria del pane | Tasks → apre il **detail** della task selezionata · Sessions → riprende (`claude --resume`) la sessione selezionata |
61
51
  | **MAIUSCOLA** | **apre un modale** | cattura tutti i tasti; `esc` annulla, non esce |
62
52
  | minuscola | azione immediata, one-shot | |
@@ -64,12 +54,7 @@ senza collisioni:
64
54
  | `1`…`9` | voce `launch` n-esima del progetto | da `.claude/loom-works.json` |
65
55
  | `q` `esc` | esce dal deck | in un modale `esc` annulla soltanto |
66
56
 
67
- `CTRL` è il terzo livello, aggiunto quando è arrivata la ricerca. Serve perché
68
- un modale con **campi di testo** mangia ogni lettera nuda: là dentro nessun
69
- comando può essere una lettera semplice. `CTRL+X` e `x` nudo condividono lo
70
- stesso `input` e si distinguono solo per `key.ctrl`, quindi in modalità normale
71
- il ramo `CTRL` è valutato **per primo** e chiude l'intera classe — senza,
72
- `CTRL+F` cadrebbe nel ramo `f` e forkerebbe una sessione invece di cercare.
57
+ `CTRL` è il terzo livello, aggiunto quando è arrivata la ricerca. Serve perché un modale con **campi di testo** mangia ogni lettera nuda: là dentro nessun comando può essere una lettera semplice. `CTRL+X` e `x` nudo condividono lo stesso `input` e si distinguono solo per `key.ctrl`, quindi in modalità normale il ramo `CTRL` è valutato **per primo** e chiude l'intera classe — senza, `CTRL+F` cadrebbe nel ramo `f` e forkerebbe una sessione invece di cercare.
73
58
 
74
59
  Assegnazioni correnti:
75
60
 
@@ -97,39 +82,20 @@ Il footer è **due righe con due nature diverse**:
97
82
  t 💻 · c 🤖 · 1 📝 codium · 2 ☕ idea
98
83
  ```
99
84
 
100
- - **tasti** — cosa puoi fare *qui e ora*. Solo le voci attive: quelle contestuali
101
- compaiono quando il pane a fuoco le rende possibili e altrimenti spariscono,
85
+ - **tasti** — cosa puoi fare *qui e ora*. Solo le voci attive: quelle contestuali compaiono quando il pane a fuoco le rende possibili e altrimenti spariscono,
102
86
  invece di mostrarsi inerti. Fuori navigazione (`↑↓` `←→`), uscita (`q`) e
103
- indicatore `focus:` — le prime due sono universali in qualunque TUI, il pane a
104
- fuoco si vede già dall'evidenziazione.
105
- - **surface** — *dove* puoi aprire qualcosa: prima le due built-in (`t`/`c`), poi
106
- le voci `launch` del progetto. Stanno insieme perché hanno la stessa natura
107
- fire-once, `cwd` = project root, nessuno stato — e differiscono solo per essere
108
- universali invece che custom.
109
-
110
- L'indice da solo è opaco (le `launch` sono custom per-progetto, senza una lettera
111
- fissa per app), quindi la riga espone la mappa e non il conteggio. Se non entrano
112
- in larghezza, si ferma a voci intere e mostra il contatore di quelle fuori riga —
113
- mai un troncamento silenzioso; le celle delle due built-in sono riservate a monte,
114
- o le voci sfonderebbero il box di quel tanto. Il cap a `9` è imposto dai
115
- tasti-cifra, non dallo schema: un progetto può dichiarare più di 9 voci, quelle
116
- oltre la nona sono configurate ma non raggiungibili (e la riga lo dice).
117
-
118
- Le emoji sono quelle del menu compass. Per il terminale compass usa 🖥️, che nel
119
- frame Ink non sopravvive: `sanitize` lo sostituisce perché VTE lo disegna largo 1
120
- mentre `string-width` ne conta 2 (invariante ① di `src/width.ts`) — 💻 è il
121
- gemello concorde.
122
-
123
- `t` e `c` sono gemelle: entrambe aprono una surface del cappello nella stessa
124
- finestra Ptyxis, senza passare da un modale. `c` (minuscola, azione) e `C`
125
- (maiuscola, modale create-task) restano distinte per la regola sopra — così come
126
- `f` (fork) e `F` (filtri).
87
+ indicatore `focus:` — le prime due sono universali in qualunque TUI, il pane a fuoco si vede già dall'evidenziazione.
88
+ - **surface** *dove* puoi aprire qualcosa: prima le due built-in (`t`/`c`), poi le voci `launch` del progetto. Stanno insieme perché hanno la stessa natura — fire-once, `cwd` = project root, nessuno stato — e differiscono solo per essere universali invece che custom.
89
+
90
+ L'indice da solo è opaco (le `launch` sono custom per-progetto, senza una lettera fissa per app), quindi la riga espone la mappa e non il conteggio. Se non entrano in larghezza, si ferma a voci intere e mostra il contatore di quelle fuori riga — mai un troncamento silenzioso; le celle delle due built-in sono riservate a monte, o le voci sfonderebbero il box di quel tanto. Il cap a `9` è imposto dai tasti-cifra, non dallo schema: un progetto può dichiarare più di 9 voci, quelle oltre la nona sono configurate ma non raggiungibili (e la riga lo dice).
91
+
92
+ Le emoji sono quelle del menu compass. Per il terminale compass usa 🖥️, che nel frame Ink non sopravvive: `sanitize` lo sostituisce perché VTE lo disegna largo 1 mentre `string-width` ne conta 2 (invariante ① di `src/width.ts`) — 💻 è il gemello concorde.
93
+
94
+ `t` e `c` sono gemelle: entrambe aprono una surface del cappello nella stessa finestra Ptyxis, senza passare da un modale. `c` (minuscola, azione) e `C` (maiuscola, modale create-task) restano distinte per la regola sopra — così come `f` (fork) e `F` (filtri).
127
95
 
128
96
  ### I cinque modi di entrare in una task
129
97
 
130
- Sulla task selezionata (focus sul pane Tasks) si apre una sessione **bound**:
131
- `LOOM_TASK` iniettata, `sessionId` pinnato, binding scritto nel sidecar. Fra i
132
- modi cambia **solo il prompt iniziale**.
98
+ Sulla task selezionata (focus sul pane Tasks) si apre una sessione **bound**: `LOOM_TASK` iniettata, `sessionId` pinnato, binding scritto nel sidecar. Fra i modi cambia **solo il prompt iniziale**.
133
99
 
134
100
  | Azione (detail) | Tasto diretto | Prompt |
135
101
  |---|---|---|
@@ -139,18 +105,11 @@ modi cambia **solo il prompt iniziale**.
139
105
  | `run` | `^R` | `/loom-works:run-task <id>`, oppure `/loom-works:run-doc <id>` se l'id è una `D` |
140
106
  | `checkpoint` | — | `/loom-works:checkpoint-task <id>` |
141
107
 
142
- Il prompt iniziale è la scelta di **chi apre**, non una proprietà della task: lo
143
- stesso task file si apre per leggerne lo stato, per congelarne le decisioni, per
144
- eseguirlo o per entrarci a mani nude — e l'azione scelta *è* quell'intento.
108
+ Il prompt iniziale è la scelta di **chi apre**, non una proprietà della task: lo stesso task file si apre per leggerne lo stato, per congelarne le decisioni, per eseguirlo o per entrarci a mani nude — e l'azione scelta *è* quell'intento.
145
109
 
146
- Le due superfici sono complementari, non alternative. `⏎` apre il **detail** e lì
147
- si legge la Description *mentre* si decide: è il caso normale, perché la domanda
148
- «quale azione?» quasi sempre si risponde leggendo la task. I tre `CTRL` saltano
149
- il passaggio per chi lo sa già. Dentro il detail sono inerti — i bottoni sono lì.
110
+ Le due superfici sono complementari, non alternative. `⏎` apre il **detail** e lì si legge la Description *mentre* si decide: è il caso normale, perché la domanda «quale azione?» quasi sempre si risponde leggendo la task. I tre `CTRL` saltano il passaggio per chi lo sa già. Dentro il detail sono inerti — i bottoni sono lì.
150
111
 
151
- Col focus sul pane Sessions né `⏎` né i `CTRL` spawnano: l'oggetto dell'azione è
152
- la task selezionata, e senza quel pane a fuoco non ce n'è una (il deck lo dice
153
- nella riga di nota, invece di aprire qualcosa a caso).
112
+ Col focus sul pane Sessions né `⏎` né i `CTRL` spawnano: l'oggetto dell'azione è la task selezionata, e senza quel pane a fuoco non ce n'è una (il deck lo dice nella riga di nota, invece di aprire qualcosa a caso).
154
113
 
155
114
  ### `⏎` — il detail della task
156
115
 
@@ -159,43 +118,25 @@ Overlay fullscreen a due zone: il **task file per intero**, scrollabile
159
118
  navigabile con `←→`, con `open` a fuoco all'apertura. `⏎` esegue l'azione
160
119
  selezionata e chiude; `esc` chiude lasciando la lista con la stessa selezione.
161
120
 
162
- Le azioni non sono un catalogo nuovo: chiamano lo **stesso** percorso di spawn
163
- dei tasti diretti. Una superficie in più, zero percorsi di spawn in più.
121
+ Le azioni non sono un catalogo nuovo: chiamano lo **stesso** percorso di spawn dei tasti diretti. Una superficie in più, zero percorsi di spawn in più.
164
122
 
165
- Sono bottoni affiancati e non voci di un menu verticale perché è la forma che
166
- sopravvive all'arrivo del mouse: un rettangolo ha già coordinate e area
167
- cliccabile, una lista di voci andrebbe rifatta.
123
+ Sono bottoni affiancati e non voci di un menu verticale perché è la forma che sopravvive all'arrivo del mouse: un rettangolo ha già coordinate e area cliccabile, una lista di voci andrebbe rifatta.
168
124
 
169
- Dentro il detail `←→` non cambia più pane il ramo `useInput` della modalità
170
- chiude prima di arrivare a quello di default. Ink non ha focus-trap: la cattura
171
- **è** l'ordine dei rami.
125
+ Dentro il detail `←→` cicla i bottoni invece di cambiare la vista del pane — il
126
+ ramo `useInput` della modalità chiude prima di arrivare a quello di default. Ink non ha focus-trap: la cattura **è** l'ordine dei rami, e cambiare cosa fa un tasto fuori dal detail non tocca cosa fa dentro.
172
127
 
173
- Il catalogo dei prompt vive in `scripts/deck-run`, non nella TUI: è il primitive
174
- UI-agnostico, il deck gli passa un **simbolo** (`--prompt-kind`) e non una
175
- stringa. Così il testo sta in un posto solo, e con lui il suo vincolo di quoting
176
- — il prompt viaggia dentro apici singoli in `bash -lc`, quindi non può contenerne.
128
+ Il catalogo dei prompt vive in `scripts/deck-run`, non nella TUI: è il primitive UI-agnostico, il deck gli passa un **simbolo** (`--prompt-kind`) e non una stringa. Così il testo sta in un posto solo, e con lui il suo vincolo di quoting — il prompt viaggia dentro apici singoli in `bash -lc`, quindi non può contenerne.
177
129
 
178
130
  ### `f` — forkare una conversazione
179
131
 
180
- Il fork rama la sessione selezionata: `claude --resume <origine> --fork-session`
181
- apre un **sessionId nuovo** con il transcript copiato, lasciando l'origine
182
- intatta. Serve quando vuoi ripartire da un certo stato senza perdere il ramo
183
- originale — e siccome i due id sono distinti, non esistono mai due processi che
184
- scrivono lo stesso file (il vincolo *single-writer* dello store di Claude Code).
132
+ Il fork rama la sessione selezionata: `claude --resume <origine> --fork-session` apre un **sessionId nuovo** con il transcript copiato, lasciando l'origine intatta. Serve quando vuoi ripartire da un certo stato senza perdere il ramo originale — e siccome i due id sono distinti, non esistono mai due processi che scrivono lo stesso file (il vincolo *single-writer* dello store di Claude Code).
185
133
 
186
134
  Il nuovo id lo genera il deck e lo pinna con `--session-id`, per due ragioni:
187
135
 
188
- - il ramo **eredita la task** dell'origine (senza id noto in anticipo il fork
189
- di una sessione scoped comparirebbe come spot);
190
- - il **lineage** finisce nel sidecar `.claude/loom/session-tasks.jsonl` come
191
- campo `forkOf`. Serve perché il transcript del fork **non nomina** la sessione
192
- d'origine da nessuna parte: è una copia verbatim (stessi uuid dei messaggi) e
193
- `parentUuid` incatena i messaggi dentro un transcript, non le sessioni fra
194
- loro. Senza quel record un ramo sarebbe una riga gemella dell'originale, di
195
- cui eredita anche il titolo.
136
+ - il ramo **eredita la task** dell'origine (senza id noto in anticipo il fork di una sessione scoped comparirebbe come spot);
137
+ - il **lineage** finisce nel sidecar `.claude/loom/session-tasks.jsonl` come campo `forkOf`. Serve perché il transcript del fork **non nomina** la sessione d'origine da nessuna parte: è una copia verbatim (stessi uuid dei messaggi) e `parentUuid` incatena i messaggi dentro un transcript, non le sessioni fra loro. Senza quel record un ramo sarebbe una riga gemella dell'originale, di cui eredita anche il titolo.
196
138
 
197
- Un ramo si riconosce dal marker `⑂` nella lista e dalla riga `⑂ da <id>` nel
198
- pannello di dettaglio; la sua tab Ptyxis titola `<label> · <task> · fork`.
139
+ Un ramo si riconosce dal marker `⑂` nella lista e dalla riga `⑂ da <id>` nel pannello di dettaglio; la sua tab Ptyxis titola `<label> · <task> · fork`.
199
140
 
200
141
  > **Nota di migrazione (0.6.0)**: `c` → **`C`** per creare una task, e le voci
201
142
  > `codium`/`idea` non hanno più una lettera dedicata (erano `C`/`I` hardcoded):
@@ -203,20 +144,14 @@ pannello di dettaglio; la sua tab Ptyxis titola `<label> · <task> · fork`.
203
144
 
204
145
  ### `^F` — cercare dentro le conversazioni
205
146
 
206
- Il navigator trova una conversazione per **metadati** (titolo, data, turni). `^F`
207
- la trova per **contenuto**: «l'IA me l'ha detto 150k di testo fa, ricordo mezza
208
- parola». Claude Code non ha un find-in-conversation, ma i transcript sono tutti
209
- su disco e il deck li legge già.
147
+ Il navigator trova una conversazione per **metadati** (titolo, data, turni). `^F` la trova per **contenuto**: «l'IA me l'ha detto 150k di testo fa, ricordo mezza parola». Claude Code non ha un find-in-conversation, ma i transcript sono tutti su disco e il deck li legge già.
210
148
 
211
149
  Due campi, `tab` per passare dall'uno all'altro:
212
150
 
213
- - **hash** — prefisso del `sessionId` (gli 8 char della statusline bastano),
214
- restringe a una conversazione. Vuoto = **tutte** quelle del progetto.
215
- - **chiave** — il termine cercato. La ricerca è **eager**: si aggiorna a ogni
216
- carattere, da 3 in su.
151
+ - **hash** — prefisso del `sessionId` (gli 8 char della statusline bastano), restringe a una conversazione. Vuoto = **tutte** quelle del progetto.
152
+ - **chiave** il termine cercato. La ricerca è **eager**: si aggiorna a ogni carattere, da 3 in su.
217
153
 
218
- Sei toggle, tutti su `CTRL` perché i campi di testo occupano le lettere nude.
219
- Lo stato si legge da `[x]`/`[ ]`, non dal solo colore:
154
+ Sei toggle, tutti su `CTRL` perché i campi di testo occupano le lettere nude. Lo stato si legge da `[x]`/`[ ]`, non dal solo colore:
220
155
 
221
156
  | Tasto | Toggle | Default |
222
157
  |---|---|---|
@@ -227,42 +162,21 @@ Lo stato si legge da `[x]`/`[ ]`, non dal solo colore:
227
162
  | `^T` | cerca nei **tool** (`tool_use` / `tool_result`) | off |
228
163
  | `^U` | cerca nei prompt **umani** | off |
229
164
 
230
- Non esiste un toggle *thinking*: quei blocchi sono persistiti **senza testo**
231
- (il transcript porta la sola firma crittografica), quindi sarebbe una casella
232
- che non può mai produrre un risultato.
165
+ Non esiste un toggle *thinking*: quei blocchi sono persistiti **senza testo** (il transcript porta la sola firma crittografica), quindi sarebbe una casella che non può mai produrre un risultato.
233
166
 
234
- Con l'hash vuoto la lista è **raggruppata per conversazione**; `⏎` è contestuale
235
- alla riga selezionata — su una riga-conversazione **riprende** la sessione
236
- (come dal pane Sessions), su una riga-occorrenza apre il **reader**. La selezione
237
- parte dalla prima *occorrenza*, non dalla riga di gruppo: quella è un segnaposto
238
- di navigazione, raggiungibile con le frecce ma non una destinazione.
167
+ Con l'hash vuoto la lista è **raggruppata per conversazione**; `⏎` è contestuale alla riga selezionata — su una riga-conversazione **riprende** la sessione (come dal pane Sessions), su una riga-occorrenza apre il **reader**. La selezione parte dalla prima *occorrenza*, non dalla riga di gruppo: quella è un segnaposto di navigazione, raggiungibile con le frecce ma non una destinazione.
239
168
 
240
- L'estratto attorno al match **si allarga col terminale**: a 190 colonne sono
241
- ~170 caratteri di contesto, non i 50 di un valore fisso — ed è il contesto la
242
- ragione per cui si legge la riga invece di aprire il reader.
169
+ L'estratto attorno al match **si allarga col terminale**: a 190 colonne sono ~170 caratteri di contesto, non i 50 di un valore fisso — ed è il contesto la ragione per cui si legge la riga invece di aprire il reader.
243
170
 
244
- Sotto la lista, un **pannello di anteprima** mostra il contesto attorno
245
- all'occorrenza selezionata e si aggiorna navigando con le frecce. Prende solo le
246
- righe che la lista non usa: con pochi risultati riempie il terminale, con molti
247
- sparisce e la lista se le riprende — quando c'è tanto da scorrere la priorità è
248
- vedere più occorrenze, il contesto è il premio per una ricerca già stretta.
171
+ Sotto la lista, un **pannello di anteprima** mostra il contesto attorno all'occorrenza selezionata e si aggiorna navigando con le frecce. Prende solo le righe che la lista non usa: con pochi risultati riempie il terminale, con molti sparisce e la lista se le riprende — quando c'è tanto da scorrere la priorità è vedere più occorrenze, il contesto è il premio per una ricerca già stretta.
249
172
 
250
173
  Il reader mostra il messaggio intero, aperto già **posizionato sull'occorrenza**
251
174
  col match evidenziato: `↑↓` riga, `PgUp`/`PgDn` pagina, `g`/`G` estremi, `esc`
252
- torna alla lista senza perdere query, toggle e selezione. (Gli estremi stanno su
253
- lettera e non su `Home`/`End` perché Ink non espone quei due tasti: arrivano
254
- indistinguibili da qualunque tasto ignoto.)
175
+ torna alla lista senza perdere query, toggle e selezione. (Gli estremi stanno su lettera e non su `Home`/`End` perché Ink non espone quei due tasti: arrivano indistinguibili da qualunque tasto ignoto.)
255
176
 
256
- I toggle e la query sono **volatili**: sopravvivono alla chiusura del modale,
257
- non al riavvio del deck — comporre una ricerca non tocca il disco.
177
+ I toggle e la query sono **volatili**: sopravvivono alla chiusura del modale, non al riavvio del deck — comporre una ricerca non tocca il disco.
258
178
 
259
- **Come fa a essere istantanea.** I corpi dei messaggi restano in RAM dentro la
260
- cache mtime-keyed che il deck usa già per la lista sessioni: quei file venivano
261
- comunque deserializzati per turni e titoli, e i corpi buttati. Trattenerli non
262
- aggiunge I/O, aggiunge memoria — e ne aggiunge poca, perché un JSONL è per l'85%
263
- overhead (misurato su un progetto reale: 57 MB su disco = 9,8 MB di testo
264
- cercabile). Cercarci dentro costa 1-9 ms; un prefiltro `grep` sugli stessi file
265
- ne costerebbe 26, perché rileggerebbe il volume pieno a ogni battuta.
179
+ **Come fa a essere istantanea.** I corpi dei messaggi restano in RAM dentro la cache mtime-keyed che il deck usa già per la lista sessioni: quei file venivano comunque deserializzati per turni e titoli, e i corpi buttati. Trattenerli non aggiunge I/O, aggiunge memoria — e ne aggiunge poca, perché un JSONL è per l'85% overhead (misurato su un progetto reale: 57 MB su disco = 9,8 MB di testo cercabile). Cercarci dentro costa 1-9 ms; un prefiltro `grep` sugli stessi file ne costerebbe 26, perché rileggerebbe il volume pieno a ogni battuta.
266
180
 
267
181
  ## Vista: filtri e ordinamenti
268
182
 
@@ -279,26 +193,44 @@ p priorità s stato i id
279
193
  Partendo da catena vuota, digitare `ppi` produce `[pri ↓, id ↑]`. Il ciclo parte
280
194
  sempre **dallo stato corrente**, che il modale mostra dal vivo mentre digiti.
281
195
 
282
- Sull'**id** il confronto è numerico (`T9` prima di `T10`, non lessicografico) e i
283
- due prefissi sono blocchi distinti — le `D` in coda alle `T`: i counter `T` e `D`
284
- sono separati nel contratto loom, quindi `T01` e `D01` non sono confrontabili
285
- come numeri soli.
286
- A parità su tutte le chiavi decide sempre l'`id` (confronto **numerico**: `T9`
196
+ Sull'**id** il confronto è numerico (`T9` prima di `T10`, non lessicografico) e i due prefissi sono blocchi distinti — le `D` in coda alle `T`: i counter `T` e `D` sono separati nel contratto loom, quindi `T01` e `D01` non sono confrontabili come numeri soli. A parità su tutte le chiavi decide sempre l'`id` (confronto **numerico**: `T9`
287
197
  prima di `T10`) → l'ordine è deterministico, mai instabile fra un refresh e l'altro.
288
198
 
289
199
  **`F` — filtri.** Un toggle per ogni priorità e per ogni stato, componibili in AND.
290
200
  `↑↓` cambia riga, `←→` scorre i valori, `spazio` mostra/nasconde.
291
201
 
292
- In entrambi i modali la lista si aggiorna **dal vivo**; `⏎` conferma, `esc`
293
- ripristina la vista com'era all'apertura.
202
+ In entrambi i modali la lista si aggiorna **dal vivo**; `⏎` conferma, `esc` ripristina la vista com'era all'apertura.
294
203
 
295
- Con un filtro attivo l'header dichiara sempre quanto sta nascondendo
296
- (`Tasks (9/25) · 16 nascoste`): il deck non finge mai una lista completa.
204
+ Con un filtro attivo l'header dichiara sempre quanto sta nascondendo (`Tasks (9/25) · 16 nascoste`): il deck non finge mai una lista completa.
297
205
 
298
- **Persistenza.** La vista non si salva da sola — sperimentare non sporca nulla.
299
- `w` la scrive in `.claude/loom/deck-view.json` (macchina-locale, da gitignorare)
206
+ **Persistenza.** La vista non si salva da sola — sperimentare non sporca nulla. `w` la scrive in `.claude/loom/deck-view.json` (macchina-locale, da gitignorare)
300
207
  e al riavvio viene ripristinata. File assente o corrotto → default puliti.
301
208
 
209
+ ## L'header è un selettore di vista
210
+
211
+ Ogni segmento dell'header nomina un sottoinsieme che il deck sa già calcolare.
212
+ `←`/`→` lo rendono **raggiungibile**: la voce attiva si evidenzia in video
213
+ inverso e la lista sotto mostra quel sottoinsieme.
214
+
215
+ ```
216
+ Tasks (20/78) · 58 nascoste · 23 archiviabili · ↑7 · ↓9
217
+ Sessions · tutte (317) · ●2 vive · 📌4 · +213 più vecchie · ↑7 · ↓9
218
+ ```
219
+
220
+ | Pane | Viste |
221
+ |---|---|
222
+ | Tasks | `Tasks (n/N)` (filtri applicati) · `N nascoste` (il complemento esatto di quei filtri) · `N archiviabili` (Done oltre `archivableDays`, cieca ai filtri) |
223
+ | Sessions | `{parent} (N)` · `●N vive` · `📌N` · `+N più vecchie` (le contestuali che il cap `maxContext` tronca) |
224
+
225
+ - **Il catalogo è fisso**, anche a contatore 0 (la voce si mostra dim). Un catalogo che si accorcia sposta le voci sotto le dita, e la vista corrente può svanire mentre la guardi — togliere un filtro mentre sei su `nascoste`.
226
+ - **`↑N` `↓N` non sono viste**: contano elementi fuori finestra per altezza del
227
+ terminale, cioè una posizione, non un insieme. Restano in coda, non selezionabili.
228
+ - **Due assi ortogonali**: il pane task sceglie il *parent* delle conversazioni (`≡ tutte` / `○ spot` / una task), l'header sceglie *quale sottoinsieme* di quel parent. Cambiare parent non azzera la vista, che si ricalcola dentro il nuovo.
229
+ - **`F` è inerte fuori dalla vista `Tasks`** e lo dice con una nota: su `nascoste` riapplicare i filtri non ha senso, `archiviabili` è cieca per scelta.
230
+ - **La vista è volatile**: non entra in `deck-view.json`, il deck riapre sempre sulla voce 1. Un filtro salvato lo si è scelto; una vista non-default riaperta a freddo si leggerebbe come la lista intera.
231
+ - **Cambiare vista riporta la selezione in cima**, righe meta comprese: nessuna regola di mantenimento, nessun tentativo di ritrovare l'elemento precedente.
232
+ - Su un terminale stretto **la voce attiva è servita per prima** dal budget di larghezza: sparisce un contatore, mai la voce che dice dove sei.
233
+
302
234
  ## Installazione
303
235
 
304
236
  ```bash
@@ -310,9 +242,7 @@ npx @lamemind/loom-deck
310
242
  ## Requisiti runtime
311
243
 
312
244
  - **Node.js ≥ 18** (dichiarato in `engines`).
313
- - **[Ptyxis](https://gitlab.gnome.org/chergert/ptyxis)** (terminale GNOME) — **dipendenza
314
- di runtime, non risolvibile da npm**. Lo spawn di una tab (`scripts/deck-run`) invoca il
315
- binario `ptyxis`: l'**install riesce anche senza**, ma al momento dello spawn il comando
245
+ - **[Ptyxis](https://gitlab.gnome.org/chergert/ptyxis)** (terminale GNOME) — **dipendenza di runtime, non risolvibile da npm**. Lo spawn di una tab (`scripts/deck-run`) invoca il binario `ptyxis`: l'**install riesce anche senza**, ma al momento dello spawn il comando
316
246
  fallisce (gestito: handler `error` → la TUI resta viva, mostra la nota). Senza una GUI
317
247
  GNOME con Ptyxis installato, il deck naviga i task ma **non apre sessioni**.
318
248
  - **[Claude Code](https://claude.com/claude-code)** nel `PATH` — la tab spawnata avvia `claude`.
@@ -325,19 +255,9 @@ scripts/deck-run T18
325
255
  scripts/deck-run T18 --prompt-kind none|recap|preflight|run|checkpoint
326
256
  ```
327
257
 
328
- Apre una tab Ptyxis nella window attiva con `LOOM_TASK=T18 claude 'recap stato task T18'`.
329
- Il prompt iniziale è il terzo asse dello spawn, ortogonale al binding task
330
- (`<TaskID>` vs `--no-task`) e alla continuità (nuova vs `--resume`/`--fork`):
331
- senza flag resta `recap`, `none` apre la sessione bound **senza** alcun prompt.
332
- `LOOM_DECK_ENTER_PROMPT` (placeholder `{TASK}`) resta un override che vince sul
333
- kind — tranne su `none`, che è una richiesta esplicita di non averne.
334
-
335
- La tab porta anche `PTYXIS_PROFILE` forzata al profilo bindato al progetto nel
336
- registry (`bindings/claude`), letto da dconf: è la chiave con cui loom-compass
337
- associa a un progetto lo stato annunciato dagli hook (running/ask/done). Una tab
338
- `ptyxis --tab` nuda erediterebbe il profilo di default, e l'annuncio finirebbe
339
- keyed su un UUID che nessun progetto dichiara — pallino fermo su idle, senza
340
- alcun errore visibile. Override o disattivazione via `LOOM_DECK_STATE_PROFILE`
258
+ Apre una tab Ptyxis nella window attiva con `LOOM_TASK=T18 claude 'recap stato task T18'`. Il prompt iniziale è il terzo asse dello spawn, ortogonale al binding task (`<TaskID>` vs `--no-task`) e alla continuità (nuova vs `--resume`/`--fork`): senza flag resta `recap`, `none` apre la sessione bound **senza** alcun prompt. `LOOM_DECK_ENTER_PROMPT` (placeholder `{TASK}`) resta un override che vince sul kind — tranne su `none`, che è una richiesta esplicita di non averne.
259
+
260
+ La tab porta anche `PTYXIS_PROFILE` forzata al profilo bindato al progetto nel registry (`bindings/claude`), letto da dconf: è la chiave con cui loom-compass associa a un progetto lo stato annunciato dagli hook (running/ask/done). Una tab `ptyxis --tab` nuda erediterebbe il profilo di default, e l'annuncio finirebbe keyed su un UUID che nessun progetto dichiara — pallino fermo su idle, senza alcun errore visibile. Override o disattivazione via `LOOM_DECK_STATE_PROFILE`
341
261
  (settata a vuoto → nessun annuncio); progetto non registrato → nessun prefisso.
342
262
 
343
263
  ## Sviluppo (TUI Ink)
@@ -349,12 +269,9 @@ npm run build # tsc → dist/
349
269
  npm test # node:test sul core vista (src/view.ts), senza Ink né terminale
350
270
  ```
351
271
 
352
- Il core di filtri e ordinamenti (`src/view.ts`) è **puro**: nessun import da Ink o
353
- React, nessun I/O. È il motivo per cui è testabile con `node:test` su array
354
- fixture, mentre la TUI resta un guscio sottile che lo consuma.
272
+ Il core di filtri e ordinamenti (`src/view.ts`) è **puro**: nessun import da Ink o React, nessun I/O. È il motivo per cui è testabile con `node:test` su array fixture, mentre la TUI resta un guscio sottile che lo consuma.
355
273
 
356
- Il deck cerca `tasks.md` in `$PWD/${LOOM_DECK_DOCS_ROOT:-docs}/tasks.md`. Progetti
357
- con docs-root non-standard esportano la variabile, es. `LOOM_DECK_DOCS_ROOT=runtime`.
274
+ Il deck cerca `tasks.md` in `$PWD/${LOOM_DECK_DOCS_ROOT:-docs}/tasks.md`. Progetti con docs-root non-standard esportano la variabile, es. `LOOM_DECK_DOCS_ROOT=runtime`.
358
275
 
359
276
  La lista si **auto-aggiorna** quando `tasks.md` cambia sotto (poll su `mtime`, ~1.5s):
360
277
  crei/checkpoint una task da un'altra sessione → il deck la riflette senza riavvio.
@@ -106,17 +106,23 @@ async function gitOut(args, cwd) {
106
106
  }
107
107
  }
108
108
  /**
109
- * Quante fra le task Done passate sono oltre soglia. Le indeterminate (nessuno
109
+ * Quali fra le task Done passate sono oltre soglia. Le indeterminate (nessuno
110
110
  * dei tre gradini risolve) e le righe orfane (Done in tasks.md ma task file
111
- * assente) NON entrano nel conteggio: il deck non chiama "vecchia" una task di
112
- * cui non sa l'età.
111
+ * assente) NON entrano: il deck non chiama "vecchia" una task di cui non sa
112
+ * l'età.
113
113
  *
114
114
  * Riceve gli id già filtrati sul glifo Done invece della lista completa: lo
115
115
  * scan resta così proporzionale al vecchiume, non alla lunghezza della lista.
116
+ *
117
+ * T100 — ritorna gli ID e non più il solo conteggio: `archiviabili` è diventata
118
+ * una vista navigabile del pane task, e il numero da solo non basta più a
119
+ * disegnarne le righe. Il contatore dell'header è la lunghezza della lista, così
120
+ * ciò che si conta e ciò che si mostra restano lo stesso insieme per
121
+ * costruzione.
116
122
  */
117
- export async function countArchivable(doneIds, opts) {
123
+ export async function archivableIds(doneIds, opts) {
118
124
  const now = opts.now ?? Date.now();
119
- let n = 0;
125
+ const out = [];
120
126
  for (const id of doneIds) {
121
127
  const taskFile = findTaskFile(opts.tasksDir, id);
122
128
  if (!taskFile)
@@ -133,7 +139,7 @@ export async function countArchivable(doneIds, opts) {
133
139
  continue;
134
140
  const age = ageDays(iso, now);
135
141
  if (age !== null && age >= opts.days)
136
- n++;
142
+ out.push(id);
137
143
  }
138
- return n;
144
+ return out;
139
145
  }