@lamemind/loom-deck 0.26.0 → 0.27.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
@@ -157,8 +157,19 @@ function spawnDeck(id, cwd, sessionId, kind) {
157
157
  // task). Spot → `--no-task --resume`: resume nudo, solo label progetto. Nessun
158
158
  // prompt iniziale in entrambi i casi: riprendere una conversazione significa
159
159
  // continuarla, non iniettarle un messaggio (lo salta deck-run).
160
- function spawnDeckResume(taskId, cwd, sessionId) {
160
+ //
161
+ // T64 — la NOTA della conversazione (se c'è) viaggia nel titolo della tab. Più
162
+ // sessioni sulla stessa task hanno oggi titoli identici (`label · T81`): la nota
163
+ // è già ciò con cui l'utente le distingue in lista, quindi è anche ciò che
164
+ // distingue le tab. La passa il DECK e non la legge deck-run perché la nota vive
165
+ // nel sidecar `session-tasks.jsonl`, che deck-run non tocca (legge solo
166
+ // `.claude/loom-works.json`): tenerlo così evita di dare al primitive un secondo
167
+ // file da conoscere. Il titolo si congela qui — `claude --name` lo setta una
168
+ // volta sola, quindi una nota cambiata DOPO non ri-titola la tab già aperta.
169
+ function spawnDeckResume(taskId, cwd, sessionId, note) {
161
170
  const args = taskId ? [taskId, '--resume', sessionId] : ['--no-task', '--resume', sessionId];
171
+ if (note)
172
+ args.push('--title-note', note);
162
173
  const child = spawnOut(DECK_RUN, args, { cwd, detached: true, stdio: 'ignore' });
163
174
  child.unref();
164
175
  return child;
@@ -171,6 +182,10 @@ function spawnDeckResume(taskId, cwd, sessionId) {
171
182
  // conoscerlo prima che la sessione esista, e senza conoscerlo non si possono
172
183
  // scrivere né il binding task né il record di lineage (il transcript del fork
173
184
  // non nomina da nessuna parte la sessione d'origine).
185
+ // Nessun `--title-note` (T64): il ramo nasce con un sessionId proprio e SENZA
186
+ // nota nel sidecar — ereditare quella dell'origine metterebbe nel titolo una
187
+ // maniglia che nella lista non compare, cioè una promessa falsa. Il fork si
188
+ // distingue col suo marcatore, `· fork`.
174
189
  function spawnDeckFork(taskId, cwd, originId, newId) {
175
190
  const args = [
176
191
  ...(taskId ? [taskId] : ['--no-task']),
@@ -1016,7 +1031,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1016
1031
  }
1017
1032
  if (row.kind === 'session') {
1018
1033
  const bound = bindings.get(row.session.sessionId) ?? null;
1019
- const child = spawnDeckResume(bound, cwd, row.session.sessionId);
1034
+ const child = spawnDeckResume(bound, cwd, row.session.sessionId, sessionNotes.get(row.session.sessionId));
1020
1035
  child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
1021
1036
  setNote(`⏎ resume ${row.session.sessionId.slice(0, 8)} → tab CC${bound ? ` (${bound})` : ' (spot)'}`);
1022
1037
  return;
@@ -1466,7 +1481,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1466
1481
  }
1467
1482
  else {
1468
1483
  const bound = bindings.get(s.sessionId) ?? null;
1469
- const child = spawnDeckResume(bound, cwd, s.sessionId);
1484
+ const child = spawnDeckResume(bound, cwd, s.sessionId, sessionNotes.get(s.sessionId));
1470
1485
  child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
1471
1486
  setNote(`⏎ resume ${s.sessionId.slice(0, 8)} → tab CC${bound ? ` (${bound})` : ' (spot)'}`);
1472
1487
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.26.0",
3
+ "version": "0.27.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
@@ -49,6 +49,7 @@ SESSION_ID=""
49
49
  RESUME_ID=""
50
50
  NO_TASK=0
51
51
  FORK=0
52
+ TITLE_NOTE=""
52
53
  # Vuoto = flag non passato (≠ un default già risolto): serve a distinguere
53
54
  # "kind implicito" da "kind chiesto", perché con --no-task il primo è legittimo
54
55
  # (nessun prompt, come sempre) e il secondo è un errore d'uso.
@@ -76,8 +77,17 @@ PROMPT_KIND=""
76
77
  # dentro apici singoli in `bash -lc`, quindi il testo va tenuto in un posto solo
77
78
  # e verificato una volta, invece di spostare il rischio di quoting su ogni
78
79
  # chiamante. Assente → `recap`, così ogni invocazione preesistente è invariata.
80
+ # --title-note <testo> (T64): appende la nota della conversazione al titolo della
81
+ # tab. È l'UNICO ingresso di testo libero nel titolo — gli altri componenti
82
+ # (emoji/name dal file committato, TaskID) sono controllati — quindi il testo non
83
+ # si quota, si RIDUCE a un alfabeto sicuro (vedi _sane_note): tutto ciò che non
84
+ # ci rientra sparisce, apici inclusi.
79
85
  while [[ $# -gt 0 ]]; do
80
86
  case "$1" in
87
+ --title-note)
88
+ TITLE_NOTE="${2:-}"; shift 2 ;;
89
+ --title-note=*)
90
+ TITLE_NOTE="${1#*=}"; shift ;;
81
91
  --prompt-kind)
82
92
  PROMPT_KIND="${2:-}"; shift 2 ;;
83
93
  --prompt-kind=*)
@@ -112,7 +122,10 @@ USAGE="uso: deck-run <TaskID> [--prompt-kind <kind>] [--session-id <uuid>]
112
122
  none nessun prompt — sessione aperta sulla task, a mani nude
113
123
  recap recap stato task <TaskID>
114
124
  preflight /loom-works:preflight-task <TaskID>
115
- run /loom-works:run-task <TaskID> (run-doc sulle task D)"
125
+ run /loom-works:run-task <TaskID> (run-doc sulle task D)
126
+
127
+ --title-note nota della conversazione, appesa al titolo tab come «nota»
128
+ (ridotta a lettere/cifre/spazi/-/_, cap 60 char)"
116
129
 
117
130
  if [[ $NO_TASK -eq 1 && -n "$TASK" ]]; then
118
131
  echo "--no-task e <TaskID> sono mutuamente esclusivi" >&2
@@ -167,6 +180,29 @@ WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
167
180
  # suffisso "· <task>" per restare distinguibili fra più tab claude (il suffisso
168
181
  # non rompe il match: è .includes, non equals — stesso pattern del "· deck").
169
182
  # Fallback su "cc <task>" se manca il file o jq (progetto non loom-registered).
183
+
184
+ # Riduzione della nota a un alfabeto sicuro (T64). Whitelist, non blacklist: il
185
+ # titolo finisce dentro apici singoli in `bash -lc "claude --name '…'"`, e la
186
+ # nota è testo digitato a mano — enumerare i caratteri pericolosi significa
187
+ # sbagliarne uno, enumerare quelli ammessi no. Restano lettere, cifre, accentate
188
+ # italiane, spazio, trattino, underscore; tutto il resto (apici, backslash,
189
+ # `$`, backtick, `;`, emoji…) cade.
190
+ # Il cap a 60 CHARACTER è largo di proposito: una tab che sfora tronca a destra
191
+ # da sola, quindi il limite serve solo a non spingere fuori vista la parte del
192
+ # titolo che compass matcha, non a far stare la nota nella tab.
193
+ _sane_note() { # <raw> → nota ridotta, spazi collassati, cap 60 char
194
+ # LC_ALL locale alla funzione: serve a bash per tagliare a 60 CARATTERI e non
195
+ # a 60 byte (a byte, un cap che cade a metà di una `à` lascia UTF-8 rotto nel
196
+ # titolo). L'assegnazione rifà setlocale, l'uscita dalla funzione lo ripristina.
197
+ local LC_ALL=C.UTF-8
198
+ local s
199
+ s="$(printf '%s' "$1" \
200
+ | sed 's/[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ-]//g' \
201
+ | tr -s ' ')"
202
+ s="${s# }"; s="${s% }"
203
+ printf '%s' "${s:0:60}"
204
+ }
205
+
170
206
  _find_project_root() { # <start-dir> → dir con .claude/loom-works.json, o vuoto
171
207
  local d="${1%/}"
172
208
  while [[ -n "$d" && "$d" != "/" ]]; do
@@ -198,6 +234,18 @@ if [[ -n "$_cfg" ]]; then
198
234
  if [[ $NO_TASK -eq 1 ]]; then TITLE="${_label}"; else TITLE="${_label} · ${TASK}"; fi
199
235
  fi
200
236
  fi
237
+ # Suffisso nota (T64): distingue fra loro le tab di più conversazioni sulla
238
+ # STESSA task, che altrimenti condividono un titolo identico (`label · T81`) —
239
+ # la nota è già la maniglia con cui l'utente le distingue in lista, e le stesse
240
+ # `«…»` della lista la rendono riconoscibile a colpo d'occhio anche nella tab
241
+ # bar. In coda per la solita ragione degli altri suffissi: il match compass è
242
+ # `.includes(label)`, la label deve restare intatta in TESTA.
243
+ # Nota ridotta a vuoto (era tutta emoji/punteggiatura) → nessun suffisso, non
244
+ # `«»` a vuoto.
245
+ if [[ -n "$TITLE_NOTE" ]]; then
246
+ _note="$(_sane_note "$TITLE_NOTE")"
247
+ [[ -n "$_note" ]] && TITLE="${TITLE} «${_note}»"
248
+ fi
201
249
  # Suffisso fork (T28): un ramo eredita task e label dell'origine, quindi senza
202
250
  # marcatore le due tab risulterebbero omonime nella stessa window. Suffisso e
203
251
  # non prefisso perché il match compass è `.includes(label)` e la label deve