@lamemind/loom-deck 0.34.3 → 0.35.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/glyphs.js CHANGED
@@ -20,6 +20,41 @@ export const SID_CHARS = 8;
20
20
  * vuota di soli spazi lascerebbe un buco che si legge come "colonna finita",
21
21
  * e la riga tornerebbe a sembrare disallineata pur non essendolo. */
22
22
  export const TASK_EMPTY = '·';
23
+ /** T110 — colonna modello della lista sessioni. Larghezza COSTANTE e non
24
+ * misurata come `taskW`/`ageW`: il dominio è chiuso e le short sono tutte
25
+ * larghe 3, quindi farla passare per `sessionCols` ricalcolerebbe a ogni
26
+ * render un numero già noto. */
27
+ export const MODEL_W = 3;
28
+ /** Le short sono CHIESTE, non derivate: `slice(0,3)` darebbe `opu` e `hai`, e
29
+ * sbaglierebbe proprio le due voci su quattro che a occhio non si controllano
30
+ * (`fab` e `son` coincidono col troncamento e coprono l'errore). La chiave è
31
+ * il token di FAMIGLIA, non l'id intero: `claude-opus-5 → ops` sarebbe un
32
+ * calco della generazione corrente, falso al primo bump e col sintomo del
33
+ * fallback «ignoto» proprio sul modello più usato. */
34
+ export const MODEL_SHORT = {
35
+ fable: 'fab',
36
+ opus: 'ops',
37
+ sonnet: 'son',
38
+ haiku: 'hak',
39
+ };
40
+ /** Nessun record assistant: la conversazione non ha ancora girato. Stesso
41
+ * glifo della cella task vuota accanto — il vocabolario non cresce. */
42
+ export const MODEL_EMPTY = TASK_EMPTY;
43
+ /** Id presente ma fuori dalle famiglie note: gira su qualcosa che la lista non
44
+ * sa nominare. Distinto dall'assenza, o un'estensione mancata del catalogo si
45
+ * legge come una riga normale e si scorre senza notarla; l'id per intero sta
46
+ * nel blocco preview. */
47
+ export const MODEL_UNKNOWN = '?';
48
+ export function modelShort(id) {
49
+ if (!id)
50
+ return MODEL_EMPTY;
51
+ const lower = id.toLowerCase();
52
+ for (const [family, short] of Object.entries(MODEL_SHORT)) {
53
+ if (lower.includes(family))
54
+ return short;
55
+ }
56
+ return MODEL_UNKNOWN;
57
+ }
23
58
  // T62 — colonna liveness, larga 1, incollata al sessionId senza gutter proprio:
24
59
  // il glifo qualifica QUELL'id, e uno spazio in mezzo lo farebbe leggere come una
25
60
  // colonna a sé. Entrambi Ambiguous (EAW) → 1 colonna per il terminale e 1 cella
package/dist/sessions.js CHANGED
@@ -54,6 +54,22 @@ const INTERRUPT_MARKER = '[Request interrupted by user';
54
54
  function isInterrupt(text) {
55
55
  return text.trimStart().startsWith(INTERRUPT_MARKER);
56
56
  }
57
+ // T110 — valore che il CLI scrive nel campo modello dei record che fabbrica da
58
+ // sé (osservato su `{"type":"text","text":"No response requested."}`). Restano
59
+ // `type:assistant` a tutti gli effetti: chi si fida del campo etichetta la
60
+ // conversazione con un modello che non esiste. Terza istanza dello stesso
61
+ // pattern dei `tool_result` fra i turni e dell'interruzione fra i prompt — un
62
+ // record col tipo giusto che non è la cosa che quel tipo promette.
63
+ const SYNTHETIC_MODEL = '<synthetic>';
64
+ // Id del modello di UN record assistant, '' se assente o sintetico.
65
+ function extractModel(message) {
66
+ if (!message || typeof message !== 'object')
67
+ return '';
68
+ const model = message.model;
69
+ if (typeof model !== 'string' || !model || model === SYNTHETIC_MODEL)
70
+ return '';
71
+ return model;
72
+ }
57
73
  // T52 — estrae i corpi cercabili di UN record, uno per `kind` presente.
58
74
  //
59
75
  // Blocchi dello stesso kind nello stesso record vengono CONCATENATI, non emessi
@@ -166,6 +182,7 @@ export function parseTranscript(content, path, mtime, sizeBytes) {
166
182
  let customTitle = '';
167
183
  let firstUserText = '';
168
184
  let lastAssistantText = '';
185
+ let model = '';
169
186
  let turns = 0;
170
187
  const bodies = [];
171
188
  let recordIdx = -1;
@@ -216,6 +233,12 @@ export function parseTranscript(content, path, mtime, sizeBytes) {
216
233
  const t = extractText(d.message);
217
234
  if (t)
218
235
  lastAssistantText = t;
236
+ // T110 — stesso regime last-wins, ma su un asse suo: un record di solo
237
+ // tool_use porta comunque il modello, e va contato. Nessun costo di I/O:
238
+ // la riga è già parsata per turni, primo prompt e ultima risposta.
239
+ const m = extractModel(d.message);
240
+ if (m)
241
+ model = m;
219
242
  }
220
243
  }
