@lamemind/loom-deck 0.20.0 → 0.21.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
@@ -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`, con un prompt di recap sullo stato della task.
7
+ già bound alla task via `LOOM_TASK`.
8
8
 
9
9
  ```
10
- ↑↓ scegli la task → ⏎ → tab CC di fianco → LOOM_TASK bound + recap stato task
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). Nessuna differenza di trattamento il prompt iniziale è un
15
- recap, non l'invocazione di una skill, quindi vale identico sulle due famiglie.
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 e toggle dentro i modali di testo | `^F` = find, come ovunque |
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,6 +79,9 @@ 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
86
  | immediata | `t` | terminale @project-root (surface standard launch) |
80
87
  | immediata | `c` | sessione Claude **nuda**: nessuna task, nessun prompt iniziale |
@@ -94,6 +101,34 @@ finestra Ptyxis, senza passare da un modale. `c` (minuscola, azione) e `C`
94
101
  (maiuscola, modale create-task) restano distinte per la regola sopra — così come
95
102
  `f` (fork) e `F` (filtri).
96
103
 
104
+ ### I quattro modi di entrare in una task
105
+
106
+ Sulla task selezionata (focus sul pane Tasks) quattro tasti aprono una sessione
107
+ **bound**: `LOOM_TASK` iniettata, `sessionId` pinnato, binding scritto nel
108
+ sidecar. Fra loro cambia **solo il prompt iniziale**.
109
+
110
+ | Tasto | Prompt |
111
+ |---|---|
112
+ | `⏎` | nessuno — il contesto lo carica l'hook `SessionStart`, il primo messaggio lo scrivi tu |
113
+ | `^K` | `recap stato task <id>` — prompt diretto, nessuna skill |
114
+ | `^P` | `/loom-works:preflight-task <id>` |
115
+ | `^R` | `/loom-works:run-task <id>`, oppure `/loom-works:run-doc <id>` se l'id è una `D` |
116
+
117
+ Il prompt iniziale è la scelta di **chi apre**, non una proprietà della task: lo
118
+ stesso task file si apre per leggerne lo stato, per congelarne le decisioni, per
119
+ eseguirlo o per entrarci a mani nude — e il tasto premuto *è* quell'intento.
120
+ Prima esisteva il solo `⏎` col recap, quindi ogni altro ingresso passava per una
121
+ sessione da correggere a mano.
122
+
123
+ Col focus sul pane Sessions i tre `CTRL` non spawnano: l'oggetto dell'azione è la
124
+ task selezionata, e senza quel pane a fuoco non ce n'è una (il deck lo dice nella
125
+ riga di nota, invece di aprire qualcosa a caso).
126
+
127
+ Il catalogo dei prompt vive in `scripts/deck-run`, non nella TUI: è il primitive
128
+ UI-agnostico, il deck gli passa un **simbolo** (`--prompt-kind`) e non una
129
+ stringa. Così il testo sta in un posto solo, e con lui il suo vincolo di quoting
130
+ — il prompt viaggia dentro apici singoli in `bash -lc`, quindi non può contenerne.
131
+
97
132
  ### `f` — forkare una conversazione
98
133
 
99
134
  Il fork rama la sessione selezionata: `claude --resume <origine> --fork-session`
@@ -241,16 +276,21 @@ npx @lamemind/loom-deck
241
276
  ```bash
242
277
  # dalla project dir con un tasks.md
243
278
  scripts/deck-run T18
279
+ scripts/deck-run T18 --prompt-kind none|recap|preflight|run
244
280
  ```
245
281
 
246
- Apre una tab Ptyxis nella window attiva con `LOOM_TASK=T18 claude 'recap stato task T18'`
247
- (prompt override-abile via `LOOM_DECK_ENTER_PROMPT`, placeholder `{TASK}`).
282
+ Apre una tab Ptyxis nella window attiva con `LOOM_TASK=T18 claude 'recap stato task T18'`.
283
+ Il prompt iniziale è il terzo asse dello spawn, ortogonale al binding task
284
+ (`<TaskID>` vs `--no-task`) e alla continuità (nuova vs `--resume`/`--fork`):
285
+ senza flag resta `recap`, `none` apre la sessione bound **senza** alcun prompt.
286
+ `LOOM_DECK_ENTER_PROMPT` (placeholder `{TASK}`) resta un override che vince sul
287
+ kind — tranne su `none`, che è una richiesta esplicita di non averne.
248
288
 
