@lamemind/loom-deck 0.65.0 → 0.65.2

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/layout.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // da scalare va tolta in un posto solo, o due chiamanti tolgono cose diverse
4
4
  // dalla stessa larghezza e il bordo viene mangiato.
5
5
  import { stripProjectCore } from './session-list.js';
6
+ import { SID_CHARS } from './glyphs.js';
6
7
  /**
7
8
  * Lunghezza in CODE POINT. Il caret ci indicizza sopra: `.length` conterebbe le
8
9
  * code unit UTF-16 e un'emoji nel titolo varrebbe 2 posizioni, cioè un cursore
@@ -96,6 +97,22 @@ export function assignTextWidth(columns) {
96
97
  export function previewTextWidth(columns) {
97
98
  return Math.max(10, (columns || 80) - 8);
98
99
  }
100
+ /**
101
+ * I tre pezzi della riga d'apertura del blocco preview di una conversazione —
102
+ * sid, nota, titolo — ognuno col proprio separatore in coda, pronti per
103
+ * `cutParts` sul budget di `previewTextWidth`.
104
+ *
105
+ * Esistono come funzione, e il taglio passa da `cutParts`, per l'invariante ③
106
+ * (§`assignTextWidth` sopra): lasciare la riga a `wrap="truncate-end"` la fa
107
+ * tagliare da `cli-truncate`, che sfora di una colonna per emoji e mangia il
108
+ * bordo destro del box. Due dei tre pezzi portano testo libero — la nota la
109
+ * scrive l'utente, e un titolo di conversazione auto-generato apre con l'emoji
110
+ * dell'azione — quindi le emoji su questa riga sono il caso normale, non il
111
+ * limite.
112
+ */
113
+ export function previewTitleParts(sessionId, note, title) {
114
+ return [`${sessionId.slice(0, SID_CHARS)} `, note ? `${note} ` : '', title];
115
+ }
99
116
  /**
100
117
  * Larghezza del TESTO dentro un pane al 50%: box esterno (2 bordi + 2 padding)
101
118
  * → metà → bordo + padding del pane.
package/dist/spawn.js CHANGED
@@ -209,6 +209,8 @@ export const ACTION_HOTKEYS = Object.fromEntries(DETAIL_ACTIONS.map((a, i) => [a
209
209
  // `deck-run` lascia passare nel titolo della tab: la whitelist è una lista
210
210
  // chiusa che nomina questi code point (P2 preflight), quindi cambiare una voce
211
211
  // qui e non là riapre la divergenza fra titolo in lista e titolo in tab.
212
+ // Le quadre che delimitavano il prefisso sono cadute: l'emoji da sola si stacca
213
+ // già dallo slug, e con lei sono uscite dalla whitelist di `_sane_note`.
212
214
  const ACTION_EMOJI = {
213
215
  preflight: '📐',
214
216
  run: '🚀',
@@ -218,7 +220,7 @@ const ACTION_EMOJI = {
218
220
  checkpoint: '🏁',
219
221
  };
220
222
  /**
221
- * Titolo di fallback per una conversazione con nota vuota: `[ {emoji} ] {slug}`,
223
+ * Titolo di fallback per una conversazione con nota vuota: `{emoji} {slug}`,
222
224
  * o il solo slug per `none` (D1/D2 preflight). Lo slug viene dal NOME del
223
225
  * task file (`findTaskFile`), non dalla descrizione di `tasks.md`: il nome è
224
226
  * già dentro l'alfabeto di `_sane_note` per costruzione — minuscolo, separato
@@ -226,10 +228,10 @@ const ACTION_EMOJI = {
226
228
  * `/` che la riduzione toglie senza sostituto, saldando le parole (D1
227
229
  * razionale).
228
230
  *
229
- * Il PREFISSO invece nell'alfabeto non ci sta per costruzione: emoji e quadre
230
- * ci entrano solo perché la whitelist di `_sane_note` le nomina una per una
231
- * (T156). È l'unica parte del titolo di fallback che dipende da una modifica
232
- * fatta dall'altro lato.
231
+ * Il PREFISSO invece nell'alfabeto non ci sta per costruzione: l'emoji ci entra
232
+ * solo perché la whitelist di `_sane_note` le nomina una per una (T156). È
233
+ * l'unica parte del titolo di fallback che dipende da una modifica fatta
234
+ * dall'altro lato.
233
235
  *
234
236
  * `null` quando il task file non si trova: il chiamante decide se ripiegare
235
237
  * su nota vuota o su un altro fallback.
@@ -240,7 +242,7 @@ export function fallbackTitle(tasksDir, id, kind) {
240
242
  return null;
241
243
  const slug = basename(path, '.md').slice(id.length + 1).replace(/-/g, ' ');
242
244
  const emoji = ACTION_EMOJI[kind];
243
- return emoji ? `[ ${emoji} ] ${slug}` : slug;
245
+ return emoji ? `${emoji} ${slug}` : slug;
244
246
  }
245
247
  // Spawn detached: il deck spawna ma NON contiene la sessione (la possiede
246
248
  // ptyxis-agent). unref + stdio ignore → ritorna subito, la TUI resta viva.
@@ -2,9 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  // Blocco preview a piena larghezza sotto i due pane: mostra la task o la
3
3
  // conversazione selezionata a seconda del pane a fuoco.
4
4
  import { Box, Text } from 'ink';
5
- import { cut, wrapLines } from '../width.js';
6
- import { previewTextWidth } from '../layout.js';
7
- import { LIVE_BUSY, LIVE_IDLE, META_KEYS, SID_CHARS, fmtDateTime, fmtSize, modelAlias, } from '../glyphs.js';
5
+ import { cut, cutParts, wrapLines } from '../width.js';
6
+ import { previewTextWidth, previewTitleParts } from '../layout.js';
7
+ import { LIVE_BUSY, LIVE_IDLE, META_KEYS, fmtDateTime, fmtSize, modelAlias, } from '../glyphs.js';
8
8
  import { NATURA_SHORT, inboxMark } from '../inbox.js';
9
9
  import { ChoiceRow } from './fields.js';
10
10
  import { MODELS } from '../spawn.js';
@@ -64,7 +64,12 @@ export function SessionPreview({ s, firstLines, lastLines, columns, origin, note
64
64
  // non c'è una provenienza vera da dichiarare.
65
65
  const originAlias = modelAlias(s.model);
66
66
  const originIndex = originAlias ? MODELS.indexOf(originAlias) : undefined;
67
- 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: [note, " "] }) : 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] }), _jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(resumeModel)), focused: false, width: previewTextWidth(columns), originIndex: originIndex }), 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}`)))] }));
67
+ // La riga del titolo si taglia QUI, con la contabilità del deck, invece di
68
+ // lasciarla a `wrap="truncate-end"`: Ink taglia con `cli-truncate`, che sfora
69
+ // di una colonna per emoji (invariante ③ di width.ts) e mangia il bordo del
70
+ // box. Pezzi e ragione stanno in `previewTitleParts`.
71
+ const [sidPart, notePart, titlePart] = cutParts(previewTitleParts(s.sessionId, note, s.title), previewTextWidth(columns));
72
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: sidPart }), notePart ? _jsx(Text, { color: "yellow", children: notePart }) : null, _jsx(Text, { dimColor: Boolean(note), children: titlePart })] }), _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] }), _jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(resumeModel)), focused: false, width: previewTextWidth(columns), originIndex: originIndex }), 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}`)))] }));
68
73
  }
69
74
  /**
70
75
  * 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.65.0",
3
+ "version": "0.65.2",
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
@@ -116,9 +116,9 @@ MODEL=""
116
116
  # tab. È l'UNICO ingresso di testo libero nel titolo — gli altri componenti
117
117
  # (emoji/name dal file committato, TaskID) sono controllati — quindi il testo non
118
118
  # si quota, si RIDUCE a un alfabeto sicuro (vedi _sane_note): tutto ciò che non
119
- # ci rientra sparisce, apici inclusi. L'alfabeto ammette le quadre e quattro
120
- # emoji (T156) perché il deck ci fa passare il prefisso del titolo di fallback
121
- # (`[ 🚀 ] slug`); ogni altra emoji cade ancora.
119
+ # ci rientra sparisce, apici inclusi. L'alfabeto ammette quattro emoji (T156)
120
+ # perché il deck ci fa passare il prefisso del titolo di fallback (`🚀 slug`);
121
+ # ogni altra emoji cade ancora.
122
122
  # --model <fable|opus|sonnet|haiku> (T108): modello della sessione. Enum e non
123
123
  # stringa libera perché il valore finisce dentro `bash -lc` e un refuso
124
124
  # produrrebbe una tab con un comando che il CLI rifiuta. Le voci sono ALIAS e
@@ -269,22 +269,22 @@ WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
269
269
  # titolo finisce dentro apici singoli in `bash -lc "claude --name '…'"`, e la
270
270
  # nota è testo digitato a mano — enumerare i caratteri pericolosi significa
271
271
  # sbagliarne uno, enumerare quelli ammessi no. Restano lettere, cifre, accentate
272
- # italiane, spazio, trattino, underscore, le quadre e quattro emoji (sotto);
273
- # tutto il resto (apici, backslash, `$`, backtick, `;`, ogni altra emoji…) cade.
272
+ # italiane, spazio, trattino, underscore e quattro emoji (sotto); tutto il resto
273
+ # (apici, backslash, `$`, backtick, `;`, quadre, ogni altra emoji…) cade.
274
274
  #
275
- # T156 — le quadre e le quattro emoji entrano per il PREFISSO del titolo di
276
- # fallback che il deck genera quando il campo nota è vuoto (`[ 🚀 ] slug`,
277
- # `ACTION_EMOJI` in `src/spawn.ts`). Senza di loro il prefisso spariva qui e
278
- # restava nella lista del deck, che riceve la nota grezza dal sidecar
279
- # `session-tasks.jsonl`: la stessa conversazione portava due titoli diversi,
280
- # senza errore.
275
+ # T156 — le quattro emoji entrano per il PREFISSO del titolo di fallback che il
276
+ # deck genera quando il campo nota è vuoto (`🚀 slug`, `ACTION_EMOJI` in
277
+ # `src/spawn.ts`). Senza di loro il prefisso spariva qui e restava nella lista
278
+ # del deck, che riceve la nota grezza dal sidecar `session-tasks.jsonl`: la
279
+ # stessa conversazione portava due titoli diversi, senza errore. Le quadre che
280
+ # delimitavano il prefisso sono cadute col prefisso stesso: l'emoji da sola si
281
+ # stacca già dallo slug.
281
282
  # La whitelist le nomina una per una e NON apre un intervallo di code point. Due
282
283
  # ragioni: un'emoji arriva nella tab solo se il deck la mostra identica in lista,
283
284
  # cioè se `sanitize` (`src/width.ts`) la lascia passare invece di renderla `·`, e
284
285
  # quel verdetto si verifica un glifo alla volta (🗺️ è astrale e finisce comunque
285
286
  # in `·`); e l'ordine di un intervallo dentro un bracket expression segue la
286
287
  # collazione della locale, non i code point.
287
- # `]` sta per PRIMO nel set negato, dove chiude la classe invece di aprirla.
288
288
  #
289
289
  # Il cap a 60 CHARACTER è largo di proposito: una tab che sfora tronca a destra
290
290
  # da sola, quindi il limite serve solo a non spingere fuori vista la parte del
@@ -305,7 +305,7 @@ _sane_note() { # <raw> → nota ridotta, spazi collassati, cap 60 char
305
305
  # `tr` non ne ha bisogno: squeeza uno spazio ASCII, e 0x20 non compare mai
306
306
  # come byte di continuazione di una sequenza UTF-8.
307
307
  s="$(printf '%s' "$1" \
308
- | LC_ALL=C.UTF-8 sed 's/[^]A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ[📐🚀📊🏁-]//g' \
308
+ | LC_ALL=C.UTF-8 sed 's/[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁-]//g' \
309
309
  | tr -s ' ')"
310
310
  s="${s# }"; s="${s% }"
311
311
  printf '%s' "${s:0:60}"