@lamemind/loom-deck 0.65.2 → 0.66.1

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/actions.js CHANGED
@@ -18,7 +18,7 @@
18
18
  import { useMemo, useRef } from 'react';
19
19
  import { randomUUID } from 'node:crypto';
20
20
  import { loadPromptCatalog, modelFor } from './prompt-catalog.js';
21
- import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, } from './task-index.js';
21
+ import { appendNote, appendPin, appendPriority, appendSessionRecord, appendTaskBinding, } from './task-index.js';
22
22
  import { neighborId } from './session-list.js';
23
23
  import { cut, cutMiddle } from './width.js';
24
24
  import { saveView, viewFilePath } from './view-store.js';
@@ -114,12 +114,21 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
114
114
  // nomi diversi per la stessa conversazione. Vale anche sugli acceleratori
115
115
  // della lista (^K/^P/^R), che passano di qui con `spawnNote` sul proprio
116
116
  // default '' (P1 preflight) — nascono quindi titolati anche loro.
117
- function spawnForTask(id, kind, modelKind, spawnNote = '', prompt) {
117
+ function spawnForTask(id, kind, modelKind, spawnNote = '', prompt, priority = false) {
118
118
  const sid = randomUUID();
119
119
  const note = spawnNote || fallbackTitle(tasksDir, id, kind) || '';
120
120
  appendTaskBinding(cwd, sid, id);
121
121
  if (note)
122
122
  appendNote(cwd, sid, note);
123
+ // T158 — la marca si appende NELLO STESSO momento del binding, prima che la
124
+ // tab esista: il `sessionId` è già noto (lo pinna `deck-run`), quindi il primo
125
+ // cambio di stato della conversazione trova la marca già su disco. Scriverla
126
+ // dopo lo spawn sarebbe una corsa contro il primo `Stop`.
127
+ //
128
+ // Solo il ramo acceso scrive: un `priority:false` allo spawn di ogni sessione
129
+ // riempirebbe il sidecar di smarcature di conversazioni mai marcate.
130
+ if (priority)
131
+ appendPriority(cwd, sid, true);
123
132
  const spawned = spawnDeck(id, cwd, sid, kind, modelKind, note, prompt);
124
133
  spawned.child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
125
134
  // Il modello resta SEMPRE visibile anche quando è il default, perché è un
@@ -279,6 +288,33 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
279
288
  model.setSelSessionId(landing);
280
289
  setNote(`${isPinned ? 'unpin' : '📌 pin'} ${sid.slice(0, 8)}`);
281
290
  }
291
+ /**
292
+ * T158 — `a`: marca/smarca la conversazione selezionata come PRIORITARIA.
293
+ *
294
+ * Gemella di `togglePin` nella forma (azione immediata, scrive il sidecar,
295
+ * ricarica subito) ma non nell'effetto: la marca non sposta e non filtra
296
+ * niente, quindi non c'è nessun vicino su cui atterrare — la riga resta dov'è
297
+ * e cambia solo il glifo.
298
+ *
299
+ * Vale anche su una pinnata STALE: il transcript non c'è più, ma la marca è un
300
+ * attributo del `sessionId` e non del transcript, e l'unico posto da cui
301
+ * toglierla resta quella riga.
302
+ */
303
+ function togglePriority() {
304
+ if (model.focus !== 'sessions') {
305
+ setNote('a → priorità: seleziona una sessione (→ per il pane)');
306
+ return;
307
+ }
308
+ const sid = model.selSessionId;
309
+ if (!sid) {
310
+ setNote('a → nessuna sessione da marcare');
311
+ return;
312
+ }
313
+ const on = model.sessionPriority.has(sid);
314
+ appendPriority(cwd, sid, !on);
315
+ model.reloadSessions();
316
+ setNote(`${on ? 'priorità via' : '🚨 prioritaria'} ${sid.slice(0, 8)}`);
317
+ }
282
318
  /** `t` — terminale a project root, con un titolo che il matcher di compass riconosce. */
283
319
  function openTerminal() {
284
320
  const title = model.identity ? `🖥️ ${model.identity.name} [term]` : null;
@@ -365,6 +401,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
365
401
  assignSession,
366
402
  forkSession,
367
403
  togglePin,
404
+ togglePriority,
368
405
  drainInbox,
369
406
  unwrapPath,
370
407
  openTerminal,
package/dist/cli.js CHANGED
@@ -270,7 +270,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
270
270
  inboxPane: model.rightPane === 'inbox',
271
271
  hasInbox: model.selInbox !== null,
272
272
  gitlink: gitlink.enabled,
273
- }), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark }), model.rightPane === 'inbox' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
273
+ }), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark }), model.rightPane === 'inbox' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, sessionPriority: model.sessionPriority, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
274
274
  }
