@lamemind/loom-deck 0.65.1 → 0.66.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 +39 -2
- package/dist/cli.js +1 -1
- package/dist/deck-model.js +2 -1
- package/dist/frame.js +4 -1
- package/dist/hooks.js +16 -6
- package/dist/input.js +6 -0
- package/dist/layout.js +2 -2
- package/dist/overlays/sheet.js +35 -8
- package/dist/spawn.js +8 -6
- package/dist/task-index.js +25 -2
- package/dist/ui/detail-screen.js +4 -4
- package/dist/ui/panes.js +14 -3
- package/dist/ui/screens.js +1 -1
- package/dist/viewport.js +6 -5
- package/package.json +1 -1
- package/scripts/deck-run +13 -13
package/dist/actions.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { useMemo, useRef } from 'react';
|
|
19
19
|
import { randomUUID } from 'node:crypto';
|
|
20
20
|
import { loadPromptCatalog, modelFor } from './prompt-catalog.js';
|
|
21
|
-
import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, } from './task-index.js';
|
|
21
|
+
import { appendNote, appendPin, appendPriority, appendSessionRecord, appendTaskBinding, } from './task-index.js';
|
|
22
22
|
import { neighborId } from './session-list.js';
|
|
23
23
|
import { cut, cutMiddle } from './width.js';
|
|
24
24
|
import { saveView, viewFilePath } from './view-store.js';
|
|
@@ -114,12 +114,21 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
114
114
|
// nomi diversi per la stessa conversazione. Vale anche sugli acceleratori
|
|
115
115
|
// della lista (^K/^P/^R), che passano di qui con `spawnNote` sul proprio
|
|
116
116
|
// default '' (P1 preflight) — nascono quindi titolati anche loro.
|
|
117
|
-
function spawnForTask(id, kind, modelKind, spawnNote = '', prompt) {
|
|
117
|
+
function spawnForTask(id, kind, modelKind, spawnNote = '', prompt, priority = false) {
|
|
118
118
|
const sid = randomUUID();
|
|
119
119
|
const note = spawnNote || fallbackTitle(tasksDir, id, kind) || '';
|
|
120
120
|
appendTaskBinding(cwd, sid, id);
|
|
121
121
|
if (note)
|
|
122
122
|
appendNote(cwd, sid, note);
|
|
123
|
+
// T158 — la marca si appende NELLO STESSO momento del binding, prima che la
|
|
124
|
+
// tab esista: il `sessionId` è già noto (lo pinna `deck-run`), quindi il primo
|
|
125
|
+
// cambio di stato della conversazione trova la marca già su disco. Scriverla
|
|
126
|
+
// dopo lo spawn sarebbe una corsa contro il primo `Stop`.
|
|
127
|
+
//
|
|
128
|
+
// Solo il ramo acceso scrive: un `priority:false` allo spawn di ogni sessione
|
|
129
|
+
// riempirebbe il sidecar di smarcature di conversazioni mai marcate.
|
|
130
|
+
if (priority)
|
|
131
|
+
appendPriority(cwd, sid, true);
|
|
123
132
|
const spawned = spawnDeck(id, cwd, sid, kind, modelKind, note, prompt);
|
|
124
133
|
spawned.child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
125
134
|
// Il modello resta SEMPRE visibile anche quando è il default, perché è un
|
|
@@ -279,6 +288,33 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
279
288
|
model.setSelSessionId(landing);
|
|
280
289
|
setNote(`${isPinned ? 'unpin' : '📌 pin'} ${sid.slice(0, 8)}`);
|
|
281
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* T158 — `a`: marca/smarca la conversazione selezionata come PRIORITARIA.
|
|
293
|
+
*
|
|
294
|
+
* Gemella di `togglePin` nella forma (azione immediata, scrive il sidecar,
|
|
295
|
+
* ricarica subito) ma non nell'effetto: la marca non sposta e non filtra
|
|
296
|
+
* niente, quindi non c'è nessun vicino su cui atterrare — la riga resta dov'è
|
|
297
|
+
* e cambia solo il glifo.
|
|
298
|
+
*
|
|
299
|
+
* Vale anche su una pinnata STALE: il transcript non c'è più, ma la marca è un
|
|
300
|
+
* attributo del `sessionId` e non del transcript, e l'unico posto da cui
|
|
301
|
+
* toglierla resta quella riga.
|
|
302
|
+
*/
|
|
303
|
+
function togglePriority() {
|
|
304
|
+
if (model.focus !== 'sessions') {
|
|
305
|
+
setNote('a → priorità: seleziona una sessione (→ per il pane)');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const sid = model.selSessionId;
|
|
309
|
+
if (!sid) {
|
|
310
|
+
setNote('a → nessuna sessione da marcare');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const on = model.sessionPriority.has(sid);
|
|
314
|
+
appendPriority(cwd, sid, !on);
|
|
315
|
+
model.reloadSessions();
|
|
316
|
+
setNote(`${on ? 'priorità via' : '🚨 prioritaria'} ${sid.slice(0, 8)}`);
|
|
317
|
+
}
|
|
282
318
|
/** `t` — terminale a project root, con un titolo che il matcher di compass riconosce. */
|
|
283
319
|
function openTerminal() {
|
|
284
320
|
const title = model.identity ? `🖥️ ${model.identity.name} [term]` : null;
|
|
@@ -365,6 +401,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
365
401
|
assignSession,
|
|
366
402
|
forkSession,
|
|
367
403
|
togglePin,
|
|
404
|
+
togglePriority,
|
|
368
405
|
drainInbox,
|
|
369
406
|
unwrapPath,
|
|
370
407
|
openTerminal,
|
package/dist/cli.js
CHANGED
|
@@ -270,7 +270,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
270
270
|
inboxPane: model.rightPane === 'inbox',
|
|
271
271
|
hasInbox: model.selInbox !== null,
|
|
272
272
|
gitlink: gitlink.enabled,
|
|
273
|
-
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark }), model.rightPane === 'inbox' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
273
|
+
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark }), model.rightPane === 'inbox' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, sessionPriority: model.sessionPriority, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
274
274
|
}
|
|
275
275
|
const cwd = process.cwd();
|
|
276
276
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
package/dist/deck-model.js
CHANGED
|
@@ -111,7 +111,7 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
111
111
|
// di STATO in fondo al frame (il feedback di un'azione). Due concetti diversi
|
|
112
112
|
// a una lettera di distanza sarebbero una trappola di lettura — e di
|
|
113
113
|
// scrittura, visto che `setNote` compare in quasi ogni ramo.
|
|
114
|
-
const { sessions, bindings, forkOf, pinned, notes: sessionNotes, live, reload: reloadSessions, } = useSessions(cwd);
|
|
114
|
+
const { sessions, bindings, forkOf, pinned, notes: sessionNotes, priority: sessionPriority, live, reload: reloadSessions, } = useSessions(cwd);
|
|
115
115
|
const [focus, setFocus] = useState('tasks');
|
|
116
116
|
// T39 — selezione KEYED SU ID, non su indice. Con una vista trasformata
|
|
117
117
|
// (filtro/sort) l'indice non identifica più la stessa task: leggere l'array
|
|
@@ -470,6 +470,7 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
470
470
|
forkOf,
|
|
471
471
|
pinned,
|
|
472
472
|
sessionNotes,
|
|
473
|
+
sessionPriority,
|
|
473
474
|
live,
|
|
474
475
|
reloadSessions,
|
|
475
476
|
detail,
|
package/dist/frame.js
CHANGED
|
@@ -100,7 +100,10 @@ export function deckLegend(state) {
|
|
|
100
100
|
// allo spawn, e due nomi per una cosa sola li paga chi legge le due
|
|
101
101
|
// schermate. Rename di sola ETICHETTA: il dato resta `note` nel sidecar e
|
|
102
102
|
// `--title-note` in deck-run, dove rinominarlo sarebbe un breaking.
|
|
103
|
-
|
|
103
|
+
// T158 — `a` sta accanto a `p` perché condivide la condizione (`canPin`:
|
|
104
|
+
// focus sul pane e una riga selezionata, stale compresa) e la forma: due
|
|
105
|
+
// attributi della stessa conversazione, scritti nello stesso sidecar.
|
|
106
|
+
...(canPin ? ['p pin', 'a priorità', 'N titolo', 'A assegna'] : []),
|
|
104
107
|
// T121 — le due voci nominano il GESTO e non l'oggetto: «status» da solo
|
|
105
108
|
// sarebbe indistinguibile dal recap della task su `^K`, che è un'altra
|
|
106
109
|
// cosa e sta a due voci di distanza.
|
package/dist/hooks.js
CHANGED
|
@@ -143,6 +143,7 @@ export function useSessions(projectRoot) {
|
|
|
143
143
|
forkOf: new Map(),
|
|
144
144
|
pinned: new Map(),
|
|
145
145
|
notes: new Map(),
|
|
146
|
+
priority: new Set(),
|
|
146
147
|
live: new Map(),
|
|
147
148
|
});
|
|
148
149
|
// T50 — pin/unpin scrive il sidecar e vuole feedback IMMEDIATO, non al
|
|
@@ -160,7 +161,13 @@ export function useSessions(projectRoot) {
|
|
|
160
161
|
}
|
|
161
162
|
catch {
|
|
162
163
|
sessions = [];
|
|
163
|
-
index = {
|
|
164
|
+
index = {
|
|
165
|
+
bindings: new Map(),
|
|
166
|
+
forkOf: new Map(),
|
|
167
|
+
pinned: new Map(),
|
|
168
|
+
notes: new Map(),
|
|
169
|
+
priority: new Set(),
|
|
170
|
+
};
|
|
164
171
|
}
|
|
165
172
|
// T62 — le vive stanno sullo STESSO tick delle altre fonti, non su una
|
|
166
173
|
// scala propria come `useArchivable`: `status` cambia a ogni turno, quindi
|
|
@@ -174,10 +181,11 @@ export function useSessions(projectRoot) {
|
|
|
174
181
|
catch {
|
|
175
182
|
live = new Map();
|
|
176
183
|
}
|
|
177
|
-
const { bindings, forkOf, pinned, notes } = index;
|
|
178
|
-
// La signature copre anche fork, pin
|
|
179
|
-
// toggle di pin
|
|
180
|
-
// quindi devono forzare il re-render come
|
|
184
|
+
const { bindings, forkOf, pinned, notes, priority } = index;
|
|
185
|
+
// La signature copre anche fork, pin, note e marca di priorità: un record
|
|
186
|
+
// di lineage, un toggle di pin, una nota appena scritta o un 🚨 acceso
|
|
187
|
+
// cambiano la lista renderizzata, quindi devono forzare il re-render come
|
|
188
|
+
// farebbe un binding nuovo.
|
|
181
189
|
const sig = sessions.map((s) => `${s.sessionId}:${s.ts}`).join('|') +
|
|
182
190
|
'#' +
|
|
183
191
|
[...bindings.entries()].map(([k, v]) => `${k}=${v}`).sort().join(',') +
|
|
@@ -188,11 +196,13 @@ export function useSessions(projectRoot) {
|
|
|
188
196
|
'#' +
|
|
189
197
|
[...notes.entries()].map(([k, v]) => `${k}"${v}`).sort().join(',') +
|
|
190
198
|
'#' +
|
|
199
|
+
[...priority].sort().join(',') +
|
|
200
|
+
'#' +
|
|
191
201
|
liveSig(live);
|
|
192
202
|
if (sig === lastSig)
|
|
193
203
|
return;
|
|
194
204
|
lastSig = sig;
|
|
195
|
-
setState({ sessions, bindings, forkOf, pinned, notes, live });
|
|
205
|
+
setState({ sessions, bindings, forkOf, pinned, notes, priority, live });
|
|
196
206
|
};
|
|
197
207
|
reloadRef.current = reload;
|
|
198
208
|
reload();
|
package/dist/input.js
CHANGED
|
@@ -479,6 +479,12 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
479
479
|
// selezionata del pane sessioni.
|
|
480
480
|
actions.togglePin();
|
|
481
481
|
}
|
|
482
|
+
else if (input === 'a') {
|
|
483
|
+
// T158 — marca prioritaria, gemella di `p`. Minuscola perché è
|
|
484
|
+
// un'azione immediata, e `a` per «allarme» (il glifo di riga è 🚨): la
|
|
485
|
+
// `A` maiuscola resta al modale di assegnazione, come `f`/`F` e `c`/`C`.
|
|
486
|
+
actions.togglePriority();
|
|
487
|
+
}
|
|
482
488
|
else if (input === 'm') {
|
|
483
489
|
// T148/T154 — bersaglio deciso dal focus E dalla selezione (P2): con
|
|
484
490
|
// una riga sessioni selezionata (stale compresa, P3) scorre il modello
|
package/dist/layout.js
CHANGED
|
@@ -106,8 +106,8 @@ export function previewTextWidth(columns) {
|
|
|
106
106
|
* (§`assignTextWidth` sopra): lasciare la riga a `wrap="truncate-end"` la fa
|
|
107
107
|
* tagliare da `cli-truncate`, che sfora di una colonna per emoji e mangia il
|
|
108
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
|
|
110
|
-
*
|
|
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
111
|
* limite.
|
|
112
112
|
*/
|
|
113
113
|
export function previewTitleParts(sessionId, note, title) {
|
package/dist/overlays/sheet.js
CHANGED
|
@@ -22,16 +22,23 @@ import { ACTION_HOTKEYS, DETAIL_ACTIONS, MODELS, MODEL_DEFAULT, specializeRecap,
|
|
|
22
22
|
import { taskIsEpic } from '../tasks.js';
|
|
23
23
|
import { fieldsKey } from '../fields.js';
|
|
24
24
|
import { loadPromptCatalog, modelFor, promptFor } from '../prompt-catalog.js';
|
|
25
|
-
// T117 — le
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
25
|
+
// T117 — le righe dell'area di compilazione del detail, nell'ordine in cui si
|
|
26
|
+
// leggono dall'alto. Gli indici sono nominati perché li usano insieme l'handler
|
|
27
|
+
// dei tasti e la resa, e un `2` nudo in due file diversi è la coordinata che
|
|
28
|
+
// scade appena una riga si sposta.
|
|
29
|
+
//
|
|
30
|
+
// T158 — `priority` sta in coda e non accanto al titolo: le prime quattro righe
|
|
31
|
+
// sono i parametri del COMANDO che parte, questa è un attributo della
|
|
32
|
+
// conversazione che nasce, e vive nel sidecar invece che nell'argv di deck-run.
|
|
33
|
+
export const DROW = { action: 0, prompt: 1, model: 2, title: 3, priority: 4 };
|
|
34
|
+
/** T158 — le due voci della riga priorità, nell'ordine degli indici. */
|
|
35
|
+
export const PRIORITY_CHOICES = ['no', '🚨 sì'];
|
|
30
36
|
export const DETAIL_FIELDS = [
|
|
31
37
|
{ kind: 'choice', count: DETAIL_ACTIONS.length, hotkeys: ACTION_HOTKEYS },
|
|
32
38
|
{ kind: 'text' },
|
|
33
39
|
{ kind: 'choice', count: MODELS.length },
|
|
34
40
|
{ kind: 'text' },
|
|
41
|
+
{ kind: 'choice', count: PRIORITY_CHOICES.length },
|
|
35
42
|
];
|
|
36
43
|
export function useSheetOverlay(deps) {
|
|
37
44
|
const { rows, columns, setMode, setNote, onAction } = deps;
|
|
@@ -51,6 +58,11 @@ export function useSheetOverlay(deps) {
|
|
|
51
58
|
// T117 — il prompt iniziale, EDITABILE. Quello che si legge nel campo è quello
|
|
52
59
|
// che parte: non un'anteprima di qualcos'altro.
|
|
53
60
|
const [prompt, setPrompt] = useState('');
|
|
61
|
+
// T158 — la conversazione nasce già marcata prioritaria? Si azzera a ogni
|
|
62
|
+
// apertura come gli altri campi: una marca armata che sopravvive alla chiusura
|
|
63
|
+
// farebbe partire prioritaria la conversazione successiva, senza che nulla a
|
|
64
|
+
// schermo lo dica.
|
|
65
|
+
const [priority, setPriority] = useState(false);
|
|
54
66
|
/** La task aperta è un cappello (`Size: Epic`)? Deciso UNA VOLTA all'apertura,
|
|
55
67
|
* dal testo che il detail ha già in mano: rifarlo a ogni cambio di azione
|
|
56
68
|
* riparserebbe l'intero task file per un campo dell'header. */
|
|
@@ -125,6 +137,7 @@ export function useSheetOverlay(deps) {
|
|
|
125
137
|
setModel(modelFor(catalog, kind) ?? MODEL_DEFAULT);
|
|
126
138
|
setSpawnNote('');
|
|
127
139
|
setPrompt(promptFor(catalog, kind, next.id));
|
|
140
|
+
setPriority(false);
|
|
128
141
|
setCursor({ row: DROW.action, caret: 0 });
|
|
129
142
|
setFind(null);
|
|
130
143
|
setOccIdx(0);
|
|
@@ -153,8 +166,21 @@ export function useSheetOverlay(deps) {
|
|
|
153
166
|
const fieldsIO = {
|
|
154
167
|
text: (row) => (row === DROW.prompt ? prompt : spawnNote),
|
|
155
168
|
setText: (row, next) => (row === DROW.prompt ? setPrompt(next) : setSpawnNote(next)),
|
|
156
|
-
choice: (row) =>
|
|
157
|
-
|
|
169
|
+
choice: (row) => row === DROW.action
|
|
170
|
+
? action
|
|
171
|
+
: row === DROW.priority
|
|
172
|
+
? priority
|
|
173
|
+
? 1
|
|
174
|
+
: 0
|
|
175
|
+
: Math.max(0, MODELS.indexOf(model)),
|
|
176
|
+
setChoice: (row, index) => {
|
|
177
|
+
if (row === DROW.action)
|
|
178
|
+
selectAction(index);
|
|
179
|
+
else if (row === DROW.priority)
|
|
180
|
+
setPriority(index === 1);
|
|
181
|
+
else
|
|
182
|
+
setModel(MODELS[index]);
|
|
183
|
+
},
|
|
158
184
|
};
|
|
159
185
|
function close() {
|
|
160
186
|
setMode('normal');
|
|
@@ -259,7 +285,7 @@ export function useSheetOverlay(deps) {
|
|
|
259
285
|
const text = prompt.trim();
|
|
260
286
|
close();
|
|
261
287
|
if (id)
|
|
262
|
-
onAction(id, specializeRecap(act.kind, epic), model, note, text);
|
|
288
|
+
onAction(id, specializeRecap(act.kind, epic), model, note, text, priority);
|
|
263
289
|
return;
|
|
264
290
|
}
|
|
265
291
|
if (key.pageUp) {
|
|
@@ -284,6 +310,7 @@ export function useSheetOverlay(deps) {
|
|
|
284
310
|
model,
|
|
285
311
|
spawnNote,
|
|
286
312
|
prompt,
|
|
313
|
+
priority,
|
|
287
314
|
cursor,
|
|
288
315
|
find,
|
|
289
316
|
lines,
|
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: `
|
|
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
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
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 ?
|
|
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.
|
package/dist/task-index.js
CHANGED
|
@@ -25,6 +25,18 @@ export function appendPin(projectRoot, sessionId, pinned) {
|
|
|
25
25
|
export function appendNote(projectRoot, sessionId, note) {
|
|
26
26
|
appendSessionRecord(projectRoot, { sessionId, note });
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* T158 — marca/smarca la conversazione come prioritaria: stessa forma di
|
|
30
|
+
* `appendPin`, quarto campo mutabile del record.
|
|
31
|
+
*
|
|
32
|
+
* Il campo lo leggono TRE attori a rilascio indipendente — il deck, compass e
|
|
33
|
+
* l'hook `announce-state.sh` del plugin. Il nome, una volta scritto nei file
|
|
34
|
+
* degli utenti, non si rinomina senza migrazione: è un contratto fra tre repo,
|
|
35
|
+
* non un dettaglio interno di questo modulo.
|
|
36
|
+
*/
|
|
37
|
+
export function appendPriority(projectRoot, sessionId, priority) {
|
|
38
|
+
appendSessionRecord(projectRoot, { sessionId, priority });
|
|
39
|
+
}
|
|
28
40
|
// Una sola lettura del JSONL per entrambe le mappe: il deck poll-a l'indice a
|
|
29
41
|
// ogni tick, leggere il file due volte raddoppierebbe l'I/O per nulla.
|
|
30
42
|
// Last-wins per campo (un re-pin dello stesso sessionId sovrascrive), e i due
|
|
@@ -35,12 +47,13 @@ export function loadSessionIndex(projectRoot) {
|
|
|
35
47
|
const forkOf = new Map();
|
|
36
48
|
const pinned = new Map();
|
|
37
49
|
const notes = new Map();
|
|
50
|
+
const priority = new Set();
|
|
38
51
|
let content;
|
|
39
52
|
try {
|
|
40
53
|
content = readFileSync(taskIndexPath(projectRoot), 'utf8');
|
|
41
54
|
}
|
|
42
55
|
catch {
|
|
43
|
-
return { bindings, forkOf, pinned, notes };
|
|
56
|
+
return { bindings, forkOf, pinned, notes, priority };
|
|
44
57
|
}
|
|
45
58
|
let order = 0; // posizione crescente dei record pinned → rango di pin (D2)
|
|
46
59
|
for (const line of content.split('\n')) {
|
|
@@ -85,10 +98,20 @@ export function loadSessionIndex(projectRoot) {
|
|
|
85
98
|
else
|
|
86
99
|
notes.delete(d.sessionId);
|
|
87
100
|
}
|
|
101
|
+
// T158 — stesso last-wins per campo: `false` è una smarcatura esplicita e
|
|
102
|
+
// toglie la chiave, il record che non nomina il campo non la tocca. Il
|
|
103
|
+
// `typeof` è quindi obbligatorio anche qui: un test di verità confonderebbe
|
|
104
|
+
// «smarcata» con «mai marcata», e la prima deve poter cancellare la seconda.
|
|
105
|
+
if (typeof d.priority === 'boolean') {
|
|
106
|
+
if (d.priority)
|
|
107
|
+
priority.add(d.sessionId);
|
|
108
|
+
else
|
|
109
|
+
priority.delete(d.sessionId);
|
|
110
|
+
}
|
|
88
111
|
}
|
|
89
112
|
catch {
|
|
90
113
|
// riga corrotta → skip
|
|
91
114
|
}
|
|
92
115
|
}
|
|
93
|
-
return { bindings, forkOf, pinned, notes };
|
|
116
|
+
return { bindings, forkOf, pinned, notes, priority };
|
|
94
117
|
}
|
package/dist/ui/detail-screen.js
CHANGED
|
@@ -7,7 +7,7 @@ import { sliceLine } from '../text-search.js';
|
|
|
7
7
|
import { sliceSpans } from '../markdown.js';
|
|
8
8
|
import { ChoiceRow, FieldText, LABEL_W } from './fields.js';
|
|
9
9
|
import { DETAIL_ACTIONS, MODELS } from '../spawn.js';
|
|
10
|
-
import { DROW } from '../overlays/sheet.js';
|
|
10
|
+
import { DROW, PRIORITY_CHOICES } from '../overlays/sheet.js';
|
|
11
11
|
import { CARET, CARET_OFF, WARN } from '../glyphs.js';
|
|
12
12
|
/** Resa di ogni costrutto markdown (T75 · D4): un solo livello di enfasi per
|
|
13
13
|
* costrutto, senza un secondo alfabeto da imparare. Heading uguali a ogni
|
|
@@ -70,7 +70,7 @@ const ACTION_KEYS = DETAIL_ACTIONS.map((a) => a.label[0]).join(' ');
|
|
|
70
70
|
* decidere QUALE azione lanciare, e con due overlay separati quella decisione
|
|
71
71
|
* costava uscire dal viewer e ricordarsi la combo.
|
|
72
72
|
*
|
|
73
|
-
* T117 — i
|
|
73
|
+
* T117 — i parametri dello spawn sono RIGHE con un fuoco solo,
|
|
74
74
|
* la stessa forma del modale edit: prima erano tre alfabeti diversi sulla stessa
|
|
75
75
|
* schermata (`←→` per l'azione, `tab` per il modello, i tasti nudi per la nota),
|
|
76
76
|
* e ogni parametro aggiunto ne chiedeva un quarto.
|
|
@@ -79,7 +79,7 @@ const ACTION_KEYS = DETAIL_ACTIONS.map((a) => a.label[0]).join(' ');
|
|
|
79
79
|
* rettangolo ha già coordinate e area cliccabile, quindi il layout sopravvive
|
|
80
80
|
* all'arrivo del mouse (T21 · SGR enable + hit-test) senza migrazione.
|
|
81
81
|
*/
|
|
82
|
-
export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, prompt, cursor, columns, find, occ, occCur, }) {
|
|
82
|
+
export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, prompt, priority, cursor, columns, find, occ, occCur, }) {
|
|
83
83
|
const last = Math.min(total, top + capacity);
|
|
84
84
|
// Il taglio lo fa il chiamante (invariante ③ di width.ts): le righe bottoni
|
|
85
85
|
// sono ASCII, quindi `truncate-end` oggi darebbe il risultato giusto per caso
|
|
@@ -91,5 +91,5 @@ export function DetailScreen({ id, title, missing, lines, spans, top, total, cap
|
|
|
91
91
|
// campo vuoto): riservarle sempre toglierebbe testo visibile a un titolo lungo
|
|
92
92
|
// per un avviso assente.
|
|
93
93
|
const textW = (extra) => Math.max(10, width - LABEL_W - CARET_OFF.length - extra);
|
|
94
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: id }), " \u00B7 ", cut(title, Math.max(10, width - 34)), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), find?.open ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " occorrenza \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " caret \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " tieni \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " testo \u00B7", ' ', _jsx(Text, { color: "yellow", children: ACTION_KEYS }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota campo \u00B7 ", _jsx(Text, { color: "yellow", children: "^F" }), " cerca"] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " task file non trovato \u00B7 le azioni restano attive (deck-run risolve la task per id)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: occ, current: occCur }, top + i)))) }), find?.open ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "cerca " }), _jsx(FieldText, { value: find.q, caret: find.caret, focused: true, cols: Math.max(10, width - 28) }), find.q.length === 0 ? (_jsx(Text, { dimColor: true, children: " \u00B7 digita per cercare" })) : occ.length === 0 ? (_jsx(Text, { color: "yellow", children: " \u00B7 nessuna occorrenza" })) : (_jsxs(Text, { color: "cyan", children: [' ', "\u00B7 ", occCur + 1, "/", occ.length] }))] }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(ChoiceRow, { label: "azione", values: DETAIL_ACTIONS.map((a) => a.label), index: action, focused: cursor.row === DROW.action, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.prompt), _jsx(Text, { dimColor: true, children: 'prompt'.padEnd(LABEL_W) }), _jsx(FieldText, { value: prompt, caret: cursor.caret, focused: cursor.row === DROW.prompt, cols: textW(prompt ? 0 : PROMPT_HINT.length) }), prompt ? null : _jsx(Text, { dimColor: true, children: PROMPT_HINT })] }), _jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(model)), focused: cursor.row === DROW.model, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.title), _jsx(Text, { dimColor: true, children: 'titolo'.padEnd(LABEL_W) }), _jsx(FieldText, { value: spawnNote, caret: cursor.caret, focused: cursor.row === DROW.title, cols: textW(spawnNote ? 0 : NOTE_HINT.length) }), spawnNote ? null : _jsx(Text, { dimColor: true, children: NOTE_HINT })] })] })] }));
|
|
94
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: id }), " \u00B7 ", cut(title, Math.max(10, width - 34)), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), find?.open ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " occorrenza \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " caret \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " tieni \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " testo \u00B7", ' ', _jsx(Text, { color: "yellow", children: ACTION_KEYS }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota campo \u00B7 ", _jsx(Text, { color: "yellow", children: "^F" }), " cerca"] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " task file non trovato \u00B7 le azioni restano attive (deck-run risolve la task per id)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: occ, current: occCur }, top + i)))) }), find?.open ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "cerca " }), _jsx(FieldText, { value: find.q, caret: find.caret, focused: true, cols: Math.max(10, width - 28) }), find.q.length === 0 ? (_jsx(Text, { dimColor: true, children: " \u00B7 digita per cercare" })) : occ.length === 0 ? (_jsx(Text, { color: "yellow", children: " \u00B7 nessuna occorrenza" })) : (_jsxs(Text, { color: "cyan", children: [' ', "\u00B7 ", occCur + 1, "/", occ.length] }))] }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(ChoiceRow, { label: "azione", values: DETAIL_ACTIONS.map((a) => a.label), index: action, focused: cursor.row === DROW.action, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.prompt), _jsx(Text, { dimColor: true, children: 'prompt'.padEnd(LABEL_W) }), _jsx(FieldText, { value: prompt, caret: cursor.caret, focused: cursor.row === DROW.prompt, cols: textW(prompt ? 0 : PROMPT_HINT.length) }), prompt ? null : _jsx(Text, { dimColor: true, children: PROMPT_HINT })] }), _jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(model)), focused: cursor.row === DROW.model, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.title), _jsx(Text, { dimColor: true, children: 'titolo'.padEnd(LABEL_W) }), _jsx(FieldText, { value: spawnNote, caret: cursor.caret, focused: cursor.row === DROW.title, cols: textW(spawnNote ? 0 : NOTE_HINT.length) }), spawnNote ? null : _jsx(Text, { dimColor: true, children: NOTE_HINT })] }), _jsx(ChoiceRow, { label: "priorit\u00E0", values: PRIORITY_CHOICES, index: priority ? 1 : 0, focused: cursor.row === DROW.priority, width: width })] })] }));
|
|
95
95
|
}
|
package/dist/ui/panes.js
CHANGED
|
@@ -148,7 +148,7 @@ export function InboxPane({ files, counts, activeView, paneCount, selectedPath,
|
|
|
148
148
|
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: mark === 'broken' ? 'red' : mark === 'queued' ? 'green' : undefined, dimColor: mark === 'held' || mark === 'branched', children: pad(NATURA_SHORT[f.natura], NATURA_W) }), ' ', _jsx(Text, { color: mark === 'branched' ? 'yellow' : undefined, children: pad(INBOX_MARK[mark], INBOX_MARK_W) }), ' ', branchMark ? _jsx(Text, { color: "yellow", children: sanitize(branchMark) }) : null, _jsx(Text, { dimColor: mark !== 'queued', children: name }), ' '.repeat(Math.max(0, inner - termWidth(name))), ' ', _jsx(Text, { dimColor: true, children: pad(age, ageW, 'right') })] }, f.path));
|
|
149
149
|
}))] }));
|
|
150
150
|
}
|
|
151
|
-
export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, counts, activeView, paneCount, selectedId, focused, above, below, columns, forkOf, sessionNotes, projectCore, live, }) {
|
|
151
|
+
export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, counts, activeView, paneCount, selectedId, focused, above, below, columns, forkOf, sessionNotes, sessionPriority, projectCore, live, }) {
|
|
152
152
|
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(SessionsHeader, { parentLabel: parentLabel, counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), paneCount === 0 ? (
|
|
153
153
|
// T100 — la nota della vista di default resta quella storica, che nomina
|
|
154
154
|
// il PARENT (task, spot o tutte); le altre tre viste portano la propria,
|
|
@@ -236,7 +236,18 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
|
|
|
236
236
|
// metterlo fuori cella sposterebbe il bordo del titolo solo sui rami —
|
|
237
237
|
// cioè rimetterebbe lo slittamento che le colonne tolgono.
|
|
238
238
|
const forkMark = forked ? '⑂ ' : '';
|
|
239
|
-
|
|
239
|
+
// T158 · P7 — 🚨 entra nella stessa cella e per la stessa ragione,
|
|
240
|
+
// davanti al segno di fork. NON nella cella di appartenenza (📌/🔗/○):
|
|
241
|
+
// lì un solo glifo ha diritto di stare, e la priorità coprirebbe il pin
|
|
242
|
+
// sulle righe che sono pinnate e prioritarie insieme. La cella titolo
|
|
243
|
+
// perde 3 colonne solo sulle righe marcate.
|
|
244
|
+
//
|
|
245
|
+
// Larghezza misurata concorde (2 colonne per `string-width` e 2 per il
|
|
246
|
+
// terminale) e carattere astrale: nessun selettore di variazione, quindi
|
|
247
|
+
// nessuna voce da aggiungere alla tabella dei glifi discordi.
|
|
248
|
+
const priorityMark = sessionPriority.has(s.sessionId) ? '🚨 ' : '';
|
|
249
|
+
const marks = priorityMark + forkMark;
|
|
250
|
+
const inner = Math.max(0, titleW - termWidth(marks));
|
|
240
251
|
// T60 — il testo arriva già ripulito di ciò che le colonne accanto
|
|
241
252
|
// dicono già (progetto e task id): senza, la cella conterrebbe
|
|
242
253
|
// `🧵 loom-works · T59` accanto a una colonna che dice `T59`.
|
|
@@ -244,6 +255,6 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
|
|
|
244
255
|
const used = (label.note ? termWidth(label.note) : 0) +
|
|
245
256
|
(label.note && label.rest ? 1 : 0) +
|
|
246
257
|
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 ? (_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));
|
|
258
|
+
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, priorityMark ? _jsx(Text, { color: "red", bold: true, children: priorityMark }) : 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
259
|
}))] }));
|
|
249
260
|
}
|
package/dist/ui/screens.js
CHANGED
|
@@ -149,7 +149,7 @@ export function screenFor(input) {
|
|
|
149
149
|
// posizione è lo scroll mosso a mano. Il clamp serve comunque — un resize
|
|
150
150
|
// può accorciare il testo sotto uno scroll già dato.
|
|
151
151
|
const start = Math.min(sheet.top, sheet.maxTop);
|
|
152
|
-
return (_jsx(DetailScreen, { id: sheet.sheet.id, title: sheet.sheet.title, missing: sheet.sheet.text === null, lines: sheet.lines.slice(start, start + sheet.capacity), spans: sheet.doc?.spans ?? [], top: start, total: sheet.lines.length, capacity: sheet.capacity, action: sheet.action, model: sheet.model, spawnNote: sheet.spawnNote, prompt: sheet.prompt, cursor: sheet.cursor, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
|
|
152
|
+
return (_jsx(DetailScreen, { id: sheet.sheet.id, title: sheet.sheet.title, missing: sheet.sheet.text === null, lines: sheet.lines.slice(start, start + sheet.capacity), spans: sheet.doc?.spans ?? [], top: start, total: sheet.lines.length, capacity: sheet.capacity, action: sheet.action, model: sheet.model, spawnNote: sheet.spawnNote, prompt: sheet.prompt, priority: sheet.priority, cursor: sheet.cursor, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
|
|
153
153
|
}
|
|
154
154
|
// ── T121 · viewer del project status ────────────────────────────────────
|
|
155
155
|
// Quinta schermata sostitutiva, stessa ragione delle altre quattro: un recap
|
package/dist/viewport.js
CHANGED
|
@@ -264,17 +264,18 @@ export function readerCapacity(rows) {
|
|
|
264
264
|
// 1 riga prompt (T117)
|
|
265
265
|
// 1 riga modello (T108)
|
|
266
266
|
// 1 riga titolo (T111)
|
|
267
|
+
// 1 riga priorità (T158)
|
|
267
268
|
//
|
|
268
|
-
// Le ultime
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
269
|
+
// Le ultime sei sono il motivo per cui il detail non può riusare READER_CHROME:
|
|
270
|
+
// le cinque righe dell'area di compilazione sono righe FISSE in più dentro
|
|
271
|
+
// l'overlay, e ogni riga fissa aggiunta va scalata dalla capienza del contenuto o
|
|
272
|
+
// il frame sfonda `rows` (stessa invariante di TASKS_PANE_CHROME).
|
|
272
273
|
//
|
|
273
274
|
// Sono fisse e non `extra` condizionali di `detailCapacity` (T111 · D1): due
|
|
274
275
|
// condizionali mutuamente esclusivi obbligherebbero il parametro a dire QUALE è
|
|
275
276
|
// a schermo — un solo booleano per entrambi sottostima quando quello aperto è il
|
|
276
277
|
// più alto, e sommarli toglie righe al testo per un campo che non c'è.
|
|
277
|
-
const DETAIL_CHROME =
|
|
278
|
+
const DETAIL_CHROME = 14;
|
|
278
279
|
// T91 — la ricerca dentro il detail: marginTop + riga del campo.
|
|
279
280
|
//
|
|
280
281
|
// `MODAL_HEIGHT` dice che il detail costa 0 ai due pane (li sostituisce), e da
|
package/package.json
CHANGED
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
|
|
120
|
-
#
|
|
121
|
-
#
|
|
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
|
|
273
|
-
#
|
|
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
|
|
276
|
-
#
|
|
277
|
-
# `
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
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/[^
|
|
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}"
|