@lamemind/loom-deck 0.41.0 → 0.43.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 +18 -7
- package/dist/cli.js +125 -28
- package/dist/model.js +7 -0
- package/dist/spawn.js +52 -1
- package/package.json +1 -1
- package/scripts/deck-run +24 -0
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
|
|
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
|
-
$
|
|
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`,
|
|
37
|
+
Task, `sessionId`, modello, titolo e prompt iniziale sono già lì 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
|
|
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
|
-
|
|
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
|
|
|
@@ -68,10 +76,13 @@ Regola unica, senza eccezioni — pensata per reggere l'aggiunta di nuove azioni
|
|
|
68
76
|
| minuscola | azione immediata, one-shot | |
|
|
69
77
|
| `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 |
|
|
70
78
|
| `1`…`9` | voce `launch` n-esima del progetto | da `.claude/loom-works.json` |
|
|
71
|
-
| `esc` | chiude l'overlay aperto | in modalità normale è inerte: **
|
|
79
|
+
| `esc` | chiude l'overlay aperto | in modalità normale è inerte: **nessuna lettera chiude il deck** |
|
|
80
|
+
| `^C` `^C` | chiude il deck | la prima pressione avverte in riga di stato, la seconda entro **5 secondi** esce; passata la finestra si riparte dall'avviso. Da dentro un modale il primo `^C` riporta alla lista — l'avviso vive nella riga di stato, che le schermate a pieno frame non disegnano |
|
|
72
81
|
|
|
73
82
|
`CTRL` è il terzo livello, aggiunto quando è arrivata la ricerca. Serve perché un modale con **campi di testo** mangia ogni lettera nuda: là dentro nessun comando può essere una lettera semplice. `CTRL+X` e `x` nudo condividono lo stesso `input` e si distinguono solo per `key.ctrl`, quindi in modalità normale il ramo `CTRL` è valutato **per primo** e chiude l'intera classe — senza, `CTRL+F` cadrebbe nel ramo `f` e forkerebbe una sessione invece di cercare.
|
|
74
83
|
|
|
84
|
+
`^C` è l'unica combo fuori da questo schema: non sta nel ramo `CTRL` di `normal` ma **sopra** il dispatch dei modi, perché non è un acceleratore — è la via d'uscita dal processo, e una via d'uscita che funziona in una schermata su undici non è una via d'uscita.
|
|
85
|
+
|
|
75
86
|
Assegnazioni correnti:
|
|
76
87
|
|
|
77
88
|
| | Tasto | Cosa fa |
|
|
@@ -101,7 +112,7 @@ t 💻 · c 🤖 · 1 📝 codium · 2 ☕ idea
|
|
|
101
112
|
|
|
102
113
|
- **tasti** — cosa puoi fare *qui e ora*. Solo le voci attive: quelle contestuali compaiono quando il pane a fuoco le rende possibili e altrimenti spariscono,
|
|
103
114
|
invece di mostrarsi inerti. Fuori navigazione (`↑↓` `←→`) e indicatore `focus:` — la prima è universale in qualunque TUI, il pane a fuoco si vede già dall'evidenziazione.
|
|
104
|
-
Nessuna voce di uscita
|
|
115
|
+
Nessuna voce di uscita: `^C` non ha bisogno di essere annunciato — è il tasto che si prova per primo, e la prima pressione risponde da sé con l'avviso.
|
|
105
116
|
- **surface** — *dove* puoi aprire qualcosa: prima le due built-in (`t`/`c`), poi le voci `launch` del progetto. Stanno insieme perché hanno la stessa natura — fire-once, `cwd` = project root, nessuno stato — e differiscono solo per essere universali invece che custom.
|
|
106
117
|
|
|
107
118
|
L'indice da solo è opaco (le `launch` sono custom per-progetto, senza una lettera fissa per app), quindi la riga espone la mappa e non il conteggio. Se non entrano in larghezza, si ferma a voci intere e mostra il contatore di quelle fuori riga — mai un troncamento silenzioso; le celle delle due built-in sono riservate a monte, o le voci sfonderebbero il box di quel tanto. Il cap a `9` è imposto dai tasti-cifra, non dallo schema: un progetto può dichiarare più di 9 voci, quelle oltre la nona sono configurate ma non raggiungibili (e la riga lo dice).
|
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
|
-
import { render, Box, Text, useInput } from 'ink';
|
|
4
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
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';
|
|
@@ -13,11 +13,11 @@ import { isCompact, layoutBudget, searchPreviewCapacity, windowRange, } from './
|
|
|
13
13
|
import { cut, cutMiddle, sanitize, termWidth } from './width.js';
|
|
14
14
|
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
15
15
|
import { initialDetail, writeTaskEdit, PRI_GLYPH, PRI_LABEL } from './task-edit.js';
|
|
16
|
-
import { ALL, EDIT_PRI, EDIT_PROG, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, ROW_ALL, ROW_SPOT, SORT_TASTI, SPOT, } from './model.js';
|
|
16
|
+
import { ALL, EDIT_PRI, EDIT_PROG, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, QUIT_WINDOW_MS, ROW_ALL, ROW_SPOT, SORT_TASTI, SPOT, } from './model.js';
|
|
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';
|
|
@@ -31,6 +31,12 @@ import { useSheetOverlay } from './overlays/sheet.js';
|
|
|
31
31
|
import { useAssignOverlay } from './overlays/assign.js';
|
|
32
32
|
import { captures } from './input-modes.js';
|
|
33
33
|
import { VERSION } from './version.js';
|
|
34
|
+
// T116 — l'avviso della prima pressione di `^C`. La durata è INTERPOLATA dalla
|
|
35
|
+
// finestra, non ricopiata: un numero scritto a mano in un testo che promette un
|
|
36
|
+
// comportamento diventa falso il giorno che la costante cambia, e nessuno strumento
|
|
37
|
+
// lo segnala. Vive a livello di modulo perché ha due lettori — il ramo che la
|
|
38
|
+
// scrive e il timer che la ritira, e quest'ultimo deve poterla riconoscere.
|
|
39
|
+
const QUIT_NOTE = `⚠ ^C di nuovo entro ${QUIT_WINDOW_MS / 1000}s per chiudere il deck`;
|
|
34
40
|
function Deck({ cwd, tasksPath, tasksDir }) {
|
|
35
41
|
const { tasks, loadError } = useTasks(tasksPath);
|
|
36
42
|
// `notes` esce dall'indice come `sessionNotes`: in questo componente `note` è
|
|
@@ -78,6 +84,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
78
84
|
const [editRow, setEditRow] = useState(0);
|
|
79
85
|
// T112 — bozza della conferma di eliminazione (null fuori dal modale).
|
|
80
86
|
const [purge, setPurge] = useState(null);
|
|
87
|
+
// T116 — uscita a doppio `^C`. Il timer È lo stato dell'armamento: finché il
|
|
88
|
+
// handle esiste la finestra è aperta, e non serve un secondo stato da tenere
|
|
89
|
+
// in fase con lui. In un `useRef` e non in `useState` perché il ramo di uscita
|
|
90
|
+
// lo legge NELLO STESSO tasto in cui potrebbe averlo scritto — un valore di
|
|
91
|
+
// stato React arriverebbe al render dopo, cioè troppo tardi per decidere.
|
|
92
|
+
const { exit } = useApp();
|
|
93
|
+
const quitTimer = useRef(null);
|
|
81
94
|
// Dimensioni vive del terminale: sono l'input del budget d'altezza sotto.
|
|
82
95
|
const { rows, columns } = useTerminalSize();
|
|
83
96
|
// Voci launch del progetto (T32): lette una volta, raggiunte per indice 1..9.
|
|
@@ -283,12 +296,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
283
296
|
// (`^K`, `⏎`, `f`), che è ciò che l'utente ha appena fatto e non ciò che il
|
|
284
297
|
// deck ha fatto per lui.
|
|
285
298
|
//
|
|
286
|
-
// Il taglio è al MEZZO (`cutMiddle`, non `cut`):
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
// stato porta anche messaggi normali, dove è la testa a
|
|
290
|
-
// che un resize successivo non ricalcola l'elisione — la
|
|
291
|
-
// `wrap="truncate-end"` resta come rete.
|
|
299
|
+
// Il taglio è al MEZZO (`cutMiddle`, non `cut`): in un comando di spawn è la
|
|
300
|
+
// coda a distinguere un'invocazione dall'altra, e un taglio dalla coda la
|
|
301
|
+
// butterebbe via per intero. Si taglia QUI, alla composizione, e non al
|
|
302
|
+
// render: la riga di stato porta anche messaggi normali, dove è la testa a
|
|
303
|
+
// contare. Ne discende che un resize successivo non ricalcola l'elisione — la
|
|
304
|
+
// nota è transitoria e `wrap="truncate-end"` resta come rete.
|
|
292
305
|
function noteCommand(cmd) {
|
|
293
306
|
// 4 = bordo + padding della cornice esterna, 2 = il prompt `$ `. Sbagliare
|
|
294
307
|
// il budget non produce un errore visibile: la `truncate-end` di Ink taglia
|
|
@@ -296,6 +309,29 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
296
309
|
// cioè con entrambi i pezzi che l'elisione al mezzo voleva salvare.
|
|
297
310
|
setNote(`$ ${cutMiddle(cmd, Math.max(8, columns - 6))}`);
|
|
298
311
|
}
|
|
312
|
+
// Le due note di uno spawn, in quest'ordine: il comando di `deck-run` subito,
|
|
313
|
+
// e appena arriva l'annuncio quello della sessione `claude` che gira DENTRO
|
|
314
|
+
// la tab — che è ciò che si vuole vedere davvero (`deck-run` è l'involucro,
|
|
315
|
+
// l'invocazione vera la compone lui).
|
|
316
|
+
//
|
|
317
|
+
// La prima non è un ripiego di stile: è l'unica che esiste quando l'annuncio
|
|
318
|
+
// non arriva (spawn inerte nei test, argomenti rifiutati, `deck-run` morto
|
|
319
|
+
// prima dell'exec), ed è anche il comando da ripetere a mano per vedere
|
|
320
|
+
// l'errore. Il salto fra le due dura i millisecondi che `deck-run` impiega a
|
|
321
|
+
// comporre.
|
|
322
|
+
const spawnSeq = useRef(0);
|
|
323
|
+
function noteSpawn(spawned) {
|
|
324
|
+
noteCommand(spawned.cmd);
|
|
325
|
+
const mine = ++spawnSeq.current;
|
|
326
|
+
onInTabCommand(spawned.child, (inTab) => {
|
|
327
|
+
// Due spawn ravvicinati: gli annunci sono asincroni e possono tornare
|
|
328
|
+
// fuori ordine, e senza guardia il più vecchio scriverebbe sopra il più
|
|
329
|
+
// recente — la riga di stato mostrerebbe un comando che non è l'ultimo
|
|
330
|
+
// partito.
|
|
331
|
+
if (spawnSeq.current === mine)
|
|
332
|
+
noteCommand(inTab);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
299
335
|
// T56 — apre una sessione bound alla task selezionata. Punto UNICO dei quattro
|
|
300
336
|
// tasti (⏎/^K/^P/^R): fra loro cambia solo il prompt iniziale, tutto il resto
|
|
301
337
|
// è identico — uuid pinnato, binding scritto PRIMA dello spawn (la sessione
|
|
@@ -343,14 +379,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
343
379
|
appendTaskBinding(cwd, sid, id);
|
|
344
380
|
if (spawnNote)
|
|
345
381
|
appendNote(cwd, sid, spawnNote);
|
|
346
|
-
const
|
|
347
|
-
child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
382
|
+
const spawned = spawnDeck(id, cwd, sid, kind, model, spawnNote);
|
|
383
|
+
spawned.child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
348
384
|
// Il modello resta SEMPRE visibile anche quando è il default, perché è un
|
|
349
385
|
// argomento esplicito del comando (T108): gli acceleratori della lista non
|
|
350
386
|
// passano dal selettore del detail e usano il default fisso, quindi senza
|
|
351
387
|
// vederlo l'utente crederebbe di aver ereditato la scelta dell'ultimo
|
|
352
388
|
// detail aperto.
|
|
353
|
-
|
|
389
|
+
noteSpawn(spawned);
|
|
354
390
|
}
|
|
355
391
|
function spawnTaskSession(kind, keyLabel) {
|
|
356
392
|
const task = selectedTaskOr(keyLabel, 'spawnare');
|
|
@@ -637,9 +673,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
637
673
|
// devono restare la stessa azione.
|
|
638
674
|
function resumeSession(sessionId) {
|
|
639
675
|
const bound = bindings.get(sessionId) ?? null;
|
|
640
|
-
const
|
|
641
|
-
child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
|
|
642
|
-
|
|
676
|
+
const spawned = spawnDeckResume(bound, cwd, sessionId, sessionNotes.get(sessionId));
|
|
677
|
+
spawned.child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
|
|
678
|
+
noteSpawn(spawned);
|
|
643
679
|
}
|
|
644
680
|
// T57 — ⏎ nel modale: riscrive il binding nel sidecar e ricarica subito.
|
|
645
681
|
//
|
|
@@ -865,6 +901,49 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
865
901
|
}
|
|
866
902
|
}
|
|
867
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* T116 — `^C`: la prima pressione ARMA, la seconda entro la finestra chiude.
|
|
906
|
+
*
|
|
907
|
+
* Prima di questa task `^C` usciva subito, e a chiuderlo era Ink stesso
|
|
908
|
+
* (`exitOnCtrlC`, di default vero) PRIMA che il tasto raggiungesse
|
|
909
|
+
* `useInput`. Spegnere quell'opzione è la condizione perché il ramo esista —
|
|
910
|
+
* ma spegnerla e basta lascerebbe il deck senza NESSUNA uscita da tastiera:
|
|
911
|
+
* il ramo qui sotto è ciò che restituisce quella via, e i due cambi non si
|
|
912
|
+
* possono separare.
|
|
913
|
+
*
|
|
914
|
+
* La prima pressione RIPORTA IN `normal` quando si è dentro un modo
|
|
915
|
+
* capturing. L'avviso vive nella riga di stato, e le tre schermate
|
|
916
|
+
* sostitutive (detail, ricerca, reader) prendono l'intero frame senza
|
|
917
|
+
* renderizzarla: armare restando lì dentro darebbe un deck che sembra
|
|
918
|
+
* ignorare il tasto e che alla pressione successiva sparisce senza aver mai
|
|
919
|
+
* avvertito. Chiudere lo strato è anche coerente con `esc` — `^C` dice
|
|
920
|
+
* «voglio uscire», e il primo passo fuori è la lista.
|
|
921
|
+
*/
|
|
922
|
+
function onQuitKey() {
|
|
923
|
+
if (quitTimer.current) {
|
|
924
|
+
clearTimeout(quitTimer.current);
|
|
925
|
+
quitTimer.current = null;
|
|
926
|
+
exit();
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (captures(mode))
|
|
930
|
+
setMode('normal');
|
|
931
|
+
setNote(QUIT_NOTE);
|
|
932
|
+
quitTimer.current = setTimeout(() => {
|
|
933
|
+
quitTimer.current = null;
|
|
934
|
+
// Solo se l'avviso è ANCORA il proprio: nei 5 secondi una qualunque altra
|
|
935
|
+
// azione può aver scritto in riga di stato, e cancellare quella nota
|
|
936
|
+
// significherebbe far sparire il feedback di un'azione che non c'entra.
|
|
937
|
+
setNote((n) => (n === QUIT_NOTE ? '' : n));
|
|
938
|
+
}, QUIT_WINDOW_MS);
|
|
939
|
+
}
|
|
940
|
+
// Il timer pendente è un handle vivo del loop di Node: lasciarlo appeso allo
|
|
941
|
+
// smontaggio terrebbe il processo in piedi fino allo scadere della finestra,
|
|
942
|
+
// con lo schermo già restituito al terminale.
|
|
943
|
+
useEffect(() => () => {
|
|
944
|
+
if (quitTimer.current)
|
|
945
|
+
clearTimeout(quitTimer.current);
|
|
946
|
+
}, []);
|
|
868
947
|
// Il dispatch consulta il CATALOGO (`input-modes.ts`), non l'ordine di una
|
|
869
948
|
// catena di `if`. `MODE_KEYS` è il custode: essendo un `Record` su
|
|
870
949
|
// `CapturingMode`, un modo nuovo senza handler non compila.
|
|
@@ -881,6 +960,17 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
881
960
|
purge: onPurgeKey,
|
|
882
961
|
};
|
|
883
962
|
useInput((input, key) => {
|
|
963
|
+
// T116 — `^C` sta SOPRA il dispatch dei modi, unico tasto a scavalcarlo.
|
|
964
|
+
// Ogni altra combo `ctrl` è un acceleratore, e un acceleratore dentro un
|
|
965
|
+
// modo capturing dev'essere inerte (`input-modes.ts`); questo non è un
|
|
966
|
+
// acceleratore ma la via d'uscita dal processo, e una via d'uscita che
|
|
967
|
+
// funziona solo in una schermata su undici non è una via d'uscita. Non è
|
|
968
|
+
// quindi una deroga di `CTRL_DEROGATIONS` — quelle vivono DENTRO un modo e
|
|
969
|
+
// le gestisce il modo, questa li precede tutti.
|
|
970
|
+
if (key.ctrl && input === 'c') {
|
|
971
|
+
onQuitKey();
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
884
974
|
// Un modo capturing consuma TUTTO — acceleratori globali compresi — e le
|
|
885
975
|
// deroghe se le gestisce da sé (`CTRL_DEROGATIONS`). Da qui in giù si è
|
|
886
976
|
// quindi in `normal`, l'unico modo che cede ai `key.ctrl`.
|
|
@@ -1043,9 +1133,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1043
1133
|
...(bound ? { taskId: bound } : {}),
|
|
1044
1134
|
forkOf: s.sessionId,
|
|
1045
1135
|
});
|
|
1046
|
-
const
|
|
1047
|
-
child.on('error', () => setNote(`⚠ fork fallito (${DECK_RUN})`));
|
|
1048
|
-
|
|
1136
|
+
const spawned = spawnDeckFork(bound, cwd, s.sessionId, newId);
|
|
1137
|
+
spawned.child.on('error', () => setNote(`⚠ fork fallito (${DECK_RUN})`));
|
|
1138
|
+
noteSpawn(spawned);
|
|
1049
1139
|
}
|
|
1050
1140
|
}
|
|
1051
1141
|
}
|
|
@@ -1122,9 +1212,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1122
1212
|
// Minuscola = azione immediata (convenzione T39), gemella di `t`: entrambe
|
|
1123
1213
|
// aprono una surface del cappello senza passare da un modale. `C` (create
|
|
1124
1214
|
// task) resta distinta — stessa lettera, ma la maiuscola è per i modali.
|
|
1125
|
-
const
|
|
1126
|
-
child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
|
|
1127
|
-
|
|
1215
|
+
const spawned = spawnClaudeEmpty(cwd);
|
|
1216
|
+
spawned.child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
|
|
1217
|
+
noteSpawn(spawned);
|
|
1128
1218
|
}
|
|
1129
1219
|
else if (input === 'w') {
|
|
1130
1220
|
// Salvataggio ESPLICITO: comporre una vista non tocca il disco, così
|
|
@@ -1148,11 +1238,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1148
1238
|
setNote(`${input} → ${entry.label} su ${projectName}`);
|
|
1149
1239
|
}
|
|
1150
1240
|
}
|
|
1151
|
-
// Nessun tasto di uscita:
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1241
|
+
// Nessun tasto NUDO di uscita: `q` resta libera per un binding futuro, e
|
|
1242
|
+
// `esc` in modalità normale è inerte — dentro un overlay continua a
|
|
1243
|
+
// chiuderlo, perché quel ramo esce prima di qui. L'unica uscita da tastiera
|
|
1244
|
+
// è `^C` battuto due volte (T116, in testa a questo handler); resta poi la
|
|
1245
|
+
// chiusura della tab Ptyxis che ospita il processo.
|
|
1156
1246
|
});
|
|
1157
1247
|
const canSpawn = focus === 'tasks' && selTask !== null;
|
|
1158
1248
|
const canResume = focus === 'sessions' && selSessionObj !== null;
|
|
@@ -1325,4 +1415,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1325
1415
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: ["v", VERSION] })] }), 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 === 'purge' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \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' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${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, mode === 'purge' && purge ? _jsx(PurgeModal, { draft: purge, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, counts: taskCounts, activeView: taskViewId, paneCount: paneTasks.length, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, focused: focus === 'tasks', loadError: loadError, windowStart: taskWin.start, above: taskWin.start, below: paneTasks.length - taskWin.end, columns: columns, dirty: dirtyFolders }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, counts: sessionCounts, activeView: sessionViewId, paneCount: sessionRows.length, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore, live: live })] }), budget.preview && previewKind === 'task' && detail ? (_jsx(PreviewPane, { kind: "task", detail: detail, maxLines: budget.detailLines, columns: columns })) : budget.preview && previewKind === 'session' && selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: selSessionObj, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, origin: forkOf.get(selSessionObj.sessionId) ?? null, note: sessionNotes.get(selSessionObj.sessionId) ?? '', live: live.get(selSessionObj.sessionId) ?? null })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1326
1416
|
}
|
|
1327
1417
|
const cwd = process.cwd();
|
|
1328
|
-
|
|
1418
|
+
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
|
1419
|
+
// il tasto non arriverebbe mai a `useInput` e il doppio colpo non sarebbe
|
|
1420
|
+
// osservabile. In raw mode `^C` non è un SIGINT — è il byte `\x03` nello
|
|
1421
|
+
// stream di input, e chi lo consuma per primo decide. Da qui in poi lo consuma
|
|
1422
|
+
// il deck, quindi l'uscita è tutta a carico del suo handler.
|
|
1423
|
+
render(_jsx(Deck, { cwd: cwd, tasksPath: resolveTasksPath(cwd), tasksDir: resolveTasksDir(cwd) }), {
|
|
1424
|
+
exitOnCtrlC: false,
|
|
1425
|
+
});
|
package/dist/model.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export const POLL_MS = 1500;
|
|
2
|
+
// Finestra entro cui la SECONDA pressione di `^C` chiude il deck. Scaduta, la
|
|
3
|
+
// prima pressione è dimenticata e la successiva riparte dall'avviso: un `^C`
|
|
4
|
+
// battuto per abitudine mezzo minuto prima non deve poter uscire a insaputa di
|
|
5
|
+
// chi preme. Il valore è anche il tempo per cui l'avviso resta in riga di stato
|
|
6
|
+
// — armamento e messaggio scadono insieme, o la riga prometterebbe un'uscita
|
|
7
|
+
// che il tasto non fa più.
|
|
8
|
+
export const QUIT_WINDOW_MS = 5000;
|
|
2
9
|
// Cap del pane sessioni: le più recenti (ts desc), le altre restano nell'indice
|
|
3
10
|
// ma fuori vista. Non-silenzioso → l'header mostra quante sono nascoste.
|
|
4
11
|
export const MAX_SESSIONS = 30;
|
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, {
|
|
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
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.
|