275
275
  const cwd = process.cwd();
276
276
  // T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
@@ -111,7 +111,7 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
111
111
  // di STATO in fondo al frame (il feedback di un'azione). Due concetti diversi
112
112
  // a una lettera di distanza sarebbero una trappola di lettura — e di
113
113
  // scrittura, visto che `setNote` compare in quasi ogni ramo.
114
- const { sessions, bindings, forkOf, pinned, notes: sessionNotes, live, reload: reloadSessions, } = useSessions(cwd);
114
+ const { sessions, bindings, forkOf, pinned, notes: sessionNotes, priority: sessionPriority, live, reload: reloadSessions, } = useSessions(cwd);
115
115
  const [focus, setFocus] = useState('tasks');
116
116
  // T39 — selezione KEYED SU ID, non su indice. Con una vista trasformata
117
117
  // (filtro/sort) l'indice non identifica più la stessa task: leggere l'array
@@ -470,6 +470,7 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
470
470
  forkOf,
471
471
  pinned,
472
472
  sessionNotes,
473
+ sessionPriority,
473
474
  live,
474
475
  reloadSessions,
475
476
  detail,
package/dist/frame.js CHANGED
@@ -100,7 +100,10 @@ export function deckLegend(state) {
100
100
  // allo spawn, e due nomi per una cosa sola li paga chi legge le due
101
101
  // schermate. Rename di sola ETICHETTA: il dato resta `note` nel sidecar e
102
102
  // `--title-note` in deck-run, dove rinominarlo sarebbe un breaking.
103
- ...(canPin ? ['p pin', 'N titolo', 'A assegna'] : []),
103
+ // T158 — `a` sta accanto a `p` perché condivide la condizione (`canPin`:
104
+ // focus sul pane e una riga selezionata, stale compresa) e la forma: due
105
+ // attributi della stessa conversazione, scritti nello stesso sidecar.
106
+ ...(canPin ? ['p pin', 'a priorità', 'N titolo', 'A assegna'] : []),
104
107
  // T121 — le due voci nominano il GESTO e non l'oggetto: «status» da solo
105
108
  // sarebbe indistinguibile dal recap della task su `^K`, che è un'altra
106
109
  // cosa e sta a due voci di distanza.
package/dist/hooks.js CHANGED
@@ -143,6 +143,7 @@ export function useSessions(projectRoot) {
143
143
  forkOf: new Map(),
144
144
  pinned: new Map(),
145
145
  notes: new Map(),
146
+ priority: new Set(),
146
147
  live: new Map(),
147
148
  });
148
149
  // T50 — pin/unpin scrive il sidecar e vuole feedback IMMEDIATO, non al
@@ -160,7 +161,13 @@ export function useSessions(projectRoot) {
160
161
  }
161
162
  catch {
162
163
  sessions = [];
163
- index = { bindings: new Map(), forkOf: new Map(), pinned: new Map(), notes: new Map() };
164
+ index = {
165
+ bindings: new Map(),
166
+ forkOf: new Map(),
167
+ pinned: new Map(),
168
+ notes: new Map(),
169
+ priority: new Set(),
170
+ };
164
171
  }
165
172
  // T62 — le vive stanno sullo STESSO tick delle altre fonti, non su una
166
173
  // scala propria come `useArchivable`: `status` cambia a ogni turno, quindi
