@lamemind/loom-deck 0.67.0 → 0.69.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.
@@ -36,6 +36,7 @@ import { DetailScreen } from './detail-screen.js';
36
36
  import { StatusScreen } from './status-screen.js';
37
37
  import { InboxScreen } from './inbox-screen.js';
38
38
  import { WrapScreen } from './wrap-screen.js';
39
+ import { SpawnScreen } from './spawn-screen.js';
39
40
  import { ReaderScreen, SearchScreen } from './search-screen.js';
40
41
  /**
41
42
  * Il ripiego per un terminale troppo basso: una riga sola al posto della
@@ -107,7 +108,7 @@ export function HintBar({ mode, purge, keyLegend, indicators, columns, }) {
107
108
  */
108
109
  export function screenFor(input) {
109
110
  const { mode, rows, columns, note, overlays } = input;
110
- const { assign, sheet, search, status, inbox, wrap } = overlays;
111
+ const { assign, sheet, search, status, inbox, wrap, spawn } = overlays;
111
112
  // ── T57 · schermata di assegnazione ─────────────────────────────────────
112
113
  // Sostitutiva come ricerca e reader (D3): la lista task non entra in un box
113
114
  // sopra i due pane, e prendendo l'intero frame non costa nulla al loro budget.
@@ -185,6 +186,16 @@ export function screenFor(input) {
185
186
  const start = Math.min(wrap.top, wrap.maxTop);
186
187
  return (_jsx(WrapScreen, { files: wrap.files.slice(start, start + wrap.capacity), count: wrap.count, mixed: wrap.mixed, mtime: wrap.mtime, top: start, capacity: wrap.capacity, path: wrap.path, caret: wrap.caret, columns: columns }));
187
188
  }
189
+ // ── T161 · pagina delle azioni di spawn ─────────────────────────────────
190
+ // Ottava schermata sostitutiva: undici righe per quattro colonne, più l'area
191
+ // di compilazione. In un box sopra i due pane al prompt resterebbero due
192
+ // colonne.
193
+ if (mode === 'spawn' && spawn.open) {
194
+ if (isCompact(spawn.capacity)) {
195
+ return _jsx(CompactNotice, { what: "azioni di spawn", esc: "chiude", rows: rows, columns: columns });
196
+ }
197
+ return (_jsx(SpawnScreen, { rows: spawn.table, sel: spawn.sel, top: Math.min(spawn.top, spawn.maxTop), capacity: spawn.capacity, configured: spawn.table.filter((r) => r.touched).length, dirty: spawn.dirty, editing: spawn.editing, editable: spawn.editable, fieldTitle: spawn.fieldTitle, fieldModel: spawn.fieldModel, fieldPrompt: spawn.fieldPrompt, cursor: spawn.cursor, example: spawn.example, columns: columns, note: note }));
198
+ }
188
199
  // ── T52 · ricerca e reader ──────────────────────────────────────────────
189
200
  // Gli unici modali che NON stanno in flusso sopra i pane: una lista di
190
201
  // occorrenze non entra in un box da 4 righe. Prendono l'intero frame, quindi
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // T161 — la pagina delle azioni di spawn: la tabella e la sua area di
3
+ // compilazione.
4
+ import { Box, Text } from 'ink';
5
+ import { cut, pad, sanitize, termWidth } from '../width.js';
6
+ import { ChoiceRow, FieldText, LABEL_W } from './fields.js';
7
+ import { CARET, CARET_OFF } from '../glyphs.js';
8
+ import { interpolate, saneNote } from '../sane-note.js';
9
+ import { MODELS, SPAWN_FIELDS, spawnCell, } from '../spawn-catalog.js';
10
+ /**
11
+ * Il glifo della riga configurata.
12
+ *
13
+ * Non è riusato da nessun'altra superficie del deck: `●`/`◍` dicono la liveness
14
+ * di un processo, `📌`/`🔗`/`○` l'appartenenza di una conversazione, `✎` il
15
+ * titolo — un falso amico costa più di un simbolo nuovo. Concorde alle due
16
+ * contabilità di larghezza (`agrees('◆')`), quindi `sanitize` lo lascia passare
17
+ * invece di renderlo `·`.
18
+ */
19
+ const TOUCHED = '◆';
20
+ /** Cella del glifo: si riempie di spazi anche quando è vuota, o le righe non
21
+ * configurate sposterebbero a sinistra tutto ciò che segue. */
22
+ const MARK_W = 2;
23
+ /** Colonna modello: il dominio è chiuso, e a decidere la larghezza non è la voce
24
+ * più lunga (`sonnet`, 6) ma l'INTESTAZIONE — `modello` ne vuole 7, e una
25
+ * testata di colonna troncata con l'ellissi si legge come un difetto. */
26
+ const MODEL_W = 7;
27
+ /** Tetto della colonna funzione: la label più lunga del catalogo (`drain di un
28
+ * file inbox`) sta esattamente dentro. Un tetto più alto le celle le
29
+ * toglierebbe al prompt, che è l'unico dei quattro valori senza una forma
30
+ * breve; più basso troncherebbe due label su undici già a piena larghezza. */
31
+ const FUNC_MAX = 22;
32
+ /** Le etichette delle righe dell'area, in italiano come ogni altra etichetta a
33
+ * schermo: la chiave del campo è un nome del codice e finisce nel file di
34
+ * progetto, non sotto gli occhi di chi compila. */
35
+ const FIELD_LABEL = {
36
+ title: 'titolo',
37
+ model: 'modello',
38
+ prompt: 'prompt',
39
+ };
40
+ /**
41
+ * Le quattro colonne, per sottrazione come ogni lista del deck: le fisse sono
42
+ * note, e ciò che resta si divide fra titolo e prompt.
43
+ *
44
+ * Il prompt prende la fetta maggiore perché è l'unico dei quattro valori che non
45
+ * ha una forma breve: un titolo tagliato resta riconoscibile dal prefisso, un
46
+ * prompt tagliato a dieci colonne dice solo `/loom-works…`.
47
+ *
48
+ * Pavimento 0 su ogni cella: è un tetto, non una preferenza, e alzarlo sopra lo
49
+ * spazio reale fa uscire la riga dal box mangiandone il bordo (invariante ③ di
50
+ * `width.ts`).
51
+ */
52
+ export function spawnColumns(width, rows) {
53
+ // `- 4` = bordo + padding del box tabella, che sta DENTRO quello esterno.
54
+ const avail = Math.max(0, width - 4);
55
+ const func = Math.min(FUNC_MAX, Math.max(0, ...rows.map((r) => termWidth(r.action.label))));
56
+ // I tre separatori fra le quattro colonne vanno contati insieme alle celle:
57
+ // sommare solo le celle è l'off-by-one che tronca la coda della riga.
58
+ const rest = Math.max(0, avail - CARET_OFF.length - MARK_W - func - MODEL_W - 3);
59
+ // Il titolo ha un TETTO oltre il quale non cresce: i template sono corti
60
+ // (`📊 {slug}`), quindi su un terminale largo ogni colonna in più gli
61
+ // resterebbe vuota mentre al prompt serve.
62
+ const title = Math.min(28, Math.floor(rest * 0.4));
63
+ return { func, title, model: MODEL_W, prompt: Math.max(0, rest - title) };
64
+ }
65
+ /** Una cella della tabella, col suo colore: il valore in chiaro, il segnaposto
66
+ * smorzato, l'override in ciano — l'origine si legge senza una legenda. */
67
+ function Cell({ row, field, value, width, }) {
68
+ const cell = spawnCell(row.action, field, value, row.raw.overridden.has(field));
69
+ const text = cut(cell.text, width);
70
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: cell.placeholder, color: cell.origin === 'override' && !cell.placeholder ? 'cyan' : undefined, children: text }), ' '.repeat(Math.max(0, width - termWidth(text)))] }));
71
+ }
72
+ export function SpawnScreen({ rows, sel, top, capacity, configured, dirty, editing, editable, fieldTitle, fieldModel, fieldPrompt, cursor, example, columns, note, }) {
73
+ const width = Math.max(20, (columns || 80) - 4);
74
+ const col = spawnColumns(width, rows);
75
+ const window = rows.slice(top, top + capacity);
76
+ const current = rows[sel];
77
+ /** La riga disegnata `field` è quella in fuoco? Il cursore indicizza le sole
78
+ * celle editabili, la resa le disegna tutte e tre. */
79
+ const focused = (field) => editing && editable[cursor.row] === field;
80
+ const textW = Math.max(10, width - LABEL_W - CARET_OFF.length);
81
+ // L'anteprima si fa sul titolo GIÀ ridotto, come lo farà `deck-run`: mostrare
82
+ // il template interpolato ma non ridotto prometterebbe un titolo che la tab
83
+ // non porta.
84
+ const rendered = saneNote(interpolate(fieldTitle, example) ?? '');
85
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: "azioni di spawn" }), ' ', _jsxs(Text, { dimColor: true, children: ["\u00B7 ", rows.length, " azioni \u00B7 ", configured, " configurate"] }), dirty ? _jsx(Text, { color: "yellow", children: " \u00B7 modifiche non salvate" }) : null] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " riga \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), ' ', editing ? 'conferma' : 'modifica', " \u00B7 ", _jsx(Text, { color: "yellow", children: "CANC" }), " torna al default \u00B7", ' ', _jsx(Text, { color: "yellow", children: "w" }), " salva \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), ' ', editing ? 'annulla' : 'chiude'] }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [CARET_OFF, ' '.repeat(MARK_W), pad('funzione', col.func), " ", pad('titolo', col.title), " ", pad('modello', col.model), ' ', pad('prompt', col.prompt)] }), window.map((r, i) => {
86
+ const at = top + i;
87
+ return (_jsxs(Text, { wrap: "truncate-end", children: [at === sel ? CARET : CARET_OFF, _jsx(Text, { color: "cyan", children: pad(r.touched ? TOUCHED : '', MARK_W) }), _jsx(Text, { bold: at === sel, children: pad(cut(r.action.label, col.func), col.func) }), ' ', _jsx(Cell, { row: r, field: "title", value: r.raw.title, width: col.title }), ' ', _jsx(Cell, { row: r, field: "model", value: r.raw.model, width: col.model }), ' ', _jsx(Cell, { row: r, field: "prompt", value: r.raw.prompt, width: col.prompt })] }, r.action.id));
88
+ })] }), editing ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [SPAWN_FIELDS.map((field) => {
89
+ const fixed = current.action.fixed[field];
90
+ if (fixed !== undefined) {
91
+ return (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [CARET_OFF, FIELD_LABEL[field].padEnd(LABEL_W), "(", fixed, ")"] }, field));
92
+ }
93
+ if (field === 'model') {
94
+ return (_jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(fieldModel)), focused: focused('model'), width: width }, field));
95
+ }
96
+ const value = field === 'title' ? fieldTitle : fieldPrompt;
97
+ return (_jsxs(Text, { wrap: "truncate-end", children: [focused(field) ? CARET : CARET_OFF, _jsx(Text, { dimColor: true, children: FIELD_LABEL[field].padEnd(LABEL_W) }), _jsx(FieldText, { value: value, caret: cursor.caret, focused: focused(field), cols: textW })] }, field));
98
+ }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [CARET_OFF, 'esempio'.padEnd(LABEL_W), current.action.title === null ? ('nessun titolo per questa azione') : (_jsx(Text, { color: rendered ? 'green' : undefined, children: rendered ? cut(rendered, textW) : 'titolo a vuoto: nessuna nota' }))] })] })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
99
+ }
package/dist/viewport.js CHANGED
@@ -95,6 +95,10 @@ export const MODAL_HEIGHT = {
95
95
  // 77 `WRAP` e 147 `misto`, cioè una lista che non entra in nessun box sopra i
96
96
  // pane. Altezza da `wrapListCapacity`.
97
97
  wrap: 0,
98
+ // T161 — la pagina delle azioni di spawn: ottava sostitutiva. Undici righe di
99
+ // tabella più l'area di compilazione: in un box sopra i pane resterebbero due
100
+ // colonne per il prompt. Altezza da `spawnListCapacity`.
101
+ spawn: 0,
98
102
  };
