@lamemind/loom-deck 0.20.0 → 0.21.1
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 +60 -12
- package/dist/cli.js +76 -18
- package/dist/config.js +5 -3
- package/package.json +1 -1
- package/scripts/deck-run +95 -18
package/README.md
CHANGED
|
@@ -4,15 +4,19 @@ Deck TUI (Ink) **per-progetto** della famiglia [loom](https://github.com/lamemin
|
|
|
4
4
|
|
|
5
5
|
Legge il `tasks.md` del progetto e, con un tasto (poi un click), **spawna** una tab
|
|
6
6
|
[Ptyxis](https://gitlab.gnome.org/chergert/ptyxis) che avvia una sessione Claude Code
|
|
7
|
-
già bound alla task via `LOOM_TASK
|
|
7
|
+
già bound alla task via `LOOM_TASK`.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
↑↓ scegli la task → ⏎ → tab CC di fianco → LOOM_TASK bound
|
|
10
|
+
↑↓ scegli la task → ⏎ → tab CC di fianco → LOOM_TASK bound
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Il tasto premuto sceglie **con quale prompt** si entra: `⏎` a mani nude, `^K`
|
|
14
|
+
recap, `^P` preflight, `^R` esecuzione (vedi [I quattro modi di entrare in una
|
|
15
|
+
task](#i-quattro-modi-di-entrare-in-una-task)).
|
|
16
|
+
|
|
13
17
|
Entrambi i prefissi del contratto loom entrano in lista: **`T`** (code task) e
|
|
14
|
-
**`D`** (doc task).
|
|
15
|
-
|
|
18
|
+
**`D`** (doc task). Il trattamento è identico ovunque tranne che su `^R`, dove il
|
|
19
|
+
prompt dispatcha sulla skill di esecuzione della rispettiva famiglia.
|
|
16
20
|
|
|
17
21
|
## Ruolo nella famiglia loom
|
|
18
22
|
|
|
@@ -52,10 +56,10 @@ senza collisioni:
|
|
|
52
56
|
|---|---|---|
|
|
53
57
|
| `↑` `↓` | naviga nella lista | |
|
|
54
58
|
| `←` `→` `tab` | cambia pane | |
|
|
55
|
-
| `⏎` | azione primaria del pane | Tasks → spawna la task selezionata · Sessions → riprende (`claude --resume`) la sessione selezionata |
|
|
59
|
+
| `⏎` | azione primaria del pane | Tasks → spawna la task selezionata (senza prompt) · Sessions → riprende (`claude --resume`) la sessione selezionata |
|
|
56
60
|
| **MAIUSCOLA** | **apre un modale** | cattura tutti i tasti; `esc` annulla, non esce |
|
|
57
61
|
| minuscola | azione immediata, one-shot | |
|
|
58
|
-
| `CTRL`+lettera | idiomi universali
|
|
62
|
+
| `CTRL`+lettera | idiomi universali, toggle dentro i modali di testo, varianti di un'azione | `^F` = find, come ovunque · `^K`/`^P`/`^R` = spawn con un altro prompt |
|
|
59
63
|
| `1`…`9` | voce `launch` n-esima del progetto | da `.claude/loom-works.json` |
|
|
60
64
|
| `q` `esc` | esce dal deck | in un modale `esc` annulla soltanto |
|
|
61
65
|
|
|
@@ -75,25 +79,64 @@ Assegnazioni correnti:
|
|
|
75
79
|
| modale | `S` | sort chain |
|
|
76
80
|
| modale | `F` | filtri |
|
|
77
81
|
| modale | `^F` | **ricerca full-text** nelle conversazioni del progetto |
|
|
82
|
+
| immediata | `^K` | spawna la task selezionata col prompt di **recap** |
|
|
83
|
+
| immediata | `^P` | spawna la task selezionata sul **preflight** |
|
|
84
|
+
| immediata | `^R` | spawna la task selezionata in **esecuzione** |
|
|
78
85
|
| immediata | `f` | **forka** la sessione selezionata (solo col focus sul pane Sessions) |
|
|
79
|
-
| immediata | `t` | terminale @project-root (surface standard launch) |
|
|
80
|
-
| immediata | `c` | sessione Claude **nuda**: nessuna task, nessun prompt iniziale |
|
|
86
|
+
| immediata | `t` 💻 | terminale @project-root (surface standard launch) |
|
|
87
|
+
| immediata | `c` 🤖 | sessione Claude **nuda**: nessuna task, nessun prompt iniziale |
|
|
81
88
|
| immediata | `w` | salva la vista corrente su disco |
|
|
82
89
|
| launch | `1`…`9` | esegue il `command` della voce, con `cwd` = project root |
|
|
83
90
|
|
|
84
91
|
Le voci `launch` sono elencate in una **riga di legenda** sotto il footer
|
|
85
|
-
(`
|
|
92
|
+
(`1 📝 codium · 2 ☕ idea`): l'indice da solo è opaco, perché le voci sono
|
|
86
93
|
custom per-progetto e non hanno una lettera fissa per app. Se non entrano in
|
|
87
94
|
larghezza, la legenda si ferma a voci intere e mostra il contatore di quelle
|
|
88
95
|
fuori riga — mai un troncamento silenzioso. Il cap a `9` è imposto dai tasti-cifra,
|
|
89
96
|
non dallo schema: un progetto può dichiarare più di 9 voci, quelle oltre la nona
|
|
90
97
|
sono configurate ma non raggiungibili (e la legenda lo dice).
|
|
91
98
|
|
|
99
|
+
In legenda le due surface del cappello portano l'emoji del menu compass (`t 💻`,
|
|
100
|
+
`c 🤖`) invece della parola. Per il terminale compass usa 🖥️, che nel frame Ink
|
|
101
|
+
non sopravvive: `sanitize` lo sostituisce perché VTE lo disegna largo 1 mentre
|
|
102
|
+
`string-width` ne conta 2 (invariante ① di `src/width.ts`) — 💻 è il gemello
|
|
103
|
+
concorde. La legenda elenca solo i tasti attivi in quel momento: navigazione,
|
|
104
|
+
uscita e indicatore di focus non ci sono, e le voci contestuali spariscono
|
|
105
|
+
invece di mostrarsi inerti.
|
|
106
|
+
|
|
92
107
|
`t` e `c` sono gemelle: entrambe aprono una surface del cappello nella stessa
|
|
93
108
|
finestra Ptyxis, senza passare da un modale. `c` (minuscola, azione) e `C`
|
|
94
109
|
(maiuscola, modale create-task) restano distinte per la regola sopra — così come
|
|
95
110
|
`f` (fork) e `F` (filtri).
|
|
96
111
|
|
|
112
|
+
### I quattro modi di entrare in una task
|
|
113
|
+
|
|
114
|
+
Sulla task selezionata (focus sul pane Tasks) quattro tasti aprono una sessione
|
|
115
|
+
**bound**: `LOOM_TASK` iniettata, `sessionId` pinnato, binding scritto nel
|
|
116
|
+
sidecar. Fra loro cambia **solo il prompt iniziale**.
|
|
117
|
+
|
|
118
|
+
| Tasto | Prompt |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `⏎` | nessuno — il contesto lo carica l'hook `SessionStart`, il primo messaggio lo scrivi tu |
|
|
121
|
+
| `^K` | `recap stato task <id>` — prompt diretto, nessuna skill |
|
|
122
|
+
| `^P` | `/loom-works:preflight-task <id>` |
|
|
123
|
+
| `^R` | `/loom-works:run-task <id>`, oppure `/loom-works:run-doc <id>` se l'id è una `D` |
|
|
124
|
+
|
|
125
|
+
Il prompt iniziale è la scelta di **chi apre**, non una proprietà della task: lo
|
|
126
|
+
stesso task file si apre per leggerne lo stato, per congelarne le decisioni, per
|
|
127
|
+
eseguirlo o per entrarci a mani nude — e il tasto premuto *è* quell'intento.
|
|
128
|
+
Prima esisteva il solo `⏎` col recap, quindi ogni altro ingresso passava per una
|
|
129
|
+
sessione da correggere a mano.
|
|
130
|
+
|
|
131
|
+
Col focus sul pane Sessions i tre `CTRL` non spawnano: l'oggetto dell'azione è la
|
|
132
|
+
task selezionata, e senza quel pane a fuoco non ce n'è una (il deck lo dice nella
|
|
133
|
+
riga di nota, invece di aprire qualcosa a caso).
|
|
134
|
+
|
|
135
|
+
Il catalogo dei prompt vive in `scripts/deck-run`, non nella TUI: è il primitive
|
|
136
|
+
UI-agnostico, il deck gli passa un **simbolo** (`--prompt-kind`) e non una
|
|
137
|
+
stringa. Così il testo sta in un posto solo, e con lui il suo vincolo di quoting
|
|
138
|
+
— il prompt viaggia dentro apici singoli in `bash -lc`, quindi non può contenerne.
|
|
139
|
+
|
|
97
140
|
### `f` — forkare una conversazione
|
|
98
141
|
|
|
99
142
|
Il fork rama la sessione selezionata: `claude --resume <origine> --fork-session`
|
|
@@ -241,16 +284,21 @@ npx @lamemind/loom-deck
|
|
|
241
284
|
```bash
|
|
242
285
|
# dalla project dir con un tasks.md
|
|
243
286
|
scripts/deck-run T18
|
|
287
|
+
scripts/deck-run T18 --prompt-kind none|recap|preflight|run
|
|
244
288
|
```
|
|
245
289
|
|
|
246
|
-
Apre una tab Ptyxis nella window attiva con `LOOM_TASK=T18 claude 'recap stato task T18'
|
|
247
|
-
|
|
290
|
+
Apre una tab Ptyxis nella window attiva con `LOOM_TASK=T18 claude 'recap stato task T18'`.
|
|
291
|
+
Il prompt iniziale è il terzo asse dello spawn, ortogonale al binding task
|
|
292
|
+
(`<TaskID>` vs `--no-task`) e alla continuità (nuova vs `--resume`/`--fork`):
|
|
293
|
+
senza flag resta `recap`, `none` apre la sessione bound **senza** alcun prompt.
|
|
294
|
+
`LOOM_DECK_ENTER_PROMPT` (placeholder `{TASK}`) resta un override che vince sul
|
|
295
|
+
kind — tranne su `none`, che è una richiesta esplicita di non averne.
|
|
248
296
|
|
|
249
297
|
## Sviluppo (TUI Ink)
|
|
250
298
|
|
|
251
299
|
```bash
|
|
252
300
|
npm install
|
|
253
|
-
npm run dev # tsx src/cli.tsx — lista tasks.md reale, ↑↓ naviga ·
|
|
301
|
+
npm run dev # tsx src/cli.tsx — lista tasks.md reale, ↑↓ naviga · ⏎/^K/^P/^R spawn · q esci
|
|
254
302
|
npm run build # tsc → dist/
|
|
255
303
|
npm test # node:test sul core vista (src/view.ts), senza Ink né terminale
|
|
256
304
|
```
|
package/dist/cli.js
CHANGED
|
@@ -120,8 +120,11 @@ function spawnOut(cmd, args, opts) {
|
|
|
120
120
|
// Spawn detached: il deck spawna ma NON contiene la sessione (la possiede
|
|
121
121
|
// ptyxis-agent). unref + stdio ignore → ritorna subito, la TUI resta viva.
|
|
122
122
|
// sessionId pinnato (T27) → il binding sidecar è deterministico allo spawn.
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
// Il kind è OBBLIGATORIO e non ha default qui: il default vive in deck-run (per
|
|
124
|
+
// le invocazioni a mano), mentre dal deck ogni tasto dichiara il proprio intento
|
|
125
|
+
// — un default silenzioso renderebbe indistinguibili `⏎` e `^K`.
|
|
126
|
+
function spawnDeck(id, cwd, sessionId, kind) {
|
|
127
|
+
const child = spawnOut(DECK_RUN, [id, '--session-id', sessionId, '--prompt-kind', kind], {
|
|
125
128
|
cwd,
|
|
126
129
|
detached: true,
|
|
127
130
|
stdio: 'ignore',
|
|
@@ -643,6 +646,36 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
643
646
|
});
|
|
644
647
|
child.on('error', () => setNote(`⚠ create-task: '${CLAUDE_CMD}' non lanciabile`));
|
|
645
648
|
}
|
|
649
|
+
// T56 — apre una sessione bound alla task selezionata. Punto UNICO dei quattro
|
|
650
|
+
// tasti (⏎/^K/^P/^R): fra loro cambia solo il prompt iniziale, tutto il resto
|
|
651
|
+
// è identico — uuid pinnato, binding scritto PRIMA dello spawn (la sessione
|
|
652
|
+
// risulta figlia della task appena il JSONL compare), handler d'errore, nota.
|
|
653
|
+
// Quattro copie sarebbero quattro posti dove dimenticare il `child.on('error')`,
|
|
654
|
+
// e uno spawn fallito è async: senza handler diventa uncaughtException e
|
|
655
|
+
// ucciderebbe il deck, che invece deve restare vivo.
|
|
656
|
+
// La guardia di focus sta qui e non nei chiamanti perché l'oggetto dell'azione
|
|
657
|
+
// è la task SELEZIONATA: senza il pane task a fuoco non ce n'è una. Per `⏎` il
|
|
658
|
+
// ramo è irraggiungibile (ci arriva già dentro `focus === 'tasks'`); per i tre
|
|
659
|
+
// CTRL, che il ramo `key.ctrl` intercetta globalmente, è l'unico posto.
|
|
660
|
+
function spawnTaskSession(kind, keyLabel) {
|
|
661
|
+
if (focus !== 'tasks') {
|
|
662
|
+
setNote(`${keyLabel} → spawn: seleziona una task (←→ per il pane)`);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (isSpot) {
|
|
666
|
+
setNote('spot: sessioni libere, nessuna task da spawnare');
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (!selTask)
|
|
670
|
+
return;
|
|
671
|
+
const task = selTask;
|
|
672
|
+
const sid = randomUUID();
|
|
673
|
+
appendTaskBinding(cwd, sid, task.id);
|
|
674
|
+
const child = spawnDeck(task.id, cwd, sid, kind);
|
|
675
|
+
child.on('error', () => setNote(`⚠ spawn ${task.id} fallito (${DECK_RUN})`));
|
|
676
|
+
const what = kind === 'none' ? '' : ` · ${kind}`;
|
|
677
|
+
setNote(`${keyLabel} spawn ${task.id}${what} → tab CC (sid ${sid.slice(0, 8)})`);
|
|
678
|
+
}
|
|
646
679
|
// T53 — apertura del modale nota sulla conversazione selezionata. Come
|
|
647
680
|
// `openEdit`, la bozza parte dal valore ATTUALE: annotare una seconda volta è
|
|
648
681
|
// quasi sempre correggere, e ripartire da vuoto costringerebbe a ridigitare
|
|
@@ -1138,6 +1171,15 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1138
1171
|
setNote('');
|
|
1139
1172
|
setMode('search');
|
|
1140
1173
|
}
|
|
1174
|
+
else if (input === 'k') {
|
|
1175
|
+
spawnTaskSession('recap', '^K');
|
|
1176
|
+
}
|
|
1177
|
+
else if (input === 'p') {
|
|
1178
|
+
spawnTaskSession('preflight', '^P');
|
|
1179
|
+
}
|
|
1180
|
+
else if (input === 'r') {
|
|
1181
|
+
spawnTaskSession('run', '^R');
|
|
1182
|
+
}
|
|
1141
1183
|
return;
|
|
1142
1184
|
}
|
|
1143
1185
|
if (key.leftArrow || key.rightArrow || key.tab) {
|
|
@@ -1157,21 +1199,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1157
1199
|
}
|
|
1158
1200
|
else if (key.return) {
|
|
1159
1201
|
if (focus === 'tasks') {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
const sid = randomUUID();
|
|
1167
|
-
appendTaskBinding(cwd, sid, selTask.id);
|
|
1168
|
-
const child = spawnDeck(selTask.id, cwd, sid);
|
|
1169
|
-
// Un errore di spawn è async: senza handler diventa uncaughtException
|
|
1170
|
-
// e ucciderebbe il deck. Lo intercetto per preservare l'invariante
|
|
1171
|
-
// "il deck resta vivo" e mostro la nota d'errore.
|
|
1172
|
-
child.on('error', () => setNote(`⚠ spawn ${selTask.id} fallito (${DECK_RUN})`));
|
|
1173
|
-
setNote(`⏎ spawn ${selTask.id} → tab CC (sid ${sid.slice(0, 8)})`);
|
|
1174
|
-
}
|
|
1202
|
+
// T56 — ⏎ apre la sessione a MANI NUDE: bound alla task (LOOM_TASK,
|
|
1203
|
+
// sessionId pinnato, binding) ma senza messaggio iniettato — il contesto
|
|
1204
|
+
// lo carica l'hook SessionStart, il primo messaggio lo scrive l'utente.
|
|
1205
|
+
// Il recap che ⏎ faceva prima è passato a ^K: resta a un tasto, ma smette
|
|
1206
|
+
// di essere l'unico ingresso possibile nella task.
|
|
1207
|
+
spawnTaskSession('none', '⏎');
|
|
1175
1208
|
}
|
|
1176
1209
|
else {
|
|
1177
1210
|
// T49 — ⏎ su una sessione = resume in nuova tab. Il binding si rilegge
|
|
@@ -1325,6 +1358,31 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1325
1358
|
// Larghezza dal medesimo hook che dà l'altezza: dopo un resize la legenda si
|
|
1326
1359
|
// ricalcola con lo stesso re-render che ridimensiona i pane.
|
|
1327
1360
|
const legend = launchLegend(launch, columns);
|
|
1361
|
+
// Legenda della modalità normale. Elenca SOLO i tasti che fanno qualcosa qui e
|
|
1362
|
+
// ora: le voci contestuali compaiono quando il pane a fuoco le rende possibili
|
|
1363
|
+
// e altrimenti spariscono, invece di annunciarsi inerti con un `—`.
|
|
1364
|
+
// Fuori: navigazione (`↑↓` `←→`) e uscita (`q`), universali in qualunque TUI, e
|
|
1365
|
+
// l'indicatore `focus:` — il pane a fuoco si vede già dall'evidenziazione, e
|
|
1366
|
+
// ridirlo a parole costava colonne su una riga che tronca in silenzio.
|
|
1367
|
+
const keyLegend = sanitize([
|
|
1368
|
+
...(canSpawn ? ['⏎/^K/^P/^R spawn'] : canResume ? ['⏎ resume'] : []),
|
|
1369
|
+
...(canResume ? ['f fork'] : []),
|
|
1370
|
+
...(canPin ? ['p pin', 'N nota'] : []),
|
|
1371
|
+
'^F cerca',
|
|
1372
|
+
'C nuova',
|
|
1373
|
+
'E edit',
|
|
1374
|
+
'S sort',
|
|
1375
|
+
'F filtri',
|
|
1376
|
+
'w salva',
|
|
1377
|
+
// Emoji delle surface, le stesse del menu compass: 🤖 = nuova sessione
|
|
1378
|
+
// claude. Per il terminale compass usa 🖥️, che nel frame Ink NON passa —
|
|
1379
|
+
// `sanitize` lo sostituisce (VTE lo disegna largo 1, string-width dice 2:
|
|
1380
|
+
// discordante, invariante ① di width.ts) e resterebbe un `·` muto. 💻 è il
|
|
1381
|
+
// gemello concorde più vicino. Il `sanitize` attorno all'array è la rete
|
|
1382
|
+
// che rende quel vincolo automatico, non una cosa da ricordarsi.
|
|
1383
|
+
't 💻',
|
|
1384
|
+
'c 🤖',
|
|
1385
|
+
].join(' · '));
|
|
1328
1386
|
// ── T52 · schermate sostitutive ─────────────────────────────────────────
|
|
1329
1387
|
// Ricerca e reader sono gli unici modali che NON stanno in flusso sopra i
|
|
1330
1388
|
// pane: una lista di occorrenze non entra in un box da 4 righe. Prendono
|
|
@@ -1407,7 +1465,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1407
1465
|
if (budget.compact) {
|
|
1408
1466
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? 'spot', " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1409
1467
|
}
|
|
1410
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nota conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (
|
|
1468
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nota conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' && launch.length > 0 ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [legend.shown, legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editRow, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, rows: windowRows, total: assembled.pinnedCount + assembled.contextTotal, pinnedCount: assembled.pinnedCount, hidden: assembled.contextHidden, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1411
1469
|
}
|
|
1412
1470
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
1413
1471
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
package/dist/config.js
CHANGED
|
@@ -69,9 +69,11 @@ export function cellWidth(s) {
|
|
|
69
69
|
export function launchLegend(entries, columns) {
|
|
70
70
|
const reachable = entries.slice(0, LAUNCH_MAX);
|
|
71
71
|
const unreachable = entries.length - reachable.length;
|
|
72
|
-
// bordo + padding del box
|
|
73
|
-
// non degenerare a legenda vuota su terminali strettissimi.
|
|
74
|
-
|
|
72
|
+
// bordo + padding del box ≈ 4 celle, più 2 di margine; pavimento a 24 per
|
|
73
|
+
// non degenerare a legenda vuota su terminali strettissimi. Il prefisso
|
|
74
|
+
// "launch " non c'è più (la riga si riconosce dalla forma `indice emoji
|
|
75
|
+
// label`), quindi le 7 celle che occupava tornano alle voci.
|
|
76
|
+
const budget = Math.max(24, columns - 6);
|
|
75
77
|
// I fallback di parseLaunch (`▸` per emoji, command per label) sono già
|
|
76
78
|
// applicati a monte: qui non si re-implementano né si assumono campi popolati.
|
|
77
79
|
const parts = reachable.map((e, i) => `${i + 1} ${e.emoji} ${e.label}`);
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -4,8 +4,20 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Apre una tab Ptyxis nella window ATTIVA (quella col focus = il deck) e vi
|
|
6
6
|
# avvia una sessione Claude Code già bound alla task via LOOM_TASK, dritta sul
|
|
7
|
-
# prompt iniziale (default: prompt diretto
|
|
8
|
-
#
|
|
7
|
+
# prompt iniziale scelto con --prompt-kind (default: `recap`, prompt diretto
|
|
8
|
+
# "recap stato task <TaskID>" — no skill, solo recap → più controllo).
|
|
9
|
+
# Riusabile identico da TUI (Ink) e da web.
|
|
10
|
+
#
|
|
11
|
+
# Tre assi ORTOGONALI, da non confondere fra loro:
|
|
12
|
+
# binding task <TaskID> ..... --no-task
|
|
13
|
+
# continuità nuova ........ --resume [--fork]
|
|
14
|
+
# prompt --prompt-kind none|recap|preflight|run
|
|
15
|
+
# Il terzo asse (T56) prima non esisteva: il prompt era una CONSEGUENZA degli
|
|
16
|
+
# altri due (bound ⇒ recap, --no-task ⇒ niente, --resume ⇒ niente), quindi
|
|
17
|
+
# "task-bound SENZA prompt" era inesprimibile e l'unico modo di non avere un
|
|
18
|
+
# prompt era perdere la task. Il catalogo dei prompt vive qui e non nel deck
|
|
19
|
+
# perché questo è il primitive UI-agnostico: il chiamante passa un SIMBOLO, non
|
|
20
|
+
# una stringa — il quoting resta verificato in un posto solo (vedi PROMPT sotto).
|
|
9
21
|
#
|
|
10
22
|
# Con --no-task la sessione è NUDA: niente LOOM_TASK, niente prompt iniziale,
|
|
11
23
|
# niente --session-id. Serve al lavoro spot che non appartiene a nessuna task.
|
|
@@ -26,7 +38,7 @@
|
|
|
26
38
|
# LOOM_DECK_SPAWN_MODE inline|profile (default: inline)
|
|
27
39
|
# LOOM_DECK_PROFILE_UUID UUID profilo Ptyxis (default: cc-host)
|
|
28
40
|
# LOOM_DECK_WORKDIR dir di lavoro della tab (default: $PWD)
|
|
29
|
-
# LOOM_DECK_ENTER_PROMPT prompt iniziale
|
|
41
|
+
# LOOM_DECK_ENTER_PROMPT override del prompt iniziale, {TASK}=TaskID (vince sul kind, tranne `none`)
|
|
30
42
|
# LOOM_DECK_INTAB_CMD override comando in-tab (default: la sessione CC; usato per i test)
|
|
31
43
|
# LOOM_DECK_PERMISSION_MODE override del permissionMode del file config (default: campo file, poi 'manual')
|
|
32
44
|
#
|
|
@@ -37,6 +49,10 @@ SESSION_ID=""
|
|
|
37
49
|
RESUME_ID=""
|
|
38
50
|
NO_TASK=0
|
|
39
51
|
FORK=0
|
|
52
|
+
# Vuoto = flag non passato (≠ un default già risolto): serve a distinguere
|
|
53
|
+
# "kind implicito" da "kind chiesto", perché con --no-task il primo è legittimo
|
|
54
|
+
# (nessun prompt, come sempre) e il secondo è un errore d'uso.
|
|
55
|
+
PROMPT_KIND=""
|
|
40
56
|
# Positional <TaskID> + flag opzionale --session-id <uuid> (T27): il deck genera
|
|
41
57
|
# l'UUID e lo pinna così il binding sidecar sessionId↔taskId è deterministico.
|
|
42
58
|
# --no-task (T42): modalità NUDA, senza TaskID — nessuna LOOM_TASK, nessun prompt
|
|
@@ -55,8 +71,17 @@ FORK=0
|
|
|
55
71
|
# Sotto --fork il --session-id torna AMMESSO (anzi: è il punto) — la mutua
|
|
56
72
|
# esclusione con --resume esiste perché una ripresa nuda riscrive il transcript
|
|
57
73
|
# dell'id ripreso, mentre il fork ne apre uno NUOVO, che possiamo quindi pinnare.
|
|
74
|
+
# --prompt-kind <none|recap|preflight|run> (T56): sceglie il prompt iniziale fra
|
|
75
|
+
# quelli del catalogo qui sotto. Enum e non stringa libera: il prompt viaggia
|
|
76
|
+
# dentro apici singoli in `bash -lc`, quindi il testo va tenuto in un posto solo
|
|
77
|
+
# e verificato una volta, invece di spostare il rischio di quoting su ogni
|
|
78
|
+
# chiamante. Assente → `recap`, così ogni invocazione preesistente è invariata.
|
|
58
79
|
while [[ $# -gt 0 ]]; do
|
|
59
80
|
case "$1" in
|
|
81
|
+
--prompt-kind)
|
|
82
|
+
PROMPT_KIND="${2:-}"; shift 2 ;;
|
|
83
|
+
--prompt-kind=*)
|
|
84
|
+
PROMPT_KIND="${1#*=}"; shift ;;
|
|
60
85
|
--session-id)
|
|
61
86
|
SESSION_ID="${2:-}"; shift 2 ;;
|
|
62
87
|
--session-id=*)
|
|
@@ -75,12 +100,19 @@ while [[ $# -gt 0 ]]; do
|
|
|
75
100
|
esac
|
|
76
101
|
done
|
|
77
102
|
|
|
78
|
-
USAGE="uso: deck-run <TaskID> [--session-id <uuid>]
|
|
103
|
+
USAGE="uso: deck-run <TaskID> [--prompt-kind <kind>] [--session-id <uuid>]
|
|
104
|
+
(es. deck-run T18)
|
|
79
105
|
| deck-run --no-task (sessione nuda, senza task)
|
|
80
106
|
| deck-run <TaskID> --resume <uuid> (riprende sessione scoped)
|
|
81
107
|
| deck-run --no-task --resume <uuid> (riprende sessione spot)
|
|
82
108
|
| deck-run <TaskID> --resume <uuid> --fork [--session-id <nuovo-uuid>]
|
|
83
|
-
(forka: ramo con id NUOVO)
|
|
109
|
+
(forka: ramo con id NUOVO)
|
|
110
|
+
|
|
111
|
+
--prompt-kind prompt iniziale della sessione bound (default: recap)
|
|
112
|
+
none nessun prompt — sessione aperta sulla task, a mani nude
|
|
113
|
+
recap recap stato task <TaskID>
|
|
114
|
+
preflight /loom-works:preflight-task <TaskID>
|
|
115
|
+
run /loom-works:run-task <TaskID> (run-doc sulle task D)"
|
|
84
116
|
|
|
85
117
|
if [[ $NO_TASK -eq 1 && -n "$TASK" ]]; then
|
|
86
118
|
echo "--no-task e <TaskID> sono mutuamente esclusivi" >&2
|
|
@@ -102,6 +134,25 @@ if [[ -n "$RESUME_ID" && -n "$SESSION_ID" && $FORK -eq 0 ]]; then
|
|
|
102
134
|
echo "$USAGE" >&2
|
|
103
135
|
exit 2
|
|
104
136
|
fi
|
|
137
|
+
# Un kind fuori catalogo è un errore d'uso del CHIAMANTE (simbolo interno, non
|
|
138
|
+
# configurazione utente) → exit, non fallback silenzioso come permissionMode:
|
|
139
|
+
# lì il valore arriva da un file editabile a mano, qui da un argomento nostro.
|
|
140
|
+
if [[ -n "$PROMPT_KIND" ]]; then
|
|
141
|
+
case "$PROMPT_KIND" in
|
|
142
|
+
none|recap|preflight|run) ;;
|
|
143
|
+
*) echo "--prompt-kind ignoto: '${PROMPT_KIND}' (usa none|recap|preflight|run)" >&2
|
|
144
|
+
echo "$USAGE" >&2
|
|
145
|
+
exit 2 ;;
|
|
146
|
+
esac
|
|
147
|
+
fi
|
|
148
|
+
# Ogni prompt del catalogo nomina il TaskID: senza task non c'è nulla da
|
|
149
|
+
# chiedere, e un `--no-task --prompt-kind run` è una richiesta contraddittoria,
|
|
150
|
+
# non una da degradare in silenzio.
|
|
151
|
+
if [[ $NO_TASK -eq 1 && -n "$PROMPT_KIND" ]]; then
|
|
152
|
+
echo "--prompt-kind richiede una task: una sessione --no-task non ha prompt iniziale" >&2
|
|
153
|
+
echo "$USAGE" >&2
|
|
154
|
+
exit 2
|
|
155
|
+
fi
|
|
105
156
|
|
|
106
157
|
MODE="${LOOM_DECK_SPAWN_MODE:-inline}"
|
|
107
158
|
PROFILE_UUID="${LOOM_DECK_PROFILE_UUID:-5a36ae48df1c4d4882f43060e3e59656}"
|
|
@@ -189,18 +240,42 @@ RES_FLAG=""
|
|
|
189
240
|
FORK_FLAG=""
|
|
190
241
|
[[ $FORK -eq 1 ]] && FORK_FLAG="--fork-session "
|
|
191
242
|
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
|
|
243
|
+
# ── Catalogo dei prompt iniziali (T56) ───────────────────────────────────────
|
|
244
|
+
# Il prompt iniziale è la scelta di CHI APRE la sessione, non una proprietà della
|
|
245
|
+
# task: lo stesso task file si apre per leggerne lo stato, per un preflight, per
|
|
246
|
+
# eseguirlo o a mani nude. Il kind è quell'intento.
|
|
247
|
+
# none → nessun prompt (≠ stringa vuota: PROMPT_ARG sparisce del tutto,
|
|
248
|
+
# come sul ramo --resume, altrimenti CC riceve un posizionale vuoto)
|
|
249
|
+
# recap → prompt DIRETTO, no skill: l'utente rivede/steera prima di lanciare
|
|
250
|
+
# preflight → skill di preflight sulla task
|
|
251
|
+
# run → skill di esecuzione sulla task
|
|
252
|
+
# Vincolo di quoting: nessun template deve contenere apici singoli (viaggia
|
|
253
|
+
# dentro '...' nel comando in-tab). `/loom-works:...` passa intero proprio perché
|
|
254
|
+
# è single-quoted — lo slash e i due punti non sono metacaratteri lì dentro.
|
|
255
|
+
#
|
|
256
|
+
# Precedenza con l'env: LOOM_DECK_ENTER_PROMPT VINCE sul kind (retro-compat: chi
|
|
257
|
+
# lo usa oggi non si accorge del cambio), tranne su `none` — chiedere
|
|
258
|
+
# esplicitamente nessun prompt non è una richiesta che un default d'ambiente
|
|
259
|
+
# possa scavalcare. Il placeholder {TASK} vale solo per il valore dell'override;
|
|
260
|
+
# i template del catalogo interpolano ${TASK} diretto (nel `${VAR:-...}` la `}`
|
|
261
|
+
# di `{TASK}` chiuderebbe in anticipo l'espansione).
|
|
262
|
+
# `run` DISPATCHA sul prefisso: la lista del deck contiene entrambe le famiglie
|
|
263
|
+
# del contratto loom (`T` code, `D` doc) e le due hanno skill di esecuzione
|
|
264
|
+
# diverse — `run-task` è dichiarata code-only, `run-doc` è la sua complementare.
|
|
265
|
+
# Su una D il prompt `run-task` invocherebbe la skill sbagliata. Gli altri kind
|
|
266
|
+
# non discriminano: `recap` è un prompt diretto (nessuna skill) e
|
|
267
|
+
# `preflight-task` risolve il task file per id, indifferente al prefisso.
|
|
268
|
+
case "${PROMPT_KIND:-recap}" in
|
|
269
|
+
none) PROMPT="" ;;
|
|
270
|
+
recap) PROMPT="recap stato task ${TASK}" ;;
|
|
271
|
+
preflight) PROMPT="/loom-works:preflight-task ${TASK}" ;;
|
|
272
|
+
run) case "$TASK" in
|
|
273
|
+
D*) PROMPT="/loom-works:run-doc ${TASK}" ;;
|
|
274
|
+
*) PROMPT="/loom-works:run-task ${TASK}" ;;
|
|
275
|
+
esac ;;
|
|
276
|
+
esac
|
|
277
|
+
if [[ "${PROMPT_KIND:-}" != "none" && -n "${LOOM_DECK_ENTER_PROMPT:-}" ]]; then
|
|
201
278
|
PROMPT="${LOOM_DECK_ENTER_PROMPT//\{TASK\}/${TASK}}"
|
|
202
|
-
else
|
|
203
|
-
PROMPT="recap stato task ${TASK}"
|
|
204
279
|
fi
|
|
205
280
|
|
|
206
281
|
# Comando eseguito dentro la tab: bind LOOM_TASK per la sessione, avvia CC sul PROMPT.
|
|
@@ -213,8 +288,10 @@ fi
|
|
|
213
288
|
# Su resume (T49) il prompt iniziale salta anche nel ramo task: riprendere una
|
|
214
289
|
# conversazione significa continuarla, non iniettarle un nuovo messaggio. Il
|
|
215
290
|
# fork (T28) ricade nello stesso ramo — è una ripresa, con id nuovo.
|
|
216
|
-
|
|
217
|
-
|
|
291
|
+
# Il kind `none` (T56) atterra sulla stessa assenza per una ragione diversa: lì
|
|
292
|
+
# la sessione è nuova e bound, ma l'utente ha chiesto di entrarci a mani nude.
|
|
293
|
+
PROMPT_ARG=""
|
|
294
|
+
[[ -n "$PROMPT" && -z "$RESUME_ID" ]] && PROMPT_ARG="'${PROMPT}'"
|
|
218
295
|
# Il SID_FLAG entra anche nel ramo --no-task, che altrimenti lo scarta insieme
|
|
219
296
|
# agli altri elementi task-bound: nel fork di una sessione SPOT il sessionId
|
|
220
297
|
# pinnato non serve a legare una task (non c'è) ma a rendere noto in anticipo
|