@lamemind/loom-deck 0.10.0 → 0.11.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
@@ -768,6 +768,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
768
768
  // T49 — il detail pane sessione esiste solo con il focus sul pane: è
769
769
  // l'hover, non uno stato persistente; navigando le task non ruba righe.
770
770
  hasSessionDetail: canResume,
771
+ // Riservo righe di preview solo per i blocchi che davvero renderizzano: il
772
+ // primo prompt aggiunge info solo con un titolo custom (senza, titolo ===
773
+ // primo prompt); l'ultima risposta solo se il modello ha già risposto.
774
+ sessionHasFirstPreview: canResume && Boolean(selSessionObj?.customTitle),
775
+ sessionHasLastPreview: canResume && Boolean(selSessionObj?.lastReply),
771
776
  });
772
777
  // Finestre di rendering. Le liste "logiche" (viewTasks, visibleSessions)
773
778
  // restano intere: navigazione, selezione e spawn continuano a ragionare su
@@ -782,7 +787,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
782
787
  if (budget.compact) {
783
788
  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"] })] }));
784
789
  }
785
- 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 === '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 \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 \u23CE ", canSpawn ? 'spawn' : canResume ? 'resume' : '—', " \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 === '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 })) : 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, sessions: windowSessions, total: childSessions.length, hidden: hiddenSessions, selectedId: selSessionId, focused: focus === 'sessions', above: sessionWin.start, below: visibleSessions.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, previewLines: budget.sessionPreviewLines, columns: columns })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: normalizeEmoji(note) }) : null] }));
790
+ 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 === '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 \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 \u23CE ", canSpawn ? 'spawn' : canResume ? 'resume' : '—', " \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 === '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 })) : 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, sessions: windowSessions, total: childSessions.length, hidden: hiddenSessions, selectedId: selSessionId, focused: focus === 'sessions', above: sessionWin.start, below: visibleSessions.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: normalizeEmoji(note) }) : null] }));
786
791
  }
787
792
  const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
788
793
  // Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
@@ -827,23 +832,25 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
827
832
  return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, task.id, " ", forceEmojiWidth(task.pri), " ", displayProg(task.prog), " ", task.desc, n > 0 ? ` (${n})` : ''] }, task.id));
828
833
  })), detail && detailLines > 0 ? (_jsx(DetailPane, { detail: detail, maxLines: detailLines, columns: columns })) : null] }));
829
834
  }
830
- function SessionsPane({ parentLabel, isSpot, sessions, total, hidden, selectedId, focused, above, below, detail, previewLines, columns, }) {
835
+ function SessionsPane({ parentLabel, isSpot, sessions, total, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, }) {
831
836
  return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: ["Sessions \u00B7 ", parentLabel, " (", total, ")", hidden > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 +", hidden, " pi\u00F9 vecchie"] }) : null, above > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2191", above] }) : null, below > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2193", below] }) : null] }), total === 0 ? (_jsx(Text, { color: "yellow", wrap: "truncate-end", children: isSpot ? 'nessuna sessione libera' : 'nessuna sessione legata a questa task' })) : (sessions.map((s) => {
832
837
  const sel = s.sessionId === selectedId;
833
838
  return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isSpot ? _jsx(Text, { dimColor: true, children: "\u25CB" }) : _jsx(Text, { color: "green", children: "\uD83D\uDD17" }), ' ', truncate(s.title, 44), ' ', _jsxs(Text, { dimColor: true, children: ["\u00B7 ", s.gitBranch || '-', " \u00B7 ", relTime(s.ts)] })] }, s.sessionId));
834
- })), detail ? (_jsx(SessionDetailPane, { s: detail, previewLines: previewLines, columns: columns })) : null] }));
839
+ })), detail ? (_jsx(SessionDetailPane, { s: detail, firstLines: firstLines, lastLines: lastLines, columns: columns })) : null] }));
835
840
  }
836
841
  // T49 — detail pane della sessione selezionata (hover), gemello del DetailPane
837
842
  // task. Tutti i campi vengono dal parse già cached dell'adapter (mtime-keyed):
