@lamemind/loom-deck 0.47.0 → 0.48.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 +37 -17
- package/dist/glyphs.js +30 -2
- package/dist/model.js +7 -3
- package/dist/task-edit.js +2 -0
- package/dist/ui/assign-screen.js +11 -10
- package/dist/ui/panes.js +8 -33
- package/dist/ui/task-row.js +58 -0
- package/dist/view.js +29 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,13 +11,13 @@ import { cellWidth, launchLegend, loadArchivableDays, loadIdentity, loadLaunch,
|
|
|
11
11
|
import { cycleSessionView, cycleTaskView, selectSessionRows, selectTasks, sessionView, taskView, TASK_VIEWS, } from './pane-views.js';
|
|
12
12
|
import { isCompact, layoutBudget, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
13
13
|
import { cut, cutMiddle, sanitize, termWidth } from './width.js';
|
|
14
|
-
import { applyView, cycleSort, describeSort,
|
|
14
|
+
import { applyView, cycleSort, describeSort, priName, progName, taskColumns, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
15
15
|
import { initialDetail, writeTaskEdit, PRI_GLYPH, PRI_LABEL } from './task-edit.js';
|
|
16
16
|
import { ALL, EDIT_FIELDS, EDIT_PRI, EDIT_PROG, editTextField, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, QUIT_WINDOW_MS, ROW_ALL, ROW_SPOT, SORT_TASTI, SPOT, } from './model.js';
|
|
17
17
|
import { purgeTargets, splitTargets } from './purge.js';
|
|
18
18
|
import { conversationLabel, isDone } from './layout.js';
|
|
19
19
|
import { fieldsKey } from './fields.js';
|
|
20
|
-
import { TASK_EMPTY, relTime, sanitizeTyped
|
|
20
|
+
import { TASK_EMPTY, relTime, sanitizeTyped } from './glyphs.js';
|
|
21
21
|
import { commitTaskEdit, runLaunch, spawnClaudeEmpty, spawnCleanTasks, spawnCreateTask, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, onInTabCommand, CLAUDE_CMD, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
|
|
22
22
|
import { EditModal, FilterModal, PurgeModal, SortModal, idList } from './ui/modals.js';
|
|
23
23
|
import { ReaderScreen, SearchScreen } from './ui/search-screen.js';
|
|
@@ -162,28 +162,43 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
162
162
|
// alla vista che sceglie l'header (D2 create).
|
|
163
163
|
const parentLabel = isAll ? 'tutte' : isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
164
164
|
// Conteggio figli per task + spot (badge nel Tasks pane).
|
|
165
|
+
//
|
|
166
|
+
// T124 — il rollup delle VIVE si deriva nello stesso ciclo, non da un secondo
|
|
167
|
+
// passaggio sul registry dei processi: quel registry conosce una conversazione
|
|
168
|
+
// PRIMA che abbia scritto il suo primo record di transcript, quindi due
|
|
169
|
+
// derivazioni indipendenti renderebbero producibile `1/0` — una viva senza
|
|
170
|
+
// totale — che non è un caso limite teorico ma la finestra normale fra lo
|
|
171
|
+
// spawn e il primo turno. Intersecando qui, `vive ≤ totali` è garantito per
|
|
172
|
+
// costruzione: la conversazione appena nata resta invisibile a entrambi i
|
|
173
|
+
// numeri finché non scrive, e il segnale arriva in ritardo di un turno invece
|
|
174
|
+
// che sbagliato.
|
|
165
175
|
const childCount = new Map();
|
|
176
|
+
const taskLive = new Map();
|
|
166
177
|
let spotCount = 0;
|
|
167
178
|
for (const s of sessions) {
|
|
168
179
|
const bound = bindings.get(s.sessionId);
|
|
169
|
-
if (bound)
|
|
170
|
-
childCount.set(bound, (childCount.get(bound) ?? 0) + 1);
|
|
171
|
-
else
|
|
180
|
+
if (!bound) {
|
|
172
181
|
spotCount++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
childCount.set(bound, (childCount.get(bound) ?? 0) + 1);
|
|
185
|
+
const entry = live.get(s.sessionId);
|
|
186
|
+
if (!entry)
|
|
187
|
+
continue;
|
|
188
|
+
const prev = taskLive.get(bound);
|
|
189
|
+
// Rollup a stato misto: vince `busy`. Fra N vive, quella che sta lavorando è
|
|
190
|
+
// la ragione per cui si guarda la riga.
|
|
191
|
+
taskLive.set(bound, {
|
|
192
|
+
count: (prev?.count ?? 0) + 1,
|
|
193
|
+
status: prev?.status === 'busy' || entry.status === 'busy' ? 'busy' : 'idle',
|
|
194
|
+
});
|
|
173
195
|
}
|
|
196
|
+
const taskRowData = { childCount, live: taskLive, dirty: dirtyFolders };
|
|
174
197
|
// T118 — colonne fisse della lista task, misurate su `paneTasks` (la vista
|
|
175
198
|
// attiva INTERA) e non sulla finestra visibile: gemelle di `sessionCols` e
|
|
176
199
|
// per la stessa ragione, che una larghezza derivata dallo schermo si muove a
|
|
177
|
-
// ogni scroll.
|
|
178
|
-
|
|
179
|
-
// colonna spenta a `0`, e le descrizioni si riprendono lo spazio.
|
|
180
|
-
const taskCols = (() => {
|
|
181
|
-
let tail = 0;
|
|
182
|
-
for (const t of paneTasks) {
|
|
183
|
-
tail = Math.max(tail, termWidth(taskTail(childCount.get(t.id) ?? 0, dirtyFolders.has(t.id))));
|
|
184
|
-
}
|
|
185
|
-
return { id: idColumnWidth(paneTasks), tail: tail > 0 ? tail + 1 : 0 };
|
|
186
|
-
})();
|
|
200
|
+
// ogni scroll.
|
|
201
|
+
const taskCols = taskColumns(paneTasks, taskRowData);
|
|
187
202
|
// Figli della selezione: tutte le conversazioni del progetto (`≡ tutte`), le
|
|
188
203
|
// sessioni bound alla task selezionata, oppure (spot) quelle senza binding.
|
|
189
204
|
// sessions è già ts desc → l'ordine si eredita in tutti e tre i rami.
|
|
@@ -1276,7 +1291,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1276
1291
|
// stale non resta nulla: il titolo si accontenta dell'hash.
|
|
1277
1292
|
const label = (assign.sid ? sessionNotes.get(assign.sid) : '') ||
|
|
1278
1293
|
(s ? conversationLabel(s, projectCore, assign.sid ? bindings.get(assign.sid) : undefined) : '');
|
|
1279
|
-
|
|
1294
|
+
// T124 — le colonne si misurano sulla lista FILTRATA di questa schermata,
|
|
1295
|
+
// non su `paneTasks`: la popolazione è un'altra, e riusare le larghezze del
|
|
1296
|
+
// pane darebbe una colonna dimensionata su righe che qui non ci sono.
|
|
1297
|
+
// `assign.list` porta in testa la riga `detach` (`null`), che non è una task.
|
|
1298
|
+
const assignCols = taskColumns(assign.list.filter((t) => t !== null), taskRowData);
|
|
1299
|
+
return (_jsx(AssignScreen, { sessionId: assign.sid ?? '', label: label, current: assign.sid ? bindings.get(assign.sid) ?? null : null, filter: assign.filter, rows: assign.list.slice(win.start, win.end), selected: assign.sel, matched: assign.list.length - 1, hidden: hiddenTasks, above: win.start, below: assign.list.length - win.end, idW: assignCols.id, tailW: assignCols.tail, data: taskRowData, columns: columns, note: note }));
|
|
1280
1300
|
}
|
|
1281
1301
|
// ── T66 · detail della task ─────────────────────────────────────────────
|
|
1282
1302
|
// Quarta schermata sostitutiva, stessa ragione delle altre tre: un task file
|
|
@@ -1381,7 +1401,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1381
1401
|
if (budget.compact) {
|
|
1382
1402
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "v", VERSION, " \u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1383
1403
|
}
|
|
1384
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: ["v", VERSION] })] }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["titolo conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'purge' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${legend.shown}` : '', legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editCursor.row, caret: editCursor.caret, columns: columns })) : null, mode === 'purge' && purge ? _jsx(PurgeModal, { draft: purge, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, counts: taskCounts, activeView: taskViewId, paneCount: paneTasks.length, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length,
|
|
1404
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: ["v", VERSION] })] }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["titolo conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'purge' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${legend.shown}` : '', legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editCursor.row, caret: editCursor.caret, columns: columns })) : null, mode === 'purge' && purge ? _jsx(PurgeModal, { draft: purge, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, counts: taskCounts, activeView: taskViewId, paneCount: paneTasks.length, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, idW: taskCols.id, tailW: taskCols.tail, focused: focus === 'tasks', loadError: loadError, windowStart: taskWin.start, above: taskWin.start, below: paneTasks.length - taskWin.end, columns: columns, data: taskRowData }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, counts: sessionCounts, activeView: sessionViewId, paneCount: sessionRows.length, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore, live: live })] }), budget.preview && previewKind === 'task' && detail ? (_jsx(PreviewPane, { kind: "task", detail: detail, maxLines: budget.detailLines, columns: columns })) : budget.preview && previewKind === 'session' && selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: selSessionObj, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, origin: forkOf.get(selSessionObj.sessionId) ?? null, note: sessionNotes.get(selSessionObj.sessionId) ?? '', live: live.get(selSessionObj.sessionId) ?? null })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1385
1405
|
}
|
|
1386
1406
|
const cwd = process.cwd();
|
|
1387
1407
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
package/dist/glyphs.js
CHANGED
|
@@ -64,6 +64,14 @@ export function modelShort(id) {
|
|
|
64
64
|
// Sulla riga CHIUSA c'è uno spazio e non un terzo glifo: le chiuse sono la
|
|
65
65
|
// maggioranza di ogni lista, e marcarle vorrebbe dire disegnare N volte «niente
|
|
66
66
|
// da dire» — la colonna diventerebbe rumore invece di un segnale.
|
|
67
|
+
//
|
|
68
|
+
// T124 — non sono più il vocabolario di una superficie sola: la riga del pane
|
|
69
|
+
// task adotta la stessa coppia, nella stessa forma (glifo attaccato all'id che
|
|
70
|
+
// qualifica, colore e grassetto sull'id) e con lo stesso significato. L'unica
|
|
71
|
+
// differenza ammessa è la POPOLAZIONE su cui il glifo si pronuncia — una
|
|
72
|
+
// conversazione di là, le N conversazioni di una task di qua, con un rollup che
|
|
73
|
+
// fa vincere `busy`. Riusarli con un significato spostato sarebbe un falso
|
|
74
|
+
// amico, peggio di un glifo nuovo.
|
|
67
75
|
export const LIVE_IDLE = '●';
|
|
68
76
|
export const LIVE_BUSY = '◍';
|
|
69
77
|
export const LIVE_NONE = ' ';
|
|
@@ -89,13 +97,33 @@ export function displayProg(prog) {
|
|
|
89
97
|
* a disegnarla (sulla finestra, in `panes.tsx`). Due formattazioni gemelle
|
|
90
98
|
* divergerebbero alla prima modifica, e la colonna risulterebbe larga quanto
|
|
91
99
|
* una stringa che nessuno scrive.
|
|
100
|
+
*
|
|
101
|
+
* T124 — il contatore spezza vive e totali (`(1/5`) invece di sommarle: un
|
|
102
|
+
* numero solo cresce e non torna più indietro, quindi dice quanta storia ha la
|
|
103
|
+
* task e non se lì sta succedendo qualcosa adesso. A zero vive resta il solo
|
|
104
|
+
* totale (`(5`): scrivere `(0/5` metterebbe uno zero su quasi tutte le righe di
|
|
105
|
+
* ogni lista, e il segnale sparirebbe nel rumore.
|
|
106
|
+
*
|
|
107
|
+
* La parentesi di CHIUSURA cade sempre. La colonna è ancorata a destra, quindi
|
|
108
|
+
* la `)` costerebbe una cella per spostare verso l'interno l'unica cosa che si
|
|
109
|
+
* legge davvero; il bordo del pane chiude già il gruppo. Quella di apertura
|
|
110
|
+
* serve invece a staccare il contatore dalla descrizione a lunghezza libera che
|
|
111
|
+
* gli sta a sinistra — l'asimmetria è voluta.
|
|
92
112
|
*/
|
|
93
|
-
export function taskTail(
|
|
94
|
-
const count =
|
|
113
|
+
export function taskTail(live, total, dirty) {
|
|
114
|
+
const count = total > 0 ? (live > 0 ? `(${live}/${total}` : `(${total}`) : '';
|
|
95
115
|
if (!dirty)
|
|
96
116
|
return count;
|
|
97
117
|
return count ? `${WARN} ${count}` : WARN;
|
|
98
118
|
}
|
|
119
|
+
/** T124 — contatore di una riga META del pane task (`≡ tutte`, `○ spot`), che
|
|
120
|
+
* non è una colonna ancorata a niente. Prende comunque la grafia senza chiusa:
|
|
121
|
+
* due forme dello stesso oggetto a due righe di distanza si leggono come un
|
|
122
|
+
* errore, mentre una forma sola applicata anche dove la sua ragione non serve
|
|
123
|
+
* si legge come una convenzione. */
|
|
124
|
+
export function metaCount(n) {
|
|
125
|
+
return n > 0 ? ` (${n}` : '';
|
|
126
|
+
}
|
|
99
127
|
// T49 — size umana compatta per il detail pane sessione.
|
|
100
128
|
export function fmtSize(bytes) {
|
|
101
129
|
if (bytes < 1024)
|
package/dist/model.js
CHANGED
|
@@ -56,10 +56,14 @@ export const KIND_LABEL = { ai: 'IA', tool: 'tools', human: 'human' };
|
|
|
56
56
|
// T41 — ordine dei valori nel modale edit. Deliberatamente DIVERSO da
|
|
57
57
|
// PRI_ENTRIES/PROG_ENTRIES (che seguono il rango di sort): qui si sceglie un
|
|
58
58
|
// valore, non si ordina, quindi vince l'ordine del CICLO DI VITA — da fare →
|
|
59
|
-
// in corso → chiusa → bloccata. La priorità resta alta→bassa,
|
|
60
|
-
// l'ordine naturale di lettura.
|
|
59
|
+
// preflight fatto → in corso → chiusa → bloccata. La priorità resta alta→bassa,
|
|
60
|
+
// che è già l'ordine naturale di lettura.
|
|
61
|
+
//
|
|
62
|
+
// L'elenco deve coprire OGNI valore di ProgName: il cursore del modale nasce da
|
|
63
|
+
// `Math.max(0, EDIT_PROG.indexOf(prog))` (cli.tsx), quindi un valore assente
|
|
64
|
+
// torna -1, viene schiacciato a 0 e salvare retrocede la task alla prima voce.
|
|
61
65
|
export const EDIT_PRI = ['high', 'med', 'low'];
|
|
62
|
-
export const EDIT_PROG = ['todo', 'wip', 'done', 'locked'];
|
|
66
|
+
export const EDIT_PROG = ['todo', 'ready', 'wip', 'done', 'locked'];
|
|
63
67
|
// T117 — le righe del modale edit come DATO, nella forma che `fields.ts`
|
|
64
68
|
// consuma: 0 priorità · 1 stato · 2 progresso libero · 3 titolo. Il modale non
|
|
65
69
|
// dà lettere di selezione alle due righe a scelta, dove i valori sono glifi e
|
package/dist/task-edit.js
CHANGED
|
@@ -14,6 +14,7 @@ export const PRI_GLYPH = { high: '🔥', med: '⚡', low: '🔹' };
|
|
|
14
14
|
// lunga resta riconosciuto da priName/progName/isDone.
|
|
15
15
|
export const PROG_GLYPH = {
|
|
16
16
|
todo: '🔵',
|
|
17
|
+
ready: '🟢',
|
|
17
18
|
wip: '🟡',
|
|
18
19
|
done: '✔️',
|
|
19
20
|
locked: '🔒',
|
|
@@ -23,6 +24,7 @@ export const PROG_GLYPH = {
|
|
|
23
24
|
// `🔵 Todo`, `✔️ Done at 2026-07-20`) → il glifo è il prefisso, il resto è prosa.
|
|
24
25
|
const PROG_DEFAULT = {
|
|
25
26
|
todo: 'Todo',
|
|
27
|
+
ready: 'Ready',
|
|
26
28
|
wip: 'In Progress',
|
|
27
29
|
done: 'Done',
|
|
28
30
|
locked: 'Locked',
|
package/dist/ui/assign-screen.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Schermata di assegnazione conversazione → task (T57).
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import { cut, sanitize
|
|
5
|
-
import { assignTextWidth
|
|
6
|
-
import { CARET, CARET_OFF
|
|
4
|
+
import { cut, sanitize } from '../width.js';
|
|
5
|
+
import { assignTextWidth } from '../layout.js';
|
|
6
|
+
import { CARET, CARET_OFF } from '../glyphs.js';
|
|
7
|
+
import { TaskRow } from './task-row.js';
|
|
7
8
|
/**
|
|
8
9
|
* Schermata di assegnazione di una conversazione a una task (T57).
|
|
9
10
|
*
|
|
@@ -16,7 +17,7 @@ import { CARET, CARET_OFF, displayProg } from '../glyphs.js';
|
|
|
16
17
|
* filtro può nascondere proprio il bersaglio, e quel prezzo non deve essere
|
|
17
18
|
* silenzioso — stessa convenzione del `+N più vecchie` del pane sessioni.
|
|
18
19
|
*/
|
|
19
|
-
export function AssignScreen({ sessionId, label, current, filter, rows, selected, matched, hidden, above, below,
|
|
20
|
+
export function AssignScreen({ sessionId, label, current, filter, rows, selected, matched, hidden, above, below, idW, tailW, data, columns, note, }) {
|
|
20
21
|
const width = assignTextWidth(columns);
|
|
21
22
|
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: ["assegna ", _jsx(Text, { color: "cyan", children: sessionId.slice(0, 8) }), label ? ` «${cut(sanitize(label), Math.max(10, Math.floor(width / 4)))}»` : '', " \u00B7 ora", ' ', current ? _jsx(Text, { color: "green", children: current }) : 'spot', " \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), ' ', "assegna \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " pulisci \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] }), _jsx(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "filtro " }), _jsx(Text, { color: "yellow", children: cut(filter, Math.max(8, width - 24)) }), _jsx(Text, { inverse: true, children: " " }), !filter ? _jsx(Text, { dimColor: true, children: " (id o titolo)" }) : null] }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [matched, " task", hidden > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", hidden, " fuori dai filtri"] }) : null, above > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2191", above] }) : null, below > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2193", below] }) : null] }), rows.map((task) => {
|
|
22
23
|
// T57/D2 — `detach` è una VOCE della lista, non un tasto a parte: un
|
|
@@ -30,11 +31,11 @@ export function AssignScreen({ sessionId, label, current, filter, rows, selected
|
|
|
30
31
|
// il nome dell'azione invece di un moncone di frase.
|
|
31
32
|
return (_jsxs(Text, { inverse: sel, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, "\u25CB detach ", cut('la sessione torna spot', width - 12)] }, "detach"));
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return (
|
|
34
|
+
// T124 — la riga è la STESSA del pane task, non più una copia
|
|
35
|
+
// ricomposta qui: id paddato alla colonna, coda ancorata al bordo
|
|
36
|
+
// destro, marker dirty ed evidenze della liveness arrivano tutti dal
|
|
37
|
+
// componente condiviso. `focused` è sempre vero — una schermata
|
|
38
|
+
// sostitutiva non cede il fuoco a nessun altro pane.
|
|
39
|
+
return (_jsx(TaskRow, { task: task, sel: selected === task.id, focused: true, width: width, idW: idW, tailW: tailW, data: data }, task.id));
|
|
39
40
|
})] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
40
41
|
}
|
package/dist/ui/panes.js
CHANGED
|
@@ -4,12 +4,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// larghezze già calcolate, non li derivano.
|
|
5
5
|
import { Box, Text } from 'ink';
|
|
6
6
|
import { cut, cutParts, pad, sanitize, termWidth } from '../width.js';
|
|
7
|
-
import {
|
|
8
|
-
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SESSION_SEP, SID_CHARS, TASK_EMPTY, WARN,
|
|
7
|
+
import { paneTextWidth } from '../layout.js';
|
|
8
|
+
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SESSION_SEP, SID_CHARS, TASK_EMPTY, WARN, metaCount, modelShort, relTime, } from '../glyphs.js';
|
|
9
|
+
import { TaskRow } from './task-row.js';
|
|
9
10
|
import { META_ROWS, ROW_ALL, ROW_SPOT } from '../model.js';
|
|
10
11
|
import { rowLabel, sessionTitle } from '../session-list.js';
|
|
11
12
|
import { sessionView, taskView, SESSION_VIEWS, TASK_VIEWS, } from '../pane-views.js';
|
|
12
|
-
import { describeSort,
|
|
13
|
+
import { describeSort, PRI_ENTRIES, PROG_ENTRIES, } from '../view.js';
|
|
13
14
|
/**
|
|
14
15
|
* Header del pane task, tagliato QUI e non da Ink — stesso motivo del gemello
|
|
15
16
|
* `SessionsHeader`, con una differenza di rischio: qui i segmenti sono tutti
|
|
@@ -49,7 +50,7 @@ export function TasksHeader({ counts, active, above, below, focused, columns, })
|
|
|
49
50
|
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns), segments.findIndex((s) => s.active));
|
|
50
51
|
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, inverse: seg.active, children: shown[i] }, i)) : null) }));
|
|
51
52
|
}
|
|
52
|
-
export function TasksPane({ tasks, counts, activeView, paneCount, view, selected, spotCount, allCount,
|
|
53
|
+
export function TasksPane({ tasks, counts, activeView, paneCount, view, selected, spotCount, allCount, idW, tailW, focused, loadError, windowStart, above, below, columns, data, }) {
|
|
53
54
|
const allSelected = selected === ROW_ALL;
|
|
54
55
|
const spotSelected = selected === ROW_SPOT;
|
|
55
56
|
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(TasksHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: cut(sanitize(`sort: ${describeSort(view.sort)}` +
|
|
@@ -61,42 +62,16 @@ export function TasksPane({ tasks, counts, activeView, paneCount, view, selected
|
|
|
61
62
|
]
|
|
62
63
|
.map((e) => `−${e.glyph}`)
|
|
63
64
|
.join(' ')
|
|
64
|
-
: '')), paneTextWidth(columns)) }), _jsxs(Text, { inverse: allSelected && focused, bold: allSelected && !focused, wrap: "truncate-end", children: [allSelected ? CARET : CARET_OFF, "\u2261 tutte le sessioni",
|
|
65
|
+
: '')), paneTextWidth(columns)) }), _jsxs(Text, { inverse: allSelected && focused, bold: allSelected && !focused, wrap: "truncate-end", children: [allSelected ? CARET : CARET_OFF, "\u2261 tutte le sessioni", metaCount(allCount)] }), _jsxs(Text, { inverse: spotSelected && focused, bold: spotSelected && !focused, wrap: "truncate-end", children: [spotSelected ? CARET : CARET_OFF, "\u25CB spot sessioni libere", metaCount(spotCount)] }), loadError ? (_jsx(Text, { color: "red", wrap: "truncate-end", children: loadError })) : paneCount === 0 ? (
|
|
65
66
|
// T100/D1 — una voce a contatore 0 resta navigabile, e selezionarla dà
|
|
66
67
|
// una lista vuota che DICE perché è vuota. Senza la nota il pane si
|
|
67
68
|
// legge come rotto: le righe meta restano, le task no, e niente spiega
|
|
68
69
|
// che è la vista scelta a non contenere nulla.
|
|
69
|
-
_jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(taskView(activeView).empty, paneTextWidth(columns)) })) : (tasks.map((task, i) => {
|
|
70
|
+
_jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(taskView(activeView).empty, paneTextWidth(columns)) })) : (tasks.map((task, i) => (_jsx(TaskRow, { task: task,
|
|
70
71
|
// windowStart riporta l'indice di finestra a quello della lista
|
|
71
72
|
// completa, su cui è keyata la selezione. +META_ROWS: le prime due
|
|
72
73
|
// righe sono le meta.
|
|
73
|
-
|
|
74
|
-
const tail = taskTail(childCount.get(task.id) ?? 0, dirty.has(task.id));
|
|
75
|
-
// T118 — l'id è paddato a `idW`, quindi `head` ha la stessa larghezza
|
|
76
|
-
// su ogni riga e Pri/Prog cadono sempre nella stessa colonna.
|
|
77
|
-
const id = padId(task.id, idW);
|
|
78
|
-
// Invariante ③: la descrizione è l'unico pezzo a lunghezza libera, e
|
|
79
|
-
// si taglia QUI sul budget che resta dopo le colonne fisse. Lasciarlo
|
|
80
|
-
// fare a `truncate-end` significa passare da `cli-truncate`, che
|
|
81
|
-
// restituisce una riga più larga del pane (una colonna per emoji) e
|
|
82
|
-
// quindi scrive sopra il bordo. Le parti fisse si misurano con
|
|
83
|
-
// `termWidth`: i due glifi Pri/Prog valgono 2 ciascuno.
|
|
84
|
-
const head = `${CARET_OFF}${id} ${sanitize(task.pri)} ${displayProg(task.prog)} `;
|
|
85
|
-
// T118 — la colonna della coda si riserva PRIMA di tagliare la
|
|
86
|
-
// descrizione, e per la stessa larghezza su ogni riga: appesa dopo,
|
|
87
|
-
// entrava nel budget solo dove c'era qualcosa da scrivere, e la
|
|
88
|
-
// descrizione si tagliava a una colonna diversa riga per riga.
|
|
89
|
-
//
|
|
90
|
-
// Pavimento `0` su tutte e tre le misure e non un minimo di cortesia:
|
|
91
|
-
// il budget è un TETTO. Un pavimento sopra lo spazio reale fa uscire
|
|
92
|
-
// la riga dal pane e le mangia il bordo. `reserve` si clampa su ciò
|
|
93
|
-
// che avanza, così `head + desc + coda` sta sempre dentro il pane.
|
|
94
|
-
const avail = Math.max(0, paneTextWidth(columns) - termWidth(head));
|
|
95
|
-
const reserve = Math.min(tailW, avail);
|
|
96
|
-
const descW = avail - reserve;
|
|
97
|
-
const desc = cut(task.desc, descW);
|
|
98
|
-
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, id, " ", sanitize(task.pri), " ", displayProg(task.prog), " ", desc, ' '.repeat(Math.max(0, descW - termWidth(desc))), pad(tail, reserve, 'right')] }, task.id));
|
|
99
|
-
}))] }));
|
|
74
|
+
sel: windowStart + i + META_ROWS === selected, focused: focused, width: paneTextWidth(columns), idW: idW, tailW: tailW, data: data }, task.id))))] }));
|
|
100
75
|
}
|
|
101
76
|
/**
|
|
102
77
|
* Header del pane sessioni, tagliato QUI e non da Ink.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// La riga di lista di UNA task, condivisa fra il pane task e la schermata di
|
|
3
|
+
// assegnazione (T124).
|
|
4
|
+
//
|
|
5
|
+
// Prima viveva solo nel pane, e la schermata di assegnazione ne teneva una copia
|
|
6
|
+
// ricomposta a mano: id non paddato alla colonna, coda non ancorata al bordo,
|
|
7
|
+
// marker dirty assente, contatore scritto in proprio. Nessuna di quelle
|
|
8
|
+
// differenze produceva un errore — la schermata rendeva righe valide, solo
|
|
9
|
+
// diverse — quindi nessun test le vedeva e ogni evoluzione della lista si
|
|
10
|
+
// fermava al suo confine. Un componente solo è ciò che toglie il gemello.
|
|
11
|
+
//
|
|
12
|
+
// Le due sedi differiscono per tre cose sole, tutte parametri: la larghezza
|
|
13
|
+
// utile, il concetto di fuoco (la schermata di assegnazione è sempre a fuoco) e
|
|
14
|
+
// la finestra di righe che il chiamante ha già ritagliato.
|
|
15
|
+
import { Text } from 'ink';
|
|
16
|
+
import { cut, pad, sanitize, termWidth } from '../width.js';
|
|
17
|
+
import { isDone } from '../layout.js';
|
|
18
|
+
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, displayProg, taskTail, } from '../glyphs.js';
|
|
19
|
+
import { padId } from '../view.js';
|
|
20
|
+
export function TaskRow({ task, sel, focused, width, idW, tailW, data, }) {
|
|
21
|
+
// T124 — il predicato è UNO e si valuta qui una volta sola: da lui dipendono
|
|
22
|
+
// tutte e tre le evidenze (id in grassetto+colore, glifo, numeratore in coda).
|
|
23
|
+
// Tre condizioni scritte in tre siti divergono alla prima modifica, e la riga
|
|
24
|
+
// finirebbe per dire due cose diverse su sé stessa.
|
|
25
|
+
const live = data.live.get(task.id) ?? null;
|
|
26
|
+
// Stessa resa dell'hash conversazione del pane sessioni, perché è lo stesso
|
|
27
|
+
// stato: `◍` giallo di là e `◍` nero di qua sarebbero due grafie di una cosa
|
|
28
|
+
// sola.
|
|
29
|
+
const color = live ? (live.status === 'busy' ? 'yellow' : 'green') : undefined;
|
|
30
|
+
// Attaccato all'id, non staccato: il glifo qualifica QUELL'id, e uno spazio in
|
|
31
|
+
// mezzo lo farebbe leggere come una colonna a sé. Consuma il primo dei due
|
|
32
|
+
// spazi verso Pri, quindi la riga non si allarga di una cella quando si
|
|
33
|
+
// accende — `LIVE_NONE` è uno spazio, e tutti e tre sono larghi 1.
|
|
34
|
+
const glyph = live ? (live.status === 'busy' ? LIVE_BUSY : LIVE_IDLE) : LIVE_NONE;
|
|
35
|
+
const tail = taskTail(live?.count ?? 0, data.childCount.get(task.id) ?? 0, data.dirty.has(task.id));
|
|
36
|
+
const id = padId(task.id, idW);
|
|
37
|
+
// Invariante ③: la descrizione è l'unico pezzo a lunghezza libera, e si taglia
|
|
38
|
+
// QUI sul budget che resta dopo le colonne fisse. Lasciarlo fare a
|
|
39
|
+
// `truncate-end` significa passare da `cli-truncate`, che restituisce una riga
|
|
40
|
+
// più larga del pane (una colonna per emoji) e quindi scrive sopra il bordo.
|
|
41
|
+
// Le parti fisse si misurano con `termWidth`: i due glifi Pri/Prog valgono 2
|
|
42
|
+
// ciascuno.
|
|
43
|
+
const head = `${CARET_OFF}${id}${glyph} ${sanitize(task.pri)} ${displayProg(task.prog)} `;
|
|
44
|
+
// T118 — la colonna della coda si riserva PRIMA di tagliare la descrizione, e
|
|
45
|
+
// per la stessa larghezza su ogni riga: appesa dopo, entrava nel budget solo
|
|
46
|
+
// dove c'era qualcosa da scrivere, e la descrizione si tagliava a una colonna
|
|
47
|
+
// diversa riga per riga.
|
|
48
|
+
//
|
|
49
|
+
// Pavimento `0` su tutte e tre le misure e non un minimo di cortesia: il
|
|
50
|
+
// budget è un TETTO. Un pavimento sopra lo spazio reale fa uscire la riga dal
|
|
51
|
+
// pane e le mangia il bordo. `reserve` si clampa su ciò che avanza, così
|
|
52
|
+
// `head + desc + coda` sta sempre dentro la sede.
|
|
53
|
+
const avail = Math.max(0, width - termWidth(head));
|
|
54
|
+
const reserve = Math.min(tailW, avail);
|
|
55
|
+
const descW = avail - reserve;
|
|
56
|
+
const desc = cut(task.desc, descW);
|
|
57
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsxs(Text, { bold: Boolean(live), color: color, children: [id, glyph] }), ` ${sanitize(task.pri)} ${displayProg(task.prog)} ${desc}`, ' '.repeat(Math.max(0, descW - termWidth(desc))), pad(tail, reserve, 'right')] }));
|
|
58
|
+
}
|
package/dist/view.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// T39 — Core della vista: ordinali, sort chain multi-chiave, filtri.
|
|
2
2
|
// Modulo PURO: nessun import da ink/react, nessun I/O → testabile senza terminale.
|
|
3
|
+
import { taskTail } from './glyphs.js';
|
|
4
|
+
import { termWidth } from './width.js';
|
|
3
5
|
// D2 (preflight T39): sort opinato, filtri off. La lista parte ordinata ma
|
|
4
6
|
// completa — nessuna task sparisce senza che l'utente abbia toccato una leva.
|
|
5
7
|
export const DEFAULT_VIEW = {
|
|
@@ -19,10 +21,15 @@ const PRI_TABLE = [
|
|
|
19
21
|
{ name: 'low', glyph: '🔹', rank: 1 },
|
|
20
22
|
];
|
|
21
23
|
// Ordine per "attivabilità" (desc = prima ciò su cui puoi agire): in corso →
|
|
22
|
-
// da fare → bloccata → chiusa. Non è l'ordine del ciclo di
|
|
23
|
-
// a scegliere su cosa lavorare, quindi le Done stanno in
|
|
24
|
+
// preflight fatto → da fare → bloccata → chiusa. Non è l'ordine del ciclo di
|
|
25
|
+
// vita: il deck serve a scegliere su cosa lavorare, quindi le Done stanno in
|
|
26
|
+
// fondo sotto `desc`. `ready` sta sopra `todo` perché il design è già congelato
|
|
27
|
+
// (run-task parte senza Q&A), e sotto `wip` perché il lavoro già aperto viene
|
|
28
|
+
// prima. Il rango governa anche l'ordine delle colonne della barra SHIFT+F,
|
|
29
|
+
// derivata da PROG_ENTRIES.
|
|
24
30
|
const PROG_TABLE = [
|
|
25
|
-
{ name: 'wip', glyph: '🟡', rank:
|
|
31
|
+
{ name: 'wip', glyph: '🟡', rank: 5 },
|
|
32
|
+
{ name: 'ready', glyph: '🟢', rank: 4 },
|
|
26
33
|
{ name: 'todo', glyph: '🔵', rank: 3 },
|
|
27
34
|
{ name: 'locked', glyph: '🔒', rank: 2 },
|
|
28
35
|
{ name: 'done', glyph: '✔', rank: 1 },
|
|
@@ -90,6 +97,25 @@ export function padId(id, cols) {
|
|
|
90
97
|
return id.padEnd(cols);
|
|
91
98
|
return m[1] + ' '.repeat(Math.max(0, cols - id.length)) + m[2];
|
|
92
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* T118/T124 — le due colonne fisse della lista task, misurate sulla popolazione
|
|
102
|
+
* COMPLETA della vista e non sulla finestra visibile.
|
|
103
|
+
*
|
|
104
|
+
* `tail` porta dentro il proprio gutter (`+1`), così l'allineamento a destra lo
|
|
105
|
+
* produce `pad` da sé; nessuna riga con qualcosa da scrivere → `0`, colonna
|
|
106
|
+
* spenta, e le descrizioni si riprendono lo spazio.
|
|
107
|
+
*
|
|
108
|
+
* La stringa misurata qui è la STESSA che il render disegna (`taskTail`): due
|
|
109
|
+
* formattazioni gemelle divergerebbero alla prima modifica, e la colonna
|
|
110
|
+
* risulterebbe larga quanto una stringa che nessuno scrive.
|
|
111
|
+
*/
|
|
112
|
+
export function taskColumns(tasks, data) {
|
|
113
|
+
let tail = 0;
|
|
114
|
+
for (const t of tasks) {
|
|
115
|
+
tail = Math.max(tail, termWidth(taskTail(data.live.get(t.id)?.count ?? 0, data.childCount.get(t.id) ?? 0, data.dirty.has(t.id))));
|
|
116
|
+
}
|
|
117
|
+
return { id: idColumnWidth(tasks), tail: tail > 0 ? tail + 1 : 0 };
|
|
118
|
+
}
|
|
93
119
|
function rankOf(task, key) {
|
|
94
120
|
if (key === 'pri')
|
|
95
121
|
return priRank(task.pri);
|
package/package.json
CHANGED