@lamemind/loom-deck 0.21.0 → 0.21.2

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 CHANGED
@@ -83,18 +83,40 @@ Assegnazioni correnti:
83
83
  | immediata | `^P` | spawna la task selezionata sul **preflight** |
84
84
  | immediata | `^R` | spawna la task selezionata in **esecuzione** |
85
85
  | immediata | `f` | **forka** la sessione selezionata (solo col focus sul pane Sessions) |
86
- | immediata | `t` | terminale @project-root (surface standard launch) |
87
- | immediata | `c` | sessione Claude **nuda**: nessuna task, nessun prompt iniziale |
86
+ | immediata | `t` 💻 | terminale @project-root (surface standard launch) |
87
+ | immediata | `c` 🤖 | sessione Claude **nuda**: nessuna task, nessun prompt iniziale |
88
88
  | immediata | `w` | salva la vista corrente su disco |
89
89
  | launch | `1`…`9` | esegue il `command` della voce, con `cwd` = project root |
90
90
 
91
- Le voci `launch` sono elencate in una **riga di legenda** sotto il footer
92
- (`launch 1 📝 codium · 2 ☕ idea`): l'indice da solo è opaco, perché le voci sono
93
- custom per-progetto e non hanno una lettera fissa per app. Se non entrano in
94
- larghezza, la legenda si ferma a voci intere e mostra il contatore di quelle
95
- fuori riga mai un troncamento silenzioso. Il cap a `9` è imposto dai tasti-cifra,
96
- non dallo schema: un progetto può dichiarare più di 9 voci, quelle oltre la nona
97
- sono configurate ma non raggiungibili (e la legenda lo dice).
91
+ Il footer è **due righe con due nature diverse**:
92
+
93
+ ```
94
+ ⏎/^K/^P/^R spawn · ^F cerca · C nuova · E edit · S sort · F filtri · w salva
95
+ t 💻 · c 🤖 · 1 📝 codium · 2 idea
96
+ ```
97
+
98
+ - **tasti** — cosa puoi fare *qui e ora*. Solo le voci attive: quelle contestuali
99
+ compaiono quando il pane a fuoco le rende possibili e altrimenti spariscono,
100
+ invece di mostrarsi inerti. Fuori navigazione (`↑↓` `←→`), uscita (`q`) e
101
+ indicatore `focus:` — le prime due sono universali in qualunque TUI, il pane a
102
+ fuoco si vede già dall'evidenziazione.
103
+ - **surface** — *dove* puoi aprire qualcosa: prima le due built-in (`t`/`c`), poi
104
+ le voci `launch` del progetto. Stanno insieme perché hanno la stessa natura —
105
+ fire-once, `cwd` = project root, nessuno stato — e differiscono solo per essere
106
+ universali invece che custom.
107
+
108
+ L'indice da solo è opaco (le `launch` sono custom per-progetto, senza una lettera
109
+ fissa per app), quindi la riga espone la mappa e non il conteggio. Se non entrano
110
+ in larghezza, si ferma a voci intere e mostra il contatore di quelle fuori riga —
111
+ mai un troncamento silenzioso; le celle delle due built-in sono riservate a monte,
112
+ o le voci sfonderebbero il box di quel tanto. Il cap a `9` è imposto dai
113
+ tasti-cifra, non dallo schema: un progetto può dichiarare più di 9 voci, quelle
114
+ oltre la nona sono configurate ma non raggiungibili (e la riga lo dice).
115
+
116
+ Le emoji sono quelle del menu compass. Per il terminale compass usa 🖥️, che nel
117
+ frame Ink non sopravvive: `sanitize` lo sostituisce perché VTE lo disegna largo 1
118
+ mentre `string-width` ne conta 2 (invariante ① di `src/width.ts`) — 💻 è il
119
+ gemello concorde.
98
120
 
99
121
  `t` e `c` sono gemelle: entrambe aprono una surface del cappello nella stessa