838
- // il pannello non costa I/O al movimento di selezione. La preview del primo
839
- // prompt compare SOLO con un titolo custom senza, il titolo È già il primo
840
- // prompt e la riga lo duplicherebbe (D4 preflight). Renderizzare meno righe del
841
- // riservato è sicuro: il frame esce più corto, mai più alto del budget.
842
- function SessionDetailPane({ s, previewLines, columns, }) {
843
- const lines = s.customTitle
844
- ? wrapLines(s.firstPrompt, detailTextWidth(columns), previewLines)
845
- : [];
846
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: s.title }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts)] }), lines.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '» ' : ' ', line] }, i)))] }));
843
+ // il pannello non costa I/O al movimento di selezione. Mostra "da dove parte,
844
+ // dove è arrivata": il primo prompt utente (`» `) e l'ultima risposta del
845
+ // modello (`« `). La preview del primo prompt compare SOLO con un titolo custom
846
+ // senza, il titolo È già il primo prompt e la riga lo duplicherebbe (D4
847
+ // preflight). Le righe rese non superano mai il riservato dal budget
848
+ // (`firstLines`/`lastLines`); renderne meno è sicuro (frame più corto).
849
+ function SessionDetailPane({ s, firstLines, lastLines, columns, }) {
850
+ const width = detailTextWidth(columns);
851
+ const first = s.customTitle && firstLines > 0 ? wrapLines(s.firstPrompt, width, firstLines) : [];
852
+ const last = s.lastReply && lastLines > 0 ? wrapLines(s.lastReply, width, lastLines) : [];
853
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: s.title }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts)] }), first.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '» ' : ' ', line] }, `f${i}`))), last.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '« ' : ' ', line] }, `l${i}`)))] }));
847
854
  }
848
855
  /**
849
856
  * Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
package/dist/sessions.js CHANGED
@@ -20,7 +20,12 @@ function cleanPreview(s) {
20
20
  .replace(/\s+/g, ' ')
21
21
  .trim();
22
22
  }
23
- function extractUserText(message) {
23
+ // Estrae il primo blocco di testo dal `message` di un record. Funziona sia sui
24
+ // record `type:user` sia sugli `type:assistant`: entrambi portano `content` come
25
+ // stringa o array di blocchi, e un blocco di testo è `{type:'text', text}`. Il
26
+ // nome è generico apposta — l'ultima risposta del modello (T49 first+last) esce
27
+ // dallo stesso estrattore del primo prompt utente.
28
+ function extractText(message) {
24
29
  if (!message || typeof message !== 'object')
25
30
  return '';
26
31
  const content = message.content;
@@ -58,6 +63,7 @@ function parseSessionFile(path, mtime, sizeBytes) {
58
63
  let parentUuid = null;
59
64
  let customTitle = '';
60
65
  let firstUserText = '';
66
+ let lastAssistantText = '';
61
67
  let turns = 0;
62
68
  for (const line of content.split('\n')) {
63
69
  if (!line.trim())
@@ -82,14 +88,22 @@ function parseSessionFile(path, mtime, sizeBytes) {
82
88
  customTitle = d.customTitle; // last-wins
83
89
  if (d.type === 'user') {
84
90
  // T49: turno = prompt umano. I tool_result viaggiano anch'essi come
85
- // type:user ma senza blocchi text → extractUserText '' li esclude.
86
- const t = extractUserText(d.message);
91
+ // type:user ma senza blocchi text → extractText '' li esclude.
92
+ const t = extractText(d.message);
87
93
  if (t) {
88
94
  turns++;
89
95
  if (!firstUserText)
90
96
  firstUserText = t;
91
97
  }
92
98
  }
99
+ else if (d.type === 'assistant') {
100
+ // Ultima risposta del modello: last-wins (come customTitle), niente
101
+ // early-stop — l'ultima riga assistant con testo è quella buona. I
102
+ // record assistant di solo tool_use danno '' e non sovrascrivono.
103
+ const t = extractText(d.message);
104
+ if (t)
105
+ lastAssistantText = t;
106
+ }
93
107
  }
94
108
  if (!cwd)
95
109
  return null; // nessuna riga con cwd → non è una sessione di progetto valida
@@ -105,6 +119,7 @@ function parseSessionFile(path, mtime, sizeBytes) {
105
119
  turns,
106
120
  customTitle,
107
121
  firstPrompt: firstUserText,
122
+ lastReply: lastAssistantText,
108
123
  };
109
124
  }
110
125
  // Discovery read-only delle sessioni del SOLO progetto corrente (D2 preflight
package/dist/viewport.js CHANGED
@@ -63,7 +63,8 @@ const TASKS_PANE_CHROME = 5; // 2 bordi + header "Tasks (n)" + riga sort + riga
63
63
  const SESSIONS_PANE_CHROME = 3; // 2 bordi + header "Sessions · …"
64
64
  const DETAIL_CHROME = 3; // marginTop + 2 bordi
65
65
  /** Detail pane sessione (T49): righe fisse = titolo + riga meta (size · turni ·
66
- * ultima attività). La preview del primo prompt è la parte variabile. */
66
+ * ultima attività). Le preview (primo prompt + ultima risposta) sono variabili,
67
+ * ciascuna al più MAX_SESSION_PREVIEW righe. */
67
68
  const SESSION_DETAIL_FIXED = 2;
