@lamemind/loom-deck 0.52.0 → 0.54.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 CHANGED
@@ -1085,6 +1085,15 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1085
1085
  * il click non sintetizza un tasto, perché nessun tasto nomina una riga o una
1086
1086
  * vista — `↑↓` e `tab` sono relativi — e chiama i setter che la tastiera
1087
1087
  * usa a sua volta. Un click sulla vista già attiva si limita al focus.
1088
+ *
1089
+ * ECCEZIONE: la riga già selezionata col pane già a fuoco. Lì un tasto che
1090
+ * la nomina ESISTE — `⏎`, che agisce proprio su «la riga selezionata» — e il
1091
+ * click lo sintetizza, come le superfici della riga launch: due click sulla
1092
+ * stessa riga aprono il detail della task o fanno il resume della
1093
+ * conversazione, e ogni caso limite (pin stale, nessuna sessione) resta per
1094
+ * costruzione quello del tasto. Le righe meta ne sono fuori: `⏎` su di loro
1095
+ * non fa nulla, e un click che lo sintetizzasse produrrebbe solo la nota di
1096
+ * scarto di `selectedTaskOr`.
1088
1097
  */
1089
1098
  function onListClick(ev) {
1090
1099
  if (!listGeometry)
@@ -1092,22 +1101,35 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1092
1101
  const hit = listHit(ev, listGeometry);
1093
1102
  if (!hit)
1094
1103
  return;
1095
- setFocus(hit.pane);
1096
- if (hit.pane === 'tasks') {
1097
- if (hit.target === 'view')
1104
+ if (hit.target === 'view') {
1105
+ setFocus(hit.pane);
1106
+ if (hit.pane === 'tasks')
1098
1107
  selectTaskView(hit.key);
1099
- // Le due righe meta hanno indice fisso; le task riportano l'indice di
1100
- // finestra a quello della lista completa, su cui è keyata la selezione.
1101
1108
  else
1102
- selectTaskRow(hit.index < META_ROWS ? hit.index : taskWin.start + hit.index);
1109
+ selectSessionView(hit.key);
1110
+ return;
1103
1111
  }
1104
- else if (hit.target === 'view') {
1105
- selectSessionView(hit.key);
1112
+ if (hit.pane === 'tasks') {
1113
+ // Le due righe meta hanno indice fisso; le task riportano l'indice di
1114
+ // finestra a quello della lista completa, su cui è keyata la selezione.
1115
+ const index = hit.index < META_ROWS ? hit.index : taskWin.start + hit.index;
1116
+ if (focus === 'tasks' && index === selIndex && index >= META_ROWS) {
1117
+ onKey('', { ...NO_MODIFIERS, return: true });
1118
+ return;
1119
+ }
1120
+ setFocus('tasks');
1121
+ selectTaskRow(index);
1106
1122
  }
1107
1123
  else {
1108
1124
  const row = windowRows[hit.index];
1109
- if (row && row.kind !== 'separator')
1110
- setSelSessionId(row.sessionId);
1125
+ if (!row || row.kind === 'separator')
1126
+ return;
1127
+ if (focus === 'sessions' && row.sessionId === selSessionId) {
1128
+ onKey('', { ...NO_MODIFIERS, return: true });
1129
+ return;
1130
+ }
1131
+ setFocus('sessions');
1132
+ setSelSessionId(row.sessionId);
1111
1133
  }
1112
1134
  }
1113
1135
  function onKey(input, key) {
@@ -14,7 +14,7 @@
14
14
  // eseguirli.
15
15
  import { useEffect, useMemo, useState } from 'react';
16
16
  import { buildRows, firstRowKey, moveRowSelection, rowIndexOfKey, searchSessions, selectedRow, DEFAULT_OPTIONS, } from '../search.js';
17
- import { readerCapacity, searchListCapacity } from '../viewport.js';
17
+ import { pageStep, readerCapacity, searchListCapacity } from '../viewport.js';
18
18
  import { wrapWithOffsets } from '../width.js';
19
19
  import { searchExcerptWidth } from '../layout.js';
20
20
  import { sanitizeTyped } from '../glyphs.js';
@@ -173,10 +173,10 @@ export function useSearchOverlay(deps) {
173
173
  scrollReader(1);
174
174
  }
175
175
  else if (key.pageUp) {
176
- scrollReader(-readerCap);
176
+ scrollReader(-pageStep(readerCap));
177
177
  }
178
178
  else if (key.pageDown) {
179
- scrollReader(readerCap);
179
+ scrollReader(pageStep(readerCap));
180
180
  }
181
181
  else if (input === 'g') {
182
182
  // Estremi su lettera e non su Home/End: Ink RICONOSCE quelle due (le
@@ -12,7 +12,7 @@
12
12
  // Lo spawn della sessione dalla barra azioni NON sta qui: arriva come callback
13
13
  // `onAction`, per lo stesso confine di `useSearchOverlay`.
14
14
  import { useEffect, useMemo, useState } from 'react';
15
- import { detailCapacity } from '../viewport.js';
15
+ import { detailCapacity, pageStep } from '../viewport.js';
16
16
  import { wrapWithOffsets } from '../width.js';
17
17
  import { scanText, topForOffset } from '../text-search.js';
18
18
  import { parseMarkdown } from '../markdown.js';
@@ -256,11 +256,11 @@ export function useSheetOverlay(deps) {
256
256
  return;
257
257
  }
258
258
  if (key.pageUp) {
259
- scroll(-capacity);
259
+ scroll(-pageStep(capacity));
260
260
  return;
261
261
  }
262
262
  if (key.pageDown) {
263
- scroll(capacity);
263
+ scroll(pageStep(capacity));
264
264
  return;
265
265
  }
266
266
  // Tutto il resto è dell'area di compilazione. Ciò che non consuma resta
@@ -11,7 +11,7 @@
11
11
  // tasto renderebbe il gesto economico un effetto collaterale di quello caro.
12
12
  import { useEffect, useMemo, useState } from 'react';
13
13
  import { randomUUID } from 'node:crypto';
14
- import { statusCapacity } from '../viewport.js';
14
+ import { pageStep, statusCapacity } from '../viewport.js';
15
15
  import { cut, wrapWithOffsets } from '../width.js';
16
16
  import { parseMarkdown } from '../markdown.js';
17
17
  import { CLAUDE_CMD, notifyDone, spawnProjectStatus } from '../spawn.js';
@@ -127,10 +127,10 @@ export function useProjectStatus(deps) {
127
127
  scroll(1);
128
128
  }
129
129
  else if (key.pageUp) {
130
- scroll(-capacity);
130
+ scroll(-pageStep(capacity));
131
131
  }
132
132
  else if (key.pageDown) {
133
- scroll(capacity);
133
+ scroll(pageStep(capacity));
134
134
  }
135
135
  else if (input === 'g') {
136
136
  // `g`/`G` sugli estremi, come nel reader e nel detail: anche questa
@@ -95,7 +95,7 @@ export function SearchPreviewPane({ p }) {
95
95
  export function ReaderScreen({ hit, lines, top, total, capacity, bound, }) {
96
96
  const last = Math.min(total, top + capacity);
97
97
  const occ = [{ start: hit.matchStart, end: hit.matchEnd }];
98
- 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: ["reader \u00B7 ", _jsx(Text, { color: "cyan", children: hit.sessionId.slice(0, 8) }), " \u00B7 record ", hit.idx, " \u00B7", ' ', KIND_LABEL[hit.kind], bound ? ` · ${bound}` : '', " \u00B7 righe ", total === 0 ? 0 : top + 1, "-", last, " di ", total] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " riga \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " pagina \u00B7", ' ', _jsx(Text, { color: "yellow", children: "g" }), " inizio \u00B7 ", _jsx(Text, { color: "yellow", children: "G" }), " fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " torna alla lista"] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: lines.map((l, i) => (_jsx(ReaderLine, { line: l, occ: occ, current: 0 }, top + i))) })] }));
98
+ 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: ["reader \u00B7 ", _jsx(Text, { color: "cyan", children: hit.sessionId.slice(0, 8) }), " \u00B7 record ", hit.idx, " \u00B7", ' ', KIND_LABEL[hit.kind], bound ? ` · ${bound}` : '', " \u00B7 righe ", total === 0 ? 0 : top + 1, "-", last, " di ", total] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " riga \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " \u00BD pagina \u00B7", ' ', _jsx(Text, { color: "yellow", children: "g" }), " inizio \u00B7 ", _jsx(Text, { color: "yellow", children: "G" }), " fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " torna alla lista"] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: lines.map((l, i) => (_jsx(ReaderLine, { line: l, occ: occ, current: 0 }, top + i))) })] }));
99
99
  }