@@ -174,10 +181,11 @@ export function useSessions(projectRoot) {
174
181
  catch {
175
182
  live = new Map();
176
183
  }
177
- const { bindings, forkOf, pinned, notes } = index;
178
- // La signature copre anche fork, pin e note: un record di lineage, un
179
- // toggle di pin o una nota appena scritta cambiano la lista renderizzata,
180
- // quindi devono forzare il re-render come farebbe un binding nuovo.
184
+ const { bindings, forkOf, pinned, notes, priority } = index;
185
+ // La signature copre anche fork, pin, note e marca di priorità: un record
186
+ // di lineage, un toggle di pin, una nota appena scritta o un 🚨 acceso
187
+ // cambiano la lista renderizzata, quindi devono forzare il re-render come
188
+ // farebbe un binding nuovo.
181
189
  const sig = sessions.map((s) => `${s.sessionId}:${s.ts}`).join('|') +
182
190
  '#' +
183
191
  [...bindings.entries()].map(([k, v]) => `${k}=${v}`).sort().join(',') +
@@ -188,11 +196,13 @@ export function useSessions(projectRoot) {
188
196
  '#' +
189
197
  [...notes.entries()].map(([k, v]) => `${k}"${v}`).sort().join(',') +
190
198
  '#' +
199
+ [...priority].sort().join(',') +
200
+ '#' +
191
201
  liveSig(live);
192
202
  if (sig === lastSig)
193
203
  return;
194
204
  lastSig = sig;
195
- setState({ sessions, bindings, forkOf, pinned, notes, live });
205
+ setState({ sessions, bindings, forkOf, pinned, notes, priority, live });
196
206
  };
197
207
  reloadRef.current = reload;
198
208
  reload();
package/dist/input.js CHANGED
@@ -479,6 +479,12 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
479
479
  // selezionata del pane sessioni.
480
480
  actions.togglePin();
481
481
  }