68
69
  const MAX_SESSION_PREVIEW = 2;
69
70
  /** Gemello di MIN_TASK_ROWS: sotto questa soglia la lista sessioni non serve
@@ -108,24 +109,34 @@ export function layoutBudget(input) {
108
109
  sessionRows: 0,
109
110
  detailLines: 0,
110
111
  sessionDetail: false,
111
- sessionPreviewLines: 0,
112
+ sessionFirstLines: 0,
113
+ sessionLastLines: 0,
112
114
  compact: true,
113
115
  };
114
116
  }
115
117
  // Detail pane sessione (T49): stesso schema del dettaglio task — prima la
116
- // lista minima, poi la cornice, la preview solo con lo spazio che avanza.
118
+ // lista minima, poi la cornice, le preview solo con lo spazio che avanza.
117
119
  // A differenza del dettaglio task il box regge anche senza righe variabili:
118
- // titolo + meta sono il valore, la preview è bonus.
120
+ // titolo + meta sono il valore, le preview (primo prompt + ultima risposta)
121
+ // sono bonus. Priorità al primo prompt, poi l'ultima risposta prende ciò che
122
+ // resta: su terminale stretto cade prima l'ultima risposta, non il primo.
123
+ // Riservo righe solo per le preview che davvero renderizzeranno (i due
124
+ // `has…Preview`), così non sottraggo righe alla lista per un blocco vuoto.
119
125
  let sessionDetail = false;
120
126
  let sessionDetailCost = 0;
121
- let sessionPreviewLines = 0;
127
+ let sessionFirstLines = 0;
128
+ let sessionLastLines = 0;
122
129
  if (input.hasSessionDetail) {
123
130
  const fixed = DETAIL_CHROME + SESSION_DETAIL_FIXED;
124
131
  const spare = avail - SESSIONS_PANE_CHROME - MIN_SESSION_ROWS - fixed;
125
132
  if (spare >= 0) {
126
133
  sessionDetail = true;
127
- sessionPreviewLines = Math.min(MAX_SESSION_PREVIEW, spare);
128
- sessionDetailCost = fixed + sessionPreviewLines;
134
+ if (input.sessionHasFirstPreview)
135
+ sessionFirstLines = Math.min(MAX_SESSION_PREVIEW, spare);
136
+ if (input.sessionHasLastPreview) {
137
+ sessionLastLines = Math.min(MAX_SESSION_PREVIEW, spare - sessionFirstLines);
138
+ }
139
+ sessionDetailCost = fixed + sessionFirstLines + sessionLastLines;
129
140
  }
130
141
  }
131
142
  const sessionRows = Math.max(0, avail - SESSIONS_PANE_CHROME - sessionDetailCost);
@@ -143,7 +154,15 @@ export function layoutBudget(input) {
143
154
  }
144
155
  }
145
156
  const taskRows = Math.max(0, avail - TASKS_PANE_CHROME - detailChrome - detailLines);
146
- return { taskRows, sessionRows, detailLines, sessionDetail, sessionPreviewLines, compact: false };
157
+ return {
158
+ taskRows,
159
+ sessionRows,
160
+ detailLines,
161
+ sessionDetail,
162
+ sessionFirstLines,
163
+ sessionLastLines,
164
+ compact: false,
165
+ };
147
166
  }
148
167
  /**
149
168
  * Finestra scorrevole su una lista più lunga della capienza.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.10.0",
3
+ "version": "0.11.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": {