@lamemind/loom-deck 0.60.0 → 0.61.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/actions.js +11 -4
- package/dist/session-list.js +4 -2
- package/dist/spawn.js +39 -1
- package/dist/ui/panes.js +4 -4
- package/dist/ui/preview.js +1 -1
- package/dist/ui/search-screen.js +8 -7
- package/package.json +1 -1
- package/scripts/deck-run +9 -8
package/dist/actions.js
CHANGED
|
@@ -21,7 +21,7 @@ import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, } from '
|
|
|
21
21
|
import { neighborId } from './session-list.js';
|
|
22
22
|
import { cut, cutMiddle } from './width.js';
|
|
23
23
|
import { saveView, viewFilePath } from './view-store.js';
|
|
24
|
-
import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
|
|
24
|
+
import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, fallbackTitle, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
|
|
25
25
|
import { useTaskOps } from './task-ops.js';
|
|
26
26
|
export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNote, }) {
|
|
27
27
|
// La riga di stato di OGNI spawn di sessione Claude: il comando esatto, come
|
|
@@ -104,12 +104,19 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
104
104
|
// il campo è editabile: quello che l'utente legge è quello che parte. Assente
|
|
105
105
|
// per gli acceleratori della lista, che non hanno un campo da cui prenderlo e
|
|
106
106
|
// viaggiano col simbolo.
|
|
107
|
+
// T150 — il vuoto si riempie QUI, prima che i due canali si separino: il
|
|
108
|
+
// sidecar (`appendNote`, sotto) e il titolo tab (`--title-note`, dentro
|
|
109
|
+
// `spawnDeck`) devono vedere lo STESSO valore, o le due superfici mostrano
|
|
110
|
+
// nomi diversi per la stessa conversazione. Vale anche sugli acceleratori
|
|
111
|
+
// della lista (^K/^P/^R), che passano di qui con `spawnNote` sul proprio
|
|
112
|
+
// default '' (P1 preflight) — nascono quindi titolati anche loro.
|
|
107
113
|
function spawnForTask(id, kind, modelKind, spawnNote = '', prompt) {
|
|
108
114
|
const sid = randomUUID();
|
|
115
|
+
const note = spawnNote || fallbackTitle(tasksDir, id, kind) || '';
|
|
109
116
|
appendTaskBinding(cwd, sid, id);
|
|
110
|
-
if (
|
|
111
|
-
appendNote(cwd, sid,
|
|
112
|
-
const spawned = spawnDeck(id, cwd, sid, kind, modelKind,
|
|
117
|
+
if (note)
|
|
118
|
+
appendNote(cwd, sid, note);
|
|
119
|
+
const spawned = spawnDeck(id, cwd, sid, kind, modelKind, note, prompt);
|
|
113
120
|
spawned.child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
114
121
|
// Il modello resta SEMPRE visibile anche quando è il default, perché è un
|
|
115
122
|
// argomento esplicito del comando (T108): gli acceleratori della lista non
|
package/dist/session-list.js
CHANGED
|
@@ -111,9 +111,11 @@ const MIN_NOTE = 14;
|
|
|
111
111
|
export function rowLabel(text, note, budget) {
|
|
112
112
|
if (!note)
|
|
113
113
|
return { note: '', rest: cut(text, Math.max(0, budget)) };
|
|
114
|
-
//
|
|
114
|
+
// 1 colonna per lo spazio prima del residuo (T150 — la nota non porta più i
|
|
115
|
+
// caporali « » che decoravano il render, quindi non riserva più le loro 2
|
|
116
|
+
// colonne).
|
|
115
117
|
const rest = text;
|
|
116
|
-
const noteBudget = Math.max(0, budget
|
|
118
|
+
const noteBudget = Math.max(0, budget);
|
|
117
119
|
if (!rest)
|
|
118
120
|
return { note: cut(note, noteBudget), rest: '' };
|
|
119
121
|
// Il `min` col budget totale non è ridondante: su un pane strettissimo
|
package/dist/spawn.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
6
|
+
import { basename, dirname, join } from 'node:path';
|
|
7
|
+
import { findTaskFile } from './tasks.js';
|
|
7
8
|
// scripts/deck-run è un sibling della dir del bundle: src/ (dev, tsx) e dist/
|
|
8
9
|
// (build, node) stanno entrambi sotto la package root → risalita di un livello.
|
|
9
10
|
export const DECK_RUN = join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'deck-run');
|
|
@@ -171,6 +172,43 @@ export const DETAIL_ACTIONS = [
|
|
|
171
172
|
// DISTINTE fra loro. Due label con la stessa lettera renderebbero la seconda
|
|
172
173
|
// irraggiungibile, e in silenzio — chi aggiunge una voce lo controlla qui.
|
|
173
174
|
export const ACTION_HOTKEYS = Object.fromEntries(DETAIL_ACTIONS.map((a, i) => [a.label[0], i]));
|
|
175
|
+
// T150 — titolo di fallback quando il campo nota è vuoto, derivato dall'AZIONE
|
|
176
|
+
// e dal task. Mappa sul KIND, non sulla label (P2 preflight): alla sede del
|
|
177
|
+
// fallback (`spawnForTask` in actions.ts) arriva il kind — già specializzato
|
|
178
|
+
// quando la chiamata viene dal detail (`recap-task`/`recap-epic`) — mai la
|
|
179
|
+
// label del catalogo.
|
|
180
|
+
// D2/P7 — `none` (label "open") non ha una parola: è "nessuna azione", e
|
|
181
|
+
// inventargliene una contraddirebbe l'intenzione dell'utente.
|
|
182
|
+
// P3 — le tre varianti di recap condividono la parola: la specializzazione
|
|
183
|
+
// sceglie quale skill parte, non cosa l'utente sta chiedendo.
|
|
184
|
+
const ACTION_WORD = {
|
|
185
|
+
preflight: 'PREFL',
|
|
186
|
+
run: 'RUN',
|
|
187
|
+
recap: 'RECAP',
|
|
188
|
+
'recap-task': 'RECAP',
|
|
189
|
+
'recap-epic': 'RECAP',
|
|
190
|
+
checkpoint: 'CHKPOINT',
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Titolo di fallback per una conversazione con nota vuota: `{AZIONE} {slug}`,
|
|
194
|
+
* o il solo slug per `none` (D1/D2 preflight). Lo slug viene dal NOME del
|
|
195
|
+
* task file (`findTaskFile`), non dalla descrizione di `tasks.md`: il nome è
|
|
196
|
+
* già dentro l'alfabeto di `_sane_note` per costruzione — minuscolo, separato
|
|
197
|
+
* da trattini, senza punteggiatura — mentre la descrizione porta apostrofi e
|
|
198
|
+
* `/` che la riduzione toglie senza sostituto, saldando le parole (D1
|
|
199
|
+
* razionale).
|
|
200
|
+
*
|
|
201
|
+
* `null` quando il task file non si trova: il chiamante decide se ripiegare
|
|
202
|
+
* su nota vuota o su un altro fallback.
|
|
203
|
+
*/
|
|
204
|
+
export function fallbackTitle(tasksDir, id, kind) {
|
|
205
|
+
const path = findTaskFile(tasksDir, id);
|
|
206
|
+
if (!path)
|
|
207
|
+
return null;
|
|
208
|
+
const slug = basename(path, '.md').slice(id.length + 1).replace(/-/g, ' ');
|
|
209
|
+
const word = ACTION_WORD[kind];
|
|
210
|
+
return word ? `${word} ${slug}` : slug;
|
|
211
|
+
}
|
|
174
212
|
// Spawn detached: il deck spawna ma NON contiene la sessione (la possiede
|
|
175
213
|
// ptyxis-agent). unref + stdio ignore → ritorna subito, la TUI resta viva.
|
|
176
214
|
// sessionId pinnato (T27) → il binding sidecar è deterministico allo spawn.
|
package/dist/ui/panes.js
CHANGED
|
@@ -181,8 +181,8 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
|
|
|
181
181
|
termWidth(`${WARN} pin stale `) +
|
|
182
182
|
SID_CHARS +
|
|
183
183
|
(staleTask ? termWidth(staleTask) + 1 : 0) +
|
|
184
|
-
|
|
185
|
-
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: true, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: "yellow", children: WARN }), " pin stale", ' ', _jsx(Text, { dimColor: true, children: row.sessionId.slice(0, SID_CHARS) }), staleTask ? _jsxs(Text, { color: "green", children: [" ", staleTask] }) : null, staleNote ? _jsxs(Text, { color: "yellow", children: ["
|
|
184
|
+
1 /* spazio prima della nota */));
|
|
185
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: true, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: "yellow", children: WARN }), " pin stale", ' ', _jsx(Text, { dimColor: true, children: row.sessionId.slice(0, SID_CHARS) }), staleTask ? _jsxs(Text, { color: "green", children: [" ", staleTask] }) : null, staleNote ? _jsxs(Text, { color: "yellow", children: [" ", cut(staleNote, staleW)] }) : null] }, row.sessionId));
|
|
186
186
|
}
|
|
187
187
|
const s = row.session;
|
|
188
188
|
const isPinnedRow = row.pinned;
|
|
@@ -241,9 +241,9 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
|
|
|
241
241
|
// dicono già (progetto e task id): senza, la cella conterrebbe
|
|
242
242
|
// `🧵 loom-works · T59` accanto a una colonna che dice `T59`.
|
|
243
243
|
const label = rowLabel(sessionTitle(s, projectCore, bound), sessionNotes.get(s.sessionId), inner);
|
|
244
|
-
const used = (label.note ? termWidth(label.note)
|
|
244
|
+
const used = (label.note ? termWidth(label.note) : 0) +
|
|
245
245
|
(label.note && label.rest ? 1 : 0) +
|
|
246
246
|
termWidth(label.rest);
|
|
247
|
-
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 ? (
|
|
247
|
+
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 ? (_jsx(Text, { color: "yellow", bold: true, children: label.note })) : 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));
|
|
248
248
|
}))] }));
|
|
249
249
|
}
|
package/dist/ui/preview.js
CHANGED
|
@@ -64,7 +64,7 @@ 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: [
|
|
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}`)))] }));
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
70
|
* Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
|
package/dist/ui/search-screen.js
CHANGED
|
@@ -55,13 +55,14 @@ export function SearchScreen({ preview, hash, query, field, opts, result, rows,
|
|
|
55
55
|
const bound = bindings.get(s.sessionId);
|
|
56
56
|
const rowNote = sessionNotes.get(s.sessionId);
|
|
57
57
|
const noteShown = rowNote ? cut(rowNote, 24) : '';
|
|
58
|
-
// `+
|
|
59
|
-
// Il pavimento non è cosmetico:
|
|
60
|
-
// l'argomento di `cut` sotto
|
|
61
|
-
// La nota si misura con
|
|
62
|
-
// testo umano, emoji
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
// `+1` = lo spazio che separa la nota dall'etichetta (T150 — niente
|
|
59
|
+
// più caporali « » a decorarla). Il pavimento non è cosmetico:
|
|
60
|
+
// senza, un terminale stretto manda l'argomento di `cut` sotto
|
|
61
|
+
// zero, cioè un budget negativo. La nota si misura con
|
|
62
|
+
// `termWidth`, non con `.length`: contiene testo umano, emoji
|
|
63
|
+
// compresi.
|
|
64
|
+
const restWidth = Math.max(8, searchTitleWidth(columns) - (noteShown ? termWidth(noteShown) + 1 : 0));
|
|
65
|
+
return (_jsxs(Text, { inverse: sel, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, pinned.has(s.sessionId) ? _jsx(Text, { color: "yellow", children: "\uD83D\uDCCC" }) : _jsx(Text, { dimColor: true, children: "\u25CB" }), ' ', _jsx(Text, { color: "cyan", children: s.sessionId.slice(0, 8) }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), bound ?? _jsx(Text, { dimColor: true, children: "spot" }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), noteShown ? _jsxs(Text, { color: "yellow", bold: true, children: [noteShown, " "] }) : null, _jsx(Text, { dimColor: Boolean(noteShown), children: cut(conversationLabel(s, projectCore, bound), restWidth) }), _jsxs(Text, { dimColor: true, children: [' ', "(", row.hitCount, row.hidden > 0 ? `+${row.hidden}` : '', ") ", fmtDateTime(s.ts)] })] }, row.key));
|
|
65
66
|
}
|
|
66
67
|
const h = row.hit;
|
|
67
68
|
return (_jsxs(Text, { inverse: sel, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { dimColor: true, children: String(h.idx).padStart(4) }), ' ', _jsx(Text, { color: KIND_COLOR[h.kind], children: KIND_TAG[h.kind] }), ' ', cut(h.excerpt, searchExcerptWidth(columns))] }, row.key));
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -183,7 +183,7 @@ USAGE="uso: deck-run <TaskID> [--model <alias>] [--prompt-kind <kind>] [--sessio
|
|
|
183
183
|
--model modello della sessione: fable|opus|sonnet|haiku (default: opus)
|
|
184
184
|
valore ignoto → fallback sul default, con avviso su stderr
|
|
185
185
|
|
|
186
|
-
--title-note nota della conversazione, appesa al titolo tab
|
|
186
|
+
--title-note nota della conversazione, appesa al titolo tab
|
|
187
187
|
(ridotta a lettere/cifre/spazi/-/_, cap 60 char)"
|
|
188
188
|
|
|
189
189
|
if [[ $NO_TASK -eq 1 && -n "$TASK" ]]; then
|
|
@@ -317,15 +317,16 @@ if [[ -n "$_cfg" ]]; then
|
|
|
317
317
|
fi
|
|
318
318
|
# Suffisso nota (T64): distingue fra loro le tab di più conversazioni sulla
|
|
319
319
|
# STESSA task, che altrimenti condividono un titolo identico (`label · T81`) —
|
|
320
|
-
# la nota è già la maniglia con cui l'utente le distingue in lista
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
320
|
+
# la nota è già la maniglia con cui l'utente le distingue in lista. In coda per
|
|
321
|
+
# la solita ragione degli altri suffissi: il match compass è `.includes(label)`,
|
|
322
|
+
# la label deve restare intatta in TESTA.
|
|
323
|
+
# T150 — nudo, senza i caporali `«…»` che lo decoravano: la nota di fallback
|
|
324
|
+
# generata dal deck quando il campo è vuoto passa dallo stesso alfabeto ridotto
|
|
325
|
+
# e non ha bisogno di una cornice per distinguersi da un titolo scritto a mano.
|
|
326
|
+
# Nota ridotta a vuoto (era tutta emoji/punteggiatura) → nessun suffisso.
|
|
326
327
|
if [[ -n "$TITLE_NOTE" ]]; then
|
|
327
328
|
_note="$(_sane_note "$TITLE_NOTE")"
|
|
328
|
-
[[ -n "$_note" ]] && TITLE="${TITLE}
|
|
329
|
+
[[ -n "$_note" ]] && TITLE="${TITLE} ${_note}"
|
|
329
330
|
fi
|
|
330
331
|
# Suffisso fork (T28): un ramo eredita task e label dell'origine, quindi senza
|
|
331
332
|
# marcatore le due tab risulterebbero omonime nella stessa window. Suffisso e
|