@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 +77 -160
- package/dist/archivable.js +13 -7
- package/dist/cli.js +244 -74
- package/dist/markdown.js +260 -0
- package/dist/pane-views.js +135 -0
- package/dist/session-list.js +5 -2
- package/dist/width.js +55 -4
- package/package.json +1 -1
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
|
-
|
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 `←→`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
(
|
|
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.
|
package/dist/archivable.js
CHANGED
|
@@ -106,17 +106,23 @@ async function gitOut(args, cwd) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
|
-
*
|
|
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
|
|
112
|
-
*
|
|
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
|
|
123
|
+
export async function archivableIds(doneIds, opts) {
|
|
118
124
|
const now = opts.now ?? Date.now();
|
|
119
|
-
|
|
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
|
-
|
|
142
|
+
out.push(id);
|
|
137
143
|
}
|
|
138
|
-
return
|
|
144
|
+
return out;
|
|
139
145
|
}
|