482
+ else if (input === 'a') {
483
+ // T158 — marca prioritaria, gemella di `p`. Minuscola perché è
484
+ // un'azione immediata, e `a` per «allarme» (il glifo di riga è 🚨): la
485
+ // `A` maiuscola resta al modale di assegnazione, come `f`/`F` e `c`/`C`.
486
+ actions.togglePriority();
487
+ }
482
488
  else if (input === 'm') {
483
489
  // T148/T154 — bersaglio deciso dal focus E dalla selezione (P2): con
484
490
  // una riga sessioni selezionata (stale compresa, P3) scorre il modello
@@ -67,6 +67,21 @@ export function parseLiveEntry(content, pid) {
67
67
  // il guard esiste per escludere), `cwd` è il filtro di progetto.
68
68
  if (!sessionId || !procStart || !cwd)
69
69
  return null;
70
+ // Il registry tiene un file anche per i processi che NON sono una
71
+ // conversazione dell'utente: subagent SDK, fork parkeggiati in background,
72
+ // spare pty host. Hanno lo stesso `cwd` del progetto e un pid davvero vivo —
73
+ // quindi `isAlive` non li scarta — ma nessuna tab a cui tornare: elencarli
74
+ // conta più conversazioni di quante ne siano aperte, e `--resume` su una di
75
+ // esse non riporta l'utente da nessuna parte.
76
+ //
77
+ // Servono DUE campi perché uno solo non discrimina: un job in background di
78
+ // Claude Code scrive `entrypoint:"cli"` come una sessione vera e si distingue
79
+ // solo per `kind:"bg"`. Entrambi assenti valgono come sessione interattiva —
80
+ // una versione del CLI che non li scrivesse non deve svuotare la lista.
81
+ if ((typeof d.entrypoint === 'string' ? d.entrypoint : 'cli') !== 'cli')
82
+ return null;
83
+ if ((typeof d.kind === 'string' ? d.kind : 'interactive') !== 'interactive')
84
+ return null;
70
85
  return {
71
86
  entry: {
72
87
  sessionId,
@@ -22,16 +22,23 @@ import { ACTION_HOTKEYS, DETAIL_ACTIONS, MODELS, MODEL_DEFAULT, specializeRecap,
22
22
  import { taskIsEpic } from '../tasks.js';
23
23
  import { fieldsKey } from '../fields.js';
24
24
  import { loadPromptCatalog, modelFor, promptFor } from '../prompt-catalog.js';
25
- // T117 — le quattro righe dell'area di compilazione del detail, nell'ordine in
26
- // cui si leggono dall'alto. Gli indici sono nominati perché li usano insieme
27
- // l'handler dei tasti e la resa, e un `2` nudo in due file diversi è la
28
- // coordinata che scade appena una riga si sposta.
29
- export const DROW = { action: 0, prompt: 1, model: 2, title: 3 };
25
+ // T117 — le righe dell'area di compilazione del detail, nell'ordine in cui si
26
+ // leggono dall'alto. Gli indici sono nominati perché li usano insieme l'handler
27
+ // dei tasti e la resa, e un `2` nudo in due file diversi è la coordinata che
28
+ // scade appena una riga si sposta.
29
+ //
30
+ // T158 — `priority` sta in coda e non accanto al titolo: le prime quattro righe
31
+ // sono i parametri del COMANDO che parte, questa è un attributo della
32
+ // conversazione che nasce, e vive nel sidecar invece che nell'argv di deck-run.
33
+ export const DROW = { action: 0, prompt: 1, model: 2, title: 3, priority: 4 };
34
+ /** T158 — le due voci della riga priorità, nell'ordine degli indici. */
35
+ export const PRIORITY_CHOICES = ['no', '🚨 sì'];
30
36
  export const DETAIL_FIELDS = [
31
37
  { kind: 'choice', count: DETAIL_ACTIONS.length, hotkeys: ACTION_HOTKEYS },
32
38
  { kind: 'text' },
33
39
  { kind: 'choice', count: MODELS.length },
34
40
  { kind: 'text' },
41
+ { kind: 'choice', count: PRIORITY_CHOICES.length },
35
42
  ];
36
43
  export function useSheetOverlay(deps) {
37
44
  const { rows, columns, setMode, setNote, onAction } = deps;
@@ -51,6 +58,11 @@ export function useSheetOverlay(deps) {
51
58
  // T117 — il prompt iniziale, EDITABILE. Quello che si legge nel campo è quello
52
59
  // che parte: non un'anteprima di qualcos'altro.
53
60
  const [prompt, setPrompt] = useState('');
61
+ // T158 — la conversazione nasce già marcata prioritaria? Si azzera a ogni
62
+ // apertura come gli altri campi: una marca armata che sopravvive alla chiusura
63
+ // farebbe partire prioritaria la conversazione successiva, senza che nulla a
64
+ // schermo lo dica.
65
+ const [priority, setPriority] = useState(false);
54
66
  /** La task aperta è un cappello (`Size: Epic`)? Deciso UNA VOLTA all'apertura,
55
67
  * dal testo che il detail ha già in mano: rifarlo a ogni cambio di azione
56
68
  * riparserebbe l'intero task file per un campo dell'header. */
@@ -125,6 +137,7 @@ export function useSheetOverlay(deps) {
125
137
  setModel(modelFor(catalog, kind) ?? MODEL_DEFAULT);
126
138
  setSpawnNote('');
127
139
  setPrompt(promptFor(catalog, kind, next.id));
140
+ setPriority(false);
128
141
  setCursor({ row: DROW.action, caret: 0 });
129
142
  setFind(null);
130
143
  setOccIdx(0);
@@ -153,8 +166,21 @@ export function useSheetOverlay(deps) {
153
166
  const fieldsIO = {
154
167
  text: (row) => (row === DROW.prompt ? prompt : spawnNote),
155
168
  setText: (row, next) => (row === DROW.prompt ? setPrompt(next) : setSpawnNote(next)),
156
- choice: (row) => (row === DROW.action ? action : Math.max(0, MODELS.indexOf(model))),
157
- setChoice: (row, index) => row === DROW.action ? selectAction(index) : setModel(MODELS[index]),
169
+ choice: (row) => row === DROW.action
170
+ ? action
171
+ : row === DROW.priority
172
+ ? priority
173
+ ? 1
174
+ : 0
175
+ : Math.max(0, MODELS.indexOf(model)),
176
+ setChoice: (row, index) => {
177
+ if (row === DROW.action)
178
+ selectAction(index);
179
+ else if (row === DROW.priority)
180
+ setPriority(index === 1);
181
+ else
182
+ setModel(MODELS[index]);
183
+ },
158
184
  };
159
185
  function close() {
160
186
  setMode('normal');
@@ -259,7 +285,7 @@ export function useSheetOverlay(deps) {
259
285
  const text = prompt.trim();
260
286
  close();
261
287
  if (id)
262
- onAction(id, specializeRecap(act.kind, epic), model, note, text);
288
+ onAction(id, specializeRecap(act.kind, epic), model, note, text, priority);
263
289
  return;
264
290
  }
265
291
  if (key.pageUp) {
@@ -284,6 +310,7 @@ export function useSheetOverlay(deps) {
284
310
  model,
285
311
  spawnNote,
286
312
  prompt,
313
+ priority,
287
314
  cursor,
288
315
  find,
289
316
  lines,
@@ -25,6 +25,18 @@ export function appendPin(projectRoot, sessionId, pinned) {
25
25
  export function appendNote(projectRoot, sessionId, note) {
26
26
  appendSessionRecord(projectRoot, { sessionId, note });
27
27
  }
28
+ /**
29
+ * T158 — marca/smarca la conversazione come prioritaria: stessa forma di
30
+ * `appendPin`, quarto campo mutabile del record.
31
+ *
32
+ * Il campo lo leggono TRE attori a rilascio indipendente — il deck, compass e
33
+ * l'hook `announce-state.sh` del plugin. Il nome, una volta scritto nei file
34
+ * degli utenti, non si rinomina senza migrazione: è un contratto fra tre repo,
35
+ * non un dettaglio interno di questo modulo.
36
+ */
37
+ export function appendPriority(projectRoot, sessionId, priority) {
38
+ appendSessionRecord(projectRoot, { sessionId, priority });
39
+ }
28
40
  // Una sola lettura del JSONL per entrambe le mappe: il deck poll-a l'indice a
29
41
  // ogni tick, leggere il file due volte raddoppierebbe l'I/O per nulla.
30
42
  // Last-wins per campo (un re-pin dello stesso sessionId sovrascrive), e i due
@@ -35,12 +47,13 @@ export function loadSessionIndex(projectRoot) {
35
47
  const forkOf = new Map();
36
48
  const pinned = new Map();
37
49
  const notes = new Map();
50
+ const priority = new Set();
38
51
  let content;
39
52
  try {
40
53
  content = readFileSync(taskIndexPath(projectRoot), 'utf8');
41
54
  }
42
55
  catch {
43
- return { bindings, forkOf, pinned, notes };
56
+ return { bindings, forkOf, pinned, notes, priority };
44
57
  }
45
58
  let order = 0; // posizione crescente dei record pinned → rango di pin (D2)
46
59
  for (const line of content.split('\n')) {
@@ -85,10 +98,20 @@ export function loadSessionIndex(projectRoot) {
85
98
  else
86
99
  notes.delete(d.sessionId);
87
100
  }
101
+ // T158 — stesso last-wins per campo: `false` è una smarcatura esplicita e
102
+ // toglie la chiave, il record che non nomina il campo non la tocca. Il
103
+ // `typeof` è quindi obbligatorio anche qui: un test di verità confonderebbe
104
+ // «smarcata» con «mai marcata», e la prima deve poter cancellare la seconda.
105
+ if (typeof d.priority === 'boolean') {
106
+ if (d.priority)
107
+ priority.add(d.sessionId);
108
+ else
109
+ priority.delete(d.sessionId);
110
+ }
88
111
  }
89
112
  catch {
90
113
  // riga corrotta → skip
91
114
  }
92
115
  }
93
- return { bindings, forkOf, pinned, notes };
116
+ return { bindings, forkOf, pinned, notes, priority };
94
117
  }
@@ -7,7 +7,7 @@ import { sliceLine } from '../text-search.js';
7
7
  import { sliceSpans } from '../markdown.js';
8
8
  import { ChoiceRow, FieldText, LABEL_W } from './fields.js';
9
9
  import { DETAIL_ACTIONS, MODELS } from '../spawn.js';
10
- import { DROW } from '../overlays/sheet.js';
10
+ import { DROW, PRIORITY_CHOICES } from '../overlays/sheet.js';
11
11
  import { CARET, CARET_OFF, WARN } from '../glyphs.js';
12
12
  /** Resa di ogni costrutto markdown (T75 · D4): un solo livello di enfasi per
13
13
  * costrutto, senza un secondo alfabeto da imparare. Heading uguali a ogni
@@ -70,7 +70,7 @@ const ACTION_KEYS = DETAIL_ACTIONS.map((a) => a.label[0]).join(' ');
70
70
  * decidere QUALE azione lanciare, e con due overlay separati quella decisione
71
71
  * costava uscire dal viewer e ricordarsi la combo.
72
72
  *
73
- * T117 — i quattro parametri dello spawn sono quattro RIGHE con un fuoco solo,
73
+ * T117 — i parametri dello spawn sono RIGHE con un fuoco solo,
74
74
  * la stessa forma del modale edit: prima erano tre alfabeti diversi sulla stessa
75
75
  * schermata (`←→` per l'azione, `tab` per il modello, i tasti nudi per la nota),
76
76
  * e ogni parametro aggiunto ne chiedeva un quarto.
@@ -79,7 +79,7 @@ const ACTION_KEYS = DETAIL_ACTIONS.map((a) => a.label[0]).join(' ');
79
79
  * rettangolo ha già coordinate e area cliccabile, quindi il layout sopravvive
80
80
  * all'arrivo del mouse (T21 · SGR enable + hit-test) senza migrazione.
81
81
  */
82
- export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, prompt, cursor, columns, find, occ, occCur, }) {
82
+ export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, prompt, priority, cursor, columns, find, occ, occCur, }) {
83
83
  const last = Math.min(total, top + capacity);
84
84
  // Il taglio lo fa il chiamante (invariante ③ di width.ts): le righe bottoni
85
85
  // sono ASCII, quindi `truncate-end` oggi darebbe il risultato giusto per caso
@@ -91,5 +91,5 @@ export function DetailScreen({ id, title, missing, lines, spans, top, total, cap
91
91
  // campo vuoto): riservarle sempre toglierebbe testo visibile a un titolo lungo
92
92
  // per un avviso assente.
93
93
  const textW = (extra) => Math.max(10, width - LABEL_W - CARET_OFF.length - extra);
94
- 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: "PgUp/PgDn" }), " testo \u00B7", ' ', _jsx(Text, { color: "yellow", children: ACTION_KEYS }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota campo \u00B7 ", _jsx(Text, { color: "yellow", children: "^F" }), " cerca"] })), _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(FieldText, { value: find.q, caret: find.caret, focused: true, 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: [_jsx(ChoiceRow, { label: "azione", values: DETAIL_ACTIONS.map((a) => a.label), index: action, focused: cursor.row === DROW.action, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.prompt), _jsx(Text, { dimColor: true, children: 'prompt'.padEnd(LABEL_W) }), _jsx(FieldText, { value: prompt, caret: cursor.caret, focused: cursor.row === DROW.prompt, cols: textW(prompt ? 0 : PROMPT_HINT.length) }), prompt ? null : _jsx(Text, { dimColor: true, children: PROMPT_HINT })] }), _jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(model)), focused: cursor.row === DROW.model, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.title), _jsx(Text, { dimColor: true, children: 'titolo'.padEnd(LABEL_W) }), _jsx(FieldText, { value: spawnNote, caret: cursor.caret, focused: cursor.row === DROW.title, cols: textW(spawnNote ? 0 : NOTE_HINT.length) }), spawnNote ? null : _jsx(Text, { dimColor: true, children: NOTE_HINT })] })] })] }));
94
+ 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: "PgUp/PgDn" }), " testo \u00B7", ' ', _jsx(Text, { color: "yellow", children: ACTION_KEYS }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota campo \u00B7 ", _jsx(Text, { color: "yellow", children: "^F" }), " cerca"] })), _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(FieldText, { value: find.q, caret: find.caret, focused: true, 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: [_jsx(ChoiceRow, { label: "azione", values: DETAIL_ACTIONS.map((a) => a.label), index: action, focused: cursor.row === DROW.action, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.prompt), _jsx(Text, { dimColor: true, children: 'prompt'.padEnd(LABEL_W) }), _jsx(FieldText, { value: prompt, caret: cursor.caret, focused: cursor.row === DROW.prompt, cols: textW(prompt ? 0 : PROMPT_HINT.length) }), prompt ? null : _jsx(Text, { dimColor: true, children: PROMPT_HINT })] }), _jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(model)), focused: cursor.row === DROW.model, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.title), _jsx(Text, { dimColor: true, children: 'titolo'.padEnd(LABEL_W) }), _jsx(FieldText, { value: spawnNote, caret: cursor.caret, focused: cursor.row === DROW.title, cols: textW(spawnNote ? 0 : NOTE_HINT.length) }), spawnNote ? null : _jsx(Text, { dimColor: true, children: NOTE_HINT })] }), _jsx(ChoiceRow, { label: "priorit\u00E0", values: PRIORITY_CHOICES, index: priority ? 1 : 0, focused: cursor.row === DROW.priority, width: width })] })] }));
95
95
  }
package/dist/ui/panes.js CHANGED
@@ -148,7 +148,7 @@ export function InboxPane({ files, counts, activeView, paneCount, selectedPath,
148
148
  return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: mark === 'broken' ? 'red' : mark === 'queued' ? 'green' : undefined, dimColor: mark === 'held' || mark === 'branched', children: pad(NATURA_SHORT[f.natura], NATURA_W) }), ' ', _jsx(Text, { color: mark === 'branched' ? 'yellow' : undefined, children: pad(INBOX_MARK[mark], INBOX_MARK_W) }), ' ', branchMark ? _jsx(Text, { color: "yellow", children: sanitize(branchMark) }) : null, _jsx(Text, { dimColor: mark !== 'queued', children: name }), ' '.repeat(Math.max(0, inner - termWidth(name))), ' ', _jsx(Text, { dimColor: true, children: pad(age, ageW, 'right') })] }, f.path));
149
149
  }))] }));