221
244
  if (!cwd)
@@ -240,6 +263,13 @@ export function parseTranscript(content, path, mtime, sizeBytes) {
240
263
  // testo, sanificare a valle sposterebbe l'a-capo già calcolato.
241
264
  firstPrompt: sanitize(firstUserText),
242
265
  lastReply: sanitize(lastAssistantText),
266
+ // Sanificato come il titolo e le due preview, e per lo stesso motivo: è una
267
+ // stringa di sorgente esterna che finisce NEL FRAME (riga meta del blocco
268
+ // preview). Il confine di sanificazione è per SORGENTE, non per campo — un
269
+ // valore che lo attraversa nudo perché «oggi è ASCII» riapre l'invariante ①
270
+ // il giorno che lo store scrive qualcos'altro. Non tocca `modelShort`: il
271
+ // match è sui token di famiglia, tutti ASCII.
272
+ model: sanitize(model),
243
273
  bodies,
244
274
  };
245
275
  }
package/dist/ui/panes.js CHANGED
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
5
5
  import { Box, Text } from 'ink';
6
6
  import { cut, cutParts, pad, sanitize, termWidth } from '../width.js';
7
7
  import { isDone, paneTextWidth } from '../layout.js';
8
- import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, SESSION_SEP, SID_CHARS, TASK_EMPTY, WARN, displayProg, relTime, } from '../glyphs.js';
8
+ import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SESSION_SEP, SID_CHARS, TASK_EMPTY, WARN, displayProg, modelShort, relTime, } from '../glyphs.js';
9
9
  import { META_ROWS, ROW_ALL, ROW_SPOT } from '../model.js';
10
10
  import { rowLabel, sessionTitle } from '../session-list.js';
11
11
  import { sessionView, taskView, SESSION_VIEWS, TASK_VIEWS, } from '../pane-views.js';
@@ -199,6 +199,8 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
199
199
  1 /* T62 · colonna liveness */ +
200
200
  SID_CHARS +
201
201
  1 /* gutter */ +
202
+ MODEL_W /* T110 · colonna modello */ +
203
+ 1 /* gutter */ +
202
204
  (taskW > 0 ? taskW + 1 : 0) +
203
205
  1 /* gutter prima della data */ +
204
206
  ageW));
@@ -215,6 +217,6 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
215
217
  const used = (label.note ? termWidth(label.note) + 2 : 0) +
216
218
  (label.note && label.rest ? 1 : 0) +
217
219
  termWidth(label.rest);
218
- 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) }), ' ', 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 ? (_jsxs(Text, { color: "yellow", bold: true, children: ["\u00AB", label.note, "\u00BB"] })) : 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));
220
+ 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 ? (_jsxs(Text, { color: "yellow", bold: true, children: ["\u00AB", label.note, "\u00BB"] })) : 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));
219
221
  }))] }));
220
222
  }
@@ -19,7 +19,7 @@ export function SessionPreview({ s, firstLines, lastLines, columns, origin, note
19
19
  const width = previewTextWidth(columns);
20
20
  const first = s.customTitle && firstLines > 0 ? wrapLines(s.firstPrompt, width, firstLines) : [];
21
21
  const last = s.lastReply && lastLines > 0 ? wrapLines(s.lastReply, width, lastLines) : [];
22
- return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: s.sessionId.slice(0, SID_CHARS) }), ' ', note ? _jsxs(Text, { color: "yellow", children: ["\u00AB", note, "\u00BB "] }) : null, _jsx(Text, { dimColor: Boolean(note), children: s.title })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts), " \u00B7 ", s.gitBranch || '-', origin ? ` · ⑂ da ${origin.slice(0, 8)}` : '', live ? (_jsx(Text, { color: live.status === 'busy' ? 'yellow' : 'green', children: ` · ${live.status === 'busy' ? LIVE_BUSY : LIVE_IDLE} viva pid ${live.pid} (${live.status})` })) : null] }), 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}`)))] }));
22
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: s.sessionId.slice(0, SID_CHARS) }), ' ', note ? _jsxs(Text, { color: "yellow", children: ["\u00AB", note, "\u00BB "] }) : null, _jsx(Text, { dimColor: Boolean(note), children: s.title })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts), " \u00B7 ", s.gitBranch || '-', s.model ? ` · ${s.model}` : '', origin ? ` · ⑂ da ${origin.slice(0, 8)}` : '', live ? (_jsx(Text, { color: live.status === 'busy' ? 'yellow' : 'green', children: ` · ${live.status === 'busy' ? LIVE_BUSY : LIVE_IDLE} viva pid ${live.pid} (${live.status})` })) : null] }), 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}`)))] }));
23
23
  }
24
24
  /**
25
25
  * Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.34.3",
3
+ "version": "0.35.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": {