@lamemind/loom-deck 0.24.0 → 0.25.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 +76 -38
- package/dist/session-list.js +56 -16
- package/dist/width.js +20 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,10 +12,10 @@ import { resolveTasksPath, resolveTasksDir, loadTasks, loadTaskDetail, } from '.
|
|
|
12
12
|
import { discoverProjectSessions } from './sessions.js';
|
|
13
13
|
import { buildRows, firstRowKey, moveRowSelection, rowIndexOfKey, searchSessions, selectedRow, DEFAULT_OPTIONS, MIN_QUERY, } from './search.js';
|
|
14
14
|
import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, loadSessionIndex, } from './task-index.js';
|
|
15
|
-
import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowIndexOf, rowLabel, selectedSession, stripProjectCore, } from './session-list.js';
|
|
15
|
+
import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowIndexOf, rowLabel, selectedSession, sessionTitle, stripProjectCore, } from './session-list.js';
|
|
16
16
|
import { cellWidth, launchLegend, loadIdentity, loadLaunch } from './config.js';
|
|
17
17
|
import { assignListCapacity, isCompact, layoutBudget, readerCapacity, searchListCapacity, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
18
|
-
import { caretWindow, cut, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
|
|
18
|
+
import { caretWindow, cut, pad, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
|
|
19
19
|
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
20
20
|
import { initialDetail, progressText, writeTaskEdit, PRI_GLYPH, PRI_LABEL, PROG_GLYPH, } from './task-edit.js';
|
|
21
21
|
import { loadView, saveView, viewFilePath } from './view-store.js';
|
|
@@ -430,10 +430,12 @@ const WARN = sanitize('⚠');
|
|
|
430
430
|
const SESSION_SEP = '─'.repeat(16);
|
|
431
431
|
// Prefisso del sessionId mostrato in lista: stesso dato e stessa lunghezza del
|
|
432
432
|
// widget `⛓ <8 char>` della statusline, così le due superfici si confrontano a
|
|
433
|
-
// occhio.
|
|
434
|
-
// label deve scalare.
|
|
433
|
+
// occhio.
|
|
435
434
|
const SID_CHARS = 8;
|
|
436
|
-
|
|
435
|
+
/** T60 — segnaposto della colonna task su una riga senza binding. Una cella
|
|
436
|
+
* vuota di soli spazi lascerebbe un buco che si legge come "colonna finita",
|
|
437
|
+
* e la riga tornerebbe a sembrare disallineata pur non essendolo. */
|
|
438
|
+
const TASK_EMPTY = '·';
|
|
437
439
|
// Marker Done per il DISPLAY. `task.prog` resta il `✔️` letto da tasks.md —
|
|
438
440
|
// `isDone()` e le lookup di `view.ts` ci confrontano sopra, e `task-edit` lo
|
|
439
441
|
// riscrive sul file: è una chiave semantica, non testo. Qui `sanitize` lo
|
|
@@ -603,6 +605,29 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
603
605
|
const assembled = useMemo(() => assembleSessionList(childSessions, sessions, pinned, isAll ? MAX_SESSIONS_ALL : MAX_SESSIONS), [childSessions, sessions, pinned, isAll]);
|
|
604
606
|
const sessionRows = assembled.rows;
|
|
605
607
|
const selSessionObj = selectedSession(sessionRows, selSessionId);
|
|
608
|
+
// T60 — larghezze delle colonne fisse della lista sessioni, misurate sulla
|
|
609
|
+
// lista INTERA e non sulla finestra visibile: derivarle dalle sole righe a
|
|
610
|
+
// schermo le farebbe cambiare a ogni scroll, cioè l'opposto di una tabella.
|
|
611
|
+
// La colonna task esiste solo nella vista "tutte" (altrove il binding è lo
|
|
612
|
+
// stesso su ogni riga e sta già nell'header del pane), e `0` la spegne.
|
|
613
|
+
const sessionCols = useMemo(() => {
|
|
614
|
+
let task = 0;
|
|
615
|
+
let age = 2;
|
|
616
|
+
for (const r of sessionRows) {
|
|
617
|
+
if (r.kind === 'separator')
|
|
618
|
+
continue;
|
|
619
|
+
if (isAll) {
|
|
620
|
+
const b = bindings.get(r.sessionId);
|
|
621
|
+
if (b)
|
|
622
|
+
task = Math.max(task, termWidth(b));
|
|
623
|
+
}
|
|
624
|
+
if (r.session)
|
|
625
|
+
age = Math.max(age, termWidth(relTime(r.session.ts)));
|
|
626
|
+
}
|
|
627
|
+
// La cella vuota deve poter entrare nella colonna, o le righe spot
|
|
628
|
+
// perderebbero il segnaposto e con lui l'allineamento.
|
|
629
|
+
return { task: task > 0 ? Math.max(task, termWidth(TASK_EMPTY)) : 0, age };
|
|
630
|
+
}, [sessionRows, bindings, isAll]);
|
|
606
631
|
// T52 — ricerca EAGER: rigira a ogni carattere digitato, non su ⏎. È
|
|
607
632
|
// sostenibile perché i corpi sono già in RAM dentro la cache mtime-keyed
|
|
608
633
|
// dell'adapter (D5): misurato su questo progetto, 0,8 ms sui soli corpi IA e
|
|
@@ -1686,7 +1711,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1686
1711
|
if (budget.compact) {
|
|
1687
1712
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1688
1713
|
}
|
|
1689
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), 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: ["nota 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 === '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: editRow, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, rows: windowRows, total: assembled.pinnedCount + assembled.contextTotal, pinnedCount: assembled.pinnedCount, hidden: assembled.contextHidden, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1714
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), 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: ["nota 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 === '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: editRow, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, total: assembled.pinnedCount + assembled.contextTotal, pinnedCount: assembled.pinnedCount, hidden: assembled.contextHidden, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1690
1715
|
}
|
|
1691
1716
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
1692
1717
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -1950,7 +1975,7 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
1950
1975
|
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, task.id, " ", sanitize(task.pri), " ", displayProg(task.prog), " ", desc, tail] }, task.id));
|
|
1951
1976
|
})), detail && detailLines > 0 ? (_jsx(DetailPane, { detail: detail, maxLines: detailLines, columns: columns })) : null] }));
|
|
1952
1977
|
}
|
|
1953
|
-
function SessionsPane({ parentLabel, isSpot, isAll, bindings, rows, total, pinnedCount, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, forkOf, sessionNotes, projectCore, }) {
|
|
1978
|
+
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, total, pinnedCount, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, forkOf, sessionNotes, projectCore, }) {
|
|
1954
1979
|
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: ["Sessions \u00B7 ", parentLabel, " (", total, ")", pinnedCount > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 \uD83D\uDCCC", pinnedCount] }) : null, hidden > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 +", hidden, " pi\u00F9 vecchie"] }) : null, above > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2191", above] }) : null, below > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2193", below] }) : null] }), total === 0 ? (_jsx(Text, { color: "yellow", wrap: "truncate-end", children: isAll
|
|
1955
1980
|
? 'nessuna conversazione nel progetto'
|
|
1956
1981
|
: isSpot
|
|
@@ -1965,49 +1990,62 @@ function SessionsPane({ parentLabel, isSpot, isAll, bindings, rows, total, pinne
|
|
|
1965
1990
|
// T50 — pin stale: transcript sparito, nessuna Session da mostrare.
|
|
1966
1991
|
// Riga navigabile e spinnabile (`p`), marcata, mai un crash.
|
|
1967
1992
|
if (row.kind === 'pinned' && row.stale) {
|
|
1968
|
-
|
|
1993
|
+
// T60 — anche qui la nota si taglia sul budget DERIVATO, non su un
|
|
1994
|
+
// 30 inchiodato: su un pane stretto quel valore fisso mandava la
|
|
1995
|
+
// riga oltre il bordo, e a ripararla arrivava `cli-truncate` (che
|
|
1996
|
+
// sfora di una colonna per emoji e mangia il bordo stesso).
|
|
1997
|
+
const staleNote = sessionNotes.get(row.sessionId);
|
|
1998
|
+
const staleW = Math.max(0, paneTextWidth(columns) - (2 /* caret */ + termWidth(`${WARN} pin stale `) + SID_CHARS + 3 /* spazio + caporali */));
|
|
1999
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: true, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: "yellow", children: WARN }), " pin stale", ' ', _jsx(Text, { dimColor: true, children: row.sessionId.slice(0, SID_CHARS) }), staleNote ? _jsxs(Text, { color: "yellow", children: [" \u00AB", cut(staleNote, staleW), "\u00BB"] }) : null] }, row.sessionId));
|
|
1969
2000
|
}
|
|
1970
2001
|
const s = row.session; // non-stale → session presente
|
|
1971
2002
|
const isPinnedRow = row.kind === 'pinned';
|
|
1972
2003
|
// T28 — un ramo eredita il titolo dell'origine: senza marcatore le due
|
|
1973
|
-
// righe sarebbero identiche a occhio.
|
|
1974
|
-
// la troncatura non arriva mai.
|
|
2004
|
+
// righe sarebbero identiche a occhio.
|
|
1975
2005
|
const forked = forkOf.has(s.sessionId);
|
|
1976
|
-
// T53 — con una nota il prefisso di progetto sparisce e le sue colonne
|
|
1977
|
-
// passano alla nota; senza, il titolo resta com'è (vedi `rowLabel`).
|
|
1978
|
-
//
|
|
1979
|
-
// Invariante ③: il budget è DERIVATO da `columns`, non inchiodato.
|
|
1980
|
-
// Con un valore fisso (erano 44) su un terminale stretto la riga
|
|
1981
|
-
// superava il pane, e a troncarla finiva `cli-truncate` — che sfora
|
|
1982
|
-
// di una colonna per emoji e si mangia il bordo destro.
|
|
1983
2006
|
// T59 D2 — nella vista "tutte" il marker è PER-SESSIONE (binding letto
|
|
1984
2007
|
// dal sidecar) e non deciso dal parent: la lista mescola scoped e spot,
|
|
1985
2008
|
// quindi un marker uniforme mentirebbe su metà delle righe. E il solo
|
|
1986
2009
|
// glifo direbbe *che* la conversazione è legata senza dire *a cosa* —
|
|
1987
2010
|
// informazione monca proprio qui, l'unica vista dove l'appartenenza
|
|
1988
|
-
// non è scritta da nessun'altra parte dello schermo
|
|
1989
|
-
//
|
|
2011
|
+
// non è scritta da nessun'altra parte dello schermo: da qui la colonna
|
|
2012
|
+
// task accanto, che esiste solo in questa vista.
|
|
1990
2013
|
const bound = bindings.get(s.sessionId) ?? null;
|
|
1991
|
-
const idTag = isAll && bound ? `${bound} ` : '';
|
|
1992
2014
|
const linked = isAll ? Boolean(bound) : !isSpot;
|
|
1993
|
-
|
|
1994
|
-
//
|
|
1995
|
-
//
|
|
1996
|
-
//
|
|
1997
|
-
//
|
|
1998
|
-
|
|
1999
|
-
//
|
|
2000
|
-
|
|
2015
|
+
// T60 — colonne VERE: ogni cella fissa è larga esattamente quanto
|
|
2016
|
+
// dichiara, riempita di spazi con `pad` (che misura in colonne, non in
|
|
2017
|
+
// caratteri). Il marker va portato a 2 anche quando è `○`, largo 1:
|
|
2018
|
+
// era lui a far slittare a sinistra di una colonna tutta la riga di
|
|
2019
|
+
// ogni sessione spot.
|
|
2020
|
+
const age = relTime(s.ts);
|
|
2021
|
+
// Il taglio del titolo è ciò che RESTA, calcolato per sottrazione: le
|
|
2022
|
+
// colonne fisse sono note, quindi l'unica cella elastica prende il
|
|
2023
|
+
// resto. Pavimento `0` e non un minimo di cortesia — è un tetto, non
|
|
2024
|
+
// una preferenza: alzarlo sopra lo spazio reale fa uscire la riga dal
|
|
2025
|
+
// pane e le mangia il bordo (invariante ③).
|
|
2026
|
+
const titleW = Math.max(0, paneTextWidth(columns) -
|
|
2001
2027
|
(2 /* caret */ +
|
|
2002
|
-
2 /*
|
|
2003
|
-
1 /*
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
(
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2028
|
+
2 /* marker */ +
|
|
2029
|
+
1 /* gutter */ +
|
|
2030
|
+
SID_CHARS +
|
|
2031
|
+
1 /* gutter */ +
|
|
2032
|
+
(taskW > 0 ? taskW + 1 : 0) +
|
|
2033
|
+
1 /* gutter prima della data */ +
|
|
2034
|
+
ageW));
|
|
2035
|
+
// T28 — `⑂` sta DENTRO la cella titolo, non in una colonna sua: una
|
|
2036
|
+
// colonna dedicata costerebbe 2 spazi vuoti su ogni riga non-fork, e
|
|
2037
|
+
// metterlo fuori cella sposterebbe il bordo del titolo solo sui rami —
|
|
2038
|
+
// cioè rimetterebbe lo slittamento che le colonne tolgono.
|
|
2039
|
+
const forkMark = forked ? '⑂ ' : '';
|
|
2040
|
+
const inner = Math.max(0, titleW - termWidth(forkMark));
|
|
2041
|
+
// T60 — il testo arriva già ripulito di ciò che le colonne accanto
|
|
2042
|
+
// dicono già (progetto e task id): senza, la cella conterrebbe
|
|
2043
|
+
// `🧵 loom-works · T59` accanto a una colonna che dice `T59`.
|
|
2044
|
+
const label = rowLabel(sessionTitle(s, projectCore, bound), sessionNotes.get(s.sessionId), inner);
|
|
2045
|
+
const used = (label.note ? termWidth(label.note) + 2 : 0) +
|
|
2046
|
+
(label.note && label.rest ? 1 : 0) +
|
|
2047
|
+
termWidth(label.rest);
|
|
2048
|
+
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: "cyan", children: s.sessionId.slice(0, SID_CHARS) }), ' ', taskW > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: bound ? 'green' : undefined, dimColor: !bound, children: pad(bound ?? TASK_EMPTY, taskW) }), ' '] })) : null, forkMark ? _jsx(Text, { color: "magenta", children: forkMark }) : null, label.note ? (_jsxs(Text, { color: "yellow", bold: true, children: ["\u00AB", label.note, "\u00BB"] })) : 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));
|
|
2011
2049
|
})), detail ? (_jsx(SessionDetailPane, { s: detail, firstLines: firstLines, lastLines: lastLines, columns: columns, origin: forkOf.get(detail.sessionId) ?? null, note: sessionNotes.get(detail.sessionId) ?? '' })) : null] }));
|
|
2012
2050
|
}
|
|
2013
2051
|
// T49 — detail pane della sessione selezionata (hover), gemello del DetailPane
|
|
@@ -2022,7 +2060,7 @@ function SessionDetailPane({ s, firstLines, lastLines, columns, origin, note, })
|
|
|
2022
2060
|
const width = detailTextWidth(columns);
|
|
2023
2061
|
const first = s.customTitle && firstLines > 0 ? wrapLines(s.firstPrompt, width, firstLines) : [];
|
|
2024
2062
|
const last = s.lastReply && lastLines > 0 ? wrapLines(s.lastReply, width, lastLines) : [];
|
|
2025
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [note ? _jsxs(Text, { color: "yellow", children: ["\u00AB", note, "\u00BB "] }) : null, _jsx(Text, { dimColor: Boolean(note), children: s.title })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts), origin ? ` · ⑂ da ${origin.slice(0, 8)}` : ''] }), first.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '» ' : ' ', line] }, `f${i}`))), last.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '« ' : ' ', line] }, `l${i}`)))] }));
|
|
2063
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [note ? _jsxs(Text, { color: "yellow", children: ["\u00AB", note, "\u00BB "] }) : null, _jsx(Text, { dimColor: Boolean(note), children: s.title })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts), " \u00B7 ", s.gitBranch || '-', origin ? ` · ⑂ da ${origin.slice(0, 8)}` : ''] }), first.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '» ' : ' ', line] }, `f${i}`))), last.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '« ' : ' ', line] }, `l${i}`)))] }));
|
|
2026
2064
|
}
|
|
2027
2065
|
/**
|
|
2028
2066
|
* Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
|
package/dist/session-list.js
CHANGED
|
@@ -38,6 +38,48 @@ export function stripProjectCore(title, core) {
|
|
|
38
38
|
}
|
|
39
39
|
return t.replace(/^[\s·]+/, '').trim();
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Toglie il token del task id dal titolo — quello e solo quello, confrontato
|
|
43
|
+
* per token intero e non per substring (`T5` non deve mordere dentro `T59`).
|
|
44
|
+
*
|
|
45
|
+
* Va in coppia con `stripProjectCore`: un titolo di tab è `<emoji> <progetto> ·
|
|
46
|
+
* <task>`, quindi tolto il progetto resta quasi sempre il solo task id.
|
|
47
|
+
*/
|
|
48
|
+
export function stripTaskId(title, taskId) {
|
|
49
|
+
if (!taskId)
|
|
50
|
+
return title;
|
|
51
|
+
return title
|
|
52
|
+
.split(/\s+/)
|
|
53
|
+
.filter((tok) => tok !== taskId)
|
|
54
|
+
.join(' ')
|
|
55
|
+
.replace(/^[\s·]+|[\s·]+$/g, '')
|
|
56
|
+
.trim();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Il testo che DISTINGUE una conversazione dalle altre — tolto tutto ciò che è
|
|
60
|
+
* già scritto altrove nella stessa riga.
|
|
61
|
+
*
|
|
62
|
+
* Il titolo di una sessione è la label della tab Ptyxis quando esiste
|
|
63
|
+
* (`🧵 loom-works · T59`), e il primo prompt dell'utente quando non esiste. I
|
|
64
|
+
* due casi non vanno trattati allo stesso modo, ed è il punto di questa
|
|
65
|
+
* funzione:
|
|
66
|
+
*
|
|
67
|
+
* - **con titolo custom** → è composto di pezzi che la riga mostra già in
|
|
68
|
+
* colonna: il progetto (costante su ogni riga di un deck per-progetto) e il
|
|
69
|
+
* task id (colonna sua). Tolti quelli non resta quasi mai nulla → si cade sul
|
|
70
|
+
* primo prompt, che è l'unica cosa lì dentro a dire di cosa si parlava.
|
|
71
|
+
* - **senza titolo custom** → `title` È già il primo prompt: intatto.
|
|
72
|
+
*
|
|
73
|
+
* `title` (non `customTitle`) è la fonte anche nel primo ramo: è la stessa
|
|
74
|
+
* stringa già sanificata al confine dell'adapter, e usare il campo grezzo
|
|
75
|
+
* rimetterebbe in circolo glifi che il frame non sa disegnare.
|
|
76
|
+
*/
|
|
77
|
+
export function sessionTitle(s, core, taskId) {
|
|
78
|
+
if (!s.customTitle)
|
|
79
|
+
return s.title;
|
|
80
|
+
const rest = stripTaskId(stripProjectCore(s.title, core), taskId);
|
|
81
|
+
return rest || s.firstPrompt || '';
|
|
82
|
+
}
|
|
41
83
|
/** Colonne minime perché un residuo dopo la nota valga la riga: sotto questa
|
|
42
84
|
* soglia si vedrebbero due lettere e un'ellissi, cioè rumore. */
|
|
43
85
|
const MIN_REST = 6;
|
|
@@ -45,27 +87,25 @@ const MIN_REST = 6;
|
|
|
45
87
|
* è la parte scelta da un umano, non è lei a cedere il posto per prima. */
|
|
46
88
|
const MIN_NOTE = 14;
|
|
47
89
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* Due regimi, ed è una decisione deliberata che NON siano lo stesso:
|
|
90
|
+
* Come dividere `budget` colonne fra la nota umana e il testo della
|
|
91
|
+
* conversazione, quando ci sono entrambi.
|
|
51
92
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
93
|
+
* `text` arriva GIÀ ripulito da `sessionTitle`: qui non si decide più *cosa*
|
|
94
|
+
* mostrare, solo *quanto*. Prima lo strip del prefisso di progetto avveniva
|
|
95
|
+
* dentro questa funzione e solo sul ramo con nota — asimmetria nata quando il
|
|
96
|
+
* task id non aveva una colonna sua e togliere il core avrebbe lasciato il
|
|
97
|
+
* nulla. Con la riga incolonnata quella premessa è caduta, e tenere due regimi
|
|
98
|
+
* diversi avrebbe reso la colonna titolo larga in un caso e stretta nell'altro.
|
|
58
99
|
*
|
|
59
|
-
* Il budget
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* ridursi a un moncone.
|
|
100
|
+
* Il budget dà la precedenza alla nota, ma senza affamare il resto: con
|
|
101
|
+
* entrambi presenti la nota cede fino a `MIN_NOTE`, e il resto sparisce del
|
|
102
|
+
* tutto sotto `MIN_REST` invece di ridursi a un moncone.
|
|
63
103
|
*/
|
|
64
|
-
export function rowLabel(
|
|
104
|
+
export function rowLabel(text, note, budget) {
|
|
65
105
|
if (!note)
|
|
66
|
-
return { note: '', rest: cut(
|
|
106
|
+
return { note: '', rest: cut(text, Math.max(0, budget)) };
|
|
67
107
|
// 2 colonne per i caporali « », 1 per lo spazio prima del residuo.
|
|
68
|
-
const rest =
|
|
108
|
+
const rest = text;
|
|
69
109
|
const noteBudget = Math.max(0, budget - 2);
|
|
70
110
|
if (!rest)
|
|
71
111
|
return { note: cut(note, noteBudget), rest: '' };
|
package/dist/width.js
CHANGED
|
@@ -202,6 +202,26 @@ export function cut(s, cols) {
|
|
|
202
202
|
// informazione, tolgono una colonna di testo.
|
|
203
203
|
return out.trimEnd().replace(/…$/, '') + '…';
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Cella di larghezza ESATTA `cols`: taglia se eccede, riempie di spazi se manca.
|
|
207
|
+
*
|
|
208
|
+
* È il gemello di `cut` e serve a una cosa sola: fare colonne vere. Una lista
|
|
209
|
+
* incolonnata a mano allinea solo finché ogni cella misura davvero quanto
|
|
210
|
+
* dichiara — e la misura è in COLONNE del terminale (`termWidth`), non in
|
|
211
|
+
* caratteri: `'○'.padEnd(2)` e `'🔗'.padEnd(2)` danno due stringhe che
|
|
212
|
+
* `String.length` giura uguali e che il terminale disegna larghe 2 e 3. È
|
|
213
|
+
* esattamente lo slittamento di una colonna che rende ragged una lista.
|
|
214
|
+
*
|
|
215
|
+
* `align: 'right'` mette il riempimento davanti: serve ai campi ancorati al
|
|
216
|
+
* margine destro (la data), dove ad allinearsi sono le unità, non l'inizio.
|
|
217
|
+
*/
|
|
218
|
+
export function pad(s, cols, align = 'left') {
|
|
219
|
+
if (cols <= 0)
|
|
220
|
+
return '';
|
|
221
|
+
const t = termWidth(s) > cols ? cut(s, cols) : s;
|
|
222
|
+
const fill = ' '.repeat(Math.max(0, cols - termWidth(t)));
|
|
223
|
+
return align === 'right' ? fill + t : t + fill;
|
|
224
|
+
}
|
|
205
225
|
/**
|
|
206
226
|
* Larghezza in colonne di UN carattere **dopo** `sanitize`.
|
|
207
227
|
*
|
package/package.json
CHANGED