150
150
  }
151
- export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, counts, activeView, paneCount, selectedId, focused, above, below, columns, forkOf, sessionNotes, projectCore, live, }) {
151
+ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, counts, activeView, paneCount, selectedId, focused, above, below, columns, forkOf, sessionNotes, sessionPriority, projectCore, live, }) {
152
152
  return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(SessionsHeader, { parentLabel: parentLabel, counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), paneCount === 0 ? (
153
153
  // T100 — la nota della vista di default resta quella storica, che nomina
154
154
  // il PARENT (task, spot o tutte); le altre tre viste portano la propria,
@@ -236,7 +236,18 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
236
236
  // metterlo fuori cella sposterebbe il bordo del titolo solo sui rami —
237
237
  // cioè rimetterebbe lo slittamento che le colonne tolgono.
238
238
  const forkMark = forked ? '⑂ ' : '';
239
- const inner = Math.max(0, titleW - termWidth(forkMark));
239
+ // T158 · P7 — 🚨 entra nella stessa cella e per la stessa ragione,
240
+ // davanti al segno di fork. NON nella cella di appartenenza (📌/🔗/○):
241
+ // lì un solo glifo ha diritto di stare, e la priorità coprirebbe il pin
242
+ // sulle righe che sono pinnate e prioritarie insieme. La cella titolo
243
+ // perde 3 colonne solo sulle righe marcate.
244
+ //
245
+ // Larghezza misurata concorde (2 colonne per `string-width` e 2 per il
246
+ // terminale) e carattere astrale: nessun selettore di variazione, quindi
247
+ // nessuna voce da aggiungere alla tabella dei glifi discordi.
248
+ const priorityMark = sessionPriority.has(s.sessionId) ? '🚨 ' : '';
249
+ const marks = priorityMark + forkMark;
250
+ const inner = Math.max(0, titleW - termWidth(marks));
240
251
  // T60 — il testo arriva già ripulito di ciò che le colonne accanto
241
252
  // dicono già (progetto e task id): senza, la cella conterrebbe
242
253
  // `🧵 loom-works · T59` accanto a una colonna che dice `T59`.
@@ -244,6 +255,6 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
244
255
  const used = (label.note ? termWidth(label.note) : 0) +
245
256
  (label.note && label.rest ? 1 : 0) +
246
257
  termWidth(label.rest);
247
- return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isPinnedRow ? (_jsx(Text, { color: "yellow", children: pad('📌', 2) })) : linked ? (_jsx(Text, { color: "green", children: pad('🔗', 2) })) : (_jsx(Text, { dimColor: true, children: pad('○', 2) })), ' ', _jsx(Text, { color: liveEntry ? (liveEntry.status === 'busy' ? 'yellow' : 'green') : undefined, children: liveEntry ? (liveEntry.status === 'busy' ? LIVE_BUSY : LIVE_IDLE) : LIVE_NONE }), _jsx(Text, { color: liveEntry ? (liveEntry.status === 'busy' ? 'yellow' : 'green') : 'cyan', bold: Boolean(liveEntry), children: s.sessionId.slice(0, SID_CHARS) }), ' ', _jsx(Text, { dimColor: !s.model, children: pad(modelShort(s.model), MODEL_W) }), ' ', taskW > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: bound && taskCell ? 'green' : undefined, dimColor: !bound, children: pad(taskCell, taskW) }), ' '] })) : null, forkMark ? _jsx(Text, { color: "magenta", children: forkMark }) : null, label.note ? (_jsx(Text, { color: "yellow", bold: true, children: label.note })) : null, label.note && label.rest ? ' ' : null, label.rest ? _jsx(Text, { dimColor: Boolean(label.note), children: label.rest }) : null, ' '.repeat(Math.max(0, inner - used)), ' ', _jsx(Text, { dimColor: true, children: pad(age, ageW, 'right') })] }, s.sessionId));
258
+ return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isPinnedRow ? (_jsx(Text, { color: "yellow", children: pad('📌', 2) })) : linked ? (_jsx(Text, { color: "green", children: pad('🔗', 2) })) : (_jsx(Text, { dimColor: true, children: pad('○', 2) })), ' ', _jsx(Text, { color: liveEntry ? (liveEntry.status === 'busy' ? 'yellow' : 'green') : undefined, children: liveEntry ? (liveEntry.status === 'busy' ? LIVE_BUSY : LIVE_IDLE) : LIVE_NONE }), _jsx(Text, { color: liveEntry ? (liveEntry.status === 'busy' ? 'yellow' : 'green') : 'cyan', bold: Boolean(liveEntry), children: s.sessionId.slice(0, SID_CHARS) }), ' ', _jsx(Text, { dimColor: !s.model, children: pad(modelShort(s.model), MODEL_W) }), ' ', taskW > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: bound && taskCell ? 'green' : undefined, dimColor: !bound, children: pad(taskCell, taskW) }), ' '] })) : null, priorityMark ? _jsx(Text, { color: "red", bold: true, children: priorityMark }) : null, forkMark ? _jsx(Text, { color: "magenta", children: forkMark }) : null, label.note ? (_jsx(Text, { color: "yellow", bold: true, children: label.note })) : null, label.note && label.rest ? ' ' : null, label.rest ? _jsx(Text, { dimColor: Boolean(label.note), children: label.rest }) : null, ' '.repeat(Math.max(0, inner - used)), ' ', _jsx(Text, { dimColor: true, children: pad(age, ageW, 'right') })] }, s.sessionId));
248
259
  }))] }));
