@lamemind/loom-deck 0.35.1 → 0.37.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/dist/cli.js +19 -7
- package/dist/overlays/sheet.js +49 -25
- package/dist/spawn.js +16 -6
- package/dist/ui/detail-screen.js +28 -11
- package/dist/version.js +22 -0
- package/dist/viewport.js +13 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -29,6 +29,7 @@ import { useSearchOverlay } from './overlays/search.js';
|
|
|
29
29
|
import { useSheetOverlay } from './overlays/sheet.js';
|
|
30
30
|
import { useAssignOverlay } from './overlays/assign.js';
|
|
31
31
|
import { captures } from './input-modes.js';
|
|
32
|
+
import { VERSION } from './version.js';
|
|
32
33
|
function Deck({ cwd, tasksPath, tasksDir }) {
|
|
33
34
|
const { exit } = useApp();
|
|
34
35
|
const { tasks, loadError } = useTasks(tasksPath);
|
|
@@ -285,17 +286,28 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
285
286
|
// Lo spawn vero, su una task GIÀ risolta. Separato dalla guardia perché il
|
|
286
287
|
// detail passa l'id fotografato all'apertura e non la selezione corrente: la
|
|
287
288
|
// lista lì sotto non è più a schermo, quindi non è più la fonte dell'oggetto.
|
|
288
|
-
|
|
289
|
+
// T111 — `spawnNote` arriva dal campo sempre attivo del detail ed è vuota per
|
|
290
|
+
// ogni altro percorso (`^K`/`^P`/`^R` non passano di lì, quindi non hanno da
|
|
291
|
+
// dove prenderla). Si scrive nel sidecar PRIMA dello spawn, accanto al
|
|
292
|
+
// binding e per la stessa ragione: la conversazione deve risultare figlia
|
|
293
|
+
// della task e portare la propria maniglia appena il suo JSONL compare, o per
|
|
294
|
+
// il tempo di un tick la riga in lista comparirebbe nuda. Due record separati
|
|
295
|
+
// sullo stesso `sessionId` sono la forma normale di un file append-only
|
|
296
|
+
// last-wins, non una scrittura da fondere.
|
|
297
|
+
function spawnForTask(id, kind, model, keyLabel, spawnNote = '') {
|
|
289
298
|
const sid = randomUUID();
|
|
290
299
|
appendTaskBinding(cwd, sid, id);
|
|
291
|
-
|
|
300
|
+
if (spawnNote)
|
|
301
|
+
appendNote(cwd, sid, spawnNote);
|
|
302
|
+
const child = spawnDeck(id, cwd, sid, kind, model, spawnNote);
|
|
292
303
|
child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
293
304
|
const what = kind === 'none' ? '' : ` · ${kind}`;
|
|
294
305
|
// Il modello è SEMPRE nominato, anche quando è il default: gli acceleratori
|
|
295
306
|
// della lista non passano dal selettore del detail e usano il default fisso
|
|
296
307
|
// (T108 · via a), quindi senza dirlo l'utente crederebbe di aver ereditato
|
|
297
308
|
// la scelta fatta nell'ultimo detail aperto.
|
|
298
|
-
|
|
309
|
+
const named = spawnNote ? ` · «${cut(spawnNote, 24)}»` : '';
|
|
310
|
+
setNote(`${keyLabel} spawn ${id}${what} · ${model}${named} → tab CC (sid ${sid.slice(0, 8)})`);
|
|
299
311
|
}
|
|
300
312
|
function spawnTaskSession(kind, keyLabel) {
|
|
301
313
|
const task = selectedTaskOr(keyLabel, 'spawnare');
|
|
@@ -490,7 +502,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
490
502
|
columns,
|
|
491
503
|
setMode,
|
|
492
504
|
setNote,
|
|
493
|
-
onAction: (id, kind, model, label) => spawnForTask(id, kind, model, label),
|
|
505
|
+
onAction: (id, kind, model, spawnNote, label) => spawnForTask(id, kind, model, label, spawnNote),
|
|
494
506
|
});
|
|
495
507
|
const search = useSearchOverlay({
|
|
496
508
|
sessions,
|
|
@@ -1023,7 +1035,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1023
1035
|
// posizione è lo scroll mosso a mano. Il clamp serve comunque — un resize
|
|
1024
1036
|
// può accorciare il testo sotto uno scroll già dato.
|
|
1025
1037
|
const start = Math.min(sheet.top, sheet.maxTop);
|
|
1026
|
-
return (_jsx(DetailScreen, { id: sheet.sheet.id, title: sheet.sheet.title, missing: sheet.sheet.text === null, lines: sheet.lines.slice(start, start + sheet.capacity), spans: sheet.doc?.spans ?? [], top: start, total: sheet.lines.length, capacity: sheet.capacity, action: sheet.action, model: sheet.model, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
|
|
1038
|
+
return (_jsx(DetailScreen, { id: sheet.sheet.id, title: sheet.sheet.title, missing: sheet.sheet.text === null, lines: sheet.lines.slice(start, start + sheet.capacity), spans: sheet.doc?.spans ?? [], top: start, total: sheet.lines.length, capacity: sheet.capacity, action: sheet.action, model: sheet.model, spawnNote: sheet.spawnNote, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
|
|
1027
1039
|
}
|
|
1028
1040
|
// ── T52 · schermate sostitutive ─────────────────────────────────────────
|
|
1029
1041
|
// Ricerca e reader sono gli unici modali che NON stanno in flusso sopra i
|
|
@@ -1112,9 +1124,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1112
1124
|
// a una riga sola. Perdere il deck per un terminale basso è meglio che
|
|
1113
1125
|
// sporcare la cronologia del terminale a ogni poll.
|
|
1114
1126
|
if (budget.compact) {
|
|
1115
|
-
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 ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1127
|
+
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"] })] }));
|
|
1116
1128
|
}
|
|
1117
|
-
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 === '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 === '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, _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 }), _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] }));
|
|
1129
|
+
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 === '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, _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 }), _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] }));
|
|
1118
1130
|
}
|
|
1119
1131
|
const cwd = process.cwd();
|
|
1120
1132
|
render(_jsx(Deck, { cwd: cwd, tasksPath: resolveTasksPath(cwd), tasksDir: resolveTasksDir(cwd) }));
|
package/dist/overlays/sheet.js
CHANGED
|
@@ -28,6 +28,13 @@ export function useSheetOverlay(deps) {
|
|
|
28
28
|
// deck: si azzera a ogni apertura come scroll e azione, quindi non esiste una
|
|
29
29
|
// selezione invisibile che cambi il comportamento dei tasti della lista.
|
|
30
30
|
const [model, setModel] = useState(MODEL_DEFAULT);
|
|
31
|
+
// T111 — la nota con cui nascerà la conversazione. Campo SEMPRE ATTIVO: riceve
|
|
32
|
+
// la scrittura appena il detail si apre, senza nessun tasto che lo apra, e per
|
|
33
|
+
// questo si prende l'alfabeto nudo del modo (le cifre del selettore modello,
|
|
34
|
+
// `g`/`G` degli estremi del testo). Si azzera a ogni apertura come scroll,
|
|
35
|
+
// azione e modello: una nota armata che sopravvive alla chiusura sarebbe uno
|
|
36
|
+
// stato invisibile che cambia il titolo dello spawn successivo.
|
|
37
|
+
const [spawnNote, setSpawnNote] = useState('');
|
|
31
38
|
// T91 — ricerca dentro il detail. `open` distingue i due modi in cui la si
|
|
32
39
|
// lascia: `esc` butta via ciò che il modale ha prodotto (`find` a null,
|
|
33
40
|
// evidenziazione via), `⏎` lo congela e restituisce il controllo allo strato
|
|
@@ -77,12 +84,13 @@ export function useSheetOverlay(deps) {
|
|
|
77
84
|
return;
|
|
78
85
|
setTop(topForOffset(lines, o.start, capacity));
|
|
79
86
|
}, [findRes, occCur, lines, capacity]);
|
|
80
|
-
/** Apre il detail su una task, azzerando scroll, azione, modello e ricerca. */
|
|
87
|
+
/** Apre il detail su una task, azzerando scroll, azione, modello, nota e ricerca. */
|
|
81
88
|
function open(next) {
|
|
82
89
|
setSheet(next);
|
|
83
90
|
setTop(0);
|
|
84
91
|
setAction(0);
|
|
85
92
|
setModel(MODEL_DEFAULT);
|
|
93
|
+
setSpawnNote('');
|
|
86
94
|
setFind(null);
|
|
87
95
|
setOccIdx(0);
|
|
88
96
|
setNote('');
|
|
@@ -150,16 +158,32 @@ export function useSheetOverlay(deps) {
|
|
|
150
158
|
// selezione per ognuna. Il modo cattura TUTTO, acceleratori `^K`/`^P`/`^R`
|
|
151
159
|
// compresi: chi è già nel detail ha i bottoni. L'unica deroga è `^F`, che qui
|
|
152
160
|
// apre la ricerca nel testo invece di quella sulle conversazioni.
|
|
161
|
+
//
|
|
162
|
+
// T111 — con il campo nota sempre attivo l'ultimo ramo è la SCRITTURA, quindi
|
|
163
|
+
// ogni funzione che resta viva deve stare su un tasto che un campo di testo
|
|
164
|
+
// non contende: `tab`, frecce, `PgUp`/`PgDn`, CTRL, `⏎`, `esc`.
|
|
153
165
|
function onKey(input, key) {
|
|
154
166
|
if (find?.open) {
|
|
155
167
|
onFindKey(input, key);
|
|
156
168
|
return;
|
|
157
169
|
}
|
|
158
|
-
if (key.ctrl
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
170
|
+
if (key.ctrl) {
|
|
171
|
+
// Ramo CTRL chiuso in testa, e non più il solo `if` su `^F`: sotto c'è la
|
|
172
|
+
// scrittura nel campo nota, e senza questo ramo `^K` ci finirebbe come
|
|
173
|
+
// lettera `k` — la combo non arriva come tasto proprio, `key.ctrl` è
|
|
174
|
+
// l'unico discriminante fra `^X` e `x`.
|
|
175
|
+
//
|
|
176
|
+
// `^F` è la deroga dichiarata (`CTRL_DEROGATIONS.detail`): fuori dal
|
|
177
|
+
// detail è la ricerca conversazioni, qui è la ricerca nel testo. La query
|
|
178
|
+
// sopravvive a una chiusura con `⏎`, quindi riaprire riprende da dov'era.
|
|
179
|
+
// `^U` svuota la nota — non è una deroga ma una combo che il modo gestisce
|
|
180
|
+
// da sé, come il filtro del modale assegnazione. Ogni altro CTRL è inerte.
|
|
181
|
+
if (input === 'f') {
|
|
182
|
+
setFind((f) => (f ? { ...f, open: true } : { q: '', caret: 0, open: true }));
|
|
183
|
+
}
|
|
184
|
+
else if (input === 'u') {
|
|
185
|
+
setSpawnNote('');
|
|
186
|
+
}
|
|
163
187
|
return;
|
|
164
188
|
}
|
|
165
189
|
if (key.escape) {
|
|
@@ -178,24 +202,18 @@ export function useSheetOverlay(deps) {
|
|
|
178
202
|
// competizione sul tasto.
|
|
179
203
|
const act = DETAIL_ACTIONS[action];
|
|
180
204
|
const id = sheet?.id;
|
|
205
|
+
const note = spawnNote.trim();
|
|
181
206
|
close();
|
|
182
207
|
if (id)
|
|
183
|
-
onAction(id, act.kind, model, `⏎ ${act.label}`);
|
|
208
|
+
onAction(id, act.kind, model, note, `⏎ ${act.label}`);
|
|
184
209
|
}
|
|
185
210
|
else if (key.tab) {
|
|
186
|
-
// T108 — `tab` scorre il catalogo dei modelli
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// alfabeto già speso torna disponibile.
|
|
211
|
+
// T108 — `tab` scorre il catalogo dei modelli, e da T111 è il SOLO canale:
|
|
212
|
+
// le cifre `1`-`4` sono passate al campo nota. Libero solo QUI: in vista
|
|
213
|
+
// normale cicla la vista del pane a fuoco. Il detail cattura l'input per
|
|
214
|
+
// intero, quindi è dove un alfabeto già speso torna disponibile.
|
|
190
215
|
setModel((m) => MODELS[(MODELS.indexOf(m) + 1) % MODELS.length]);
|
|
191
216
|
}
|
|
192
|
-
else if (input.length === 1 && input >= '1' && input <= String(MODELS.length)) {
|
|
193
|
-
// Scelta diretta: chi sa già quale vuole non paga lo scorrimento. Il test
|
|
194
|
-
// sulla lunghezza non è pleonastico: `useInput` consegna il CHUNK letto da
|
|
195
|
-
// stdin, quindi un incollato come `12` passerebbe il confronto fra
|
|
196
|
-
// stringhe e indicizzerebbe il catalogo fuori range.
|
|
197
|
-
setModel(MODELS[Number(input) - 1]);
|
|
198
|
-
}
|
|
199
217
|
else if (key.leftArrow || key.rightArrow) {
|
|
200
218
|
// Scorrimento CICLICO come le righe di scelta del modale edit: cinque
|
|
201
219
|
// voci, arrivare in fondo e ripartire costa meno che invertire direzione.
|
|
@@ -214,14 +232,19 @@ export function useSheetOverlay(deps) {
|
|
|
214
232
|
else if (key.pageDown) {
|
|
215
233
|
scroll(capacity);
|
|
216
234
|
}
|
|
217
|
-
else if (
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
235
|
+
else if (key.backspace || key.delete) {
|
|
236
|
+
// Nessun movimento di caret nel campo (D3): `←→` sono già delle azioni e
|
|
237
|
+
// riprenderle costerebbe la barra. Si cancella quindi in coda — e
|
|
238
|
+
// `key.delete` è ANCHE Backspace in Ink, come nel campo di ricerca.
|
|
239
|
+
setSpawnNote((s) => removeAt(s, cpLen(s) - 1));
|
|
222
240
|
}
|
|
223
|
-
else if (input
|
|
224
|
-
|
|
241
|
+
else if (input && !key.meta) {
|
|
242
|
+
// Ultimo ramo: tutto ciò che nessun tasto vivo ha reclamato è testo. Ci
|
|
243
|
+
// cadono le cifre `1`-`4` (prima il selettore modello) e `g`/`G` (prima
|
|
244
|
+
// gli estremi del testo, che nel detail si raggiungono con
|
|
245
|
+
// `PgUp`/`PgDn`); nel reader fullscreen restano, perché lì nessun campo
|
|
246
|
+
// contende le lettere.
|
|
247
|
+
setSpawnNote((s) => s + sanitizeTyped(input));
|
|
225
248
|
}
|
|
226
249
|
}
|
|
227
250
|
return {
|
|
@@ -230,6 +253,7 @@ export function useSheetOverlay(deps) {
|
|
|
230
253
|
top,
|
|
231
254
|
action,
|
|
232
255
|
model,
|
|
256
|
+
spawnNote,
|
|
233
257
|
find,
|
|
234
258
|
lines,
|
|
235
259
|
capacity,
|
package/dist/spawn.js
CHANGED
|
@@ -32,8 +32,9 @@ export function spawnOut(cmd, args, opts) {
|
|
|
32
32
|
fake.unref = () => fake;
|
|
33
33
|
return fake;
|
|
34
34
|
}
|
|
35
|
-
// L'ordine È il
|
|
36
|
-
//
|
|
35
|
+
// L'ordine È il giro di `tab` nel detail, non una preferenza di lettura:
|
|
36
|
+
// cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
|
|
37
|
+
// anche il binding delle cifre `1`-`4`, passate poi al campo nota.
|
|
37
38
|
export const MODELS = ['fable', 'opus', 'sonnet', 'haiku'];
|
|
38
39
|
// Default del selettore e di ogni percorso di spawn che non passa da lui
|
|
39
40
|
// (`^K`/`^P`/`^R` dalla lista, resume, fork). Duplicato del default di deck-run
|
|
@@ -65,11 +66,20 @@ export const DETAIL_ACTIONS = [
|
|
|
65
66
|
// Il modello, al contrario del kind, ha un default (T108): i percorsi che non
|
|
66
67
|
// passano dal selettore del detail non devono nominarlo per forza, e l'unico
|
|
67
68
|
// valore sensato per loro è quello che il selettore stesso mostra all'apertura.
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// T111 — `spawnNote` è la nota data alla NASCITA della conversazione, dal campo
|
|
70
|
+
// sempre attivo del detail. Stesso flag `--title-note` del resume (T64): il
|
|
71
|
+
// suffisso `«nota»` viene appeso a `TITLE` dentro deck-run PRIMA che i rami si
|
|
72
|
+
// separino, quindi il flag non appartiene alla ripresa — vale su ogni spawn.
|
|
73
|
+
// Assente quando la nota è vuota, e non passato vuoto: `--title-note ''`
|
|
74
|
+
// produrrebbe un `«»` a vuoto nel titolo.
|
|
75
|
+
export function deckArgs(id, sessionId, kind, model = MODEL_DEFAULT, spawnNote) {
|
|
76
|
+
const args = [id, '--session-id', sessionId, '--prompt-kind', kind, '--model', model];
|
|
77
|
+
if (spawnNote)
|
|
78
|
+
args.push('--title-note', spawnNote);
|
|
79
|
+
return args;
|
|
70
80
|
}
|
|
71
|
-
export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT) {
|
|
72
|
-
const child = spawnOut(DECK_RUN, deckArgs(id, sessionId, kind, model), {
|
|
81
|
+
export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT, spawnNote) {
|
|
82
|
+
const child = spawnOut(DECK_RUN, deckArgs(id, sessionId, kind, model, spawnNote), {
|
|
73
83
|
cwd,
|
|
74
84
|
detached: true,
|
|
75
85
|
stdio: 'ignore',
|
package/dist/ui/detail-screen.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Box, Text } from 'ink';
|
|
|
5
5
|
import { caretWindow, cut, cutParts, sanitize } from '../width.js';
|
|
6
6
|
import { sliceLine } from '../text-search.js';
|
|
7
7
|
import { sliceSpans } from '../markdown.js';
|
|
8
|
+
import { cpLen } from '../layout.js';
|
|
8
9
|
import { DETAIL_ACTIONS, MODELS } from '../spawn.js';
|
|
9
10
|
import { WARN } from '../glyphs.js';
|
|
10
11
|
/** Resa di ogni costrutto markdown (T75 · D4): un solo livello di enfasi per
|
|
@@ -51,14 +52,19 @@ export function DetailLine({ line, spans, occ, current, }) {
|
|
|
51
52
|
return (_jsx(Text, { bold: st?.bold, color: st?.color, children: sliceLine(seg.text, at, occ, current).map((p, j) => p.hit ? (_jsx(Text, { backgroundColor: p.current ? 'cyan' : 'yellow', color: "black", children: p.text }, j)) : (_jsx(Text, { children: p.text }, j))) }, i));
|
|
52
53
|
}) }));
|
|
53
54
|
}
|
|
54
|
-
/** Campo
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* non su una riga
|
|
58
|
-
|
|
55
|
+
/** Campo di testo del detail: finestra ancorata al caret, cursore inverso sulla
|
|
56
|
+
* cella reale. Gemello di `EditTextField` senza la label, che qui sta fuori
|
|
57
|
+
* perché il campo vive in FLUSSO su una riga condivisa con altro (il contatore
|
|
58
|
+
* di occorrenze per la ricerca, il segnaposto per la nota) — non su una riga
|
|
59
|
+
* propria. Lo usano i due campi del detail: la ricerca `^F`, dove il caret si
|
|
60
|
+
* muove, e la nota (T111), dove il caret sta sempre in coda. */
|
|
61
|
+
export function DetailTextField({ value, caret, cols }) {
|
|
59
62
|
const win = caretWindow(value, caret, cols);
|
|
60
63
|
return (_jsxs(_Fragment, { children: [_jsx(Text, { children: sanitize(win.head) }), _jsx(Text, { inverse: true, children: sanitize(win.at) }), _jsx(Text, { children: sanitize(win.tail) })] }));
|
|
61
64
|
}
|
|
65
|
+
/** Segnaposto della riga nota quando il campo è vuoto. Costante e non letterale
|
|
66
|
+
* inline perché la sua larghezza entra nel budget del campo accanto. */
|
|
67
|
+
const NOTE_HINT = ' · nome della conversazione';
|
|
62
68
|
/**
|
|
63
69
|
* Detail della task (T66): il task file scrollabile più la barra azioni.
|
|
64
70
|
*
|
|
@@ -72,7 +78,7 @@ export function DetailFindField({ value, caret, cols }) {
|
|
|
72
78
|
* all'arrivo del mouse (T21 · SGR enable + hit-test) senza migrazione. La
|
|
73
79
|
* navigazione da tastiera ci si sovrappone senza conflitti.
|
|
74
80
|
*/
|
|
75
|
-
export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, columns, find, occ, occCur, }) {
|
|
81
|
+
export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, columns, find, occ, occCur, }) {
|
|
76
82
|
const last = Math.min(total, top + capacity);
|
|
77
83
|
// Il taglio lo fa il chiamante (invariante ③ di width.ts): la riga bottoni è
|
|
78
84
|
// ASCII, quindi `truncate-end` oggi darebbe il risultato giusto per caso — ma
|
|
@@ -98,14 +104,16 @@ export function DetailScreen({ id, title, missing, lines, spans, top, total, cap
|
|
|
98
104
|
// enfasi diverse su due righe adiacenti (là `inverse`, qui un grassetto
|
|
99
105
|
// colorato) obbligano a guardare da vicino per capire quale voce è scelta —
|
|
100
106
|
// la stessa resa si legge di colpo su entrambe.
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
107
|
+
// T111 — la cifra è USCITA dalla quadra: `1`-`4` sono passate al campo nota e
|
|
108
|
+
// il modello si scorre col solo `tab`. Un'etichetta che nomina un tasto è
|
|
109
|
+
// accoppiata al binding, e tenerla dopo che il binding è morto non è
|
|
110
|
+
// un'informazione parziale — è una resa che dichiara un tasto che non fa più
|
|
111
|
+
// quella cosa, cioè peggio di nessuna indicazione.
|
|
104
112
|
// `priority` sulla voce SELEZIONATA e non sulla prima: qui il troncamento
|
|
105
113
|
// cancellerebbe l'unica informazione che la riga esiste per dare — quale
|
|
106
114
|
// modello sta per essere usato — mentre nella barra azioni la voce attiva è
|
|
107
115
|
// comunque nota dal tasto appena premuto.
|
|
108
|
-
const mSegs = MODELS.map((m
|
|
116
|
+
const mSegs = MODELS.map((m) => `[ ${m} ]`);
|
|
109
117
|
const mParts = [];
|
|
110
118
|
mSegs.forEach((s, i) => {
|
|
111
119
|
if (i > 0)
|
|
@@ -120,5 +128,14 @@ export function DetailScreen({ id, title, missing, lines, spans, top, total, cap
|
|
|
120
128
|
if (dropped(mShown, mSegs) > 0)
|
|
121
129
|
mShown = cutParts(mParts, Math.max(0, mWidth - 6), mIdx * 2);
|
|
122
130
|
const mCut = dropped(mShown, mSegs);
|
|
123
|
-
|
|
131
|
+
// T111 — riga della nota. FISSA (D1): il valore armato si vede sempre, e il
|
|
132
|
+
// suo costo sta in `DETAIL_CHROME` invece che in un secondo `extra`
|
|
133
|
+
// condizionale di `detailCapacity`. Prefisso largo 8 come `modello `, così i
|
|
134
|
+
// due parametri dello spawn si leggono incolonnati.
|
|
135
|
+
// Il campo non ha nessun tasto che lo apra: il cursore è l'unica cosa che lo
|
|
136
|
+
// dichiara attivo, e il segnaposto dice cosa ci si scrive. Le colonne del
|
|
137
|
+
// segnaposto si riservano SOLO quando c'è (cioè a nota vuota): riservarle
|
|
138
|
+
// sempre toglierebbe testo visibile a una nota lunga per un avviso assente.
|
|
139
|
+
const nWidth = Math.max(10, width - 8 - (spawnNote ? 0 : NOTE_HINT.length));
|
|
140
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: id }), " \u00B7 ", cut(title, Math.max(10, width - 34)), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), find?.open ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " occorrenza \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " caret \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " tieni \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193/PgUp/PgDn" }), " testo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "tab" }), " modello \u00B7", ' ', _jsx(Text, { color: "yellow", children: "scrivi" }), " nota \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^F" }), " cerca \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " esegui \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " chiudi"] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " task file non trovato \u00B7 le azioni restano attive (deck-run risolve la task per id)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: occ, current: occCur }, top + i)))) }), find?.open ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "cerca " }), _jsx(DetailTextField, { value: find.q, caret: find.caret, cols: Math.max(10, width - 28) }), find.q.length === 0 ? (_jsx(Text, { dimColor: true, children: " \u00B7 digita per cercare" })) : occ.length === 0 ? (_jsx(Text, { color: "yellow", children: " \u00B7 nessuna occorrenza" })) : (_jsxs(Text, { color: "cyan", children: [' ', "\u00B7 ", occCur + 1, "/", occ.length] }))] }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "modello " }), mShown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === mIdx, color: i / 2 === mIdx ? 'green' : 'gray', children: part }, i))), mCut > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", mCut] }) : null] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "nota " }), _jsx(DetailTextField, { value: spawnNote, caret: cpLen(spawnNote), cols: nWidth }), spawnNote ? null : _jsx(Text, { dimColor: true, children: NOTE_HINT })] }), _jsxs(Text, { wrap: "truncate-end", children: [shown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === action, color: i / 2 === action ? 'green' : 'gray', children: part }, i))), cutCount > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", cutCount] }) : null] })] })] }));
|
|
124
141
|
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Versione del pacchetto, letta dal `package.json` vero invece che cablata in
|
|
2
|
+
// un `const`: una costante duplicata dal manifest è un calco — al primo bump
|
|
3
|
+
// dimenticato il deck mostrerebbe con sicurezza un numero falso, ed è
|
|
4
|
+
// esattamente il caso in cui serve (capire quale build sto guardando).
|
|
5
|
+
//
|
|
6
|
+
// Il path relativo `../package.json` vale per entrambe le forme in cui questo
|
|
7
|
+
// modulo gira: `src/version.ts` sotto `tsx` e `dist/version.js` dopo il build —
|
|
8
|
+
// il manifest sta un livello sopra in tutti e due i casi.
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
function readVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const raw = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
return typeof parsed.version === 'string' ? parsed.version : '?';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Installazione monca o manifest illeggibile: la versione è cosmetica,
|
|
18
|
+
// non vale far cadere il deck per averla persa.
|
|
19
|
+
return '?';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const VERSION = readVersion();
|
package/dist/viewport.js
CHANGED
|
@@ -218,13 +218,21 @@ export function readerCapacity(rows) {
|
|
|
218
218
|
// 2 bordi del box testo
|
|
219
219
|
// 1 marginTop della riga azioni
|
|
220
220
|
// 1 riga modello (T108)
|
|
221
|
+
// 1 riga nota (T111)
|
|
221
222
|
// 1 riga azioni
|
|
222
223
|
//
|
|
223
|
-
// Le ultime
|
|
224
|
-
// barra bottoni
|
|
225
|
-
// ogni riga fissa aggiunta va scalata dalla capienza
|
|
226
|
-
// sfonda `rows` (stessa invariante di
|
|
227
|
-
|
|
224
|
+
// Le ultime quattro sono il motivo per cui il detail non può riusare
|
|
225
|
+
// READER_CHROME: barra bottoni, selettore modello e campo nota sono righe FISSE
|
|
226
|
+
// in più dentro l'overlay, e ogni riga fissa aggiunta va scalata dalla capienza
|
|
227
|
+
// del contenuto o il frame sfonda `rows` (stessa invariante di
|
|
228
|
+
// TASKS_PANE_CHROME).
|
|
229
|
+
//
|
|
230
|
+
// La nota è una riga fissa e non un secondo `extra` di `detailCapacity` (T111 ·
|
|
231
|
+
// D1): due condizionali mutuamente esclusivi obbligherebbero il parametro a dire
|
|
232
|
+
// QUALE è aperto — un solo booleano per entrambi sottostima quando quello aperto
|
|
233
|
+
// è il più alto, e sommarli toglie righe al testo per un campo che non è a
|
|
234
|
+
// schermo.
|
|
235
|
+
const DETAIL_CHROME = 12;
|
|
228
236
|
// T91 — la ricerca dentro il detail: marginTop + riga del campo.
|
|
229
237
|
//
|
|
230
238
|
// `MODAL_HEIGHT` dice che il detail costa 0 ai due pane (li sostituisce), e da
|
package/package.json
CHANGED