99
103
  /**
100
104
  * Distribuisce le righe disponibili fra lista task, lista sessioni e preview.
@@ -335,6 +339,37 @@ const WRAP_CHROME = 12;
335
339
  export function wrapListCapacity(rows) {
336
340
  return Math.max(0, (rows || 24) - SLACK - WRAP_CHROME);
337
341
  }
342
+ // T161 — cornice della pagina delle azioni di spawn.
343
+ // 2 bordi del box esterno
344
+ // 1 riga di testata (nome della pagina · quante righe · quante configurate)
345
+ // 1 riga hint
346
+ // 1 marginTop del box tabella
347
+ // 2 bordi del box tabella
348
+ // 1 riga di intestazione delle colonne
349
+ const SPAWN_CHROME = 8;
350
+ // L'area di compilazione: marginTop + le TRE righe (titolo, modello, prompt) +
351
+ // la riga dell'anteprima resa.
352
+ //
353
+ // Fisse e non proporzionali alle celle editabili della riga aperta: su `bare`,
354
+ // che ha il solo modello, le altre due si disegnano comunque per dire perché non
355
+ // si modificano. Un'altezza che dipendesse dalla riga selezionata farebbe
356
+ // saltare la tabella di due righe a ogni movimento del caret, e il budget
357
+ // dovrebbe sapere quale riga è aperta per non sottostimare — lo stesso motivo
358
+ // per cui le cinque righe del detail sono dentro `DETAIL_CHROME` invece che
359
+ // condizionali (T111 · D1).
360
+ const SPAWN_FIELDS_CHROME = 5;
361
+ /**
362
+ * Righe di tabella che entrano nel terminale.
363
+ *
364
+ * Le righe dell'area aperta e la riga di stato si pagano QUI, non altrove: la
365
+ * pagina è una schermata sostitutiva, quindi la nota che le sue azioni scrivono
366
+ * la disegna lei — a differenza della lista, dove la riga di stato sta fuori dai
367
+ * pane e il suo costo è già in `layoutBudget`.
368
+ */
369
+ export function spawnListCapacity(rows, editing = false, noteLine = false) {
370
+ const extra = (editing ? SPAWN_FIELDS_CHROME : 0) + (noteLine ? 1 : 0);
371
+ return Math.max(0, (rows || 24) - SLACK - SPAWN_CHROME - extra);
372
+ }
338
373
  /**
339
374
  * Righe scorse da una pressione di PagGiù/PagSu: METÀ della capienza visibile.
340
375
  *
package/dist/wrap-scan.js CHANGED
@@ -84,48 +84,29 @@ export function mixedCount(files) {
84
84
  /** Radice di default dell'area di srotolamento: la project root intera (D4). */
