@lamemind/loom-deck 0.41.0 → 0.42.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
@@ -28,17 +28,25 @@ Il deck è **UN processo Node**: spawna ma **non contiene** le sessioni CC — l
28
28
 
29
29
  ### La riga di stato dice il comando, non una parafrasi
30
30
 
31
- Ogni spawn di una sessione Claude — task (`⏎`/`^K`/`^P`/`^R`), resume, fork, sessione nuda (`c`) — scrive nella riga di stato in fondo al frame il **comando esatto** che è stato eseguito, quotato come lo si scriverebbe in bash:
31
+ Ogni spawn di una sessione Claude — task (`⏎`/`^K`/`^P`/`^R`), resume, fork, sessione nuda (`c`) — scrive nella riga di stato in fondo al frame il **comando esatto** che gira dentro la tab, cioè l'invocazione `claude` vera con le env che la legano a task e progetto:
32
32
 
33
33
  ```
34
- $ /home/tizio/.local/lib/node_modules/@lamemind/loom-deck/scripts/deck-run T115 --session-id 9f3a… --prompt-kind run --model opus --title-note 'parser'
34
+ $ LOOM_TASK=T115 claude --name '🧵 loom-works · T115 «parser»' --permission-mode auto --model opus --session-id 9f3a… '/loom-works:run-task T115'
35
35
  ```
36
36
 
37
- Task, `sessionId`, prompt-kind, modello e nota del titolo sono già argomenti del comando: una nota che li elencasse a parole li direbbe una seconda volta, in una grafia che non si può ricopiare in un terminale. Quello che si perde è il **tasto premuto**, che è ciò che hai appena fatto tu e non ciò che il deck ha fatto per te.
37
+ Task, `sessionId`, modello, titolo e prompt iniziale sono già dentro: una nota che li elencasse a parole li direbbe una seconda volta, in una grafia che non si può ricopiare in un terminale. Quello che si perde è il **tasto premuto**, che è ciò che hai appena fatto tu e non ciò che il deck ha fatto per te.
38
38
 