100
122
  finestra Ptyxis, senza passare da un modale. `c` (minuscola, azione) e `C`
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import { discoverProjectSessions } from './sessions.js';
13
13
  import { buildRows, firstRowKey, moveRowSelection, rowIndexOfKey, searchSessions, selectedRow, DEFAULT_OPTIONS, MIN_QUERY, } from './search.js';
14
14
  import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, loadSessionIndex, } from './task-index.js';
15
15
  import { assembleSessionList, firstSelectableId, moveSelection, rowIndexOf, rowLabel, selectedSession, stripProjectCore, } from './session-list.js';
16
- import { launchLegend, loadIdentity, loadLaunch } from './config.js';
16
+ import { cellWidth, launchLegend, loadIdentity, loadLaunch } from './config.js';
17
17
  import { isCompact, layoutBudget, readerCapacity, searchListCapacity, searchPreviewCapacity, windowRange, } from './viewport.js';
18
18
  import { caretWindow, cut, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
19
19
  import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
@@ -1355,9 +1355,38 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1355
1355
  // T50 — il pin agisce su qualunque riga selezionata (anche stale, per
1356
1356
  // spinnarla); basta il focus sul pane e una selezione.
1357
1357
  const canPin = focus === 'sessions' && selSessionId !== null;
1358
+ // Le due surface built-in del cappello stanno in testa alla riga launch, non
1359
+ // fra i tasti: hanno la stessa natura delle voci `launch` — fire-once, cwd =
1360
+ // project root, nessuno stato — e la differenza è solo che sono universali
1361
+ // (nessun progetto le dichiara) invece che custom. Emoji del menu compass:
1362
+ // 🤖 = nuova sessione claude. Per il terminale compass usa 🖥️, che nel frame
1363
+ // Ink NON passa — `sanitize` lo sostituisce (VTE lo disegna largo 1,
1364
+ // string-width dice 2: discordante, invariante ① di width.ts) e resterebbe un
1365
+ // `·` muto. 💻 è il gemello concorde; il `sanitize` qui rende il vincolo
1366
+ // automatico invece che da ricordare.
1367
+ const surfaceLegend = sanitize('t 💻 · c 🤖');
1358
1368
  // Larghezza dal medesimo hook che dà l'altezza: dopo un resize la legenda si
1359
- // ricalcola con lo stesso re-render che ridimensiona i pane.
1360
- const legend = launchLegend(launch, columns);
1369
+ // ricalcola con lo stesso re-render che ridimensiona i pane. Le celle delle
1370
+ // surface (più il ` · ` che le separa dalle voci) sono già spese sulla riga →
1371
+ // vanno riservate, o le voci launch la sfonderebbero di quel tanto.
1372
+ const legend = launchLegend(launch, columns, cellWidth(surfaceLegend) + 3);
1373
+ // Legenda della modalità normale. Elenca SOLO i tasti che fanno qualcosa qui e
1374
+ // ora: le voci contestuali compaiono quando il pane a fuoco le rende possibili
1375
+ // e altrimenti spariscono, invece di annunciarsi inerti con un `—`.
1376
+ // Fuori: navigazione (`↑↓` `←→`) e uscita (`q`), universali in qualunque TUI, e
1377
+ // l'indicatore `focus:` — il pane a fuoco si vede già dall'evidenziazione, e
1378
+ // ridirlo a parole costava colonne su una riga che tronca in silenzio.
1379
+ const keyLegend = sanitize([
1380
+ ...(canSpawn ? ['⏎/^K/^P/^R spawn'] : canResume ? ['⏎ resume'] : []),
1381
+ ...(canResume ? ['f fork'] : []),
1382
+ ...(canPin ? ['p pin', 'N nota'] : []),
1383
+ '^F cerca',
1384
+ 'C nuova',
1385
+ 'E edit',
1386
+ 'S sort',
1387
+ 'F filtri',
1388
+ 'w salva',
1389
+ ].join(' · '));
1361
1390
  // ── T52 · schermate sostitutive ─────────────────────────────────────────
1362
1391
  // Ricerca e reader sono gli unici modali che NON stanno in flusso sopra i
1363
1392
  // pane: una lista di occorrenze non entra in un box da 4 righe. Prendono
@@ -1408,7 +1437,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1408
1437
  // Ptyxis significa un frame intero versato nello scrollback per ogni tick del
1409
1438
  // poll. Tutto ciò che varia in altezza (le due liste e la descrizione del
1410
1439
  // dettaglio) riceve qui la propria capienza.
1411
- const launchLine = mode === 'normal' && launch.length > 0;
1440
+ // Le surface built-in `t`/`c` la rendono sempre presente in modalità normale:
1441
+ // non dipende più da quante voci `launch` il progetto dichiara.
1442
+ const launchLine = mode === 'normal';
1412
1443
  const detailParts = detail ? detailMetaOf(detail) : null;
1413
1444
  const budget = layoutBudget({
1414
1445
  rows,
@@ -1440,7 +1471,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1440
1471
  if (budget.compact) {
1441
1472
  return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? 'spot', " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
1442
1473
  }
1443
- 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"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7", ' ', canSpawn ? '⏎/^K/^P/^R spawn' : canResume ? '⏎ resume' : '⏎ —', canResume ? ' · f fork' : '', canPin ? ' · p pin · N nota' : '', " \u00B7 ^F cerca \u00B7 C nuova \u00B7 E edit \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7 t term \u00B7 c claude \u00B7 q esci \u00B7 focus:", ' ', _jsx(Text, { color: "cyan", children: focus })] })), mode === 'normal' && launch.length > 0 ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["launch ", legend.shown, legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === '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, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, rows: windowRows, total: assembled.pinnedCount + assembled.contextTotal, pinnedCount: assembled.pinnedCount, hidden: assembled.contextHidden, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
1474
+ 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, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, rows: windowRows, total: assembled.pinnedCount + assembled.contextTotal, pinnedCount: assembled.pinnedCount, hidden: assembled.contextHidden, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
1444
1475
  }
