@lamemind/loom-deck 0.39.0 → 0.41.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 +24 -6
- package/dist/cli.js +64 -27
- package/dist/overlays/sheet.js +1 -1
- package/dist/spawn.js +38 -28
- package/dist/width.js +50 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,22 @@ Entrambi i prefissi del contratto loom entrano in lista: **`T`** (code task) e *
|
|
|
26
26
|
Il deck è **UN processo Node**: spawna ma **non contiene** le sessioni CC — le possiede Ptyxis. Chiudere il deck non uccide le sessioni. La tab nasce nella window *attiva*
|
|
27
27
|
(quella col focus = il deck) → desktop isolation "gratis".
|
|
28
28
|
|
|
29
|
+
### La riga di stato dice il comando, non una parafrasi
|
|
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:
|
|
32
|
+
|
|
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'
|
|
35
|
+
```
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
+
|
|
43
|
+
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
|
+
|
|
29
45
|
## Stato
|
|
30
46
|
|
|
31
47
|
Bootstrap + spike ① + **TUI ③** funzionante (legge `tasks.md`, `⏎` apre il detail). Roadmap:
|
|
@@ -66,7 +82,7 @@ Assegnazioni correnti:
|
|
|
66
82
|
| modale | `F` | filtri |
|
|
67
83
|
| modale | `^F` | **ricerca full-text** nelle conversazioni del progetto |
|
|
68
84
|
| modale | `⏎` | **detail** della task selezionata: task file scrollabile + barra azioni |
|
|
69
|
-
| modale | `CANC` | **elimina** — la task selezionata
|
|
85
|
+
| modale | `CANC` | **elimina** — la task selezionata; su una riga meta della vista `archiviabili`, l'intero insieme |
|
|
70
86
|
| immediata | `^K` | spawna la task selezionata col prompt di **recap** |
|
|
71
87
|
| immediata | `^P` | spawna la task selezionata sul **preflight** |
|
|
72
88
|
| immediata | `^R` | spawna la task selezionata in **esecuzione** |
|
|
@@ -98,12 +114,14 @@ Le emoji sono quelle del menu compass. Per il terminale compass usa 🖥️, che
|
|
|
98
114
|
|
|
99
115
|
Il deck **ordina** la potatura, non la esegue: la fa `loom-works:clean-tasks`, invocato da un processo Claude headless. Nessun `git rm` e nessuna riscrittura di `tasks.md` vivono qui — quella sequenza (task file + folder dot-prefixed + riga in `tasks.md`, un commit atomico per task, symlink `current-task.md` rimosso, righe orfane riconciliate) è implementata una volta sola, e averne una seconda darebbe due rimozioni capaci di divergere.
|
|
100
116
|
|
|
101
|
-
Lo **stesso tasto ha due bersagli di taglia diversa**, e
|
|
117
|
+
Lo **stesso tasto ha due bersagli di taglia diversa**, e a discriminarli è la **selezione prima della vista**:
|
|
102
118
|
|
|
103
|
-
|
|
|
104
|
-
|
|
105
|
-
| `archiviabili` | **tutte** le task che la vista mostra |
|
|
106
|
-
| qualunque altra | la
|
|
119
|
+
| | riga meta (`≡ tutte` / `○ spot`) | riga task |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| vista `archiviabili` | **tutte** le task che la vista mostra | quella task |
|
|
122
|
+
| qualunque altra vista | rifiuto, con la nota che dice quale riga meta è | quella task |
|
|
123
|
+
|
|
124
|
+
Con una riga task sotto il caret il tasto tocca **sempre e solo quella task**. Il bulk richiede quindi di essere usciti da ogni riga task, cioè un movimento in più: l'azione di massa non è mai a un tasto di distanza da quella singola, e non la si preme guardando una task evidenziata credendo che il bersaglio sia lei. Il costo è che la regola non si legge dallo schermo — la legenda la dice (`CANC elimina` contro `CANC elimina tutte`), ed è il solo canale che la rende visibile prima della conferma.
|
|
107
125
|
|
|
108
126
|
Da qui la forma della conferma: nomina il bersaglio (quante e quali ID, troncando con `+N`) invece dell'azione, e dice l'**effetto sul disco** — mai «archiviare», che prometterebbe uno spostamento in un archivio che non esiste. I commit restano **locali**: `clean-tasks` non pusha, quindi finché nessuno lo fa un altro worktree continua a vedere le task potate.
|
|
109
127
|
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowI
|
|
|
10
10
|
import { cellWidth, launchLegend, loadArchivableDays, loadIdentity, loadLaunch, } from './config.js';
|
|
11
11
|
import { cycleSessionView, cycleTaskView, selectSessionRows, selectTasks, sessionView, taskView, TASK_VIEWS, } from './pane-views.js';
|
|
12
12
|
import { isCompact, layoutBudget, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
13
|
-
import { cut, sanitize, termWidth } from './width.js';
|
|
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
16
|
import { ALL, EDIT_PRI, EDIT_PROG, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, ROW_ALL, ROW_SPOT, SORT_TASTI, SPOT, } from './model.js';
|
|
@@ -123,6 +123,19 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
123
123
|
: selectTasks(tasks, taskViewId, { view, archivable }), [taskViewId, viewTasks, tasks, view, archivable]);
|
|
124
124
|
const isSpot = sel === SPOT;
|
|
125
125
|
const isAll = sel === ALL;
|
|
126
|
+
// T112 — quando `CANC` pota in BLOCCO invece della sola task selezionata.
|
|
127
|
+
//
|
|
128
|
+
// La SELEZIONE batte la vista, non il contrario: con una riga task sotto il
|
|
129
|
+
// caret il tasto tocca quella task e basta, in ogni vista. Il bulk vive solo
|
|
130
|
+
// sulle righe meta della vista `archiviabili`, cioè richiede di essere usciti
|
|
131
|
+
// da ogni riga task — così l'azione di massa non è mai a un tasto di distanza
|
|
132
|
+
// da quella singola, e non si preme guardando una task evidenziata credendo
|
|
133
|
+
// che il bersaglio sia lei.
|
|
134
|
+
//
|
|
135
|
+
// Derivato una volta e letto sia dal ramo di apertura sia dalla legenda: due
|
|
136
|
+
// condizioni scritte due volte direbbero due cose diverse sullo stesso tasto
|
|
137
|
+
// alla prima modifica.
|
|
138
|
+
const purgeBulk = taskViewId === 'archivable' && (isAll || isSpot);
|
|
126
139
|
const projectName = cwd.split('/').pop() || cwd;
|
|
127
140
|
// Unica fonte della selezione: si legge SEMPRE dalla vista, mai dall'array
|
|
128
141
|
// grezzo — è l'invariante che tiene allineati dettaglio mostrato e spawn.
|
|
@@ -260,6 +273,29 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
260
273
|
});
|
|
261
274
|
child.on('error', () => setNote(`⚠ create-task: '${CLAUDE_CMD}' non lanciabile`));
|
|
262
275
|
}
|
|
276
|
+
// La riga di stato di OGNI spawn di sessione Claude: il comando esatto, come
|
|
277
|
+
// lo si scriverebbe in bash, invece di una parafrasi.
|
|
278
|
+
//
|
|
279
|
+
// Non è un di più sulla nota descrittiva, la sostituisce: task, sessionId,
|
|
280
|
+
// prompt-kind, modello e nota del titolo sono già tutti argomenti del comando,
|
|
281
|
+
// quindi elencarli a parole li direbbe una seconda volta in una grafia che non
|
|
282
|
+
// si può ricopiare in un terminale. Ciò che va perso è il tasto premuto
|
|
283
|
+
// (`^K`, `⏎`, `f`), che è ciò che l'utente ha appena fatto e non ciò che il
|
|
284
|
+
// deck ha fatto per lui.
|
|
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.
|
|
292
|
+
function noteCommand(cmd) {
|
|
293
|
+
// 4 = bordo + padding della cornice esterna, 2 = il prompt `$ `. Sbagliare
|
|
294
|
+
// il budget non produce un errore visibile: la `truncate-end` di Ink taglia
|
|
295
|
+
// il resto dalla CODA, e il comando esce col mezzo eliso E la fine persa —
|
|
296
|
+
// cioè con entrambi i pezzi che l'elisione al mezzo voleva salvare.
|
|
297
|
+
setNote(`$ ${cutMiddle(cmd, Math.max(8, columns - 6))}`);
|
|
298
|
+
}
|
|
263
299
|
// T56 — apre una sessione bound alla task selezionata. Punto UNICO dei quattro
|
|
264
300
|
// tasti (⏎/^K/^P/^R): fra loro cambia solo il prompt iniziale, tutto il resto
|
|
265
301
|
// è identico — uuid pinnato, binding scritto PRIMA dello spawn (la sessione
|
|
@@ -302,25 +338,24 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
302
338
|
// il tempo di un tick la riga in lista comparirebbe nuda. Due record separati
|
|
303
339
|
// sullo stesso `sessionId` sono la forma normale di un file append-only
|
|
304
340
|
// last-wins, non una scrittura da fondere.
|
|
305
|
-
function spawnForTask(id, kind, model,
|
|
341
|
+
function spawnForTask(id, kind, model, spawnNote = '') {
|
|
306
342
|
const sid = randomUUID();
|
|
307
343
|
appendTaskBinding(cwd, sid, id);
|
|
308
344
|
if (spawnNote)
|
|
309
345
|
appendNote(cwd, sid, spawnNote);
|
|
310
|
-
const child = spawnDeck(id, cwd, sid, kind, model, spawnNote);
|
|
346
|
+
const { child, cmd } = spawnDeck(id, cwd, sid, kind, model, spawnNote);
|
|
311
347
|
child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
setNote(`${keyLabel} spawn ${id}${what} · ${model}${named} → tab CC (sid ${sid.slice(0, 8)})`);
|
|
348
|
+
// Il modello resta SEMPRE visibile anche quando è il default, perché è un
|
|
349
|
+
// argomento esplicito del comando (T108): gli acceleratori della lista non
|
|
350
|
+
// passano dal selettore del detail e usano il default fisso, quindi senza
|
|
351
|
+
// vederlo l'utente crederebbe di aver ereditato la scelta dell'ultimo
|
|
352
|
+
// detail aperto.
|
|
353
|
+
noteCommand(cmd);
|
|
319
354
|
}
|
|
320
355
|
function spawnTaskSession(kind, keyLabel) {
|
|
321
356
|
const task = selectedTaskOr(keyLabel, 'spawnare');
|
|
322
357
|
if (task)
|
|
323
|
-
spawnForTask(task.id, kind, MODEL_DEFAULT
|
|
358
|
+
spawnForTask(task.id, kind, MODEL_DEFAULT);
|
|
324
359
|
}
|
|
325
360
|
// T53 — apertura del modale nota sulla conversazione selezionata. Come
|
|
326
361
|
// `openEdit`, la bozza parte dal valore ATTUALE: annotare una seconda volta è
|
|
@@ -425,10 +460,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
425
460
|
setNote(ok ? `✔ ${task.id} → ${summary} · committato` : `⚠ ${task.id} salvato, commit fallito: ${err}`);
|
|
426
461
|
});
|
|
427
462
|
}
|
|
428
|
-
// T112 — apertura della conferma di eliminazione. Il tasto è uno
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
463
|
+
// T112 — apertura della conferma di eliminazione. Il tasto è uno e il
|
|
464
|
+
// bersaglio ha due taglie: la task selezionata, o l'insieme intero della vista
|
|
465
|
+
// `archiviabili`. A discriminare è `purgeBulk` — la selezione prima della
|
|
466
|
+
// vista. Il modale nomina comunque il bersaglio (quante e quali) e non
|
|
467
|
+
// l'azione: è l'unico punto in cui la differenza fra i due gesti è leggibile.
|
|
432
468
|
//
|
|
433
469
|
// Il bersaglio del bulk si legge da `paneTasks`, cioè dalla stessa fonte che
|
|
434
470
|
// disegna le righe e alimenta il contatore in header (D6): mai il `Set` grezzo
|
|
@@ -444,7 +480,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
444
480
|
setNote('CANC → eliminare: seleziona una task (← per il pane)');
|
|
445
481
|
return;
|
|
446
482
|
}
|
|
447
|
-
const bulk =
|
|
483
|
+
const bulk = purgeBulk;
|
|
448
484
|
const ids = bulk ? paneTasks.map((t) => t.id) : [];
|
|
449
485
|
if (!bulk) {
|
|
450
486
|
const task = selectedTaskOr('CANC', 'eliminare');
|
|
@@ -601,9 +637,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
601
637
|
// devono restare la stessa azione.
|
|
602
638
|
function resumeSession(sessionId) {
|
|
603
639
|
const bound = bindings.get(sessionId) ?? null;
|
|
604
|
-
const child = spawnDeckResume(bound, cwd, sessionId, sessionNotes.get(sessionId));
|
|
640
|
+
const { child, cmd } = spawnDeckResume(bound, cwd, sessionId, sessionNotes.get(sessionId));
|
|
605
641
|
child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
|
|
606
|
-
|
|
642
|
+
noteCommand(cmd);
|
|
607
643
|
}
|
|
608
644
|
// T57 — ⏎ nel modale: riscrive il binding nel sidecar e ricarica subito.
|
|
609
645
|
//
|
|
@@ -641,7 +677,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
641
677
|
columns,
|
|
642
678
|
setMode,
|
|
643
679
|
setNote,
|
|
644
|
-
onAction: (id, kind, model, spawnNote
|
|
680
|
+
onAction: (id, kind, model, spawnNote) => spawnForTask(id, kind, model, spawnNote),
|
|
645
681
|
});
|
|
646
682
|
const search = useSearchOverlay({
|
|
647
683
|
sessions,
|
|
@@ -1007,9 +1043,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1007
1043
|
...(bound ? { taskId: bound } : {}),
|
|
1008
1044
|
forkOf: s.sessionId,
|
|
1009
1045
|
});
|
|
1010
|
-
const child = spawnDeckFork(bound, cwd, s.sessionId, newId);
|
|
1046
|
+
const { child, cmd } = spawnDeckFork(bound, cwd, s.sessionId, newId);
|
|
1011
1047
|
child.on('error', () => setNote(`⚠ fork fallito (${DECK_RUN})`));
|
|
1012
|
-
|
|
1048
|
+
noteCommand(cmd);
|
|
1013
1049
|
}
|
|
1014
1050
|
}
|
|
1015
1051
|
}
|
|
@@ -1086,9 +1122,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1086
1122
|
// Minuscola = azione immediata (convenzione T39), gemella di `t`: entrambe
|
|
1087
1123
|
// aprono una surface del cappello senza passare da un modale. `C` (create
|
|
1088
1124
|
// task) resta distinta — stessa lettera, ma la maiuscola è per i modali.
|
|
1089
|
-
const child = spawnClaudeEmpty(cwd);
|
|
1125
|
+
const { child, cmd } = spawnClaudeEmpty(cwd);
|
|
1090
1126
|
child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
|
|
1091
|
-
|
|
1127
|
+
noteCommand(cmd);
|
|
1092
1128
|
}
|
|
1093
1129
|
else if (input === 'w') {
|
|
1094
1130
|
// Salvataggio ESPLICITO: comporre una vista non tocca il disco, così
|
|
@@ -1147,11 +1183,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1147
1183
|
// Nessuna voce di uscita: non esiste più un tasto che chiuda il deck.
|
|
1148
1184
|
const keyLegend = sanitize([
|
|
1149
1185
|
...(canSpawn ? ['⏎ detail', '^K/^P/^R spawn'] : canResume ? ['⏎ resume'] : []),
|
|
1150
|
-
// T112 — la voce nomina il BERSAGLIO, che cambia
|
|
1151
|
-
//
|
|
1152
|
-
// che
|
|
1186
|
+
// T112 — la voce nomina il BERSAGLIO, che cambia di taglia senza che
|
|
1187
|
+
// cambi il tasto. Legge `purgeBulk`, la stessa condizione del ramo di
|
|
1188
|
+
// apertura: una legenda che annunciasse «tutte» dove il tasto ne pota una
|
|
1189
|
+
// sola sarebbe peggio di nessuna legenda.
|
|
1153
1190
|
...(focus === 'tasks'
|
|
1154
|
-
? [
|
|
1191
|
+
? [purgeBulk ? 'CANC elimina tutte' : 'CANC elimina']
|
|
1155
1192
|
: []),
|
|
1156
1193
|
...(canResume ? ['f fork'] : []),
|
|
1157
1194
|
...(canPin ? ['p pin', 'N nota', 'A assegna'] : []),
|
package/dist/overlays/sheet.js
CHANGED
|
@@ -205,7 +205,7 @@ export function useSheetOverlay(deps) {
|
|
|
205
205
|
const note = spawnNote.trim();
|
|
206
206
|
close();
|
|
207
207
|
if (id)
|
|
208
|
-
onAction(id, act.kind, model, note
|
|
208
|
+
onAction(id, act.kind, model, note);
|
|
209
209
|
}
|
|
210
210
|
else if (key.tab) {
|
|
211
211
|
// T108 — `tab` scorre il catalogo dei modelli, e da T111 è il SOLO canale:
|
package/dist/spawn.js
CHANGED
|
@@ -32,6 +32,40 @@ export function spawnOut(cmd, args, opts) {
|
|
|
32
32
|
fake.unref = () => fake;
|
|
33
33
|
return fake;
|
|
34
34
|
}
|
|
35
|
+
// Caratteri che una shell POSIX passa attraverso senza interpretarli: tutto il
|
|
36
|
+
// resto (spazi, apici, `$`, glob, non-ASCII) va quotato o il comando ricopiato
|
|
37
|
+
// dalla riga di stato non farebbe la stessa cosa di quello eseguito.
|
|
38
|
+
const SHELL_SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
39
|
+
/**
|
|
40
|
+
* Un argomento come lo si scriverebbe in bash.
|
|
41
|
+
*
|
|
42
|
+
* Apici SINGOLI, non doppi: dentro i singoli nessun carattere resta speciale,
|
|
43
|
+
* quindi non c'è da enumerare cosa sfuggire. L'unico caso da chiudere è l'apice
|
|
44
|
+
* stesso, che si esce dalla stringa (`'\''`) invece di essere escapato dentro.
|
|
45
|
+
*
|
|
46
|
+
* Il deck non passa MAI da una shell — `spawn` senza `shell:true` consegna
|
|
47
|
+
* l'argv a execve così com'è. Questo quoting esiste per la sola RESA: rende la
|
|
48
|
+
* riga ricopiabile in un terminale senza cambiare di una virgola ciò che il
|
|
49
|
+
* deck ha davvero eseguito.
|
|
50
|
+
*/
|
|
51
|
+
export function shellQuote(arg) {
|
|
52
|
+
if (arg === '')
|
|
53
|
+
return "''";
|
|
54
|
+
if (SHELL_SAFE.test(arg))
|
|
55
|
+
return arg;
|
|
56
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
57
|
+
}
|
|
58
|
+
export function shellCommand(cmd, args) {
|
|
59
|
+
return [cmd, ...args].map(shellQuote).join(' ');
|
|
60
|
+
}
|
|
61
|
+
// I quattro percorsi di spawn di una sessione Claude interattiva differiscono
|
|
62
|
+
// SOLO per l'argv: stesso eseguibile, stesso detached, stesso `unref`. Il corpo
|
|
63
|
+
// sta qui una volta sola, così anche il comando mostrato nasce in un punto solo.
|
|
64
|
+
function launchDeckRun(args, cwd) {
|
|
65
|
+
const child = spawnOut(DECK_RUN, args, { cwd, detached: true, stdio: 'ignore' });
|
|
66
|
+
child.unref();
|
|
67
|
+
return { child, cmd: shellCommand(DECK_RUN, args) };
|
|
68
|
+
}
|
|
35
69
|
// L'ordine È il giro di `tab` nel detail, non una preferenza di lettura:
|
|
36
70
|
// cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
|
|
37
71
|
// anche il binding delle cifre `1`-`4`, passate poi al campo nota.
|
|
@@ -79,13 +113,7 @@ export function deckArgs(id, sessionId, kind, model = MODEL_DEFAULT, spawnNote)
|
|
|
79
113
|
return args;
|
|
80
114
|
}
|
|
81
115
|
export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT, spawnNote) {
|
|
82
|
-
|
|
83
|
-
cwd,
|
|
84
|
-
detached: true,
|
|
85
|
-
stdio: 'ignore',
|
|
86
|
-
});
|
|
87
|
-
child.unref();
|
|
88
|
-
return child;
|
|
116
|
+
return launchDeckRun(deckArgs(id, sessionId, kind, model, spawnNote), cwd);
|
|
89
117
|
}
|
|
90
118
|
// T49 — resume di una sessione esistente come nuova tab Ptyxis. Scoped (taskId
|
|
91
119
|
// presente) → `deck-run <task> --resume <sid>`: la ripresa eredita LOOM_TASK +
|
|
@@ -109,13 +137,7 @@ export function resumeArgs(taskId, sessionId, note) {
|
|
|
109
137
|
return args;
|
|
110
138
|
}
|
|
111
139
|
export function spawnDeckResume(taskId, cwd, sessionId, note) {
|
|
112
|
-
|
|
113
|
-
cwd,
|
|
114
|
-
detached: true,
|
|
115
|
-
stdio: 'ignore',
|
|
116
|
-
});
|
|
117
|
-
child.unref();
|
|
118
|
-
return child;
|
|
140
|
+
return launchDeckRun(resumeArgs(taskId, sessionId, note), cwd);
|
|
119
141
|
}
|
|
120
142
|
// T28 — FORK: `deck-run <task|--no-task> --resume <origine> --fork --session-id
|
|
121
143
|
// <nuovo>`. Variante del resume, non una terza forma: cambia solo che CC apre un
|
|
@@ -140,13 +162,7 @@ export function forkArgs(taskId, originId, newId) {
|
|
|
140
162
|
];
|
|
141
163
|
}
|
|
142
164
|
export function spawnDeckFork(taskId, cwd, originId, newId) {
|
|
143
|
-
|
|
144
|
-
cwd,
|
|
145
|
-
detached: true,
|
|
146
|
-
stdio: 'ignore',
|
|
147
|
-
});
|
|
148
|
-
child.unref();
|
|
149
|
-
return child;
|
|
165
|
+
return launchDeckRun(forkArgs(taskId, originId, newId), cwd);
|
|
150
166
|
}
|
|
151
167
|
// T42 — sessione Claude NUDA: nessuna task, nessun prompt iniziale, nessun
|
|
152
168
|
// sessionId pinnato (quindi nessuna entry nel sidecar session-tasks.jsonl: senza
|
|
@@ -155,13 +171,7 @@ export function spawnDeckFork(taskId, cwd, originId, newId) {
|
|
|
155
171
|
// sporcherebbe il percorso bound. Il titolo tab resta la label loom — lo mette
|
|
156
172
|
// deck-run, perché il match compass è window-level e non sa nulla di task.
|
|
157
173
|
export function spawnClaudeEmpty(cwd) {
|
|
158
|
-
|
|
159
|
-
cwd,
|
|
160
|
-
detached: true,
|
|
161
|
-
stdio: 'ignore',
|
|
162
|
-
});
|
|
163
|
-
child.unref();
|
|
164
|
-
return child;
|
|
174
|
+
return launchDeckRun(['--no-task'], cwd);
|
|
165
175
|
}
|
|
166
176
|
// T39/T32: voce `launch` custom del file config, eseguita con cwd = project root.
|
|
167
177
|
// Spawn detached come spawnDeck: il deck lancia ma non possiede il processo.
|
package/dist/width.js
CHANGED
|
@@ -328,6 +328,56 @@ export function pad(s, cols, align = 'left') {
|
|
|
328
328
|
const fill = ' '.repeat(Math.max(0, cols - termWidth(t)));
|
|
329
329
|
return align === 'right' ? fill + t : t + fill;
|
|
330
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Taglio a un budget di COLONNE che sacrifica il MEZZO invece della coda.
|
|
333
|
+
*
|
|
334
|
+
* Serve dove la testa e la coda portano informazione diversa e la TESTA è quella
|
|
335
|
+
* costante: il comando di uno spawn è sempre lo stesso path assoluto di
|
|
336
|
+
* `deck-run`, e quel che cambia da un'invocazione all'altra (task, sessionId,
|
|
337
|
+
* prompt-kind, modello, nota) sta tutto in fondo. Un `cut()` su quella riga
|
|
338
|
+
* mostrerebbe l'unica parte che non cambia mai.
|
|
339
|
+
*
|
|
340
|
+
* La coda prende due terzi del budget e la testa il resto: basta a riconoscere
|
|
341
|
+
* l'eseguibile senza mangiare gli argomenti. Testa e coda non possono
|
|
342
|
+
* sovrapporsi — insieme occupano al più `cols - 1` colonne, cioè meno della
|
|
343
|
+
* stringa intera (che qui è già più larga del budget).
|
|
344
|
+
*
|
|
345
|
+
* Niente collasso del whitespace (a differenza di `cut`): la stringa in ingresso
|
|
346
|
+
* è un comando, e due spazi dentro un argomento quotato sono suoi. Restano
|
|
347
|
+
* appiattiti i soli a-capo, che sfonderebbero la riga singola.
|
|
348
|
+
*/
|
|
349
|
+
export function cutMiddle(s, cols) {
|
|
350
|
+
const flat = s.replace(/[\r\n\t]+/g, ' ');
|
|
351
|
+
if (cols <= 0)
|
|
352
|
+
return '';
|
|
353
|
+
if (termWidth(flat) <= cols)
|
|
354
|
+
return flat;
|
|
355
|
+
if (cols === 1)
|
|
356
|
+
return '…';
|
|
357
|
+
const budget = cols - 1; // -1 = la colonna dell'ellissi, che qui c'è di sicuro
|
|
358
|
+
const headCols = budget - Math.ceil((budget * 2) / 3);
|
|
359
|
+
let head = '';
|
|
360
|
+
let hw = 0;
|
|
361
|
+
for (const ch of flat) {
|
|
362
|
+
const cw = termWidth(ch);
|
|
363
|
+
if (hw + cw > headCols)
|
|
364
|
+
break;
|
|
365
|
+
head += ch;
|
|
366
|
+
hw += cw;
|
|
367
|
+
}
|
|
368
|
+
const chars = [...flat];
|
|
369
|
+
let tail = '';
|
|
370
|
+
let tw = 0;
|
|
371
|
+
for (let i = chars.length - 1; i >= 0; i--) {
|
|
372
|
+
const ch = chars[i];
|
|
373
|
+
const cw = termWidth(ch);
|
|
374
|
+
if (hw + tw + cw > budget)
|
|
375
|
+
break;
|
|
376
|
+
tail = ch + tail;
|
|
377
|
+
tw += cw;
|
|
378
|
+
}
|
|
379
|
+
return head + '…' + tail;
|
|
380
|
+
}
|
|
331
381
|
/**
|
|
332
382
|
* Larghezza in colonne di UN carattere **dopo** `sanitize`.
|
|
333
383
|
*
|
package/package.json
CHANGED