85
85
  export const WRAP_DEFAULT_PATH = '.';
86
86
  /**
87
- * Il prompt della sessione che srotola un path.
87
+ * T161 — il buco `{path}` del titolo e del prompt di una sessione di
88
+ * srotolamento: il path reso a parole.
88
89
  *
89
- * CABLATO per decisione, non per fretta (D9): parametrizzarlo prima di aver
90
- * visto come si comporta significherebbe scegliere i parametri a occhio. Le tre
91
- * cose che dice sono le tre che rendono l'operazione reversibile e verificabile
92
- * — backup fuori dal repo, rapporto, commit solo se il diff è chiaramente
93
- * innocuo — e nessuna delle tre è una preferenza da esporre.
94
- *
95
- * Il `<path>` è l'unico buco. Arriva dal campo della lista e finisce dentro
96
- * `--prompt` come argv singolo, quindi non passa da nessuna shell prima di
97
- * `deck-run`, che lo quota ad apici singoli: non c'è quoting da fare qui.
98
- */
99
- export function wrapPrompt(path) {
100
- const target = path.trim() || WRAP_DEFAULT_PATH;
101
- return (`lancia md-wrap modo apply su ${target} e backup in cartella tmp dedicata. ` +
102
- 'verifica risultato e fai rapporto. se le modifiche sono tutte chiaramente safe, ' +
103
- 'puoi committare direttamente');
104
- }
105
- /** Prefisso del titolo di una sessione di srotolamento. Astrale, come le altre
106
- * emoji che finiscono in un titolo: le BMP `sanitize` (`src/width.ts`) le rende
107
- * `·`. Va tenuta allineata alla whitelist di `_sane_note` in `deck-run`. */
108
- const WRAP_EMOJI = '📏';
109
- /**
110
- * Il titolo della sessione che srotola un path: `📏 <path a parole>`.
111
- *
112
- * Le barre diventano spazi e l'estensione cade PRIMA di `_sane_note`, il cui
113
- * alfabeto non ha né `/` né `.`: lasciandoceli cadere le parole si salderebbero
114
- * (`runtimereferencedoc-systemmd`), e il titolo della tab divergerebbe da quello
115
- * che la lista del deck legge dal sidecar.
90
+ * Le barre diventano spazi e l'estensione cade QUI, prima di `_sane_note`, il
91
+ * cui alfabeto non ha né `/` né `.`: lasciandoceli cadere le parole si
92
+ * salderebbero (`runtimereferencedoc-systemmd`), e il titolo della tab
93
+ * divergerebbe da quello che la lista del deck legge dal sidecar.
116
94
  *
117
95
  * Il path di default (`.`, la project root intera) non ha parole da mostrare e
118
- * diventa una frase: un titolo `📏 ` sarebbe il solo prefisso.
96
+ * diventa una frase: interpolato in `📏 {path}` darebbe il solo prefisso, cioè
97
+ * un titolo uguale per ogni srotolamento.
98
+ *
99
+ * L'emoji del prefisso non sta più qui: è dentro il template della riga
100
+ * `unwrap` del catalogo delle azioni, dove il progetto può riscriverla.
119
101
  */