249
260
  }
@@ -149,7 +149,7 @@ export function screenFor(input) {
149
149
  // posizione è lo scroll mosso a mano. Il clamp serve comunque — un resize
150
150
  // può accorciare il testo sotto uno scroll già dato.
151
151
  const start = Math.min(sheet.top, sheet.maxTop);
152
- 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, prompt: sheet.prompt, cursor: sheet.cursor, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
152
+ 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, prompt: sheet.prompt, priority: sheet.priority, cursor: sheet.cursor, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
153
153
  }
154
154
  // ── T121 · viewer del project status ────────────────────────────────────
155
155
  // Quinta schermata sostitutiva, stessa ragione delle altre quattro: un recap
package/dist/viewport.js CHANGED
@@ -264,17 +264,18 @@ export function readerCapacity(rows) {
264
264
  // 1 riga prompt (T117)
265
265
  // 1 riga modello (T108)
266
266
  // 1 riga titolo (T111)
267
+ // 1 riga priorità (T158)
267
268
  //
268
- // Le ultime cinque sono il motivo per cui il detail non può riusare
269
- // READER_CHROME: le quattro righe dell'area di compilazione sono righe FISSE in
270
- // più dentro l'overlay, e ogni riga fissa aggiunta va scalata dalla capienza del
271
- // contenuto o il frame sfonda `rows` (stessa invariante di TASKS_PANE_CHROME).
269
+ // Le ultime sei sono il motivo per cui il detail non può riusare READER_CHROME:
270
+ // le cinque righe dell'area di compilazione sono righe FISSE in più dentro
271
+ // l'overlay, e ogni riga fissa aggiunta va scalata dalla capienza del contenuto o
272
+ // il frame sfonda `rows` (stessa invariante di TASKS_PANE_CHROME).
272
273
  //
273
274
  // Sono fisse e non `extra` condizionali di `detailCapacity` (T111 · D1): due
274
275
  // condizionali mutuamente esclusivi obbligherebbero il parametro a dire QUALE è
275
276
  // a schermo — un solo booleano per entrambi sottostima quando quello aperto è il
276
277
  // più alto, e sommarli toglie righe al testo per un campo che non c'è.
277
- const DETAIL_CHROME = 13;
278
+ const DETAIL_CHROME = 14;
278
279
  // T91 — la ricerca dentro il detail: marginTop + riga del campo.
279
280
  //
280
281
  // `MODAL_HEIGHT` dice che il detail costa 0 ai due pane (li sostituisce), e da
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.65.2",
3
+ "version": "0.66.1",
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": {