100
100
  /** Una riga con le porzioni di match evidenziate. Gli offset sono quelli del
101
101
  * testo sorgente, quindi un match a cavallo dell'a-capo si colora su entrambe
@@ -52,5 +52,5 @@ export function StatusHeadline({ name, label, building, failed, cols, }) {
52
52
  export function StatusScreen({ name, label, building, failed, view, lines, spans, top, total, capacity, columns, }) {
53
53
  const last = Math.min(total, top + capacity);
54
54
  const width = Math.max(20, (columns || 80) - 4);
55
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(StatusHeadline, { name: name, label: label, building: building, failed: failed, cols: width }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["generato alle ", _jsx(Text, { color: "cyan", children: fmtTime(view.mtime) }), " \u00B7 righe", ' ', total === 0 ? 0 : top + 1, "-", last, " di ", total, view.stale ? (_jsx(Text, { color: "yellow", children: " \u00B7 versione precedente, generazione in corso" })) : null] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " riga \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " pagina \u00B7", ' ', _jsx(Text, { color: "yellow", children: "g/G" }), " estremi \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " chiude"] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: NO_OCC, current: -1 }, top + i))) })] }));
55
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(StatusHeadline, { name: name, label: label, building: building, failed: failed, cols: width }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["generato alle ", _jsx(Text, { color: "cyan", children: fmtTime(view.mtime) }), " \u00B7 righe", ' ', total === 0 ? 0 : top + 1, "-", last, " di ", total, view.stale ? (_jsx(Text, { color: "yellow", children: " \u00B7 versione precedente, generazione in corso" })) : null] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " riga \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " \u00BD pagina \u00B7", ' ', _jsx(Text, { color: "yellow", children: "g/G" }), " estremi \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " chiude"] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: NO_OCC, current: -1 }, top + i))) })] }));
56
56
  }
package/dist/viewport.js CHANGED
@@ -272,6 +272,20 @@ const STATUS_CHROME = 8;
272
272
  export function statusCapacity(rows) {
273
273
  return Math.max(0, (rows || 24) - SLACK - STATUS_CHROME);
274
274
  }
275
+ /**
276
+ * Righe scorse da una pressione di PagGiù/PagSu: METÀ della capienza visibile.
277
+ *
278
+ * Un salto di pagina intera non lascia nessuna riga in comune fra il prima e il
279
+ * dopo, quindi chi legge deve ricostruire da zero dove si trovava; con mezza
280
+ * pagina la metà già letta resta a schermo e fa da aggancio. È il compromesso
281
+ * dello scroll a mezza pagina classico (`^D`/`^U` di vi/less).
282
+ *
283
+ * Pavimento a 1: su un terminale bassissimo la capienza può valere 1 o 0, e un
284
+ * passo 0 renderebbe il tasto inerte invece che lento.
285
+ */
286
+ export function pageStep(capacity) {
287
+ return Math.max(1, Math.floor(capacity / 2));
288
+ }
275
289
  // T52 — cornice del pannello di anteprima sotto la lista occorrenze:
276
290
  // marginTop + 2 bordi + riga meta.
277
291
  const SEARCH_PREVIEW_CHROME = 4;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.52.0",
3
+ "version": "0.54.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": {