120
- export function wrapTitle(path) {
102
+ export function wrapWords(path) {
121
103
  const target = path.trim() || WRAP_DEFAULT_PATH;
122
104
  if (target === WRAP_DEFAULT_PATH)
123
- return `${WRAP_EMOJI} tutto il progetto`;
124
- const words = target
105
+ return 'tutto il progetto';
106
+ return target
125
107
  .replace(/\.md$/i, '')
126
108
  .replace(/[./]+/g, ' ')
127
109
  .trim();
128
- return `${WRAP_EMOJI} ${words}`;
129
110
  }
130
111
  /**
131
112
  * Cartella della cache, una per utente e con `mode 0700` — stessa ragione del
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.67.0",
3
+ "version": "0.69.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
@@ -13,6 +13,8 @@
13
13
  # continuità nuova ........ --resume [--fork]
14
14
  # prompt --prompt-kind none|recap|preflight|run|checkpoint | --prompt <testo>
15
15
  # modello --model fable|opus|sonnet|haiku
16
+ # Fuori dai quattro, e volutamente non un asse: --new-window, che cambia SOLO il
17
+ # verbo dell'exec finale (tab nella finestra attiva → finestra nuova).
16
18
  # Il terzo asse (T56) prima non esisteva: il prompt era una CONSEGUENZA degli
17
19
  # altri due (bound ⇒ recap, --no-task ⇒ niente, --resume ⇒ niente), quindi
18
20
  # "task-bound SENZA prompt" era inesprimibile e l'unico modo di non avere un
@@ -56,6 +58,9 @@
56
58
  # LOOM_DECK_PROFILE_UUID UUID profilo Ptyxis (default: cc-host)
57
59
  # LOOM_DECK_WORKDIR dir di lavoro della tab (default: $PWD)
58
60
  # LOOM_DECK_ENTER_PROMPT override del prompt iniziale, {TASK}=TaskID (vince sul kind, tranne `none`)
61
+ # T161: inerte sugli spawn che arrivano dal deck TUI —
62
+ # quelli passano SEMPRE `--prompt`, su cui non ha mai
63
+ # vinto. Resta per le invocazioni a mano e per i test.
59
64
  # LOOM_DECK_INTAB_CMD override comando in-tab (default: la sessione CC; usato per i test)
60
65
  # LOOM_DECK_STATE_PROFILE PTYXIS_PROFILE annunciata a compass (default: bindings/claude
61
66
  # del progetto, letto da dconf; settata a vuoto = nessun annuncio)
@@ -75,6 +80,17 @@ SESSION_ID=""
75
80
  RESUME_ID=""
76
81
  NO_TASK=0
77
82
  FORK=0
83
+ # T162 — finestra NUOVA invece di una tab nella finestra attiva.
84
+ #
85
+ # `ptyxis --tab` va sempre nella finestra col focus, e va bene per il deck, che
86
+ # è già una tab della finestra del progetto. Un chiamante che vive FUORI da
87
+ # quella finestra — compass, che riapre una conversazione pinnata di un
88
+ # progetto chiuso — non ha una finestra attiva giusta: senza un verbo diverso
89
+ # la tab finisce nella finestra di un altro progetto, o in una a caso.
90
+ #
91
+ # Non è un quinto asse: non cambia cosa gira nella tab, solo dove la tab nasce.
92
+ # Vale identico nei due rami di spawn (inline e profile).
93
+ NEW_WINDOW=0
78
94
  TITLE_NOTE=""
79
95
  # Vuoto = flag non passato (≠ un default già risolto): serve a distinguere
80
96
  # "kind implicito" da "kind chiesto", perché con --no-task il primo è legittimo
@@ -154,6 +170,8 @@ while [[ $# -gt 0 ]]; do
154
170
  FORK=1; shift ;;
155
171
  --no-task)
156
172
  NO_TASK=1; shift ;;
173
+ --new-window)
174
+ NEW_WINDOW=1; shift ;;
157
175
  *)
158
176
  if [[ -z "$TASK" ]]; then TASK="$1"; shift
159
177
  else echo "argomento inatteso: '$1'" >&2; exit 2; fi ;;
@@ -187,7 +205,10 @@ USAGE="uso: deck-run <TaskID> [--model <alias>] [--prompt-kind <kind>] [--sessio
187
205
  valore ignoto → fallback sul default, con avviso su stderr
188
206
 
189
207
  --title-note nota della conversazione, appesa al titolo tab
190
- (ridotta a lettere/cifre/spazi/-/_, cap 60 char)"
208
+ (ridotta a lettere/cifre/spazi/-/_, cap 60 char)
209
+
210
+ --new-window apre una FINESTRA nuova invece di una tab nella finestra
211
+ attiva. Per chi chiama da fuori dalla finestra del progetto"
191
212
 
192
213
  if [[ $NO_TASK -eq 1 && -n "$TASK" ]]; then
193
214
  echo "--no-task e <TaskID> sono mutuamente esclusivi" >&2
@@ -272,17 +293,22 @@ WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
272
293
  # italiane, spazio, trattino, underscore e sei emoji (sotto); tutto il resto
273
294
  # (apici, backslash, `$`, backtick, `;`, quadre, ogni altra emoji…) cade.
274
295
  #
275
- # T156 — le emoji entrano per il PREFISSO del titolo di fallback che il deck
276
- # genera quando il campo nota è vuoto (`🚀 slug`, `ACTION_EMOJI` in
277
- # `src/spawn.ts`). Senza di loro il prefisso spariva qui e restava nella lista
278
- # del deck, che riceve la nota grezza dal sidecar `session-tasks.jsonl`: la
279
- # stessa conversazione portava due titoli diversi, senza errore. Le quadre che
296
+ # T156 — le emoji entrano per il PREFISSO del titolo che il deck compone
297
+ # (`🚀 slug`). Senza di loro il prefisso spariva qui e restava nella lista del
298
+ # deck, che riceve la nota grezza dal sidecar `session-tasks.jsonl`: la stessa
299
+ # conversazione portava due titoli diversi, senza errore. Le quadre che
280
300
  # delimitavano il prefisso sono cadute col prefisso stesso: l'emoji da sola si
281
301
  # stacca già dallo slug.
282
- # Quattro nominano un'azione su una task (📐🚀📊🏁); le altre due nominano
283
- # l'oggetto di una sessione che una task non ce l'ha — 🧹 il file inbox che si
284
- # drena (`inboxTitle`, `src/inbox.ts`), 📏 il path che si srotola (`wrapTitle`,
285
- # `src/wrap-scan.ts`).
302
+ # T161 — le sei stanno tutte nei TEMPLATE di titolo del catalogo delle azioni
303
+ # (`SPAWN_ACTIONS` in `src/spawn-catalog.ts`), che il progetto può riscrivere:
304
+ # quattro nominano un'azione su una task (📐🚀📊🏁), 🧹 il file inbox che si
305
+ # drena, 📏 il path che si srotola.
306
+ # Questa whitelist ha un GEMELLO TypeScript (`saneNote` in `src/sane-note.ts`),
307
+ # che riduce il template al salvataggio perché quello che sta nel file di
308
+ # progetto sia quello che la tab mostrerà. I due lati non si leggono a vicenda e
309
+ # vanno cambiati insieme: il custode è il gate di `test/deck-run.test.ts`, che
310
+ # passa la stessa batteria di input a questa funzione e al gemello e pretende
311
+ # l'uguaglianza.
286
312
  # La whitelist le nomina una per una e NON apre un intervallo di code point. Due
287
313
  # ragioni: un'emoji arriva nella tab solo se il deck la mostra identica in lista,
288
314
  # cioè se `sanitize` (`src/width.ts`) la lascia passare invece di renderla `·`, e
@@ -386,7 +412,15 @@ MODE_FLAG="--permission-mode ${PERM_MODE} "
386
412
 
387
413
  # ── modello (T108 + T152) ────────────────────────────────────────────────────
388
414
  # Precedenza: --model > env LOOM_DECK_MODEL > modello del kind nel catalogo >
389
- # 'fable'. L'ultimo gradino è il gemello bash di `MODEL_DEFAULT` in spawn.ts e
415
+ # 'fable'.
416
+ #
417
+ # T161 — questa cascata vale per chi invoca deck-run A MANO. Il deck TUI passa
418
+ # SEMPRE `--model` già risolto (default del deck ← override di progetto nel
419
+ # blocco `spawn` di `.claude/loom-works.json`), quindi dal deck si ferma al primo
420
+ # gradino e i tre sotto non intervengono mai. Il blocco di progetto qui non si
421
+ # legge di proposito: `jq` è dipendenza opzionale di questo script, e dargli un
422
+ # JSON da consultare la renderebbe obbligatoria su un percorso che oggi degrada
423
+ # senza. L'ultimo gradino è il gemello bash di `MODEL_DEFAULT` in spawn.ts e
390
424
  # va tenuto allineato a mano: i due lati non si leggono a vicenda (il deck deve
391
425
  # poter MOSTRARE la selezione iniziale prima che deck-run esista come processo),
392
426
  # quindi un cambio qui senza l'altro fa partire dalla lista un modello diverso
@@ -653,19 +687,30 @@ IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"
653
687
  # vedono comunque quello che vedevano prima.
654
688
  printf 'LOOM_DECK_INTAB %s\n' "$IN_TAB_CMD"
655
689
 
690
+ # Verbo di apertura (T162): tab nella finestra attiva, o finestra nuova. Una
691
+ # variabile e non due rami duplicati per ogni modalità di spawn — `--new-window`
692
+ # è l'unica differenza fra i due casi.
693
+ if [[ $NEW_WINDOW -eq 1 ]]; then WIN_FLAG="--new-window"; else WIN_FLAG="--tab"; fi
694
+
656
695
  case "$MODE" in
657
696
  inline)
658
697
  # --tab: tab nella window attiva. -- <cmd>: comando inline nella tab.
659
698
  # LOOM_TASK viaggia dentro il comando → nessuna scrittura dconf.
660
- exec ptyxis --tab -d "$WORKDIR" -T "${TITLE}" -- bash -lc "$IN_TAB_CMD"
699
+ exec ptyxis "$WIN_FLAG" -d "$WORKDIR" -T "${TITLE}" -- bash -lc "$IN_TAB_CMD"
661
700
  ;;
662
701
  profile)
663
702
  # --tab-with-profile: riusa il profilo (custom-command FISSO) → per iniettare
664
703
  # LOOM_TASK+task va riscritto il custom-command al volo via dconf (stato condiviso).
704
+ # Con --new-window i due flag convivono: la finestra nasce e il profilo
705
+ # decide cosa gira nella sua prima tab.
665
706
  dpath="/org/gnome/Ptyxis/Profiles/${PROFILE_UUID}/"
666
707
  dconf write "${dpath}use-custom-command" true
667
708
  dconf write "${dpath}custom-command" "'bash -lc \"${IN_TAB_CMD//\"/\\\"}\"'"
668
- exec ptyxis --tab-with-profile="$PROFILE_UUID" -d "$WORKDIR" -T "${TITLE}"
709
+ if [[ $NEW_WINDOW -eq 1 ]]; then
710
+ exec ptyxis --new-window --tab-with-profile="$PROFILE_UUID" -d "$WORKDIR" -T "${TITLE}"
711
+ else
712
+ exec ptyxis --tab-with-profile="$PROFILE_UUID" -d "$WORKDIR" -T "${TITLE}"
713
+ fi
669
714
  ;;
670
715
  *)
671
716
  echo "LOOM_DECK_SPAWN_MODE ignoto: '$MODE' (usa 'inline' o 'profile')" >&2
@@ -8,9 +8,23 @@
8
8
  #
9
9
  # Perché un file dati e non un `case` dentro deck-run: due lati hanno bisogno
10
10
  # dello stesso testo — deck-run per comporre il comando in-tab, il deck TUI per
11
- # pre-riempire il campo prompt (e ora anche il modello) del detail con ciò che
12
- # sta per essere eseguito. Come codice, il secondo lato dovrebbe riscriverlo, e
13
- # le due copie divergono alla prima voce aggiunta da una parte sola.
11
+ # pre-riempire i campi del detail con ciò che sta per essere eseguito. Come
12
+ # codice, il secondo lato dovrebbe riscriverlo, e le due copie divergono alla
13
+ # prima voce aggiunta da una parte sola.
14
+ #
15
+ # T161 — questo file resta la FONTE UNICA dei sette kind, e non è diventato un
16
+ # blocco JSON nel file di progetto per una ragione precisa: `jq` è dipendenza
17
+ # OPZIONALE di deck-run, e un JSON da consultare la renderebbe obbligatoria su
18
+ # un percorso che oggi degrada senza. Le sue righe sono il primo gradino del
19
+ # default di un'azione su task; il secondo è la riga di catalogo in
20
+ # `src/spawn-catalog.ts`, e sopra entrambi sta l'override di progetto nel blocco
21
+ # `spawn` di `.claude/loom-works.json`, che legge il solo deck.
22
+ #
23
+ # Le azioni SENZA kind (la sessione nuda, il drain di un file inbox, lo
24
+ # srotolamento dell'hard-wrap, il project status) non hanno una riga qui e non
25
+ # devono averla: le chiavi di questo file sono i valori di `--prompt-kind`, e
26
+ # quelle quattro non ne passano nessuno — una riga qui sarebbe irraggiungibile
27
+ # da ogni chiamante. I loro default vivono in `src/spawn-catalog.ts`.
14
28
  #
15
29
  # `none` non compare: nessun prompt non è un template vuoto, è l'assenza della
16
30
  # voce. Chi chiede un kind fuori da questo file riceve un prompt vuoto e nessun