@lamemind/loom-deck 0.42.0 → 0.44.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 +5 -2
- package/dist/cli.js +98 -11
- package/dist/glyphs.js +22 -0
- package/dist/model.js +7 -0
- package/dist/ui/panes.js +23 -9
- package/dist/view.js +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,10 +76,13 @@ Regola unica, senza eccezioni — pensata per reggere l'aggiunta di nuove azioni
|
|
|
76
76
|
| minuscola | azione immediata, one-shot | |
|
|
77
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 |
|
|
78
78
|
| `1`…`9` | voce `launch` n-esima del progetto | da `.claude/loom-works.json` |
|
|
79
|
-
| `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 |
|
|
80
81
|
|
|
81
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.
|
|
82
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
|
+
|
|
83
86
|
Assegnazioni correnti:
|
|
84
87
|
|
|
85
88
|
| | Tasto | Cosa fa |
|
|
@@ -109,7 +112,7 @@ t 💻 · c 🤖 · 1 📝 codium · 2 ☕ idea
|
|
|
109
112
|
|
|
110
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,
|
|
111
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.
|
|
112
|
-
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.
|
|
113
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.
|
|
114
117
|
|
|
115
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,6 +1,6 @@
|
|
|
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';
|
|
3
|
+
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
4
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';
|
|
@@ -11,12 +11,12 @@ import { cellWidth, launchLegend, loadArchivableDays, loadIdentity, loadLaunch,
|
|
|
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
13
|
import { cut, cutMiddle, sanitize, termWidth } from './width.js';
|
|
14
|
-
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
14
|
+
import { applyView, cycleSort, describeSort, idColumnWidth, 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
|
-
import { TASK_EMPTY, relTime, sanitizeTyped } from './glyphs.js';
|
|
19
|
+
import { TASK_EMPTY, relTime, sanitizeTyped, taskTail } from './glyphs.js';
|
|
20
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';
|
|
@@ -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.
|
|
@@ -156,6 +169,19 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
156
169
|
else
|
|
157
170
|
spotCount++;
|
|
158
171
|
}
|
|
172
|
+
// T118 — colonne fisse della lista task, misurate su `paneTasks` (la vista
|
|
173
|
+
// attiva INTERA) e non sulla finestra visibile: gemelle di `sessionCols` e
|
|
174
|
+
// per la stessa ragione, che una larghezza derivata dallo schermo si muove a
|
|
175
|
+
// ogni scroll. La coda porta dentro il proprio gutter, così l'allineamento a
|
|
176
|
+
// destra lo produce `pad` da sé; nessuna riga con qualcosa da scrivere →
|
|
177
|
+
// colonna spenta a `0`, e le descrizioni si riprendono lo spazio.
|
|
178
|
+
const taskCols = (() => {
|
|
179
|
+
let tail = 0;
|
|
180
|
+
for (const t of paneTasks) {
|
|
181
|
+
tail = Math.max(tail, termWidth(taskTail(childCount.get(t.id) ?? 0, dirtyFolders.has(t.id))));
|
|
182
|
+
}
|
|
183
|
+
return { id: idColumnWidth(paneTasks), tail: tail > 0 ? tail + 1 : 0 };
|
|
184
|
+
})();
|
|
159
185
|
// Figli della selezione: tutte le conversazioni del progetto (`≡ tutte`), le
|
|
160
186
|
// sessioni bound alla task selezionata, oppure (spot) quelle senza binding.
|
|
161
187
|
// sessions è già ts desc → l'ordine si eredita in tutti e tre i rami.
|
|
@@ -888,6 +914,49 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
888
914
|
}
|
|
889
915
|
}
|
|
890
916
|
}
|
|
917
|
+
/**
|
|
918
|
+
* T116 — `^C`: la prima pressione ARMA, la seconda entro la finestra chiude.
|
|
919
|
+
*
|
|
920
|
+
* Prima di questa task `^C` usciva subito, e a chiuderlo era Ink stesso
|
|
921
|
+
* (`exitOnCtrlC`, di default vero) PRIMA che il tasto raggiungesse
|
|
922
|
+
* `useInput`. Spegnere quell'opzione è la condizione perché il ramo esista —
|
|
923
|
+
* ma spegnerla e basta lascerebbe il deck senza NESSUNA uscita da tastiera:
|
|
924
|
+
* il ramo qui sotto è ciò che restituisce quella via, e i due cambi non si
|
|
925
|
+
* possono separare.
|
|
926
|
+
*
|
|
927
|
+
* La prima pressione RIPORTA IN `normal` quando si è dentro un modo
|
|
928
|
+
* capturing. L'avviso vive nella riga di stato, e le tre schermate
|
|
929
|
+
* sostitutive (detail, ricerca, reader) prendono l'intero frame senza
|
|
930
|
+
* renderizzarla: armare restando lì dentro darebbe un deck che sembra
|
|
931
|
+
* ignorare il tasto e che alla pressione successiva sparisce senza aver mai
|
|
932
|
+
* avvertito. Chiudere lo strato è anche coerente con `esc` — `^C` dice
|
|
933
|
+
* «voglio uscire», e il primo passo fuori è la lista.
|
|
934
|
+
*/
|
|
935
|
+
function onQuitKey() {
|
|
936
|
+
if (quitTimer.current) {
|
|
937
|
+
clearTimeout(quitTimer.current);
|
|
938
|
+
quitTimer.current = null;
|
|
939
|
+
exit();
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (captures(mode))
|
|
943
|
+
setMode('normal');
|
|
944
|
+
setNote(QUIT_NOTE);
|
|
945
|
+
quitTimer.current = setTimeout(() => {
|
|
946
|
+
quitTimer.current = null;
|
|
947
|
+
// Solo se l'avviso è ANCORA il proprio: nei 5 secondi una qualunque altra
|
|
948
|
+
// azione può aver scritto in riga di stato, e cancellare quella nota
|
|
949
|
+
// significherebbe far sparire il feedback di un'azione che non c'entra.
|
|
950
|
+
setNote((n) => (n === QUIT_NOTE ? '' : n));
|
|
951
|
+
}, QUIT_WINDOW_MS);
|
|
952
|
+
}
|
|
953
|
+
// Il timer pendente è un handle vivo del loop di Node: lasciarlo appeso allo
|
|
954
|
+
// smontaggio terrebbe il processo in piedi fino allo scadere della finestra,
|
|
955
|
+
// con lo schermo già restituito al terminale.
|
|
956
|
+
useEffect(() => () => {
|
|
957
|
+
if (quitTimer.current)
|
|
958
|
+
clearTimeout(quitTimer.current);
|
|
959
|
+
}, []);
|
|
891
960
|
// Il dispatch consulta il CATALOGO (`input-modes.ts`), non l'ordine di una
|
|
892
961
|
// catena di `if`. `MODE_KEYS` è il custode: essendo un `Record` su
|
|
893
962
|
// `CapturingMode`, un modo nuovo senza handler non compila.
|
|
@@ -904,6 +973,17 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
904
973
|
purge: onPurgeKey,
|
|
905
974
|
};
|
|
906
975
|
useInput((input, key) => {
|
|
976
|
+
// T116 — `^C` sta SOPRA il dispatch dei modi, unico tasto a scavalcarlo.
|
|
977
|
+
// Ogni altra combo `ctrl` è un acceleratore, e un acceleratore dentro un
|
|
978
|
+
// modo capturing dev'essere inerte (`input-modes.ts`); questo non è un
|
|
979
|
+
// acceleratore ma la via d'uscita dal processo, e una via d'uscita che
|
|
980
|
+
// funziona solo in una schermata su undici non è una via d'uscita. Non è
|
|
981
|
+
// quindi una deroga di `CTRL_DEROGATIONS` — quelle vivono DENTRO un modo e
|
|
982
|
+
// le gestisce il modo, questa li precede tutti.
|
|
983
|
+
if (key.ctrl && input === 'c') {
|
|
984
|
+
onQuitKey();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
907
987
|
// Un modo capturing consuma TUTTO — acceleratori globali compresi — e le
|
|
908
988
|
// deroghe se le gestisce da sé (`CTRL_DEROGATIONS`). Da qui in giù si è
|
|
909
989
|
// quindi in `normal`, l'unico modo che cede ai `key.ctrl`.
|
|
@@ -1171,11 +1251,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1171
1251
|
setNote(`${input} → ${entry.label} su ${projectName}`);
|
|
1172
1252
|
}
|
|
1173
1253
|
}
|
|
1174
|
-
// Nessun tasto di uscita:
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
1254
|
+
// Nessun tasto NUDO di uscita: `q` resta libera per un binding futuro, e
|
|
1255
|
+
// `esc` in modalità normale è inerte — dentro un overlay continua a
|
|
1256
|
+
// chiuderlo, perché quel ramo esce prima di qui. L'unica uscita da tastiera
|
|
1257
|
+
// è `^C` battuto due volte (T116, in testa a questo handler); resta poi la
|
|
1258
|
+
// chiusura della tab Ptyxis che ospita il processo.
|
|
1179
1259
|
});
|
|
1180
1260
|
const canSpawn = focus === 'tasks' && selTask !== null;
|
|
1181
1261
|
const canResume = focus === 'sessions' && selSessionObj !== null;
|
|
@@ -1345,7 +1425,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1345
1425
|
if (budget.compact) {
|
|
1346
1426
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "v", VERSION, " \u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1347
1427
|
}
|
|
1348
|
-
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] }));
|
|
1428
|
+
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, idW: taskCols.id, tailW: taskCols.tail, 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] }));
|
|
1349
1429
|
}
|
|
1350
1430
|
const cwd = process.cwd();
|
|
1351
|
-
|
|
1431
|
+
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
|
1432
|
+
// il tasto non arriverebbe mai a `useInput` e il doppio colpo non sarebbe
|
|
1433
|
+
// osservabile. In raw mode `^C` non è un SIGINT — è il byte `\x03` nello
|
|
1434
|
+
// stream di input, e chi lo consuma per primo decide. Da qui in poi lo consuma
|
|
1435
|
+
// il deck, quindi l'uscita è tutta a carico del suo handler.
|
|
1436
|
+
render(_jsx(Deck, { cwd: cwd, tasksPath: resolveTasksPath(cwd), tasksDir: resolveTasksDir(cwd) }), {
|
|
1437
|
+
exitOnCtrlC: false,
|
|
1438
|
+
});
|
package/dist/glyphs.js
CHANGED
|
@@ -74,6 +74,28 @@ export const LIVE_NONE = ' ';
|
|
|
74
74
|
export function displayProg(prog) {
|
|
75
75
|
return sanitize(prog);
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* T118 — coda della riga task: marker dirty e contatore di conversazioni, nella
|
|
79
|
+
* colonna ancorata al bordo destro del pane.
|
|
80
|
+
*
|
|
81
|
+
* Il contatore va per ULTIMO perché è lui ad allinearsi al bordo — la colonna è
|
|
82
|
+
* resa `pad(…, 'right')`, quindi ciò che sta in fondo alla stringa cade sempre
|
|
83
|
+
* nella stessa colonna. Il marker gli sta a sinistra, dentro la stessa cella
|
|
84
|
+
* invece che in una colonna propria: è raro, e una colonna dedicata costerebbe
|
|
85
|
+
* 3 spazi vuoti su ogni riga di un pane largo la metà del terminale.
|
|
86
|
+
*
|
|
87
|
+
* Vive qui e non nel sito di render perché la STESSA stringa serve due volte —
|
|
88
|
+
* a misurare la larghezza della colonna (sulla vista completa, in `cli.tsx`) e
|
|
89
|
+
* a disegnarla (sulla finestra, in `panes.tsx`). Due formattazioni gemelle
|
|
90
|
+
* divergerebbero alla prima modifica, e la colonna risulterebbe larga quanto
|
|
91
|
+
* una stringa che nessuno scrive.
|
|
92
|
+
*/
|
|
93
|
+
export function taskTail(childCount, dirty) {
|
|
94
|
+
const count = childCount > 0 ? `(${childCount})` : '';
|
|
95
|
+
if (!dirty)
|
|
96
|
+
return count;
|
|
97
|
+
return count ? `${WARN} ${count}` : WARN;
|
|
98
|
+
}
|
|
77
99
|
// T49 — size umana compatta per il detail pane sessione.
|
|
78
100
|
export function fmtSize(bytes) {
|
|
79
101
|
if (bytes < 1024)
|
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/ui/panes.js
CHANGED
|
@@ -5,11 +5,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
import { Box, Text } from 'ink';
|
|
6
6
|
import { cut, cutParts, pad, sanitize, termWidth } from '../width.js';
|
|
7
7
|
import { isDone, paneTextWidth } from '../layout.js';
|
|
8
|
-
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SESSION_SEP, SID_CHARS, TASK_EMPTY, WARN, displayProg, modelShort, relTime, } from '../glyphs.js';
|
|
8
|
+
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SESSION_SEP, SID_CHARS, TASK_EMPTY, WARN, displayProg, modelShort, relTime, taskTail, } from '../glyphs.js';
|
|
9
9
|
import { META_ROWS, ROW_ALL, ROW_SPOT } from '../model.js';
|
|
10
10
|
import { rowLabel, sessionTitle } from '../session-list.js';
|
|
11
11
|
import { sessionView, taskView, SESSION_VIEWS, TASK_VIEWS, } from '../pane-views.js';
|
|
12
|
-
import { describeSort, PRI_ENTRIES, PROG_ENTRIES } from '../view.js';
|
|
12
|
+
import { describeSort, padId, PRI_ENTRIES, PROG_ENTRIES } from '../view.js';
|
|
13
13
|
/**
|
|
14
14
|
* Header del pane task, tagliato QUI e non da Ink — stesso motivo del gemello
|
|
15
15
|
* `SessionsHeader`, con una differenza di rischio: qui i segmenti sono tutti
|
|
@@ -49,7 +49,7 @@ export function TasksHeader({ counts, active, above, below, focused, columns, })
|
|
|
49
49
|
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns), segments.findIndex((s) => s.active));
|
|
50
50
|
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, inverse: seg.active, children: shown[i] }, i)) : null) }));
|
|
51
51
|
}
|
|
52
|
-
export function TasksPane({ tasks, counts, activeView, paneCount, view, selected, spotCount, allCount, childCount, focused, loadError, windowStart, above, below, columns, dirty, }) {
|
|
52
|
+
export function TasksPane({ tasks, counts, activeView, paneCount, view, selected, spotCount, allCount, childCount, idW, tailW, focused, loadError, windowStart, above, below, columns, dirty, }) {
|
|
53
53
|
const allSelected = selected === ROW_ALL;
|
|
54
54
|
const spotSelected = selected === ROW_SPOT;
|
|
55
55
|
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(TasksHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: cut(sanitize(`sort: ${describeSort(view.sort)}` +
|
|
@@ -71,17 +71,31 @@ export function TasksPane({ tasks, counts, activeView, paneCount, view, selected
|
|
|
71
71
|
// completa, su cui è keyata la selezione. +META_ROWS: le prime due
|
|
72
72
|
// righe sono le meta.
|
|
73
73
|
const sel = windowStart + i + META_ROWS === selected;
|
|
74
|
-
const
|
|
74
|
+
const tail = taskTail(childCount.get(task.id) ?? 0, dirty.has(task.id));
|
|
75
|
+
// T118 — l'id è paddato a `idW`, quindi `head` ha la stessa larghezza
|
|
76
|
+
// su ogni riga e Pri/Prog cadono sempre nella stessa colonna.
|
|
77
|
+
const id = padId(task.id, idW);
|
|
75
78
|
// Invariante ③: la descrizione è l'unico pezzo a lunghezza libera, e
|
|
76
79
|
// si taglia QUI sul budget che resta dopo le colonne fisse. Lasciarlo
|
|
77
80
|
// fare a `truncate-end` significa passare da `cli-truncate`, che
|
|
78
81
|
// restituisce una riga più larga del pane (una colonna per emoji) e
|
|
79
82
|
// quindi scrive sopra il bordo. Le parti fisse si misurano con
|
|
80
|
-
// `termWidth`:
|
|
81
|
-
const head = `${CARET_OFF}${
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
// `termWidth`: i due glifi Pri/Prog valgono 2 ciascuno.
|
|
84
|
+
const head = `${CARET_OFF}${id} ${sanitize(task.pri)} ${displayProg(task.prog)} `;
|
|
85
|
+
// T118 — la colonna della coda si riserva PRIMA di tagliare la
|
|
86
|
+
// descrizione, e per la stessa larghezza su ogni riga: appesa dopo,
|
|
87
|
+
// entrava nel budget solo dove c'era qualcosa da scrivere, e la
|
|
88
|
+
// descrizione si tagliava a una colonna diversa riga per riga.
|
|
89
|
+
//
|
|
90
|
+
// Pavimento `0` su tutte e tre le misure e non un minimo di cortesia:
|
|
91
|
+
// il budget è un TETTO. Un pavimento sopra lo spazio reale fa uscire
|
|
92
|
+
// la riga dal pane e le mangia il bordo. `reserve` si clampa su ciò
|
|
93
|
+
// che avanza, così `head + desc + coda` sta sempre dentro il pane.
|
|
94
|
+
const avail = Math.max(0, paneTextWidth(columns) - termWidth(head));
|
|
95
|
+
const reserve = Math.min(tailW, avail);
|
|
96
|
+
const descW = avail - reserve;
|
|
97
|
+
const desc = cut(task.desc, descW);
|
|
98
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, id, " ", sanitize(task.pri), " ", displayProg(task.prog), " ", desc, ' '.repeat(Math.max(0, descW - termWidth(desc))), pad(tail, reserve, 'right')] }, task.id));
|
|
85
99
|
}))] }));
|
|
86
100
|
}
|
|
87
101
|
/**
|
package/dist/view.js
CHANGED
|
@@ -59,6 +59,37 @@ export function idNum(id) {
|
|
|
59
59
|
const m = /^T(\d+)$/.exec(id);
|
|
60
60
|
return m ? Number(m[1]) : Number.MAX_SAFE_INTEGER;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* T118 — larghezza della colonna id, DERIVATA dalla popolazione.
|
|
64
|
+
*
|
|
65
|
+
* La popolazione è la vista attiva COMPLETA, non la finestra visibile: una
|
|
66
|
+
* larghezza misurata sulle sole righe renderizzate sposta le colonne mentre si
|
|
67
|
+
* scorre, cioè una tabella corretta a ogni frame e illeggibile nel movimento.
|
|
68
|
+
* Il chiamante deve quindi passare `paneTasks`, non l'array che finisce nel
|
|
69
|
+
* pane (`tasks` in `TaskPane` è già la finestra).
|
|
70
|
+
*
|
|
71
|
+
* Gli id sono `T` + cifre (`TASK_ID_RE`), cioè ASCII: `.length` conta colonne.
|
|
72
|
+
*/
|
|
73
|
+
export function idColumnWidth(tasks) {
|
|
74
|
+
let w = 0;
|
|
75
|
+
for (const t of tasks)
|
|
76
|
+
w = Math.max(w, t.id.length);
|
|
77
|
+
return w;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* T118 — id allineato a destra DENTRO il prefisso: `T102` in lista rende `T90`
|
|
81
|
+
* come `T 90`, non come ` T90`.
|
|
82
|
+
*
|
|
83
|
+
* Il prefisso di tipo è l'ancora con cui l'occhio riconosce la colonna:
|
|
84
|
+
* spostarlo riga per riga toglie proprio ciò che l'allineamento dava. Sono le
|
|
85
|
+
* cifre ad allinearsi fra loro, la `T` resta ferma.
|
|
86
|
+
*/
|
|
87
|
+
export function padId(id, cols) {
|
|
88
|
+
const m = /^([A-Za-z]+)(\d+)$/.exec(id);
|
|
89
|
+
if (!m)
|
|
90
|
+
return id.padEnd(cols);
|
|
91
|
+
return m[1] + ' '.repeat(Math.max(0, cols - id.length)) + m[2];
|
|
92
|
+
}
|
|
62
93
|
function rankOf(task, key) {
|
|
63
94
|
if (key === 'pri')
|
|
64
95
|
return priRank(task.pri);
|
package/package.json
CHANGED