249
289
  ## Sviluppo (TUI Ink)
250
290
 
251
291
  ```bash
252
292
  npm install
253
- npm run dev # tsx src/cli.tsx — lista tasks.md reale, ↑↓ naviga · spawn · q esci
293
+ npm run dev # tsx src/cli.tsx — lista tasks.md reale, ↑↓ naviga · ⏎/^K/^P/^R spawn · q esci
254
294
  npm run build # tsc → dist/
255
295
  npm test # node:test sul core vista (src/view.ts), senza Ink né terminale
256
296
  ```
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
- function spawnDeck(id, cwd, sessionId) {
124
- const child = spawnOut(DECK_RUN, [id, '--session-id', sessionId], {
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
- if (isSpot) {
1161
- setNote('spot: sessioni libere, nessuna task da spawnare');
1162
- }
1163
- else if (selTask) {
1164
- // sessionId pinnato lato deck binding sidecar scritto PRIMA dello
1165
- // spawn: la sessione risulta figlia della task appena il JSONL compare.
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
@@ -1407,7 +1440,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1407
1440
  if (budget.compact) {
1408
1441
  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
1442
  }
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"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7 \u23CE ", canSpawn ? 'spawn' : canResume ? 'resume' : '—', canResume ? ' · f fork' : '', canPin ? ' · p pin · N nota' : '', " \u00B7 ^F cerca \u00B7 C nuova \u00B7 E edit \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7 t term \u00B7 c claude \u00B7 q esci \u00B7 focus:", ' ', _jsx(Text, { color: "cyan", children: focus })] })), mode === 'normal' && launch.length > 0 ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["launch ", 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] }));
1443
+ 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"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7", ' ', canSpawn ? '⏎/^K/^P/^R spawn' : canResume ? 'resume' : '—', canResume ? ' · f fork' : '', canPin ? ' · p pin · N nota' : '', " \u00B7 ^F cerca \u00B7 C nuova \u00B7 E edit \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7 t term \u00B7 c claude \u00B7 q esci \u00B7 focus:", ' ', _jsx(Text, { color: "cyan", children: focus })] })), mode === 'normal' && launch.length > 0 ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["launch ", 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
1444
  }
1412
1445
  const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
1413
1446
  // Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Deck TUI Ink per-progetto della famiglia loom: legge tasks.md e spawna sessioni Claude Code bound via LOOM_TASK",
5
5
  "type": "module",
6
6
  "bin": {
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 "recap stato task <TaskID>" — no skill,
8
- # solo recap più controllo). Riusabile identico da TUI (Ink) e da web.
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 sessione, {TASK}=TaskID (default: "recap stato task <TaskID>")
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>] (es. deck-run T18)
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
- # Prompt iniziale della sessione. Default: prompt DIRETTO (no skill) che chiede
193
- # a Claude un recap dello stato della task l'utente rivede/steera prima di
194
- # lanciare (= più controllo), nessuna esecuzione automatica. Override completo via
195
- # LOOM_DECK_ENTER_PROMPT, con placeholder {TASK} rimpiazzato dal TaskID. Vincolo: il
196
- # template NON deve contenere apici singoli (viaggia dentro '...' nel comando in-tab).
197
- # NB: il default NON può stare nel `${VAR:-...}` perché conterrebbe `{TASK}` e la sua
198
- # `}` chiuderebbe in anticipo l'espansione → if/else esplicito (default interpola
199
- # ${TASK} diretto; il placeholder {TASK} vale solo per il valore dell'override).
200
- if [[ -n "${LOOM_DECK_ENTER_PROMPT:-}" ]]; then
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
- PROMPT_ARG="'${PROMPT}'"
217
- [[ -n "$RESUME_ID" ]] && PROMPT_ARG=""
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