1445
1476
  const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
1446
1477
  // Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
package/dist/config.js CHANGED
@@ -66,12 +66,17 @@ export function cellWidth(s) {
66
66
  // custom per-progetto, non hanno una lettera fissa per app), quindi la resa deve
67
67
  // esporre la mappa, non il conteggio. Degradazione mai silenziosa: ciò che non
68
68
  // entra in larghezza finisce in un contatore esplicito, non troncato a metà.
69
- export function launchLegend(entries, columns) {
69
+ // `reserved` = celle già occupate sulla stessa riga da chi la renderizza (le
70
+ // surface built-in `t`/`c`, che precedono le voci). Parametro e non aritmetica
71
+ // sul chiamante: `columns` significa "colonne del terminale" e passargliene di
72
+ // meno per fargli credere a uno schermo più stretto renderebbe il calcolo
73
+ // illeggibile al primo che lo rilegge.
74
+ export function launchLegend(entries, columns, reserved = 0) {
70
75
  const reachable = entries.slice(0, LAUNCH_MAX);
71
76
  const unreachable = entries.length - reachable.length;
72
- // bordo + padding del box + prefisso "launch " 12 celle; pavimento a 24 per
77
+ // bordo + padding del box 4 celle, più 2 di margine; pavimento a 24 per
73
78
  // non degenerare a legenda vuota su terminali strettissimi.
74
- const budget = Math.max(24, columns - 12);
79
+ const budget = Math.max(24, columns - 6 - reserved);
75
80
  // I fallback di parseLaunch (`▸` per emoji, command per label) sono già
76
81
  // applicati a monte: qui non si re-implementano né si assumono campi popolati.
77
82
  const parts = reachable.map((e, i) => `${i + 1} ${e.emoji} ${e.label}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.21.0",
3
+ "version": "0.21.2",
4
4
  "description": "Deck TUI Ink per-progetto della famiglia loom: legge tasks.md e spawna sessioni Claude Code bound via LOOM_TASK",
5
5
  "type": "module",
6
6
  "bin": {