@lamemind/loom-deck 0.21.1 → 0.22.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 CHANGED
@@ -88,21 +88,35 @@ Assegnazioni correnti:
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
- (`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).
98
-
99
- In legenda le due surface del cappello portano l'emoji del menu compass (`t 💻`,
100
- `c 🤖`) invece della parola. Per il terminale compass usa 🖥️, che nel frame Ink
101
- non sopravvive: `sanitize` lo sostituisce perché VTE lo disegna largo 1 mentre
102
- `string-width` ne conta 2 (invariante ① di `src/width.ts`) — 💻 è il gemello
103
- concorde. La legenda elenca solo i tasti attivi in quel momento: navigazione,
104
- uscita e indicatore di focus non ci sono, e le voci contestuali spariscono
105
- invece di mostrarsi inerti.
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.
106
120
 
107
121
  `t` e `c` sono gemelle: entrambe aprono una surface del cappello nella stessa
108
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';
@@ -203,7 +203,7 @@ function runLaunch(entry, cwd) {
203
203
  // sé nella stessa finestra, invece di sparpagliare finestre.
204
204
  // Nessun `-- CMD`: l'azione È aprire la shell (differenza dalle launch custom,
205
205
  // che eseguono un comando dentro `bash -lic`).
206
- // `-T <title>` col core `<owner> <name>` tiene la finestra matchabile da compass
206
+ // `-T <title>` con la chiave `🖥️ <name>` tiene la finestra matchabile da compass
207
207
  // anche mentre la tab attiva è il terminale; senza identità nel file config si
208
208
  // spawna senza titolo (la surface resta funzionante, il progetto risulta assente
209
209
  // dal radar finché quella tab è in primo piano).
@@ -517,11 +517,15 @@ function Deck({ cwd, tasksPath, tasksDir }) {
517
517
  const launch = useMemo(() => loadLaunch(cwd), [cwd]);
518
518
  // Identità (T37): titolo delle tab terminale spawnate col tasto `t`.
519
519
  const identity = useMemo(() => loadIdentity(cwd), [cwd]);
520
- // T53 — `<owner> <name>`: il core che ogni titolo di tab porta, quindi la
521
- // colonna costante da togliere quando serve spazio. Hoistato qui perché ora
522
- // lo consumano DUE schermate (lista e ricerca): calcolarlo su ogni call site
523
- // è il modo in cui le due smettono di togliere la stessa cosa.
524
- const projectCore = identity ? `${identity.owner} ${identity.name}` : null;
520
+ // T53 — il core che ogni titolo di tab porta, quindi la colonna costante da
521
+ // togliere quando serve spazio. Hoistato qui perché ora lo consumano DUE
522
+ // schermate (lista e ricerca): calcolarlo su ogni call site è il modo in cui
523
+ // le due smettono di togliere la stessa cosa.
524
+ // T58 il core è il solo `name`, non `<emoji> <name>` come la chiave di match
525
+ // di compass: qui non serve entropia (è un taglio cosmetico, non un matcher) e
526
+ // il nome nudo ripulisce anche i titoli storici, scritti quando la formula
527
+ // includeva l'owner.
528
+ const projectCore = identity ? identity.name : null;
525
529
  // La vista è una trasformazione DERIVATA, applicata a valle del load: il
526
530
  // polling di tasks.md continua a funzionare senza saperne nulla.
527
531
  const { visible: viewTasks, hidden: hiddenTasks } = useMemo(() => applyView(tasks, view), [tasks, view]);
@@ -1310,7 +1314,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1310
1314
  }
1311
1315
  }
1312
1316
  else if (input === 't') {
1313
- const title = identity ? `🖥️ ${identity.owner} ${identity.name} [term]` : null;
1317
+ const title = identity ? `🖥️ ${identity.name} [term]` : null;
1314
1318
  const child = spawnTerminal(cwd, title);
1315
1319
  child.on('error', () => setNote('⚠ t → ptyxis non lanciabile'));
1316
1320
  setNote(`t → terminale su ${projectName}`);
@@ -1355,9 +1359,21 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1355
1359
  // T50 — il pin agisce su qualunque riga selezionata (anche stale, per
1356
1360
  // spinnarla); basta il focus sul pane e una selezione.
1357
1361
  const canPin = focus === 'sessions' && selSessionId !== null;
1362
+ // Le due surface built-in del cappello stanno in testa alla riga launch, non
1363
+ // fra i tasti: hanno la stessa natura delle voci `launch` — fire-once, cwd =
1364
+ // project root, nessuno stato — e la differenza è solo che sono universali
1365
+ // (nessun progetto le dichiara) invece che custom. Emoji del menu compass:
1366
+ // 🤖 = nuova sessione claude. Per il terminale compass usa 🖥️, che nel frame
1367
+ // Ink NON passa — `sanitize` lo sostituisce (VTE lo disegna largo 1,
1368
+ // string-width dice 2: discordante, invariante ① di width.ts) e resterebbe un
1369
+ // `·` muto. 💻 è il gemello concorde; il `sanitize` qui rende il vincolo
1370
+ // automatico invece che da ricordare.
1371
+ const surfaceLegend = sanitize('t 💻 · c 🤖');
1358
1372
  // 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);
1373
+ // ricalcola con lo stesso re-render che ridimensiona i pane. Le celle delle
1374
+ // surface (più il ` · ` che le separa dalle voci) sono già spese sulla riga →
1375
+ // vanno riservate, o le voci launch la sfonderebbero di quel tanto.
1376
+ const legend = launchLegend(launch, columns, cellWidth(surfaceLegend) + 3);
1361
1377
  // Legenda della modalità normale. Elenca SOLO i tasti che fanno qualcosa qui e
1362
1378
  // ora: le voci contestuali compaiono quando il pane a fuoco le rende possibili
1363
1379
  // e altrimenti spariscono, invece di annunciarsi inerti con un `—`.
@@ -1374,14 +1390,6 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1374
1390
  'S sort',
1375
1391
  'F filtri',
1376
1392
  'w salva',
1377
- // Emoji delle surface, le stesse del menu compass: 🤖 = nuova sessione
1378
- // claude. Per il terminale compass usa 🖥️, che nel frame Ink NON passa —
1379
- // `sanitize` lo sostituisce (VTE lo disegna largo 1, string-width dice 2:
1380
- // discordante, invariante ① di width.ts) e resterebbe un `·` muto. 💻 è il
1381
- // gemello concorde più vicino. Il `sanitize` attorno all'array è la rete
1382
- // che rende quel vincolo automatico, non una cosa da ricordarsi.
1383
- 't 💻',
1384
- 'c 🤖',
1385
1393
  ].join(' · '));
1386
1394
  // ── T52 · schermate sostitutive ─────────────────────────────────────────
1387
1395
  // Ricerca e reader sono gli unici modali che NON stanno in flusso sopra i
@@ -1433,7 +1441,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1433
1441
  // Ptyxis significa un frame intero versato nello scrollback per ogni tick del
1434
1442
  // poll. Tutto ciò che varia in altezza (le due liste e la descrizione del
1435
1443
  // dettaglio) riceve qui la propria capienza.
1436
- const launchLine = mode === 'normal' && launch.length > 0;
1444
+ // Le surface built-in `t`/`c` la rendono sempre presente in modalità normale:
1445
+ // non dipende più da quante voci `launch` il progetto dichiara.
1446
+ const launchLine = mode === 'normal';
1437
1447
  const detailParts = detail ? detailMetaOf(detail) : null;
1438
1448
  const budget = layoutBudget({
1439
1449
  rows,
@@ -1465,7 +1475,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1465
1475
  if (budget.compact) {
1466
1476
  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"] })] }));
1467
1477
  }
1468
- 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' && launch.length > 0 ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [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] }));
1478
+ 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] }));
1469
1479
  }
1470
1480
  const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
1471
1481
  // Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
@@ -1550,11 +1560,12 @@ function searchTitleWidth(columns) {
1550
1560
  * Cosa scrivere sulla riga-gruppo per distinguere una conversazione dall'altra.
1551
1561
  *
1552
1562
  * Non basta `session.title`: quel titolo è la label della tab Ptyxis, cioè
1553
- * `<emoji> <owner> <name>` più un eventuale suffisso. Su una lista tutta dello
1563
+ * `<emoji> <name>` più un eventuale suffisso. Su una lista tutta dello
1554
1564
  * stesso progetto (D3) è una COLONNA COSTANTE — tre righe su quattro
1555
1565
  * identiche, che è esattamente il difetto che la riga-gruppo doveva evitare.
1556
1566
  *
1557
- * Si toglie quindi il core `<owner> <name>` (noto dal file config), e se ciò
1567
+ * Si toglie quindi il core `<name>` con tutto ciò che lo precede (noto dal file
1568
+ * config), e se ciò
1558
1569
  * che resta è vuoto o duplica la task già mostrata nella sua colonna, si
1559
1570
  * ripiega sul primo prompt — l'unica cosa che davvero identifica quella
1560
1571
  * conversazione e non un'altra.
package/dist/config.js CHANGED
@@ -66,14 +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
77
  // bordo + padding del box ≈ 4 celle, più 2 di margine; pavimento a 24 per
73
- // non degenerare a legenda vuota su terminali strettissimi. Il prefisso
74
- // "launch " non c'è più (la riga si riconosce dalla forma `indice emoji
75
- // label`), quindi le 7 celle che occupava tornano alle voci.
76
- const budget = Math.max(24, columns - 6);
78
+ // non degenerare a legenda vuota su terminali strettissimi.
79
+ const budget = Math.max(24, columns - 6 - reserved);
77
80
  // I fallback di parseLaunch (`▸` per emoji, command per label) sono già
78
81
  // applicati a monte: qui non si re-implementano né si assumono campi popolati.
79
82
  const parts = reachable.map((e, i) => `${i + 1} ${e.emoji} ${e.label}`);
@@ -99,10 +102,10 @@ export function launchLegend(entries, columns) {
99
102
  export function parseIdentity(raw) {
100
103
  if (!raw || typeof raw !== 'object')
101
104
  return null;
102
- const { owner, name } = raw;
103
- if (typeof owner !== 'string' || !owner || typeof name !== 'string' || !name)
105
+ const { name } = raw;
106
+ if (typeof name !== 'string' || !name)
104
107
  return null;
105
- return { owner, name };
108
+ return { name };
106
109
  }
107
110
  /** File assente o malformato → nessuna identità. Mai un throw. */
108
111
  export function loadIdentity(projectRoot) {
@@ -17,13 +17,14 @@
17
17
  import { cut, termWidth } from './width.js';
18
18
  // ── T53 · etichetta di riga ────────────────────────────────────────────────
19
19
  /**
20
- * Toglie dal titolo il core `<owner> <name>` del progetto e la punteggiatura di
21
- * giunzione che resta appesa davanti.
20
+ * Toglie dal titolo il core del progetto (il suo `name`) e tutto ciò che lo
21
+ * precede — emoji inclusa — più la punteggiatura di giunzione che resta appesa
22
+ * davanti.
22
23
  *
23
- * Il titolo di una sessione È la label della tab Ptyxis (`🧵 LOCAL loom-works ·
24
- * T52`): dentro un deck che mostra un progetto solo, il core è una COLONNA
25
- * COSTANTE ripetuta su ogni riga. Toglierlo libera le colonne per ciò che
26
- * distingue davvero una conversazione dall'altra.
24
+ * Il titolo di una sessione È la label della tab Ptyxis (`🧵 loom-works · T52`):
25
+ * dentro un deck che mostra un progetto solo, il core è una COLONNA COSTANTE
26
+ * ripetuta su ogni riga. Toglierlo libera le colonne per ciò che distingue
27
+ * davvero una conversazione dall'altra.
27
28
  *
28
29
  * `core` null (file config assente o senza identità) → titolo intatto: senza
29
30
  * sapere cosa togliere non si indovina, si lascia stare.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
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": {
package/scripts/deck-run CHANGED
@@ -159,11 +159,11 @@ PROFILE_UUID="${LOOM_DECK_PROFILE_UUID:-5a36ae48df1c4d4882f43060e3e59656}"
159
159
  WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
160
160
 
161
161
  # ── Titolo tab = label standard loom (matchabile da compass) ──────────────────
162
- # compass matcha le finestre con `window.title.includes(project.label)`, dove la
163
- # label è DERIVATA dal contratto committato .claude/loom-works.json:
164
- # `{emoji} {owner} {name}` → es. "🧵 LOCAL loom-works". Un titolo "cc <task>"
165
- # non contiene la label → finestra ORFANA (progetto appare spento anche mentre ci
166
- # lavori dentro). Deriviamo la label dal file e la usiamo come titolo, con
162
+ # compass matcha le finestre cercando nel titolo un prefisso `{emoji} {name}`,
163
+ # dove i due token sono DERIVATI dal contratto committato .claude/loom-works.json
164
+ # → es. "🧵 loom-works". Un titolo "cc <task>" non contiene la label → finestra
165
+ # ORFANA (progetto appare spento anche mentre ci lavori dentro). Deriviamo la
166
+ # label dal file e la usiamo come titolo, con
167
167
  # suffisso "· <task>" per restare distinguibili fra più tab claude (il suffisso
168
168
  # non rompe il match: è .includes, non equals — stesso pattern del "· deck").
169
169
  # Fallback su "cc <task>" se manca il file o jq (progetto non loom-registered).
@@ -191,8 +191,8 @@ _cfg=""
191
191
  # la sessione è bound, nessun suffisso quando è nuda.
192
192
  if [[ $NO_TASK -eq 1 ]]; then TITLE="cc"; else TITLE="cc ${TASK}"; fi
193
193
  if [[ -n "$_cfg" ]]; then
194
- _label="$(jq -r 'if (.emoji and .owner and .name)
195
- then "\(.emoji) \(.owner) \(.name)" else empty end' \
194
+ _label="$(jq -r 'if (.emoji and .name)
195
+ then "\(.emoji) \(.name)" else empty end' \
196
196
  "$_cfg" 2>/dev/null || true)"
197
197
  if [[ -n "${_label:-}" ]]; then
198
198
  if [[ $NO_TASK -eq 1 ]]; then TITLE="${_label}"; else TITLE="${_label} · ${TASK}"; fi