@lamemind/loom-deck 0.56.1 → 0.57.1
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 +34 -1
- package/dist/cli.js +65 -9
- package/dist/deck-model.js +98 -1
- package/dist/frame.js +122 -4
- package/dist/glyphs.js +24 -0
- package/dist/hooks.js +72 -1
- package/dist/inbox-views.js +67 -0
- package/dist/inbox.js +83 -1
- package/dist/input-modes.js +3 -1
- package/dist/input.js +85 -6
- package/dist/mouse.js +17 -5
- package/dist/overlays/inbox.js +90 -0
- package/dist/overlays/wrap.js +144 -0
- package/dist/pane-header.js +25 -0
- package/dist/spawn.js +27 -0
- package/dist/tasks.js +17 -4
- package/dist/ui/inbox-screen.js +21 -0
- package/dist/ui/panes.js +67 -2
- package/dist/ui/preview.js +41 -3
- package/dist/ui/screens.js +57 -14
- package/dist/ui/wrap-screen.js +30 -0
- package/dist/viewport.js +60 -0
- package/dist/wrap-scan.js +30 -1
- package/package.json +1 -1
- package/scripts/deck-run +45 -9
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// T134 — la lista hard-wrap: settima schermata sostitutiva, apri-e-chiudi.
|
|
2
|
+
//
|
|
3
|
+
// Non è un pane e non è un catalogo: è un elenco di sola LETTURA più un campo
|
|
4
|
+
// che nomina il perimetro dello srotolamento. Nessuna selezione di riga, e non
|
|
5
|
+
// è una mancanza — l'azione non ha per oggetto un file ma un PATH, che può
|
|
6
|
+
// essere una cartella intera; una selezione prometterebbe un'azione per riga
|
|
7
|
+
// che non esiste. Da qui anche la rotella, che qui scorre il testo perché non
|
|
8
|
+
// c'è nessuna selezione da muovere (D5 di T21 vale al contrario).
|
|
9
|
+
//
|
|
10
|
+
// Lo spawn NON sta qui: arriva come callback `onApply`, per lo stesso confine
|
|
11
|
+
// degli altri overlay.
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
import { useWrapScan } from '../hooks.js';
|
|
14
|
+
import { pageStep, wrapListCapacity } from '../viewport.js';
|
|
15
|
+
import { cpLen, insertAt, removeAt } from '../layout.js';
|
|
16
|
+
import { sanitizeTyped } from '../glyphs.js';
|
|
17
|
+
import { WRAP_DEFAULT_PATH, wrapPrompt } from '../wrap-scan.js';
|
|
18
|
+
export function useWrapOverlay(deps) {
|
|
19
|
+
const { cwd, rows, setMode, setNote, onApply } = deps;
|
|
20
|
+
// Lo stato dello scan vive QUI e non nel modello, sul precedente di
|
|
21
|
+
// `useProjectStatus`: non è solo il contenuto di una schermata — i suoi
|
|
22
|
+
// numeri si leggono in testata a schermata chiusa, e la lista è una delle sue
|
|
23
|
+
// superfici invece che il suo contenuto. Tenerli in due posti significherebbe
|
|
24
|
+
// che l'indicatore e la lista possono dire cose diverse dello stesso scan.
|
|
25
|
+
const measure = useWrapScan(cwd);
|
|
26
|
+
/** Le righe FOTOGRAFATE all'apertura: uno scan che finisse mentre la lista è
|
|
27
|
+
* a schermo la riscriverebbe sotto gli occhi, e la posizione di scroll non
|
|
28
|
+
* avrebbe più un riferimento. Stessa regola del viewer del project status. */
|
|
29
|
+
const [files, setFiles] = useState(null);
|
|
30
|
+
const [top, setTop] = useState(0);
|
|
31
|
+
const [path, setPath] = useState(WRAP_DEFAULT_PATH);
|
|
32
|
+
const [caret, setCaret] = useState(cpLen(WRAP_DEFAULT_PATH));
|
|
33
|
+
const capacity = wrapListCapacity(rows);
|
|
34
|
+
const maxTop = Math.max(0, (files?.length ?? 0) - capacity);
|
|
35
|
+
/**
|
|
36
|
+
* Apre la lista, o dichiara perché non si apre.
|
|
37
|
+
*
|
|
38
|
+
* Rifiuta di aprirsi su una lista vuota, e i due modi di esserlo si dicono
|
|
39
|
+
* distinti: «non ho ancora misurato» e «ho misurato e non c'è niente» sono
|
|
40
|
+
* due stati del progetto diversi, e una schermata vuota li confonderebbe in
|
|
41
|
+
* uno solo. È la stessa forma di `^O` senza cache.
|
|
42
|
+
*/
|
|
43
|
+
function open() {
|
|
44
|
+
if (measure.files.length === 0) {
|
|
45
|
+
setNote(measure.mtime === null
|
|
46
|
+
? '^W → hard-wrap non ancora misurato · ^E per lo scan'
|
|
47
|
+
: '^W → nessun file con l\'a-capo rientrato: niente da srotolare');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setFiles([...measure.files]);
|
|
51
|
+
setTop(0);
|
|
52
|
+
setPath(WRAP_DEFAULT_PATH);
|
|
53
|
+
setCaret(cpLen(WRAP_DEFAULT_PATH));
|
|
54
|
+
setNote('');
|
|
55
|
+
setMode('wrap');
|
|
56
|
+
}
|
|
57
|
+
function close() {
|
|
58
|
+
setMode('normal');
|
|
59
|
+
setFiles(null);
|
|
60
|
+
}
|
|
61
|
+
function scroll(delta) {
|
|
62
|
+
setTop((t) => Math.max(0, Math.min(maxTop, t + delta)));
|
|
63
|
+
}
|
|
64
|
+
function onKey(input, key) {
|
|
65
|
+
if (key.escape) {
|
|
66
|
+
close();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (key.return) {
|
|
70
|
+
const target = path.trim() || WRAP_DEFAULT_PATH;
|
|
71
|
+
close();
|
|
72
|
+
onApply(target, wrapPrompt(target));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (key.upArrow) {
|
|
76
|
+
scroll(-1);
|
|
77
|
+
}
|
|
78
|
+
else if (key.downArrow) {
|
|
79
|
+
scroll(1);
|
|
80
|
+
}
|
|
81
|
+
else if (key.pageUp) {
|
|
82
|
+
scroll(-pageStep(capacity));
|
|
83
|
+
}
|
|
84
|
+
else if (key.pageDown) {
|
|
85
|
+
scroll(pageStep(capacity));
|
|
86
|
+
}
|
|
87
|
+
else if (key.leftArrow || key.rightArrow) {
|
|
88
|
+
const d = key.leftArrow ? -1 : 1;
|
|
89
|
+
setCaret((c) => Math.max(0, Math.min(cpLen(path), c + d)));
|
|
90
|
+
}
|
|
91
|
+
else if (key.backspace || key.delete) {
|
|
92
|
+
if (caret > 0) {
|
|
93
|
+
setPath((p) => removeAt(p, caret - 1));
|
|
94
|
+
setCaret((c) => c - 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (key.ctrl) {
|
|
98
|
+
// `^U` svuota, come il campo di ricerca e il filtro dell'assegnazione.
|
|
99
|
+
// Ogni altra combo è no-op: dentro un modo capturing gli acceleratori
|
|
100
|
+
// globali restano inerti.
|
|
101
|
+
if (input === 'u') {
|
|
102
|
+
setPath('');
|
|
103
|
+
setCaret(0);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (input && !key.meta) {
|
|
107
|
+
// `↑↓` sono già state consumate sopra: qui arrivano solo i caratteri, che
|
|
108
|
+
// vanno nel campo. Il testo si sanifica al confine come ogni altro campo
|
|
109
|
+
// del deck — un incollaggio porta byte di controllo che Ink conterebbe
|
|
110
|
+
// nella larghezza della riga.
|
|
111
|
+
const ins = sanitizeTyped(input);
|
|
112
|
+
setPath((p) => insertAt(p, caret, ins));
|
|
113
|
+
setCaret((c) => c + cpLen(ins));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Lo scan a richiesta, con la nota che dice cosa sta succedendo: cammina
|
|
117
|
+
* l'albero del progetto intero, quindi dura, e un tasto che non desse segno
|
|
118
|
+
* si leggerebbe come inerte. */
|
|
119
|
+
function scan() {
|
|
120
|
+
if (measure.scanning) {
|
|
121
|
+
setNote('^E → scan hard-wrap già in corso');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
setNote('⏳ scan hard-wrap sul progetto intero…');
|
|
125
|
+
measure.scan();
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
files,
|
|
129
|
+
top,
|
|
130
|
+
capacity,
|
|
131
|
+
maxTop,
|
|
132
|
+
path,
|
|
133
|
+
caret,
|
|
134
|
+
count: measure.count,
|
|
135
|
+
mixed: measure.mixed,
|
|
136
|
+
mtime: measure.mtime,
|
|
137
|
+
ok: measure.ok,
|
|
138
|
+
scanning: measure.scanning,
|
|
139
|
+
open,
|
|
140
|
+
scan,
|
|
141
|
+
onKey,
|
|
142
|
+
scroll,
|
|
143
|
+
};
|
|
144
|
+
}
|
package/dist/pane-header.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { cutParts } from './width.js';
|
|
13
13
|
import { paneTextWidth } from './layout.js';
|
|
14
14
|
import { SESSION_VIEWS, TASK_VIEWS, } from './pane-views.js';
|
|
15
|
+
import { INBOX_VIEWS } from './inbox-views.js';
|
|
15
16
|
function finish(parts, columns) {
|
|
16
17
|
const shown = cutParts(parts.map((p) => p.text), paneTextWidth(columns), parts.findIndex((p) => p.active));
|
|
17
18
|
return { parts, shown };
|
|
@@ -55,6 +56,30 @@ export function sessionHeaderParts(parentLabel, counts, active, above, below, co
|
|
|
55
56
|
{ key: null, text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false },
|
|
56
57
|
], columns);
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* T134 — header del pane inbox, terzo gemello: `Inbox` nomina il pane e non si
|
|
61
|
+
* seleziona, poi le quattro viste del catalogo, poi `↑N`/`↓N`.
|
|
62
|
+
*
|
|
63
|
+
* L'attenuazione la decide il catalogo (`v.dim(counts)`) e non il contatore
|
|
64
|
+
* della voce, a differenza dei due header storici: la voce `Tutti` conta ogni
|
|
65
|
+
* file in lista ma si grigia sulla somma delle tre nature (D7), perché è quella
|
|
66
|
+
* a dire se c'è lavoro da prendere.
|
|
67
|
+
*/
|
|
68
|
+
export function inboxHeaderParts(counts, active, above, below, columns) {
|
|
69
|
+
const views = INBOX_VIEWS.map((v) => ({
|
|
70
|
+
key: v.id,
|
|
71
|
+
text: ` · ${v.label(counts)}`,
|
|
72
|
+
color: v.color,
|
|
73
|
+
dim: v.dim(counts),
|
|
74
|
+
active: v.id === active,
|
|
75
|
+
}));
|
|
76
|
+
return finish([
|
|
77
|
+
{ key: null, text: 'Inbox', dim: false, active: false },
|
|
78
|
+
...views,
|
|
79
|
+
{ key: null, text: above > 0 ? ` · ↑${above}` : '', dim: true, active: false },
|
|
80
|
+
{ key: null, text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false },
|
|
81
|
+
], columns);
|
|
82
|
+
}
|
|
58
83
|
/** Le parti COME SONO A SCHERMO — testo tagliato, chiave intatta — nella forma
|
|
59
84
|
* che `inlineRegions` (`mouse.ts`) misura per l'hit-test. */
|
|
60
85
|
export function headerItems(h) {
|
package/dist/spawn.js
CHANGED
|
@@ -267,6 +267,33 @@ export function spawnDeckFork(taskId, cwd, originId, newId) {
|
|
|
267
267
|
export function spawnClaudeEmpty(cwd) {
|
|
268
268
|
return launchDeckRun(['--no-task'], cwd);
|
|
269
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* T134 — sessione NUDA con un prompt letterale: `deck-run --no-task --prompt
|
|
272
|
+
* <testo> --model <alias>`.
|
|
273
|
+
*
|
|
274
|
+
* Il lavoro che apre — drenare un file inbox, srotolare l'hard-wrap di un path
|
|
275
|
+
* — sta sulla DOC, non su una task (D10 preflight). Legarlo a un cappello
|
|
276
|
+
* esporterebbe `LOOM_TASK`, farebbe scattare l'hook `SessionStart` che inietta
|
|
277
|
+
* in contesto il task file di un lavoro spesso già chiuso, e metterebbe
|
|
278
|
+
* `· T<n>` nel titolo della tab; per un file `sweep` senza cappello la strada
|
|
279
|
+
* non esisterebbe nemmeno.
|
|
280
|
+
*
|
|
281
|
+
* Presidiata, non headless (D11): `spawnSkill` resta dov'è e non si estende
|
|
282
|
+
* qui. Ne discendono due cose. Il confine presidiata / non presidiata delle
|
|
283
|
+
* skill di doc smette di essere un vincolo — anche `align-doc`, che apre un
|
|
284
|
+
* branch con PR, è offribile, perché c'è un umano nella tab che la guarda. E la
|
|
285
|
+
* guardia d'ingresso `doc-guard.sh worktree`, che esce 2 su worktree sporco
|
|
286
|
+
* sotto `inbox/`, `reference/` o `CLAUDE.md`, la incontra la sessione e la
|
|
287
|
+
* mostra a chi l'ha lanciata: il deck non deve replicarla per sapere in
|
|
288
|
+
* anticipo se l'azione morirebbe allo step 0.
|
|
289
|
+
*
|
|
290
|
+
* Il modello è passato SEMPRE, anche sul default, per la stessa ragione di
|
|
291
|
+
* `permissionMode`: lo spawn resta deterministico e leggibile nel process tree
|
|
292
|
+
* invece di dipendere da un default che può cambiare fra versioni.
|
|
293
|
+
*/
|
|
294
|
+
export function spawnBare(cwd, prompt, model) {
|
|
295
|
+
return launchDeckRun(['--no-task', '--prompt', prompt, '--model', model], cwd);
|
|
296
|
+
}
|
|
270
297
|
// T39/T32: voce `launch` custom del file config, eseguita con cwd = project root.
|
|
271
298
|
// Spawn detached come spawnDeck: il deck lancia ma non possiede il processo.
|
|
272
299
|
// Shell login+interattiva (bash -lic) perché i comandi tipici sono alias o
|
package/dist/tasks.js
CHANGED
|
@@ -8,11 +8,24 @@ import { sanitize } from './width.js';
|
|
|
8
8
|
* divergano (uno accetterebbe righe che l'altro rifiuta).
|
|
9
9
|
*/
|
|
10
10
|
export const TASK_ID_RE = /^T\d+$/;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Nome della cartella doc del progetto (`docs`, `runtime`, …).
|
|
13
|
+
*
|
|
14
|
+
* D1 (preflight T20): override via env `LOOM_DECK_DOCS_ROOT`, nessun
|
|
15
|
+
* auto-detect e nessuna lettura del file config. Il deck non si lancia mai a
|
|
16
|
+
* mano — nasce da compass, che legge `docsRoot` dal registry dconf e lo
|
|
17
|
+
* antepone al comando, o dal custom-command del profilo Ptyxis, che ce l'ha
|
|
18
|
+
* cablato — quindi l'env c'è sempre nei percorsi di avvio reali.
|
|
19
|
+
*
|
|
20
|
+
* T134 — funzione e non più letterale ripetuto: la legge anche lo scan della
|
|
21
|
+
* coda inbox, e due copie della stessa cascata divergerebbero appena una delle
|
|
22
|
+
* due imparasse a leggere il file config.
|
|
23
|
+
*/
|
|
24
|
+
export function docsRootName() {
|
|
25
|
+
return process.env.LOOM_DECK_DOCS_ROOT || 'docs';
|
|
26
|
+
}
|
|
13
27
|
export function resolveTasksPath(cwd = process.cwd()) {
|
|
14
|
-
|
|
15
|
-
return join(cwd, docsRoot, 'tasks.md');
|
|
28
|
+
return join(cwd, docsRootName(), 'tasks.md');
|
|
16
29
|
}
|
|
17
30
|
// I task file vivono in `<docsRoot>/tasks/` — sibling di tasks.md. Derivo la
|
|
18
31
|
// dir dallo stesso path per rispettare l'override LOOM_DECK_DOCS_ROOT.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// T134 — il detail di un file inbox: testo scorrevole più la riga che dichiara
|
|
3
|
+
// cosa partirà su `⏎`.
|
|
4
|
+
//
|
|
5
|
+
// Riusa `DetailLine` del detail della task invece di ri-renderizzare il
|
|
6
|
+
// markdown per conto proprio: sono lo stesso costrutto — un documento reso in
|
|
7
|
+
// span tipizzati dentro un box — e due rese dello stesso markdown
|
|
8
|
+
// divergerebbero al primo stile aggiunto. L'evidenziazione della ricerca non
|
|
9
|
+
// serve qui (nessuna ricerca dentro l'inbox), quindi le occorrenze arrivano
|
|
10
|
+
// vuote e il secondo taglio di `DetailLine` non gira nemmeno.
|
|
11
|
+
import { Box, Text } from 'ink';
|
|
12
|
+
import { cut } from '../width.js';
|
|
13
|
+
import { DetailLine } from './detail-screen.js';
|
|
14
|
+
import { INBOX_MARK, WARN, fmtDateTime, fmtSize } from '../glyphs.js';
|
|
15
|
+
import { NATURA_SHORT, inboxMark } from '../inbox.js';
|
|
16
|
+
export function InboxScreen({ file, missing, lines, spans, top, total, capacity, prompt, columns, }) {
|
|
17
|
+
const last = Math.min(total, top + capacity);
|
|
18
|
+
const width = Math.max(20, (columns || 80) - 4);
|
|
19
|
+
const mark = inboxMark(file);
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: NATURA_SHORT[file.natura] }), ' ', cut(file.basename, Math.max(10, width - 8))] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [INBOX_MARK[mark] ? `${INBOX_MARK[mark]} ` : '', mark, file.branch ? ` ⟨${file.branch}⟩` : '', file.cappello ? ` · ${file.cappello}` : '', " \u00B7 ", file.nozioni, " nozioni \u00B7 ", file.aperte, ' ', "aperte \u00B7 ", fmtSize(file.chars), " \u00B7 ", fmtDateTime(file.created * 1000), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193/PgUp/PgDn" }), " testo \u00B7 ", _jsx(Text, { color: "yellow", children: "g/G" }), " estremi \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " apre la sessione \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " chiude"] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " file non leggibile \u00B7 l'azione resta attiva (la skill risolve il file per nome)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: [], current: -1 }, top + i)))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "\u23CE \u203A " }), _jsx(Text, { color: "green", children: cut(prompt, Math.max(10, width - 4)) })] }) })] }));
|
|
21
|
+
}
|
package/dist/ui/panes.js
CHANGED
|
@@ -5,12 +5,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
import { Box, Text } from 'ink';
|
|
6
6
|
import { cut, pad, sanitize, termWidth } from '../width.js';
|
|
7
7
|
import { paneTextWidth } from '../layout.js';
|
|
8
|
-
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SID_CHARS, TASK_EMPTY, WARN, metaCount, modelShort, relTime, } from '../glyphs.js';
|
|
8
|
+
import { CARET, CARET_OFF, INBOX_MARK, INBOX_MARK_W, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SID_CHARS, TASK_EMPTY, WARN, metaCount, modelShort, relTime, } from '../glyphs.js';
|
|
9
|
+
import { NATURA_SHORT, NATURA_W, inboxMark } from '../inbox.js';
|
|
10
|
+
import { inboxView, } from '../inbox-views.js';
|
|
9
11
|
import { TaskRow } from './task-row.js';
|
|
10
12
|
import { META_ROWS, ROW_ALL, ROW_SPOT } from '../model.js';
|
|
11
13
|
import { rowLabel, sessionTitle } from '../session-list.js';
|
|
12
14
|
import { sessionView, taskView, } from '../pane-views.js';
|
|
13
|
-
import { sessionHeaderParts, taskHeaderParts } from '../pane-header.js';
|
|
15
|
+
import { inboxHeaderParts, sessionHeaderParts, taskHeaderParts, } from '../pane-header.js';
|
|
14
16
|
import { describeSort, PRI_ENTRIES, PROG_ENTRIES, } from '../view.js';
|
|
15
17
|
/**
|
|
16
18
|
* Header del pane task, tagliato QUI e non da Ink — stesso motivo del gemello
|
|
@@ -83,6 +85,69 @@ export function SessionsHeader({ parentLabel, counts, active, above, below, focu
|
|
|
83
85
|
const { parts, shown } = sessionHeaderParts(parentLabel, counts, active, above, below, columns);
|
|
84
86
|
return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
|
|
85
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* T134 — header del pane inbox, terzo gemello dei due sopra: le parti e il loro
|
|
90
|
+
* taglio vengono da `pane-header.ts`, lo stesso modulo da cui `frame.ts` ricava
|
|
91
|
+
* le colonne cliccabili.
|
|
92
|
+
*/
|
|
93
|
+
export function InboxHeader({ counts, active, above, below, focused, columns, }) {
|
|
94
|
+
const { parts, shown } = inboxHeaderParts(counts, active, above, below, columns);
|
|
95
|
+
return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* T134 — il pane inbox, alternativo a quello delle sessioni nello slot destro
|
|
99
|
+
* (D6). Uno dei due è montato e l'altro non esiste: `^B` li scambia.
|
|
100
|
+
*
|
|
101
|
+
* Mostra SEMPRE TUTTO, senza filtro sulla task selezionata a sinistra (D7
|
|
102
|
+
* preflight, che supersede la clausola di D6 congelate): niente righe meta,
|
|
103
|
+
* nessun trattamento speciale per il cappello vuoto o per un cappello che
|
|
104
|
+
* nomina una task non più in `tasks.md`. Ne discende che la colonna `CAPPELLO`
|
|
105
|
+
* di `doc-metrics` non serve al filtro — resta nel blocco preview, dove dice
|
|
106
|
+
* qualcosa senza costare una colonna su ogni riga.
|
|
107
|
+
*
|
|
108
|
+
* La riga porta marcatori, branch e nome (D8). Non le cifre (nozioni, aperte,
|
|
109
|
+
* char): stanno nella preview, e su un pane largo la metà del terminale ogni
|
|
110
|
+
* colonna fissa la paga la cella elastica del nome — che è l'unica cosa con cui
|
|
111
|
+
* si riconosce un file.
|
|
112
|
+
*/
|
|
113
|
+
export function InboxPane({ files, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, }) {
|
|
114
|
+
const width = paneTextWidth(columns);
|
|
115
|
+
// La cella elastica si calcola per SOTTRAZIONE dalle colonne fisse, come il
|
|
116
|
+
// titolo della lista sessioni: pavimento 0 e non un minimo di cortesia — è un
|
|
117
|
+
// tetto, non una preferenza, e alzarlo sopra lo spazio reale fa uscire la riga
|
|
118
|
+
// dal pane mangiandone il bordo (invariante ③ di width.ts).
|
|
119
|
+
const ageW = 3;
|
|
120
|
+
const nameW = Math.max(0, width -
|
|
121
|
+
(2 /* caret */ +
|
|
122
|
+
NATURA_W +
|
|
123
|
+
1 /* gutter */ +
|
|
124
|
+
INBOX_MARK_W +
|
|
125
|
+
1 /* gutter */ +
|
|
126
|
+
1 /* gutter prima della data */ +
|
|
127
|
+
ageW));
|
|
128
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(InboxHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), paneCount === 0 ? (
|
|
129
|
+
// Tre vuoti diversi, e dirlo è il punto: «non ho ancora misurato» non è
|
|
130
|
+
// «ho misurato e non c'è niente», e «la misura si è rotta» non è nessuno
|
|
131
|
+
// dei due (D2 preflight). Una lista vuota che non dice quale dei tre è
|
|
132
|
+
// si legge come un pane rotto.
|
|
133
|
+
_jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(!scanned
|
|
134
|
+
? 'coda non ancora misurata'
|
|
135
|
+
: !ok
|
|
136
|
+
? 'misura della coda fallita: plugin assente o script in errore'
|
|
137
|
+
: inboxView(activeView).empty, width) })) : (files.map((f) => {
|
|
138
|
+
const sel = f.path === selectedPath;
|
|
139
|
+
const mark = inboxMark(f);
|
|
140
|
+
// Il branch sta DENTRO la cella del nome, non in una colonna propria:
|
|
141
|
+
// è raro, e una colonna dedicata costerebbe spazio vuoto su ogni riga
|
|
142
|
+
// non-branchata di un pane largo la metà del terminale. Stessa scelta
|
|
143
|
+
// del marcatore di fork nella lista sessioni.
|
|
144
|
+
const branchMark = f.branch ? `⟨${f.branch}⟩ ` : '';
|
|
145
|
+
const inner = Math.max(0, nameW - termWidth(sanitize(branchMark)));
|
|
146
|
+
const name = cut(f.basename.replace(/\.md$/, ''), inner);
|
|
147
|
+
const age = relTime(f.created * 1000);
|
|
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
|
+
}))] }));
|
|
150
|
+
}
|
|
86
151
|
export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, counts, activeView, paneCount, selectedId, focused, above, below, columns, forkOf, sessionNotes, projectCore, live, }) {
|
|
87
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 ? (
|
|
88
153
|
// T100 — la nota della vista di default resta quella storica, che nomina
|
package/dist/ui/preview.js
CHANGED
|
@@ -2,11 +2,41 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
// Blocco preview a piena larghezza sotto i due pane: mostra la task o la
|
|
3
3
|
// conversazione selezionata a seconda del pane a fuoco.
|
|
4
4
|
import { Box, Text } from 'ink';
|
|
5
|
-
import { wrapLines } from '../width.js';
|
|
5
|
+
import { cut, wrapLines } from '../width.js';
|
|
6
6
|
import { previewTextWidth } from '../layout.js';
|
|
7
7
|
import { LIVE_BUSY, LIVE_IDLE, META_KEYS, SID_CHARS, fmtDateTime, fmtSize } from '../glyphs.js';
|
|
8
|
+
import { NATURA_SHORT, inboxMark } from '../inbox.js';
|
|
9
|
+
/** Colonne del prefisso delle due anteprime della preview sessione (`» `, `« `,
|
|
10
|
+
* e i due spazi delle righe di continuazione). Costante perché entra nel
|
|
11
|
+
* budget di wrap, che è il posto in cui dimenticarla non produce un errore ma
|
|
12
|
+
* un bordo mangiato. */
|
|
13
|
+
const PREVIEW_PREFIX_W = 2;
|
|
8
14
|
export function PreviewPane(p) {
|
|
9
|
-
return (_jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: p.kind === 'task' ? (_jsx(TaskPreview, { detail: p.detail, maxLines: p.maxLines, columns: p.columns })) : (_jsx(SessionPreview, { s: p.s, firstLines: p.firstLines, lastLines: p.lastLines, columns: p.columns, origin: p.origin, note: p.note, live: p.live })) }));
|
|
15
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: p.kind === 'task' ? (_jsx(TaskPreview, { detail: p.detail, maxLines: p.maxLines, columns: p.columns })) : p.kind === 'inbox' ? (_jsx(InboxPreview, { file: p.file, columns: p.columns })) : (_jsx(SessionPreview, { s: p.s, firstLines: p.firstLines, lastLines: p.lastLines, columns: p.columns, origin: p.origin, note: p.note, live: p.live })) }));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* T134 — corpo della preview inbox: due righe FISSE, contate da
|
|
19
|
+
* `INBOX_DETAIL_FIXED`. Ogni riga aggiunta va scalata lì, o il frame sfonda
|
|
20
|
+
* `rows` e Ink passa a `clearTerminal` — che su VTE riversa un frame nello
|
|
21
|
+
* scrollback a ogni tick del poll.
|
|
22
|
+
*
|
|
23
|
+
* Porta ciò che la riga di lista non può: il nome INTERO (in lista si tronca
|
|
24
|
+
* su un pane largo la metà del terminale), le cifre del marker e il cappello.
|
|
25
|
+
* Il perché di uno stato — `branch:` che congela per chiunque, `drainable`
|
|
26
|
+
* assente che tiene fuori dalla coda ma non vieta l'esecuzione — è scritto per
|
|
27
|
+
* esteso, perché il glifo della lista dice solo QUALE stato, non cosa comporta.
|
|
28
|
+
*/
|
|
29
|
+
export function InboxPreview({ file, columns }) {
|
|
30
|
+
const mark = inboxMark(file);
|
|
31
|
+
const width = previewTextWidth(columns);
|
|
32
|
+
const state = mark === 'broken'
|
|
33
|
+
? 'marker illeggibile: nessuna skill lo prende, va riparato'
|
|
34
|
+
: mark === 'branched'
|
|
35
|
+
? `congelato su ${file.branch}: lo sblocca pull-repos quando il branch è su main`
|
|
36
|
+
: mark === 'held'
|
|
37
|
+
? 'fuori dalla coda automatica (nessun drainable): eseguibile se lo nomini'
|
|
38
|
+
: 'in coda: una skill può prenderlo da sola';
|
|
39
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: NATURA_SHORT[file.natura] }), ' ', cut(file.basename, Math.max(10, width - 8))] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [file.nozioni, " nozioni \u00B7 ", file.aperte, " aperte \u00B7 ", fmtSize(file.chars), " \u00B7", ' ', fmtDateTime(file.created * 1000), file.cappello ? ` · ${file.cappello}` : '', file.indexed ? ' · indexed' : '', " \u00B7 ", state] })] }));
|
|
10
40
|
}
|
|
11
41
|
// T49 — corpo della preview sessione. Tutti i campi vengono dal parse già
|
|
12
42
|
// cached dell'adapter (mtime-keyed): non costa I/O al movimento di selezione.
|
|
@@ -16,7 +46,15 @@ export function PreviewPane(p) {
|
|
|
16
46
|
// lo duplicherebbe (D4 preflight). Le righe rese non superano mai il riservato
|
|
17
47
|
// dal budget (`firstLines`/`lastLines`); renderne meno è sicuro (frame più corto).
|
|
18
48
|
export function SessionPreview({ s, firstLines, lastLines, columns, origin, note, live, }) {
|
|
19
|
-
|
|
49
|
+
// Il prefisso `» `/`« ` è largo 2 e sta SULLA riga: va tolto dal budget di
|
|
50
|
+
// wrap, o una riga piena esce dal box di quelle due colonne e a raccoglierla
|
|
51
|
+
// arriva `cli-truncate`, che sfora di una colonna per emoji e mangia il bordo
|
|
52
|
+
// (invariante ③ di width.ts). Il difetto si vede solo quando una riga cade
|
|
53
|
+
// esattamente sul budget, cioè su una risposta abbastanza lunga: da qui un
|
|
54
|
+
// gate che passa o fallisce a seconda delle conversazioni che trova sul
|
|
55
|
+
// disco. Le righe di continuazione portano due spazi, quindi il prefisso
|
|
56
|
+
// costa 2 su OGNI riga, non solo sulla prima.
|
|
57
|
+
const width = previewTextWidth(columns) - PREVIEW_PREFIX_W;
|
|
20
58
|
const first = s.customTitle && firstLines > 0 ? wrapLines(s.firstPrompt, width, firstLines) : [];
|
|
21
59
|
const last = s.lastReply && lastLines > 0 ? wrapLines(s.lastReply, width, lastLines) : [];
|
|
22
60
|
return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: s.sessionId.slice(0, SID_CHARS) }), ' ', note ? _jsxs(Text, { color: "yellow", children: ["\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 || '-', s.model ? ` · ${s.model}` : '', origin ? ` · ⑂ da ${origin.slice(0, 8)}` : '', live ? (_jsx(Text, { color: live.status === 'busy' ? 'yellow' : 'green', children: ` · ${live.status === 'busy' ? LIVE_BUSY : LIVE_IDLE} viva pid ${live.pid} (${live.status})` })) : null] }), 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}`)))] }));
|
package/dist/ui/screens.js
CHANGED
|
@@ -2,12 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
// Le SCHERMATE SOSTITUTIVE del deck e i due frammenti che le accompagnano.
|
|
3
3
|
//
|
|
4
4
|
// Una schermata sostitutiva prende il frame intero invece di stare in un box
|
|
5
|
-
// sopra i due pane: assegnazione, detail della task, project status,
|
|
6
|
-
// reader. Il criterio è la taglia del contenuto — una
|
|
7
|
-
//
|
|
8
|
-
// d'altezza dei pane non viene nemmeno
|
|
5
|
+
// sopra i due pane: assegnazione, detail della task, project status, detail di
|
|
6
|
+
// un file inbox, ricerca e reader. Il criterio è la taglia del contenuto — una
|
|
7
|
+
// lista di occorrenze, un task file o un file di nozioni non entrano in quattro
|
|
8
|
+
// righe — e la conseguenza è che il budget d'altezza dei pane non viene nemmeno
|
|
9
|
+
// calcolato, perché il render esce prima.
|
|
9
10
|
//
|
|
10
|
-
// `screenFor` è il ROUTER: sceglie fra le
|
|
11
|
+
// `screenFor` è il ROUTER: sceglie fra le sei e restituisce `null` quando
|
|
11
12
|
// nessuna è attiva, cioè quando si resta sulla lista. È una funzione e non un
|
|
12
13
|
// componente proprio per questo — il chiamante deve poter distinguere «ecco la
|
|
13
14
|
// schermata» da «non è il tuo turno», e un componente che rende `null` non gli
|
|
@@ -25,12 +26,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
25
26
|
// primo file di `ui/` a farlo, quindi va detto invece che scoperto a grep.
|
|
26
27
|
import { Box, Text } from 'ink';
|
|
27
28
|
import { rowIndexOfKey, selectedRow } from '../search.js';
|
|
29
|
+
import { cut } from '../width.js';
|
|
30
|
+
import { legendWidth } from '../frame.js';
|
|
28
31
|
import { isCompact, searchPreviewCapacity, windowRange } from '../viewport.js';
|
|
29
32
|
import { conversationLabel } from '../layout.js';
|
|
30
33
|
import { taskColumns } from '../view.js';
|
|
31
34
|
import { AssignScreen } from './assign-screen.js';
|
|
32
35
|
import { DetailScreen } from './detail-screen.js';
|
|
33
36
|
import { StatusScreen } from './status-screen.js';
|
|
37
|
+
import { InboxScreen } from './inbox-screen.js';
|
|
38
|
+
import { WrapScreen } from './wrap-screen.js';
|
|
34
39
|
import { ReaderScreen, SearchScreen } from './search-screen.js';
|
|
35
40
|
/**
|
|
36
41
|
* Il ripiego per un terminale troppo basso: una riga sola al posto della
|
|
@@ -51,11 +56,17 @@ export function CompactNotice({ what, esc, rows, columns, dot = true, }) {
|
|
|
51
56
|
* flusso le sue istruzioni. I modali sostitutivi non arrivano qui — hanno già
|
|
52
57
|
* preso il frame.
|
|
53
58
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
59
|
+
* T134 — in `normal` la riga non è più tutta della legenda: divide lo spazio con
|
|
60
|
+
* gli INDICATORI ancorati a destra, e sono loro ad avere la precedenza sul
|
|
61
|
+
* budget (D5 preflight). `keyLegend` è già troncabile per costruzione e ciò che
|
|
62
|
+
* perde si ricorda; un contatore troncato invece mente.
|
|
63
|
+
*
|
|
64
|
+
* Dentro un modale in flusso gli indicatori non compaiono, e non è un
|
|
65
|
+
* nascondimento: la riga è dell'istruzione del modale, che sta a schermo per il
|
|
66
|
+
* tempo di una domanda. «Mai nascondibile» (D9) dice che non esiste un tasto per
|
|
67
|
+
* spegnerli, non che ogni riga del frame debba ospitarli.
|
|
57
68
|
*/
|
|
58
|
-
export function HintBar({ mode, purge, keyLegend, }) {
|
|
69
|
+
export function HintBar({ mode, purge, keyLegend, indicators, columns, }) {
|
|
59
70
|
if (mode === 'create') {
|
|
60
71
|
return (_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"] }));
|
|
61
72
|
}
|
|
@@ -74,19 +85,29 @@ export function HintBar({ mode, purge, keyLegend, }) {
|
|
|
74
85
|
if (mode === 'edit') {
|
|
75
86
|
return (_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"] }));
|
|
76
87
|
}
|
|
77
|
-
|
|
88
|
+
// Riga a piena larghezza nella stessa forma della testata: un `Box` con
|
|
89
|
+
// `justifyContent="space-between"` e due figli a stringa piatta. Il blocco
|
|
90
|
+
// indicatori arriva già unito (`indicators.text`) invece che come segmenti
|
|
91
|
+
// resi uno per uno — da cui il colore unico per il blocco.
|
|
92
|
+
//
|
|
93
|
+
// Il taglio della legenda resta del deck e non di Ink: la stringa porta emoji
|
|
94
|
+
// e frecce, e `cli-truncate` le conta con un budget in colonne indicizzando
|
|
95
|
+
// per code point — una colonna di troppo finisce sopra il bordo destro, che
|
|
96
|
+
// sparisce dalla riga (invariante ③ di width.ts).
|
|
97
|
+
return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: cut(keyLegend, legendWidth(columns, indicators.width)) }), _jsx(Text, { color: "cyan", children: indicators.text })] }));
|
|
78
98
|
}
|
|
79
99
|
/**
|
|
80
100
|
* Sceglie la schermata sostitutiva attiva, o `null` per restare sulla lista.
|
|
81
101
|
*
|
|
82
102
|
* L'ordine dei rami è quello che avevano in `cli.tsx` e non è indifferente: i
|
|
83
|
-
* modi si escludono a vicenda, ma `detail` e `
|
|
84
|
-
* proprio contenuto esista (`sheet.sheet`, `status.view`)
|
|
85
|
-
* senza contenuto deve cadere alla lista, non a una
|
|
103
|
+
* modi si escludono a vicenda, ma `detail`, `status` e `inbox` chiedono anche
|
|
104
|
+
* che il proprio contenuto esista (`sheet.sheet`, `status.view`, `inbox.sheet`)
|
|
105
|
+
* — un modo dichiarato senza contenuto deve cadere alla lista, non a una
|
|
106
|
+
* schermata vuota.
|
|
86
107
|
*/
|
|
87
108
|
export function screenFor(input) {
|
|
88
109
|
const { mode, rows, columns, note, overlays } = input;
|
|
89
|
-
const { assign, sheet, search, status } = overlays;
|
|
110
|
+
const { assign, sheet, search, status, inbox, wrap } = overlays;
|
|
90
111
|
// ── T57 · schermata di assegnazione ─────────────────────────────────────
|
|
91
112
|
// Sostitutiva come ricerca e reader (D3): la lista task non entra in un box
|
|
92
113
|
// sopra i due pane, e prendendo l'intero frame non costa nulla al loro budget.
|
|
@@ -142,6 +163,28 @@ export function screenFor(input) {
|
|
|
142
163
|
const start = Math.min(status.top, status.maxTop);
|
|
143
164
|
return (_jsx(StatusScreen, { name: input.projectCore ?? input.projectName, label: status.label, building: status.building, failed: status.failed, view: status.view, lines: status.lines.slice(start, start + status.capacity), spans: status.doc?.spans ?? [], top: start, total: status.lines.length, capacity: status.capacity, columns: columns }));
|
|
144
165
|
}
|
|
166
|
+
// ── T134 · detail di un file inbox ──────────────────────────────────────
|
|
167
|
+
// Sesta schermata sostitutiva, stessa ragione delle altre cinque: un file di
|
|
168
|
+
// nozioni supera i 28KB, cioè è più lungo di ogni task file del progetto.
|
|
169
|
+
if (mode === 'inbox' && inbox.sheet) {
|
|
170
|
+
if (isCompact(inbox.capacity)) {
|
|
171
|
+
return (_jsx(CompactNotice, { what: inbox.sheet.file.basename, esc: "chiude", rows: rows, columns: columns }));
|
|
172
|
+
}
|
|
173
|
+
// Il clamp serve anche qui: un resize può accorciare il testo sotto uno
|
|
174
|
+
// scroll già dato.
|
|
175
|
+
const start = Math.min(inbox.top, inbox.maxTop);
|
|
176
|
+
return (_jsx(InboxScreen, { file: inbox.sheet.file, missing: inbox.sheet.text === null, lines: inbox.lines.slice(start, start + inbox.capacity), spans: inbox.doc?.spans ?? [], top: start, total: inbox.lines.length, capacity: inbox.capacity, prompt: inbox.prompt, columns: columns }));
|
|
177
|
+
}
|
|
178
|
+
// ── T134 · lista hard-wrap ──────────────────────────────────────────────
|
|
179
|
+
// Settima schermata sostitutiva: sul cappello lo scan trova 77 `WRAP` e 147
|
|
180
|
+
// `misto`, cioè una lista che in un box sopra i pane non entrerebbe.
|
|
181
|
+
if (mode === 'wrap' && wrap.files) {
|
|
182
|
+
if (isCompact(wrap.capacity)) {
|
|
183
|
+
return _jsx(CompactNotice, { what: "hard-wrap", esc: "chiude", rows: rows, columns: columns });
|
|
184
|
+
}
|
|
185
|
+
const start = Math.min(wrap.top, wrap.maxTop);
|
|
186
|
+
return (_jsx(WrapScreen, { files: wrap.files.slice(start, start + wrap.capacity), count: wrap.count, mixed: wrap.mixed, mtime: wrap.mtime, top: start, capacity: wrap.capacity, path: wrap.path, caret: wrap.caret, columns: columns }));
|
|
187
|
+
}
|
|
145
188
|
// ── T52 · ricerca e reader ──────────────────────────────────────────────
|
|
146
189
|
// Gli unici modali che NON stanno in flusso sopra i pane: una lista di
|
|
147
190
|
// occorrenze non entra in un box da 4 righe. Prendono l'intero frame, quindi
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// T134 — la lista hard-wrap: i file con l'a-capo automatico rientrato, più il
|
|
3
|
+
// campo che nomina il perimetro dello srotolamento.
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
import { cut, pad, termWidth } from '../width.js';
|
|
6
|
+
import { FieldText } from './fields.js';
|
|
7
|
+
import { fmtTime } from '../glyphs.js';
|
|
8
|
+
/** Larghezza della colonna verdetto: il dominio è chiuso (`WRAP`, `misto`) e la
|
|
9
|
+
* più lunga è larga 5, quindi misurarla a ogni render calcolerebbe un numero
|
|
10
|
+
* già noto. */
|
|
11
|
+
const VERDICT_W = 5;
|
|
12
|
+
export function WrapScreen({ files, count, mixed, mtime, top, capacity, path, caret, columns, }) {
|
|
13
|
+
const width = Math.max(20, (columns || 80) - 4);
|
|
14
|
+
const total = count + mixed;
|
|
15
|
+
const last = Math.min(total, top + capacity);
|
|
16
|
+
// Cella elastica per sottrazione, come ogni lista del deck: le colonne fisse
|
|
17
|
+
// sono note, quindi il path prende il resto. Pavimento 0 — è un tetto, non
|
|
18
|
+
// una preferenza, e alzarlo sopra lo spazio reale fa uscire la riga dal box
|
|
19
|
+
// mangiandone il bordo (invariante ③ di width.ts).
|
|
20
|
+
//
|
|
21
|
+
// Le due celle numeriche vanno contate INSIEME al loro separatore: `col=` è
|
|
22
|
+
// larga 8, `br=` 6, e fra loro c'è uno spazio. Sommarne solo due su tre è
|
|
23
|
+
// l'off-by-one che tronca la coda della riga.
|
|
24
|
+
const COL_W = 8;
|
|
25
|
+
const BREAKS_W = 6;
|
|
26
|
+
const numsW = COL_W + 1 + BREAKS_W;
|
|
27
|
+
// `- 4` = bordo + padding del box della lista, che sta DENTRO quello esterno.
|
|
28
|
+
const pathW = Math.max(0, width - 4 - VERDICT_W - 1 - numsW);
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: "hard-wrap" }), ' ', _jsxs(Text, { color: "red", children: [count, " WRAP"] }), ' ', _jsxs(Text, { dimColor: true, children: ["\u00B7 ", mixed, " misto"] }), ' ', _jsxs(Text, { dimColor: true, children: ["\u00B7 scan ", mtime === null ? 'mai' : fmtTime(mtime), " \u00B7 righe ", total === 0 ? 0 : top + 1, "-", last, " di ", total] })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193/PgUp/PgDn" }), " scorre \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota il path \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " srotola \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " chiude \u00B7 misto = falso allarme probabile, fuori dal contatore"] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: files.map((f) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: f.verdict === 'WRAP' ? 'red' : undefined, dimColor: f.verdict !== 'WRAP', children: pad(f.verdict, VERDICT_W) }), ' ', _jsx(Text, { dimColor: f.verdict !== 'WRAP', children: cut(f.path, pathW) }), ' '.repeat(Math.max(0, pathW - termWidth(cut(f.path, pathW)))), _jsxs(Text, { dimColor: true, children: [pad(`col=${f.column ?? '?'}`, COL_W, 'right'), ' ', pad(`br=${f.breaks}`, BREAKS_W, 'right')] })] }, f.path))) }), _jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "path \u203A " }), _jsx(FieldText, { value: path, caret: caret, focused: true, cols: Math.max(10, width - 12) })] })] }));
|
|
30
|
+
}
|