@lamemind/loom-deck 0.42.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 +5 -2
- package/dist/cli.js +82 -8
- package/dist/model.js +7 -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';
|
|
@@ -13,7 +13,7 @@ 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';
|
|
@@ -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.
|
|
@@ -888,6 +901,49 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
888
901
|
}
|
|
889
902
|
}
|
|
890
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
|
+
}, []);
|
|
891
947
|
// Il dispatch consulta il CATALOGO (`input-modes.ts`), non l'ordine di una
|
|
892
948
|
// catena di `if`. `MODE_KEYS` è il custode: essendo un `Record` su
|
|
893
949
|
// `CapturingMode`, un modo nuovo senza handler non compila.
|
|
@@ -904,6 +960,17 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
904
960
|
purge: onPurgeKey,
|
|
905
961
|
};
|
|
906
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
|
+
}
|
|
907
974
|
// Un modo capturing consuma TUTTO — acceleratori globali compresi — e le
|
|
908
975
|
// deroghe se le gestisce da sé (`CTRL_DEROGATIONS`). Da qui in giù si è
|
|
909
976
|
// quindi in `normal`, l'unico modo che cede ai `key.ctrl`.
|
|
@@ -1171,11 +1238,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1171
1238
|
setNote(`${input} → ${entry.label} su ${projectName}`);
|
|
1172
1239
|
}
|
|
1173
1240
|
}
|
|
1174
|
-
// Nessun tasto di uscita:
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
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.
|
|
1179
1246
|
});
|
|
1180
1247
|
const canSpawn = focus === 'tasks' && selTask !== null;
|
|
1181
1248
|
const canResume = focus === 'sessions' && selSessionObj !== null;
|
|
@@ -1348,4 +1415,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1348
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] }));
|
|
1349
1416
|
}
|
|
1350
1417
|
const cwd = process.cwd();
|
|
1351
|
-
|
|
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/package.json
CHANGED