39
- Il deck non passa mai da una shell (`spawn` senza `shell:true` consegna l'argv a `execve`): il quoting esiste per la sola resa, e serve a rendere la riga ricopiabile senza cambiare di una virgola ciò che è stato eseguito.
39
+ **Il comando non lo compone il deck**, e non potrebbe senza riscrivere le stesse regole due volte: catalogo dei prompt, quoting, permission mode, profilo di stato e titolo vivono in `deck-run`. Il primitive lo **annuncia** su stdout prima di `exec`, con una riga
40
40
 
41
- Su un terminale stretto il taglio è **al mezzo** (`…`), non dalla coda: la testa è il path assoluto di `deck-run`, identico a ogni invocazione, mentre quel che distingue una sessione dall'altra sta tutto in fondo.
41
+ ```
42
+ LOOM_DECK_INTAB <comando>
43
+ ```
44
+
45
+ e il deck la legge da lì. L'annuncio è utile anche a chi lancia `deck-run` a mano: dice cosa sta per partire, mentre le diagnostiche restano su stderr.
46
+
47
+ Ne discende che il comando in-tab arriva **asincrono**, qualche millisecondo dopo lo spawn. Nel frattempo la riga mostra il comando `deck-run` — che resta l'unica cosa visibile quando l'annuncio non arriva affatto (argomenti rifiutati, `deck-run` morto prima dell'exec), ed è anche il comando da ripetere a mano per vedere l'errore.
48
+
49
+ Su un terminale stretto il taglio è **al mezzo** (`…`), non dalla coda: in un comando di spawn è la coda a distinguere un'invocazione dall'altra, e tagliare da lì la butterebbe via per intero.
42
50
 
43
51
  Restano fuori gli spawn che non aprono una sessione Claude interattiva: terminale (`t`), voci `launch` (`1`-`9`) e le skill headless (`C` create-task, `CANC` clean-tasks), dove la riga di stato serve a riportare l'**esito** di un'operazione asincrona.
44
52
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { render, Box, Text, useInput } from 'ink';
4
- import { useState, useEffect, useMemo } from 'react';
4
+ import { useState, useEffect, useMemo, useRef } from 'react';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { resolveTasksPath, resolveTasksDir, loadTasks, loadTaskFileText, } from './tasks.js';
7
7
  import { rowIndexOfKey, selectedRow, } from './search.js';
@@ -17,7 +17,7 @@ import { ALL, EDIT_PRI, EDIT_PROG, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, RO
17
17
  import { purgeTargets, splitTargets } from './purge.js';
18
18
  import { conversationLabel, cpLen, editField, insertAt, isDone, isTextRow, removeAt, EDIT_ROWS, } from './layout.js';
19
19
  import { TASK_EMPTY, relTime, sanitizeTyped } from './glyphs.js';
20
- import { commitTaskEdit, runLaunch, spawnClaudeEmpty, spawnCleanTasks, spawnCreateTask, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, CLAUDE_CMD, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
20
+ import { commitTaskEdit, runLaunch, spawnClaudeEmpty, spawnCleanTasks, spawnCreateTask, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, onInTabCommand, CLAUDE_CMD, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
21
21
  import { EditModal, FilterModal, PurgeModal, SortModal, idList } from './ui/modals.js';
22
22
  import { ReaderScreen, SearchScreen } from './ui/search-screen.js';
23
23
  import { DetailScreen } from './ui/detail-screen.js';
@@ -283,12 +283,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
283
283
  // (`^K`, `⏎`, `f`), che è ciò che l'utente ha appena fatto e non ciò che il
284
284
  // deck ha fatto per lui.
285
285
  //
286
- // Il taglio è al MEZZO (`cutMiddle`, non `cut`): la testa è il path assoluto
287
- // di `deck-run`, identico a ogni spawn, e un taglio dalla coda mostrerebbe
288
- // solo quello. Si taglia QUI, alla composizione, e non al render: la riga di
289
- // stato porta anche messaggi normali, dove è la testa a contare. Ne discende
290
- // che un resize successivo non ricalcola l'elisione — la nota è transitoria e
291
- // `wrap="truncate-end"` resta come rete.
286
+ // Il taglio è al MEZZO (`cutMiddle`, non `cut`): in un comando di spawn è la
287
+ // coda a distinguere un'invocazione dall'altra, e un taglio dalla coda la
288
+ // butterebbe via per intero. Si taglia QUI, alla composizione, e non al
289
+ // render: la riga di stato porta anche messaggi normali, dove è la testa a
290
+ // contare. Ne discende che un resize successivo non ricalcola l'elisione — la
291
+ // nota è transitoria e `wrap="truncate-end"` resta come rete.
292
292
  function noteCommand(cmd) {
293
293
  // 4 = bordo + padding della cornice esterna, 2 = il prompt `$ `. Sbagliare
294
294
  // il budget non produce un errore visibile: la `truncate-end` di Ink taglia
@@ -296,6 +296,29 @@ function Deck({ cwd, tasksPath, tasksDir }) {
296
296
  // cioè con entrambi i pezzi che l'elisione al mezzo voleva salvare.
297
297
  setNote(`$ ${cutMiddle(cmd, Math.max(8, columns - 6))}`);
298
298
  }
299
+ // Le due note di uno spawn, in quest'ordine: il comando di `deck-run` subito,
300
+ // e appena arriva l'annuncio quello della sessione `claude` che gira DENTRO
301
+ // la tab — che è ciò che si vuole vedere davvero (`deck-run` è l'involucro,
302
+ // l'invocazione vera la compone lui).
303
+ //
304
+ // La prima non è un ripiego di stile: è l'unica che esiste quando l'annuncio
305
+ // non arriva (spawn inerte nei test, argomenti rifiutati, `deck-run` morto
306
+ // prima dell'exec), ed è anche il comando da ripetere a mano per vedere
307
+ // l'errore. Il salto fra le due dura i millisecondi che `deck-run` impiega a
308
+ // comporre.
309
+ const spawnSeq = useRef(0);
310
+ function noteSpawn(spawned) {
311
+ noteCommand(spawned.cmd);
312
+ const mine = ++spawnSeq.current;
313
+ onInTabCommand(spawned.child, (inTab) => {
314
+ // Due spawn ravvicinati: gli annunci sono asincroni e possono tornare
315
+ // fuori ordine, e senza guardia il più vecchio scriverebbe sopra il più
316
+ // recente — la riga di stato mostrerebbe un comando che non è l'ultimo
317
+ // partito.
318
+ if (spawnSeq.current === mine)
319
+ noteCommand(inTab);
320
+ });
321
+ }
299
322
  // T56 — apre una sessione bound alla task selezionata. Punto UNICO dei quattro
300
323
  // tasti (⏎/^K/^P/^R): fra loro cambia solo il prompt iniziale, tutto il resto
301
324
  // è identico — uuid pinnato, binding scritto PRIMA dello spawn (la sessione
@@ -343,14 +366,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
343
366
  appendTaskBinding(cwd, sid, id);
344
367
  if (spawnNote)
345
368
  appendNote(cwd, sid, spawnNote);
346
- const { child, cmd } = spawnDeck(id, cwd, sid, kind, model, spawnNote);
347
- child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
369
+ const spawned = spawnDeck(id, cwd, sid, kind, model, spawnNote);
370
+ spawned.child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
348
371
  // Il modello resta SEMPRE visibile anche quando è il default, perché è un
349
372
  // argomento esplicito del comando (T108): gli acceleratori della lista non
350
373
  // passano dal selettore del detail e usano il default fisso, quindi senza
351
374
  // vederlo l'utente crederebbe di aver ereditato la scelta dell'ultimo
352
375
  // detail aperto.
353
- noteCommand(cmd);
376
+ noteSpawn(spawned);
354
377
  }
355
378
  function spawnTaskSession(kind, keyLabel) {
356
379
  const task = selectedTaskOr(keyLabel, 'spawnare');
@@ -637,9 +660,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
637
660
  // devono restare la stessa azione.
638
661
  function resumeSession(sessionId) {
639
662
  const bound = bindings.get(sessionId) ?? null;
640
- const { child, cmd } = spawnDeckResume(bound, cwd, sessionId, sessionNotes.get(sessionId));
641
- child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
642
- noteCommand(cmd);
663
+ const spawned = spawnDeckResume(bound, cwd, sessionId, sessionNotes.get(sessionId));
664
+ spawned.child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
665
+ noteSpawn(spawned);
643
666
  }
644
667
  // T57 — ⏎ nel modale: riscrive il binding nel sidecar e ricarica subito.
645
668
  //
@@ -1043,9 +1066,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1043
1066
  ...(bound ? { taskId: bound } : {}),
1044
1067
  forkOf: s.sessionId,
1045
1068
  });
1046
- const { child, cmd } = spawnDeckFork(bound, cwd, s.sessionId, newId);
1047
- child.on('error', () => setNote(`⚠ fork fallito (${DECK_RUN})`));
1048
- noteCommand(cmd);
1069
+ const spawned = spawnDeckFork(bound, cwd, s.sessionId, newId);
1070
+ spawned.child.on('error', () => setNote(`⚠ fork fallito (${DECK_RUN})`));
1071
+ noteSpawn(spawned);
1049
1072
  }
1050
1073
  }
1051
1074
  }
@@ -1122,9 +1145,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1122
1145
  // Minuscola = azione immediata (convenzione T39), gemella di `t`: entrambe
1123
1146
  // aprono una surface del cappello senza passare da un modale. `C` (create
1124
1147
  // task) resta distinta — stessa lettera, ma la maiuscola è per i modali.
1125
- const { child, cmd } = spawnClaudeEmpty(cwd);
1126
- child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
1127
- noteCommand(cmd);
1148
+ const spawned = spawnClaudeEmpty(cwd);
1149
+ spawned.child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
1150
+ noteSpawn(spawned);
1128
1151
  }
