@lamemind/loom-deck 0.64.0 → 0.65.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/spawn.js CHANGED
@@ -191,20 +191,34 @@ export const ACTION_HOTKEYS = Object.fromEntries(DETAIL_ACTIONS.map((a, i) => [a
191
191
  // fallback (`spawnForTask` in actions.ts) arriva il kind — già specializzato
192
192
  // quando la chiamata viene dal detail (`recap-task`/`recap-epic`) — mai la
193
193
  // label del catalogo.
194
- // D2/P7 — `none` (label "open") non ha una parola: è "nessuna azione", e
195
- // inventargliene una contraddirebbe l'intenzione dell'utente.
196
- // P3 — le tre varianti di recap condividono la parola: la specializzazione
194
+ // D2/P7 — `none` (label "open") non ha un prefisso: è "nessuna azione", e
195
+ // inventargliene uno contraddirebbe l'intenzione dell'utente.
196
+ // P3 — le tre varianti di recap condividono il prefisso: la specializzazione
197
197
  // sceglie quale skill parte, non cosa l'utente sta chiedendo.
198
- const ACTION_WORD = {
199
- preflight: 'PREFL',
200
- run: 'RUN',
201
- recap: 'RECAP',
202
- 'recap-task': 'RECAP',
203
- 'recap-epic': 'RECAP',
204
- checkpoint: 'CHKPOINT',
198
+ //
199
+ // T156 — emoji invece della parola in maiuscolo (`PREFL`, `RUN`, …): il titolo
200
+ // si legge in lista deck e nella tab Ptyxis, dove l'azione va riconosciuta a
201
+ // colpo d'occhio e non letta.
202
+ // Le quattro sono ASTRALI (code point ≥ U+10000) per necessità, non per gusto:
203
+ // `sanitize` (`src/width.ts`) sostituisce con `·` ogni glifo su cui le due
204
+ // contabilità di larghezza divergono, e le emoji del BMP a presentazione testo
205
+ // (⚙️ ⏱️ 🛠️ …) divergono sempre — string-width le conta 2, VTE ne disegna 1, e
206
+ // il VS16 non sposta nessuno dei due verdetti. Un'emoji aggiunta qui va prima
207
+ // passata in `sanitize` per vedere se sopravvive.
208
+ // Le stesse quattro sono anche l'unica classe di emoji che `_sane_note` di
209
+ // `deck-run` lascia passare nel titolo della tab: la whitelist è una lista
210
+ // chiusa che nomina questi code point (P2 preflight), quindi cambiare una voce
211
+ // qui e non là riapre la divergenza fra titolo in lista e titolo in tab.
212
+ const ACTION_EMOJI = {
213
+ preflight: '📐',
214
+ run: '🚀',
215
+ recap: '📊',
216
+ 'recap-task': '📊',
217
+ 'recap-epic': '📊',
218
+ checkpoint: '🏁',
205
219
  };
206
220
  /**
207
- * Titolo di fallback per una conversazione con nota vuota: `{AZIONE} {slug}`,
221
+ * Titolo di fallback per una conversazione con nota vuota: `[ {emoji} ] {slug}`,
208
222
  * o il solo slug per `none` (D1/D2 preflight). Lo slug viene dal NOME del
209
223
  * task file (`findTaskFile`), non dalla descrizione di `tasks.md`: il nome è
210
224
  * già dentro l'alfabeto di `_sane_note` per costruzione — minuscolo, separato
@@ -212,6 +226,11 @@ const ACTION_WORD = {
212
226
  * `/` che la riduzione toglie senza sostituto, saldando le parole (D1
213
227
  * razionale).
214
228
  *
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.
233
+ *
215
234
  * `null` quando il task file non si trova: il chiamante decide se ripiegare
216
235
  * su nota vuota o su un altro fallback.
217
236
  */
@@ -220,8 +239,8 @@ export function fallbackTitle(tasksDir, id, kind) {
220
239
  if (!path)
221
240
  return null;
222
241
  const slug = basename(path, '.md').slice(id.length + 1).replace(/-/g, ' ');
223
- const word = ACTION_WORD[kind];
224
- return word ? `${word} ${slug}` : slug;
242
+ const emoji = ACTION_EMOJI[kind];
243
+ return emoji ? `[ ${emoji} ] ${slug}` : slug;
225
244
  }
226
245
  // Spawn detached: il deck spawna ma NON contiene la sessione (la possiede
227
246
  // ptyxis-agent). unref + stdio ignore → ritorna subito, la TUI resta viva.
@@ -283,11 +302,24 @@ export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT, spawn
283
302
  // default (P2 preflight). Qui però non c'è un default da cablare: chi
284
303
  // chiama ha già risolto il valore — il modello della conversazione d'origine,
285
304
  // o quello scelto al suo posto — e lo passa come argomento obbligatorio.
305
+ // La NOTA sta PRIMA di `--resume`, non in coda, ed è l'unico argomento di questa
306
+ // forma che non segue l'ordine di lettura. La ragione è la riga di stato: il
307
+ // comando ci arriva tagliato al mezzo (`noteCommand`, che tiene un terzo di
308
+ // testa e due terzi di coda), e `--title-note` è l'unico argomento a lunghezza
309
+ // LIBERA — porta il titolo della conversazione, che l'utente scrive come vuole.
310
+ // In coda si mangia i due terzi riservati, e a spingere fuori è proprio
311
+ // `--resume <uuid>`, cioè l'unica cosa che distingue una ripresa da un'altra: su
312
+ // una conversazione dal titolo lungo la riga di stato finiva per dire quale
313
+ // modello e quale titolo, mai quale conversazione. Spostata qui, la coda porta
314
+ // sempre `--resume <uuid> --model <alias>` — sessanta colonne su un budget di
315
+ // settantasei, indipendenti dal titolo. `deck-run` legge i flag in qualunque
316
+ // ordine (un solo posizionale, il TaskID), quindi lo spostamento non cambia
317
+ // niente per chi esegue.
286
318
  export function resumeArgs(taskId, sessionId, model, note) {
287
- const args = taskId ? [taskId, '--resume', sessionId] : ['--no-task', '--resume', sessionId];
288
- args.push('--model', model);
319
+ const args = taskId ? [taskId] : ['--no-task'];
289
320
  if (note)
290
321
  args.push('--title-note', note);
322
+ args.push('--resume', sessionId, '--model', model);
291
323
  return args;
292
324
  }
293
325
  export function spawnDeckResume(taskId, cwd, sessionId, model, note) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.64.0",
3
+ "version": "0.65.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": {
package/scripts/deck-run CHANGED
@@ -116,7 +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.
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.
120
122
  # --model <fable|opus|sonnet|haiku> (T108): modello della sessione. Enum e non
121
123
  # stringa libera perché il valore finisce dentro `bash -lc` e un refuso
122
124
  # produrrebbe una tab con un comando che il CLI rifiuta. Le voci sono ALIAS e
@@ -267,8 +269,23 @@ WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
267
269
  # titolo finisce dentro apici singoli in `bash -lc "claude --name '…'"`, e la
268
270
  # nota è testo digitato a mano — enumerare i caratteri pericolosi significa
269
271
  # sbagliarne uno, enumerare quelli ammessi no. Restano lettere, cifre, accentate
270
- # italiane, spazio, trattino, underscore; tutto il resto (apici, backslash,
271
- # `$`, backtick, `;`, emoji…) cade.
272
+ # italiane, spazio, trattino, underscore, le quadre e quattro emoji (sotto);
273
+ # tutto il resto (apici, backslash, `$`, backtick, `;`, ogni altra emoji…) cade.
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.
281
+ # La whitelist le nomina una per una e NON apre un intervallo di code point. Due
282
+ # ragioni: un'emoji arriva nella tab solo se il deck la mostra identica in lista,
283
+ # cioè se `sanitize` (`src/width.ts`) la lascia passare invece di renderla `·`, e
284
+ # quel verdetto si verifica un glifo alla volta (🗺️ è astrale e finisce comunque
285
+ # in `·`); e l'ordine di un intervallo dentro un bracket expression segue la
286
+ # collazione della locale, non i code point.
287
+ # `]` sta per PRIMO nel set negato, dove chiude la classe invece di aprirla.
288
+ #
272
289
  # Il cap a 60 CHARACTER è largo di proposito: una tab che sfora tronca a destra
273
290
  # da sola, quindi il limite serve solo a non spingere fuori vista la parte del
274
291
  # titolo che compass matcha, non a far stare la nota nella tab.
@@ -278,8 +295,17 @@ _sane_note() { # <raw> → nota ridotta, spazi collassati, cap 60 char
278
295
  # titolo). L'assegnazione rifà setlocale, l'uscita dalla funzione lo ripristina.
279
296
  local LC_ALL=C.UTF-8
280
297
  local s
298
+ # La locale va rimessa SUL `sed`, non ereditata da qui: `local LC_ALL` non
299
+ # porta l'attributo export quando `LC_ALL` non era già esportata nell'ambiente
300
+ # (misurato: il figlio la vede vuota), quindi `sed` ricade su `LANG` — sotto
301
+ # una locale `C` lavora a byte e un carattere multibyte fuori whitelist lascia
302
+ # nel titolo i byte che coincidono con quelli ammessi. Le emoji astrali, che
303
+ # condividono i primi due byte `f0 9f`, ne lasciano sempre due: `🔥 fuoco`
304
+ # diventa `\xf0\x9f fuoco`, cioè UTF-8 rotto dentro il titolo della tab.
305
+ # `tr` non ne ha bisogno: squeeza uno spazio ASCII, e 0x20 non compare mai
306
+ # come byte di continuazione di una sequenza UTF-8.
281
307
  s="$(printf '%s' "$1" \
282
- | sed 's/[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ-]//g' \
308
+ | LC_ALL=C.UTF-8 sed 's/[^]A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ[📐🚀📊🏁-]//g' \
283
309
  | tr -s ' ')"
284
310
  s="${s# }"; s="${s% }"
285
311
  printf '%s' "${s:0:60}"