@lamemind/loom-deck 0.9.2 → 0.11.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 +1 -1
- package/dist/cli.js +67 -5
- package/dist/sessions.js +35 -7
- package/dist/viewport.js +52 -3
- package/package.json +1 -1
- package/scripts/deck-run +30 -3
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ senza collisioni:
|
|
|
48
48
|
|---|---|---|
|
|
49
49
|
| `↑` `↓` | naviga nella lista | |
|
|
50
50
|
| `←` `→` `tab` | cambia pane | |
|
|
51
|
-
| `⏎` | azione primaria del pane | Tasks → spawna la task selezionata |
|
|
51
|
+
| `⏎` | azione primaria del pane | Tasks → spawna la task selezionata · Sessions → riprende (`claude --resume`) la sessione selezionata |
|
|
52
52
|
| **MAIUSCOLA** | **apre un modale** | cattura tutti i tasti; `esc` annulla, non esce |
|
|
53
53
|
| minuscola | azione immediata, one-shot | |
|
|
54
54
|
| `1`…`9` | voce `launch` n-esima del progetto | da `.claude/loom-works.json` |
|
package/dist/cli.js
CHANGED
|
@@ -51,6 +51,18 @@ function spawnDeck(id, cwd, sessionId) {
|
|
|
51
51
|
child.unref();
|
|
52
52
|
return child;
|
|
53
53
|
}
|
|
54
|
+
// T49 — resume di una sessione esistente come nuova tab Ptyxis. Scoped (taskId
|
|
55
|
+
// presente) → `deck-run <task> --resume <sid>`: la ripresa eredita LOOM_TASK +
|
|
56
|
+
// titolo `· <task>` (D2 preflight, l'hook SessionStart ricarica il contesto
|
|
57
|
+
// task). Spot → `--no-task --resume`: resume nudo, solo label progetto. Nessun
|
|
58
|
+
// prompt iniziale in entrambi i casi: riprendere una conversazione significa
|
|
59
|
+
// continuarla, non iniettarle un messaggio (lo salta deck-run).
|
|
60
|
+
function spawnDeckResume(taskId, cwd, sessionId) {
|
|
61
|
+
const args = taskId ? [taskId, '--resume', sessionId] : ['--no-task', '--resume', sessionId];
|
|
62
|
+
const child = spawn(DECK_RUN, args, { cwd, detached: true, stdio: 'ignore' });
|
|
63
|
+
child.unref();
|
|
64
|
+
return child;
|
|
65
|
+
}
|
|
54
66
|
// T42 — sessione Claude NUDA: nessuna task, nessun prompt iniziale, nessun
|
|
55
67
|
// sessionId pinnato (quindi nessuna entry nel sidecar session-tasks.jsonl: senza
|
|
56
68
|
// task non c'è nulla da legare). Funzione separata e non un parametro opzionale
|
|
@@ -302,6 +314,21 @@ const CARET_OFF = ' ';
|
|
|
302
314
|
function displayProg(prog) {
|
|
303
315
|
return forceEmojiWidth(prog.replace(/✔️?/g, '✅'));
|
|
304
316
|
}
|
|
317
|
+
// T49 — size umana compatta per il detail pane sessione.
|
|
318
|
+
function fmtSize(bytes) {
|
|
319
|
+
if (bytes < 1024)
|
|
320
|
+
return `${bytes} B`;
|
|
321
|
+
if (bytes < 1024 * 1024)
|
|
322
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
323
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
324
|
+
}
|
|
325
|
+
// T49 — ultima attività ESTESA (giorno/mese ora:minuti) per il detail pane;
|
|
326
|
+
// nella riga di lista resta il relTime compatto.
|
|
327
|
+
function fmtDateTime(ts) {
|
|
328
|
+
const d = new Date(ts);
|
|
329
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
330
|
+
return `${p(d.getDate())}/${p(d.getMonth() + 1)} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
331
|
+
}
|
|
305
332
|
// Età relativa compatta (ms epoch → "2m"/"3h"/"5d") per il preview sessioni.
|
|
306
333
|
function relTime(ts) {
|
|
307
334
|
const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
@@ -640,7 +667,18 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
640
667
|
}
|
|
641
668
|
}
|
|
642
669
|
else {
|
|
643
|
-
|
|
670
|
+
// T49 — ⏎ su una sessione = resume in nuova tab. Il binding si rilegge
|
|
671
|
+
// dal sidecar (non dal padre selezionato): vale anche per le spot.
|
|
672
|
+
const s = visibleSessions[selSession];
|
|
673
|
+
if (!s) {
|
|
674
|
+
setNote('nessuna sessione da riprendere');
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
const bound = bindings.get(s.sessionId) ?? null;
|
|
678
|
+
const child = spawnDeckResume(bound, cwd, s.sessionId);
|
|
679
|
+
child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
|
|
680
|
+
setNote(`⏎ resume ${s.sessionId.slice(0, 8)} → tab CC${bound ? ` (${bound})` : ' (spot)'}`);
|
|
681
|
+
}
|
|
644
682
|
}
|
|
645
683
|
}
|
|
646
684
|
else if (input === 'C') {
|
|
@@ -704,9 +742,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
704
742
|
exit();
|
|
705
743
|
}
|
|
706
744
|
});
|
|
707
|
-
const
|
|
745
|
+
const selSessionObj = visibleSessions[selSession] ?? null;
|
|
746
|
+
const selSessionId = selSessionObj?.sessionId;
|
|
708
747
|
const parentLabel = isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
709
748
|
const canSpawn = focus === 'tasks' && !isSpot;
|
|
749
|
+
const canResume = focus === 'sessions' && selSessionObj !== null;
|
|
710
750
|
// Larghezza dal medesimo hook che dà l'altezza: dopo un resize la legenda si
|
|
711
751
|
// ricalcola con lo stesso re-render che ridimensiona i pane.
|
|
712
752
|
const legend = launchLegend(launch, columns);
|
|
@@ -725,6 +765,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
725
765
|
noteLine: Boolean(note),
|
|
726
766
|
hasDetail: Boolean(detail),
|
|
727
767
|
detailMetaLines: detailParts?.metaLines ?? 0,
|
|
768
|
+
// T49 — il detail pane sessione esiste solo con il focus sul pane: è
|
|
769
|
+
// l'hover, non uno stato persistente; navigando le task non ruba righe.
|
|
770
|
+
hasSessionDetail: canResume,
|
|
771
|
+
// Riservo righe di preview solo per i blocchi che davvero renderizzano: il
|
|
772
|
+
// primo prompt aggiunge info solo con un titolo custom (senza, titolo ===
|
|
773
|
+
// primo prompt); l'ultima risposta solo se il modello ha già risposto.
|
|
774
|
+
sessionHasFirstPreview: canResume && Boolean(selSessionObj?.customTitle),
|
|
775
|
+
sessionHasLastPreview: canResume && Boolean(selSessionObj?.lastReply),
|
|
728
776
|
});
|
|
729
777
|
// Finestre di rendering. Le liste "logiche" (viewTasks, visibleSessions)
|
|
730
778
|
// restano intere: navigazione, selezione e spawn continuano a ragionare su
|
|
@@ -739,7 +787,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
739
787
|
if (budget.compact) {
|
|
740
788
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? 'spot', " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
741
789
|
}
|
|
742
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === '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 \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7 \u23CE ", canSpawn ? 'spawn' : '—', " \u00B7 C nuova \u00B7 E edit \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7 t term \u00B7 c claude \u00B7 q esci \u00B7 focus:
|
|
790
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === '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 \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7 \u23CE ", canSpawn ? 'spawn' : canResume ? 'resume' : '—', " \u00B7 C nuova \u00B7 E edit \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7 t term \u00B7 c claude \u00B7 q esci \u00B7 focus:", ' ', _jsx(Text, { color: "cyan", children: focus })] })), mode === 'normal' && launch.length > 0 ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["launch ", legend.shown, legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === '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 })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, sessions: windowSessions, total: childSessions.length, hidden: hiddenSessions, selectedId: selSessionId, focused: focus === 'sessions', above: sessionWin.start, below: visibleSessions.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: normalizeEmoji(note) }) : null] }));
|
|
743
791
|
}
|
|
744
792
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
745
793
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -784,11 +832,25 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
784
832
|
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, task.id, " ", forceEmojiWidth(task.pri), " ", displayProg(task.prog), " ", task.desc, n > 0 ? ` (${n})` : ''] }, task.id));
|
|
785
833
|
})), detail && detailLines > 0 ? (_jsx(DetailPane, { detail: detail, maxLines: detailLines, columns: columns })) : null] }));
|
|
786
834
|
}
|
|
787
|
-
function SessionsPane({ parentLabel, isSpot, sessions, total, hidden, selectedId, focused, above, below, }) {
|
|
835
|
+
function SessionsPane({ parentLabel, isSpot, sessions, total, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, }) {
|
|
788
836
|
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: ["Sessions \u00B7 ", parentLabel, " (", total, ")", hidden > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 +", hidden, " pi\u00F9 vecchie"] }) : null, above > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2191", above] }) : null, below > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2193", below] }) : null] }), total === 0 ? (_jsx(Text, { color: "yellow", wrap: "truncate-end", children: isSpot ? 'nessuna sessione libera' : 'nessuna sessione legata a questa task' })) : (sessions.map((s) => {
|
|
789
837
|
const sel = s.sessionId === selectedId;
|
|
790
838
|
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isSpot ? _jsx(Text, { dimColor: true, children: "\u25CB" }) : _jsx(Text, { color: "green", children: "\uD83D\uDD17" }), ' ', truncate(s.title, 44), ' ', _jsxs(Text, { dimColor: true, children: ["\u00B7 ", s.gitBranch || '-', " \u00B7 ", relTime(s.ts)] })] }, s.sessionId));
|
|
791
|
-
}))] }));
|
|
839
|
+
})), detail ? (_jsx(SessionDetailPane, { s: detail, firstLines: firstLines, lastLines: lastLines, columns: columns })) : null] }));
|
|
840
|
+
}
|
|
841
|
+
// T49 — detail pane della sessione selezionata (hover), gemello del DetailPane
|
|
842
|
+
// task. Tutti i campi vengono dal parse già cached dell'adapter (mtime-keyed):
|
|
843
|
+
// il pannello non costa I/O al movimento di selezione. Mostra "da dove parte,
|
|
844
|
+
// dove è arrivata": il primo prompt utente (`» `) e l'ultima risposta del
|
|
845
|
+
// modello (`« `). La preview del primo prompt compare SOLO con un titolo custom
|
|
846
|
+
// — senza, il titolo È già il primo prompt e la riga lo duplicherebbe (D4
|
|
847
|
+
// preflight). Le righe rese non superano mai il riservato dal budget
|
|
848
|
+
// (`firstLines`/`lastLines`); renderne meno è sicuro (frame più corto).
|
|
849
|
+
function SessionDetailPane({ s, firstLines, lastLines, columns, }) {
|
|
850
|
+
const width = detailTextWidth(columns);
|
|
851
|
+
const first = s.customTitle && firstLines > 0 ? wrapLines(s.firstPrompt, width, firstLines) : [];
|
|
852
|
+
const last = s.lastReply && lastLines > 0 ? wrapLines(s.lastReply, width, lastLines) : [];
|
|
853
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: s.title }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts)] }), first.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '» ' : ' ', line] }, `f${i}`))), last.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '« ' : ' ', line] }, `l${i}`)))] }));
|
|
792
854
|
}
|
|
793
855
|
/**
|
|
794
856
|
* Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
|
package/dist/sessions.js
CHANGED
|
@@ -20,7 +20,12 @@ function cleanPreview(s) {
|
|
|
20
20
|
.replace(/\s+/g, ' ')
|
|
21
21
|
.trim();
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
// Estrae il primo blocco di testo dal `message` di un record. Funziona sia sui
|
|
24
|
+
// record `type:user` sia sugli `type:assistant`: entrambi portano `content` come
|
|
25
|
+
// stringa o array di blocchi, e un blocco di testo è `{type:'text', text}`. Il
|
|
26
|
+
// nome è generico apposta — l'ultima risposta del modello (T49 first+last) esce
|
|
27
|
+
// dallo stesso estrattore del primo prompt utente.
|
|
28
|
+
function extractText(message) {
|
|
24
29
|
if (!message || typeof message !== 'object')
|
|
25
30
|
return '';
|
|
26
31
|
const content = message.content;
|
|
@@ -44,7 +49,7 @@ function extractUserText(message) {
|
|
|
44
49
|
// poll-a resta economico anche con decine di sessioni multi-MB. Lo stato vive
|
|
45
50
|
// dentro l'adapter così l'invariante "unico modulo che tocca lo store" regge.
|
|
46
51
|
const cache = new Map();
|
|
47
|
-
function parseSessionFile(path, mtime) {
|
|
52
|
+
function parseSessionFile(path, mtime, sizeBytes) {
|
|
48
53
|
let content;
|
|
49
54
|
try {
|
|
50
55
|
content = readFileSync(path, 'utf8');
|
|
@@ -58,6 +63,8 @@ function parseSessionFile(path, mtime) {
|
|
|
58
63
|
let parentUuid = null;
|
|
59
64
|
let customTitle = '';
|
|
60
65
|
let firstUserText = '';
|
|
66
|
+
let lastAssistantText = '';
|
|
67
|
+
let turns = 0;
|
|
61
68
|
for (const line of content.split('\n')) {
|
|
62
69
|
if (!line.trim())
|
|
63
70
|
continue;
|
|
@@ -79,10 +86,23 @@ function parseSessionFile(path, mtime) {
|
|
|
79
86
|
}
|
|
80
87
|
if (typeof d.customTitle === 'string' && d.customTitle)
|
|
81
88
|
customTitle = d.customTitle; // last-wins
|
|
82
|
-
if (
|
|
83
|
-
|
|
89
|
+
if (d.type === 'user') {
|
|
90
|
+
// T49: turno = prompt umano. I tool_result viaggiano anch'essi come
|
|
91
|
+
// type:user ma senza blocchi text → extractText '' li esclude.
|
|
92
|
+
const t = extractText(d.message);
|
|
93
|
+
if (t) {
|
|
94
|
+
turns++;
|
|
95
|
+
if (!firstUserText)
|
|
96
|
+
firstUserText = t;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (d.type === 'assistant') {
|
|
100
|
+
// Ultima risposta del modello: last-wins (come customTitle), niente
|
|
101
|
+
// early-stop — l'ultima riga assistant con testo è quella buona. I
|
|
102
|
+
// record assistant di solo tool_use danno '' e non sovrascrivono.
|
|
103
|
+
const t = extractText(d.message);
|
|
84
104
|
if (t)
|
|
85
|
-
|
|
105
|
+
lastAssistantText = t;
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
if (!cwd)
|
|
@@ -95,6 +115,11 @@ function parseSessionFile(path, mtime) {
|
|
|
95
115
|
title: normalizeEmoji(customTitle || firstUserText || '(senza titolo)'),
|
|
96
116
|
ts: mtime,
|
|
97
117
|
path,
|
|
118
|
+
sizeBytes,
|
|
119
|
+
turns,
|
|
120
|
+
customTitle,
|
|
121
|
+
firstPrompt: firstUserText,
|
|
122
|
+
lastReply: lastAssistantText,
|
|
98
123
|
};
|
|
99
124
|
}
|
|
100
125
|
// Discovery read-only delle sessioni del SOLO progetto corrente (D2 preflight
|
|
@@ -115,8 +140,11 @@ export function discoverProjectSessions(projectRoot) {
|
|
|
115
140
|
const path = join(dir, f);
|
|
116
141
|
seen.add(path);
|
|
117
142
|
let mtime;
|
|
143
|
+
let sizeBytes;
|
|
118
144
|
try {
|
|
119
|
-
|
|
145
|
+
const st = statSync(path);
|
|
146
|
+
mtime = st.mtimeMs;
|
|
147
|
+
sizeBytes = st.size;
|
|
120
148
|
}
|
|
121
149
|
catch {
|
|
122
150
|
continue;
|
|
@@ -127,7 +155,7 @@ export function discoverProjectSessions(projectRoot) {
|
|
|
127
155
|
session = cached.session;
|
|
128
156
|
}
|
|
129
157
|
else {
|
|
130
|
-
session = parseSessionFile(path, mtime);
|
|
158
|
+
session = parseSessionFile(path, mtime, sizeBytes);
|
|
131
159
|
cache.set(path, { mtime, session });
|
|
132
160
|
}
|
|
133
161
|
if (session && (session.cwd === projectRoot || session.cwd.startsWith(projectRoot + '/'))) {
|
package/dist/viewport.js
CHANGED
|
@@ -62,6 +62,14 @@ const MAX_DETAIL_LINES = 4;
|
|
|
62
62
|
const TASKS_PANE_CHROME = 5; // 2 bordi + header "Tasks (n)" + riga sort + riga spot
|
|
63
63
|
const SESSIONS_PANE_CHROME = 3; // 2 bordi + header "Sessions · …"
|
|
64
64
|
const DETAIL_CHROME = 3; // marginTop + 2 bordi
|
|
65
|
+
/** Detail pane sessione (T49): righe fisse = titolo + riga meta (size · turni ·
|
|
66
|
+
* ultima attività). Le preview (primo prompt + ultima risposta) sono variabili,
|
|
67
|
+
* ciascuna al più MAX_SESSION_PREVIEW righe. */
|
|
68
|
+
const SESSION_DETAIL_FIXED = 2;
|
|
69
|
+
const MAX_SESSION_PREVIEW = 2;
|
|
70
|
+
/** Gemello di MIN_TASK_ROWS: sotto questa soglia la lista sessioni non serve
|
|
71
|
+
* più — meglio sacrificare il detail pane. */
|
|
72
|
+
const MIN_SESSION_ROWS = 3;
|
|
65
73
|
/** Altezza di ciascuna modale, marginTop incluso. In flusso, non in overlay:
|
|
66
74
|
* spingono giù i pane, quindi il loro costo va scalato dal budget. */
|
|
67
75
|
export const MODAL_HEIGHT = {
|
|
@@ -96,9 +104,42 @@ export function layoutBudget(input) {
|
|
|
96
104
|
// regolamentare con zero task dentro — occupa 5 righe per non mostrare nulla,
|
|
97
105
|
// mentre la riga compatta dice le stesse cose in una.
|
|
98
106
|
if (avail < TASKS_PANE_CHROME + 1) {
|
|
99
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
taskRows: 0,
|
|
109
|
+
sessionRows: 0,
|
|
110
|
+
detailLines: 0,
|
|
111
|
+
sessionDetail: false,
|
|
112
|
+
sessionFirstLines: 0,
|
|
113
|
+
sessionLastLines: 0,
|
|
114
|
+
compact: true,
|
|
115
|
+
};
|
|
100
116
|
}
|
|
101
|
-
|
|
117
|
+
// Detail pane sessione (T49): stesso schema del dettaglio task — prima la
|
|
118
|
+
// lista minima, poi la cornice, le preview solo con lo spazio che avanza.
|
|
119
|
+
// A differenza del dettaglio task il box regge anche senza righe variabili:
|
|
120
|
+
// titolo + meta sono il valore, le preview (primo prompt + ultima risposta)
|
|
121
|
+
// sono bonus. Priorità al primo prompt, poi l'ultima risposta prende ciò che
|
|
122
|
+
// resta: su terminale stretto cade prima l'ultima risposta, non il primo.
|
|
123
|
+
// Riservo righe solo per le preview che davvero renderizzeranno (i due
|
|
124
|
+
// `has…Preview`), così non sottraggo righe alla lista per un blocco vuoto.
|
|
125
|
+
let sessionDetail = false;
|
|
126
|
+
let sessionDetailCost = 0;
|
|
127
|
+
let sessionFirstLines = 0;
|
|
128
|
+
let sessionLastLines = 0;
|
|
129
|
+
if (input.hasSessionDetail) {
|
|
130
|
+
const fixed = DETAIL_CHROME + SESSION_DETAIL_FIXED;
|
|
131
|
+
const spare = avail - SESSIONS_PANE_CHROME - MIN_SESSION_ROWS - fixed;
|
|
132
|
+
if (spare >= 0) {
|
|
133
|
+
sessionDetail = true;
|
|
134
|
+
if (input.sessionHasFirstPreview)
|
|
135
|
+
sessionFirstLines = Math.min(MAX_SESSION_PREVIEW, spare);
|
|
136
|
+
if (input.sessionHasLastPreview) {
|
|
137
|
+
sessionLastLines = Math.min(MAX_SESSION_PREVIEW, spare - sessionFirstLines);
|
|
138
|
+
}
|
|
139
|
+
sessionDetailCost = fixed + sessionFirstLines + sessionLastLines;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const sessionRows = Math.max(0, avail - SESSIONS_PANE_CHROME - sessionDetailCost);
|
|
102
143
|
let detailLines = 0;
|
|
103
144
|
let detailChrome = 0;
|
|
104
145
|
if (input.hasDetail) {
|
|
@@ -113,7 +154,15 @@ export function layoutBudget(input) {
|
|
|
113
154
|
}
|
|
114
155
|
}
|
|
115
156
|
const taskRows = Math.max(0, avail - TASKS_PANE_CHROME - detailChrome - detailLines);
|
|
116
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
taskRows,
|
|
159
|
+
sessionRows,
|
|
160
|
+
detailLines,
|
|
161
|
+
sessionDetail,
|
|
162
|
+
sessionFirstLines,
|
|
163
|
+
sessionLastLines,
|
|
164
|
+
compact: false,
|
|
165
|
+
};
|
|
117
166
|
}
|
|
118
167
|
/**
|
|
119
168
|
* Finestra scorrevole su una lista più lunga della capienza.
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -30,18 +30,29 @@ set -euo pipefail
|
|
|
30
30
|
|
|
31
31
|
TASK=""
|
|
32
32
|
SESSION_ID=""
|
|
33
|
+
RESUME_ID=""
|
|
33
34
|
NO_TASK=0
|
|
34
35
|
# Positional <TaskID> + flag opzionale --session-id <uuid> (T27): il deck genera
|
|
35
36
|
# l'UUID e lo pinna così il binding sidecar sessionId↔taskId è deterministico.
|
|
36
37
|
# --no-task (T42): modalità NUDA, senza TaskID — nessuna LOOM_TASK, nessun prompt
|
|
37
38
|
# iniziale, nessun sessionId pinnato. Flag esplicito e non "positional opzionale"
|
|
38
39
|
# perché un `deck-run` a mani vuote resta un errore d'uso, non una sessione spot.
|
|
40
|
+
# --resume <sid> (T49): riapre una sessione esistente con `claude --resume`.
|
|
41
|
+
# Componibile con entrambe le forme: `deck-run <TaskID> --resume <sid>` (scoped:
|
|
42
|
+
# la ripresa eredita LOOM_TASK + titolo · task) e `deck-run --no-task --resume
|
|
43
|
+
# <sid>` (spot: resume nudo). Su resume niente prompt iniziale (si continua la
|
|
44
|
+
# conversazione, non se ne inietta una nuova) e niente --session-id (l'id ce
|
|
45
|
+
# l'ha già la sessione ripresa).
|
|
39
46
|
while [[ $# -gt 0 ]]; do
|
|
40
47
|
case "$1" in
|
|
41
48
|
--session-id)
|
|
42
49
|
SESSION_ID="${2:-}"; shift 2 ;;
|
|
43
50
|
--session-id=*)
|
|
44
51
|
SESSION_ID="${1#*=}"; shift ;;
|
|
52
|
+
--resume)
|
|
53
|
+
RESUME_ID="${2:-}"; shift 2 ;;
|
|
54
|
+
--resume=*)
|
|
55
|
+
RESUME_ID="${1#*=}"; shift ;;
|
|
45
56
|
--no-task)
|
|
46
57
|
NO_TASK=1; shift ;;
|
|
47
58
|
*)
|
|
@@ -51,7 +62,9 @@ while [[ $# -gt 0 ]]; do
|
|
|
51
62
|
done
|
|
52
63
|
|
|
53
64
|
USAGE="uso: deck-run <TaskID> [--session-id <uuid>] (es. deck-run T18)
|
|
54
|
-
| deck-run --no-task (sessione nuda, senza task)
|
|
65
|
+
| deck-run --no-task (sessione nuda, senza task)
|
|
66
|
+
| deck-run <TaskID> --resume <uuid> (riprende sessione scoped)
|
|
67
|
+
| deck-run --no-task --resume <uuid> (riprende sessione spot)"
|
|
55
68
|
|
|
56
69
|
if [[ $NO_TASK -eq 1 && -n "$TASK" ]]; then
|
|
57
70
|
echo "--no-task e <TaskID> sono mutuamente esclusivi" >&2
|
|
@@ -62,6 +75,11 @@ if [[ $NO_TASK -eq 0 && -z "$TASK" ]]; then
|
|
|
62
75
|
echo "$USAGE" >&2
|
|
63
76
|
exit 2
|
|
64
77
|
fi
|
|
78
|
+
if [[ -n "$RESUME_ID" && -n "$SESSION_ID" ]]; then
|
|
79
|
+
echo "--resume e --session-id sono mutuamente esclusivi (la sessione ripresa ha già il suo id)" >&2
|
|
80
|
+
echo "$USAGE" >&2
|
|
81
|
+
exit 2
|
|
82
|
+
fi
|
|
65
83
|
|
|
66
84
|
MODE="${LOOM_DECK_SPAWN_MODE:-inline}"
|
|
67
85
|
PROFILE_UUID="${LOOM_DECK_PROFILE_UUID:-5a36ae48df1c4d4882f43060e3e59656}"
|
|
@@ -132,6 +150,11 @@ MODE_FLAG="--permission-mode ${PERM_MODE} "
|
|
|
132
150
|
SID_FLAG=""
|
|
133
151
|
[[ -n "$SESSION_ID" ]] && SID_FLAG="--session-id ${SESSION_ID} "
|
|
134
152
|
|
|
153
|
+
# --resume per la tab (T49): riprende la sessione esistente. Stesso pattern
|
|
154
|
+
# flag-opzionale di SID_FLAG — la variante resta dentro l'unico IN_TAB_CMD.
|
|
155
|
+
RES_FLAG=""
|
|
156
|
+
[[ -n "$RESUME_ID" ]] && RES_FLAG="--resume ${RESUME_ID} "
|
|
157
|
+
|
|
135
158
|
# Prompt iniziale della sessione. Default: prompt DIRETTO (no skill) che chiede
|
|
136
159
|
# a Claude un recap dello stato della task — l'utente rivede/steera prima di
|
|
137
160
|
# lanciare (= più controllo), nessuna esecuzione automatica. Override completo via
|
|
@@ -153,10 +176,14 @@ fi
|
|
|
153
176
|
# Ramo --no-task (T42): saltano i TRE elementi che legano la sessione alla task —
|
|
154
177
|
# iniezione LOOM_TASK, prompt iniziale, --session-id pinnato. Resta claude nudo,
|
|
155
178
|
# titolato e con il permission mode del progetto.
|
|
179
|
+
# Su resume (T49) il prompt iniziale salta anche nel ramo task: riprendere una
|
|
180
|
+
# conversazione significa continuarla, non iniettarle un nuovo messaggio.
|
|
181
|
+
PROMPT_ARG="'${PROMPT}'"
|
|
182
|
+
[[ -n "$RESUME_ID" ]] && PROMPT_ARG=""
|
|
156
183
|
if [[ $NO_TASK -eq 1 ]]; then
|
|
157
|
-
_default_intab="claude --name '${TITLE}' ${MODE_FLAG}"
|
|
184
|
+
_default_intab="claude --name '${TITLE}' ${MODE_FLAG}${RES_FLAG}"
|
|
158
185
|
else
|
|
159
|
-
_default_intab="LOOM_TASK=${TASK} claude --name '${TITLE}' ${MODE_FLAG}${SID_FLAG}
|
|
186
|
+
_default_intab="LOOM_TASK=${TASK} claude --name '${TITLE}' ${MODE_FLAG}${SID_FLAG}${RES_FLAG}${PROMPT_ARG}"
|
|
160
187
|
fi
|
|
161
188
|
IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"
|
|
162
189
|
|