1129
1152
  else if (input === 'w') {
1130
1153
  // Salvataggio ESPLICITO: comporre una vista non tocca il disco, così
package/dist/spawn.js CHANGED
@@ -61,11 +61,62 @@ export function shellCommand(cmd, args) {
61
61
  // I quattro percorsi di spawn di una sessione Claude interattiva differiscono
62
62
  // SOLO per l'argv: stesso eseguibile, stesso detached, stesso `unref`. Il corpo
63
63
  // sta qui una volta sola, così anche il comando mostrato nasce in un punto solo.
64
+ //
65
+ // stdout in PIPE e non `ignore`: è il canale su cui `deck-run` annuncia il
66
+ // comando in-tab (vedi `onInTabCommand`). Il figlio resta detached — la pipe
67
+ // serve a leggere una riga, non a possedere il processo.
64
68
  function launchDeckRun(args, cwd) {
65
- const child = spawnOut(DECK_RUN, args, { cwd, detached: true, stdio: 'ignore' });
69
+ const child = spawnOut(DECK_RUN, args, {
70
+ cwd,
71
+ detached: true,
72
+ stdio: ['ignore', 'pipe', 'ignore'],
73
+ });
66
74
  child.unref();
67
75
  return { child, cmd: shellCommand(DECK_RUN, args) };
68
76
  }
77
+ /** Prefisso della riga con cui `deck-run` annuncia il comando in-tab. */
78
+ export const INTAB_MARKER = 'LOOM_DECK_INTAB ';
79
+ /**
80
+ * Il comando che gira DENTRO la tab: la sessione `claude` vera, con le env che
81
+ * la legano a task e progetto (`LOOM_TASK`, `PTYXIS_PROFILE`), il titolo, il
82
+ * permission mode e il prompt iniziale già quotato.
83
+ *
84
+ * Non si compone qui e non si può: lo compone `deck-run`, che è il primitive
85
+ * dove vivono catalogo dei prompt, quoting, permission mode, profilo di stato e
86
+ * titolo. Ricostruirlo nel deck sarebbe una seconda scrittura delle stesse
87
+ * regole, che diverge al primo flag aggiunto da una parte sola — quindi
88
+ * `deck-run` lo annuncia su stdout prima di `exec` e qui lo si legge.
89
+ *
90
+ * Arriva ASINCRONO, millisecondi dopo lo spawn: chi mostra il comando deve
91
+ * avere già scritto qualcosa (il comando di `deck-run`), perché l'annuncio può
92
+ * non arrivare affatto — spawn inerte (`LOOM_DECK_NO_SPAWN`, che non ha nemmeno
93
+ * uno stdout), validazione degli argomenti fallita, `deck-run` morto prima
94
+ * dell'exec.
95
+ *
96
+ * Dopo la riga attesa lo stdout si drena e basta: da lì in poi appartiene a
97
+ * `ptyxis`, che ha preso il posto del processo.
98
+ */
99
+ export function onInTabCommand(child, cb) {
100
+ if (!child.stdout)
101
+ return;
102
+ let buf = '';
103
+ let seen = false;
104
+ child.stdout.on('data', (chunk) => {
105
+ if (seen)
106
+ return;
107
+ buf += chunk.toString();
108
+ let nl;
109
+ while ((nl = buf.indexOf('\n')) >= 0) {
110
+ const line = buf.slice(0, nl);
111
+ buf = buf.slice(nl + 1);
112
+ if (line.startsWith(INTAB_MARKER)) {
113
+ seen = true;
114
+ cb(line.slice(INTAB_MARKER.length));
115
+ return;
116
+ }
117
+ }
118
+ });
119
+ }
69
120
  // L'ordine È il giro di `tab` nel detail, non una preferenza di lettura:
70
121
  // cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
71
122
  // anche il binding delle cifre `1`-`4`, passate poi al campo nota.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.41.0",
3
+ "version": "0.42.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
@@ -29,6 +29,12 @@
29
29
  # riprende RAMANDOLA (`--fork-session`): id nuovo, transcript copiato, l'origine
30
30
  # resta intatta e scrivibile.
31
31
  #
32
+ # Su stdout, prima di aprire la tab, stampa una riga
33
+ # LOOM_DECK_INTAB <comando>
34
+ # col comando che girerà DENTRO la tab (la sessione `claude` con le sue env).
35
+ # Serve a chi spawna deck-run e vuole mostrare l'invocazione vera senza
36
+ # ricomporla per conto proprio; le diagnostiche restano tutte su stderr.
37
+ #
32
38
  # Modalità spawn (LOOM_DECK_SPAWN_MODE):
33
39
  # inline → ptyxis --tab -- <cmd> (default) comando inline, LOOM_TASK diretta, zero dconf
34
40
  # profile → ptyxis --tab-with-profile=<UUID> riusa il profilo, riscrive il custom-command via dconf
@@ -440,6 +446,24 @@ else
440
446
  fi
441
447
  IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"
442
448
 
449
+ # ANNUNCIO del comando in-tab, su stdout, prima dell'exec.
450
+ #
451
+ # È l'unico modo che ha un chiamante di sapere cosa gira davvero dentro la tab:
452
+ # il comando si compone QUI — catalogo dei prompt, quoting, permission mode,
453
+ # profilo di stato, titolo — e ricomporlo dall'altro lato sarebbe una seconda
454
+ # scrittura delle stesse regole, divergente al primo flag aggiunto. Il deck lo
455
+ # legge da questa riga e la mostra nella propria riga di stato.
456
+ #
457
+ # Deve stare DOPO l'ultima assegnazione di IN_TAB_CMD e PRIMA dell'exec (che
458
+ # sostituisce il processo): annunciare un valore ancora modificabile
459
+ # annuncerebbe qualcosa di diverso da quello eseguito.
460
+ #
461
+ # stdout e non stderr: qui non c'è nessuna anomalia da segnalare, è l'esito
462
+ # normale del lavoro dello script. Le diagnostiche restano su stderr, e i
463
+ # chiamanti che leggono l'ULTIMA riga di stdout (lo shim `ptyxis` dei test)
464
+ # vedono comunque quello che vedevano prima.
465
+ printf 'LOOM_DECK_INTAB %s\n' "$IN_TAB_CMD"
466
+
443
467
  case "$MODE" in
444
468
  inline)
445
469
  # --tab: tab nella window attiva. -- <cmd>: comando inline nella tab.