@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
package/dist/actions.js
CHANGED
|
@@ -21,7 +21,7 @@ import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, } from '
|
|
|
21
21
|
import { neighborId } from './session-list.js';
|
|
22
22
|
import { cut, cutMiddle } from './width.js';
|
|
23
23
|
import { saveView, viewFilePath } from './view-store.js';
|
|
24
|
-
import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
|
|
24
|
+
import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
|
|
25
25
|
import { useTaskOps } from './task-ops.js';
|
|
26
26
|
export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNote, }) {
|
|
27
27
|
// La riga di stato di OGNI spawn di sessione Claude: il comando esatto, come
|
|
@@ -239,6 +239,37 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
239
239
|
child.on('error', () => setNote('⚠ t → ptyxis non lanciabile'));
|
|
240
240
|
setNote(`t → terminale su ${model.projectName}`);
|
|
241
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* T134 — apre la sessione PRESIDIATA che drena un file inbox.
|
|
244
|
+
*
|
|
245
|
+
* Il prompt arriva già composto dall'overlay (`inboxPrompt`), che è l'unico
|
|
246
|
+
* posto in cui vive la mappa natura → skill: derivarlo qui una seconda volta
|
|
247
|
+
* darebbe due tabelle capaci di divergere, e la seconda si scoprirebbe solo
|
|
248
|
+
* il giorno in cui una natura nuova apre la skill sbagliata.
|
|
249
|
+
*
|
|
250
|
+
* Sessione NUDA e modello esplicito: il drain lavora sulla doc, non sulla
|
|
251
|
+
* task (D10 preflight), e `opus` passa nell'argv anche essendo il default,
|
|
252
|
+
* come `permissionMode`.
|
|
253
|
+
*/
|
|
254
|
+
function drainInbox(file, prompt) {
|
|
255
|
+
const spawned = spawnBare(cwd, prompt, MODEL_DEFAULT);
|
|
256
|
+
spawned.child.on('error', () => setNote(`⚠ drain ${file.basename} fallito (${DECK_RUN})`));
|
|
257
|
+
noteSpawn(spawned);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* T134 — apre la sessione che SROTOLA l'hard-wrap di un path.
|
|
261
|
+
*
|
|
262
|
+
* `sonnet` e non il default (D9): lo srotolamento è una passata meccanica con
|
|
263
|
+
* un verificatore deterministico dietro (`md-wrap --apply` confronta le due
|
|
264
|
+
* versioni normalizzate `\s+ → spazio` e rimette indietro il file se
|
|
265
|
+
* differiscono), quindi il giudizio richiesto al modello è leggere un diff e
|
|
266
|
+
* decidere se committarlo — non progettare niente.
|
|
267
|
+
*/
|
|
268
|
+
function unwrapPath(path, prompt) {
|
|
269
|
+
const spawned = spawnBare(cwd, prompt, 'sonnet');
|
|
270
|
+
spawned.child.on('error', () => setNote(`⚠ srotolamento ${path} fallito (${DECK_RUN})`));
|
|
271
|
+
noteSpawn(spawned);
|
|
272
|
+
}
|
|
242
273
|
/** `c` — sessione claude a mani nude, senza task e senza prompt. */
|
|
243
274
|
function openClaude() {
|
|
244
275
|
const spawned = spawnClaudeEmpty(cwd);
|
|
@@ -283,6 +314,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
283
314
|
assignSession,
|
|
284
315
|
forkSession,
|
|
285
316
|
togglePin,
|
|
317
|
+
drainInbox,
|
|
318
|
+
unwrapPath,
|
|
286
319
|
openTerminal,
|
|
287
320
|
openClaude,
|
|
288
321
|
saveCurrentView,
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import { resolveTasksPath, resolveTasksDir } from './tasks.js';
|
|
|
19
19
|
import { LAUNCH_SEP } from './config.js';
|
|
20
20
|
import { anchorFrame, enableMouse } from './mouse.js';
|
|
21
21
|
import { sanitize } from './width.js';
|
|
22
|
-
import { deckLegend, frameGeometry, headlineWidth, launchRow } from './frame.js';
|
|
22
|
+
import { deckLegend, frameGeometry, headlineWidth, indicatorRow, launchRow } from './frame.js';
|
|
23
23
|
import { useDeckModel } from './deck-model.js';
|
|
24
24
|
import { useDeckActions } from './actions.js';
|
|
25
25
|
import { useDeckInput } from './input.js';
|
|
@@ -27,12 +27,14 @@ import { useSearchOverlay } from './overlays/search.js';
|
|
|
27
27
|
import { useSheetOverlay } from './overlays/sheet.js';
|
|
28
28
|
import { useAssignOverlay } from './overlays/assign.js';
|
|
29
29
|
import { useProjectStatus } from './overlays/status.js';
|
|
30
|
+
import { useInboxOverlay } from './overlays/inbox.js';
|
|
31
|
+
import { useWrapOverlay } from './overlays/wrap.js';
|
|
30
32
|
import { usePurgeOverlay } from './overlays/purge.js';
|
|
31
33
|
import { useTextModals, useViewModals } from './overlays/modals.js';
|
|
32
34
|
import { useTerminalSize } from './hooks.js';
|
|
33
35
|
import { EditModal, FilterModal, PurgeModal, SortModal } from './ui/modals.js';
|
|
34
36
|
import { StatusHeadline } from './ui/status-screen.js';
|
|
35
|
-
import { SessionsPane, TasksPane } from './ui/panes.js';
|
|
37
|
+
import { InboxPane, SessionsPane, TasksPane } from './ui/panes.js';
|
|
36
38
|
import { PreviewPane, detailMetaOf } from './ui/preview.js';
|
|
37
39
|
import { CompactNotice, HintBar, TextBox, screenFor } from './ui/screens.js';
|
|
38
40
|
import { VERSION } from './version.js';
|
|
@@ -84,6 +86,25 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
84
86
|
setMode,
|
|
85
87
|
setNote,
|
|
86
88
|
});
|
|
89
|
+
// T134 — il detail di un file inbox. Come lo sheet della task riceve lo spawn
|
|
90
|
+
// come callback: un hook di overlay non esce dal deck.
|
|
91
|
+
const inbox = useInboxOverlay({
|
|
92
|
+
rows,
|
|
93
|
+
columns,
|
|
94
|
+
setMode,
|
|
95
|
+
setNote,
|
|
96
|
+
onDrain: actions.drainInbox,
|
|
97
|
+
});
|
|
98
|
+
// T134 — l'hard-wrap. Come il project status non è solo un overlay: i suoi
|
|
99
|
+
// numeri si leggono in testata a schermata chiusa, e la lista è una delle sue
|
|
100
|
+
// superfici invece che il suo contenuto.
|
|
101
|
+
const wrap = useWrapOverlay({
|
|
102
|
+
cwd,
|
|
103
|
+
rows,
|
|
104
|
+
setMode,
|
|
105
|
+
setNote,
|
|
106
|
+
onApply: actions.unwrapPath,
|
|
107
|
+
});
|
|
87
108
|
const purge = usePurgeOverlay({
|
|
88
109
|
setMode,
|
|
89
110
|
setNote,
|
|
@@ -106,7 +127,17 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
106
127
|
editDraftFor: actions.editDraftFor,
|
|
107
128
|
currentNote: (sid) => model.sessionNotes.get(sid) ?? '',
|
|
108
129
|
});
|
|
109
|
-
const overlays = {
|
|
130
|
+
const overlays = {
|
|
131
|
+
assign,
|
|
132
|
+
sheet,
|
|
133
|
+
search,
|
|
134
|
+
status,
|
|
135
|
+
inbox,
|
|
136
|
+
wrap,
|
|
137
|
+
purge,
|
|
138
|
+
view: viewModals,
|
|
139
|
+
text: textModals,
|
|
140
|
+
};
|
|
110
141
|
// T70 — un solo blocco preview, sotto i due pane, e il FOCUS decide cosa
|
|
111
142
|
// contiene: a sinistra la task selezionata, a destra la conversazione. È il
|
|
112
143
|
// focus e non la selezione perché il blocco è uno — averne due significava
|
|
@@ -118,11 +149,27 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
118
149
|
? model.detail
|
|
119
150
|
? 'task'
|
|
120
151
|
: 'none'
|
|
121
|
-
: model.
|
|
122
|
-
?
|
|
123
|
-
|
|
152
|
+
: model.focus === 'inbox'
|
|
153
|
+
? model.selInbox
|
|
154
|
+
? 'inbox'
|
|
155
|
+
: 'none'
|
|
156
|
+
: model.selSessionObj
|
|
157
|
+
? 'session'
|
|
158
|
+
: 'none';
|
|
124
159
|
const detailParts = model.detail ? detailMetaOf(model.detail) : null;
|
|
125
160
|
const launch = launchRow(model.launch, columns);
|
|
161
|
+
// T134 — gli indicatori ancorati a destra della riga legenda. Si compongono
|
|
162
|
+
// PRIMA della legenda perché è la loro larghezza a decidere quanto ne resta:
|
|
163
|
+
// hanno la precedenza sul budget (D5 preflight).
|
|
164
|
+
const indicators = indicatorRow({
|
|
165
|
+
inbox: {
|
|
166
|
+
counts: model.inboxCounts,
|
|
167
|
+
stale: model.inboxStale,
|
|
168
|
+
scanned: model.inboxScanned,
|
|
169
|
+
ok: model.inboxOk,
|
|
170
|
+
},
|
|
171
|
+
wrap: { count: wrap.count, mtime: wrap.mtime, ok: wrap.ok, scanning: wrap.scanning },
|
|
172
|
+
}, columns);
|
|
126
173
|
const frame = frameGeometry({
|
|
127
174
|
rows,
|
|
128
175
|
columns,
|
|
@@ -142,8 +189,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
142
189
|
sessionViewId: model.sessionViewId,
|
|
143
190
|
parentLabel: model.parentLabel,
|
|
144
191
|
hasLoadError: Boolean(model.loadError),
|
|
192
|
+
rightPane: model.rightPane,
|
|
193
|
+
inboxFiles: model.inboxFiles,
|
|
194
|
+
selInboxPath: model.selInboxPath,
|
|
195
|
+
inboxCounts: model.inboxCounts,
|
|
196
|
+
inboxViewId: model.inboxViewId,
|
|
145
197
|
});
|
|
146
198
|
useDeckInput({
|
|
199
|
+
cwd,
|
|
147
200
|
tasksDir,
|
|
148
201
|
mode,
|
|
149
202
|
setMode,
|
|
@@ -153,8 +206,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
153
206
|
overlays,
|
|
154
207
|
frame,
|
|
155
208
|
launchRegions: launch.regions,
|
|
209
|
+
indicatorRegions: indicators.regions,
|
|
156
210
|
});
|
|
157
|
-
// Le
|
|
211
|
+
// Le sei schermate sostitutive prendono il frame intero: se una è attiva
|
|
158
212
|
// il render finisce qui, e il budget dei pane resta calcolato ma inutilizzato.
|
|
159
213
|
const screen = screenFor({
|
|
160
214
|
mode,
|
|
@@ -169,7 +223,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
169
223
|
projectName: model.projectName,
|
|
170
224
|
taskRowData: model.taskRowData,
|
|
171
225
|
hiddenTasks: model.hiddenTasks,
|
|
172
|
-
overlays: { assign, sheet, search, status },
|
|
226
|
+
overlays: { assign, sheet, search, status, inbox, wrap },
|
|
173
227
|
});
|
|
174
228
|
if (screen)
|
|
175
229
|
return screen;
|
|
@@ -185,7 +239,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
185
239
|
hasSession: model.selSessionObj !== null,
|
|
186
240
|
hasSessionId: model.selSessionId !== null,
|
|
187
241
|
purgeBulk: model.purgeBulk,
|
|
188
|
-
|
|
242
|
+
inboxPane: model.rightPane === 'inbox',
|
|
243
|
+
hasInbox: model.selInbox !== null,
|
|
244
|
+
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData }), model.rightPane === 'inbox' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
189
245
|
}
|
|
190
246
|
const cwd = process.cwd();
|
|
191
247
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
package/dist/deck-model.js
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
// la ragione per cui i loro conti di parametri restano leggibili.
|
|
15
15
|
import { useEffect, useMemo, useState } from 'react';
|
|
16
16
|
import { loadArchivableDays, loadIdentity, loadLaunch } from './config.js';
|
|
17
|
-
import { useArchivable, useDirtyFolders, useSessions, useTaskDetail, useTasks, } from './hooks.js';
|
|
17
|
+
import { useArchivable, useDirtyFolders, useInboxScan, useSessions, useTaskDetail, useTasks, } from './hooks.js';
|
|
18
|
+
import { DEFAULT_INBOX_STALE_HOURS, staleCount } from './inbox.js';
|
|
19
|
+
import { cycleInboxView, inboxCounts as deriveInboxCounts, inboxView, selectInboxRows, } from './inbox-views.js';
|
|
20
|
+
import { docsRootName } from './tasks.js';
|
|
18
21
|
import { isDone } from './layout.js';
|
|
19
22
|
import { TASK_EMPTY, relTime } from './glyphs.js';
|
|
20
23
|
import { assembleSessionList, firstSelectableId, rowIndexOf, selectedSession, } from './session-list.js';
|
|
@@ -133,6 +136,15 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
133
136
|
// riaperta a freddo si legge come la lista intera.
|
|
134
137
|
const [taskViewId, setTaskViewId] = useState('tasks');
|
|
135
138
|
const [sessionViewId, setSessionViewId] = useState('context');
|
|
139
|
+
// T134 — quale pane occupa lo slot destro, e la vista attiva di quello inbox.
|
|
140
|
+
// Volatili entrambi (D6 preflight): il deck riapre sempre sulle sessioni.
|
|
141
|
+
const [rightPane, setRightPane] = useState('sessions');
|
|
142
|
+
const [inboxViewId, setInboxViewId] = useState('all');
|
|
143
|
+
// Selezione KEYED SUL PATH, mai su indice: la lista si riordina sotto le
|
|
144
|
+
// viste e si accorcia a ogni scan, e un indice grezzo punterebbe alla riga
|
|
145
|
+
// sbagliata in silenzio (stessa trappola di T39 sulle task e T50 sulle
|
|
146
|
+
// conversazioni).
|
|
147
|
+
const [selInboxPath, setSelInboxPath] = useState(null);
|
|
136
148
|
// Voci launch del progetto (T32): lette una volta, raggiunte per indice 1..9.
|
|
137
149
|
const launch = useMemo(() => loadLaunch(cwd), [cwd]);
|
|
138
150
|
// Identità (T37): titolo delle tab terminale spawnate col tasto `t`.
|
|
@@ -162,6 +174,19 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
162
174
|
// una task sola e il dato lo ricalcola al momento.
|
|
163
175
|
const archivableSig = useMemo(() => [...archivable].sort().join(','), [archivable]);
|
|
164
176
|
const dirtyFolders = useDirtyFolders(archivableSig, tasksDir, cwd);
|
|
177
|
+
// T134 — terzo scan della famiglia (D1): coda inbox per natura, età del più
|
|
178
|
+
// vecchio. La docs-root arriva dalla sola env, come `resolveTasksPath` — il
|
|
179
|
+
// limite si eredita invece di aprire qui una cascata che nessun percorso di
|
|
180
|
+
// avvio reale userebbe (D1 preflight).
|
|
181
|
+
const docsRoot = useMemo(() => docsRootName(), []);
|
|
182
|
+
const inbox = useInboxScan(cwd, docsRoot);
|
|
183
|
+
const inboxCounts = useMemo(() => deriveInboxCounts(inbox.files), [inbox.files]);
|
|
184
|
+
// Il numero accanto alla sirena: solo i file in coda oltre soglia. `Date.now()`
|
|
185
|
+
// letto a ogni render e non memoizzato — è una sottrazione, e congelarlo
|
|
186
|
+
// lascerebbe la sirena ferma sull'ora dello scan invece che sull'ora corrente.
|
|
187
|
+
const inboxStale = staleCount(inbox.files, DEFAULT_INBOX_STALE_HOURS, Date.now());
|
|
188
|
+
const inboxFiles = useMemo(() => selectInboxRows(inboxViewId, inbox.files), [inboxViewId, inbox.files]);
|
|
189
|
+
const selInbox = inboxFiles.find((f) => f.path === selInboxPath) ?? null;
|
|
165
190
|
// T100 — le task effettivamente a schermo: la vista principale coincide con
|
|
166
191
|
// `viewTasks` (nessun ricalcolo sul cammino di default), le altre due passano
|
|
167
192
|
// dal predicato del catalogo. I CONTATORI restano misurati sulla vista di
|
|
@@ -245,6 +270,17 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
245
270
|
setSelSessionId(firstSelectableId(sessionRows));
|
|
246
271
|
}
|
|
247
272
|
}, [sessionRows, selSessionId]);
|
|
273
|
+
// T134 — gemello dei due sopra: un file drenato sparisce dalla coda al primo
|
|
274
|
+
// scan successivo, e la selezione cade sulla prima riga della lista invece
|
|
275
|
+
// che su una posizione a caso.
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (selInboxPath !== null && !inboxFiles.some((f) => f.path === selInboxPath)) {
|
|
278
|
+
setSelInboxPath(inboxFiles[0]?.path ?? null);
|
|
279
|
+
}
|
|
280
|
+
else if (selInboxPath === null && inboxFiles.length > 0) {
|
|
281
|
+
setSelInboxPath(inboxFiles[0].path);
|
|
282
|
+
}
|
|
283
|
+
}, [inboxFiles, selInboxPath]);
|
|
248
284
|
/** Selezione per INDICE nella vista, riconvertita subito in sentinella o id.
|
|
249
285
|
* T21 — la chiama anche il click su una riga del pane task. */
|
|
250
286
|
function selectTaskRow(index) {
|
|
@@ -285,12 +321,58 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
285
321
|
// parent delle sessioni che torna a `tutte` è un effetto accettato); sul pane
|
|
286
322
|
// sessioni basta invalidare l'id, e l'effect di validità atterra sulla prima
|
|
287
323
|
// riga selezionabile della vista nuova.
|
|
324
|
+
function selectInboxView(next) {
|
|
325
|
+
if (next === inboxViewId)
|
|
326
|
+
return;
|
|
327
|
+
setInboxViewId(next);
|
|
328
|
+
setSelInboxPath(null);
|
|
329
|
+
setNote(`vista inbox: ${inboxView(next).label(inboxCounts)}`);
|
|
330
|
+
}
|
|
288
331
|
function cycleView(delta) {
|
|
289
332
|
if (focus === 'tasks')
|
|
290
333
|
selectTaskView(cycleTaskView(taskViewId, delta));
|
|
334
|
+
else if (focus === 'inbox')
|
|
335
|
+
selectInboxView(cycleInboxView(inboxViewId, delta));
|
|
291
336
|
else
|
|
292
337
|
selectSessionView(cycleSessionView(sessionViewId, delta));
|
|
293
338
|
}
|
|
339
|
+
/** Selezione per INDICE nella finestra visibile: la chiama il click. */
|
|
340
|
+
function selectInboxRow(index) {
|
|
341
|
+
const f = inboxFiles[index];
|
|
342
|
+
if (f)
|
|
343
|
+
setSelInboxPath(f.path);
|
|
344
|
+
}
|
|
345
|
+
function moveInboxSel(delta) {
|
|
346
|
+
const at = inboxFiles.findIndex((f) => f.path === selInboxPath);
|
|
347
|
+
if (inboxFiles.length === 0)
|
|
348
|
+
return;
|
|
349
|
+
const next = Math.max(0, Math.min(inboxFiles.length - 1, (at < 0 ? 0 : at) + delta));
|
|
350
|
+
setSelInboxPath(inboxFiles[next].path);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* T134 — `^B` scambia i due pane dello slot destro (D8 preflight).
|
|
354
|
+
*
|
|
355
|
+
* Il FOCUS segue il pane: chi stava guardando a destra continua a guardare a
|
|
356
|
+
* destra, ma quello che c'è adesso è l'altro pane. Lasciarlo su `sessions`
|
|
357
|
+
* con l'inbox montato darebbe un focus su un pane che non è a schermo, e le
|
|
358
|
+
* azioni della lista sessioni resterebbero attive su una selezione invisibile.
|
|
359
|
+
*/
|
|
360
|
+
function toggleInboxPane() {
|
|
361
|
+
const next = rightPane === 'inbox' ? 'sessions' : 'inbox';
|
|
362
|
+
setRightPane(next);
|
|
363
|
+
setFocus((f) => (f === 'tasks' ? f : next));
|
|
364
|
+
setNote(next === 'inbox' ? '^B → pane inbox' : '^B → pane sessioni');
|
|
365
|
+
}
|
|
366
|
+
/** `esc` in vista normale: torna alle sessioni. Solo in quel verso — `esc`
|
|
367
|
+
* dice «esci da dove sei», e sulle sessioni non c'è più niente da cui uscire
|
|
368
|
+
* (il tasto resta inerte, come è sempre stato). */
|
|
369
|
+
function closeInboxPane() {
|
|
370
|
+
if (rightPane !== 'inbox')
|
|
371
|
+
return false;
|
|
372
|
+
setRightPane('sessions');
|
|
373
|
+
setFocus((f) => (f === 'inbox' ? 'sessions' : f));
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
294
376
|
return {
|
|
295
377
|
// fonti
|
|
296
378
|
tasks,
|
|
@@ -338,11 +420,26 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
338
420
|
sessionCounts,
|
|
339
421
|
selSessionObj,
|
|
340
422
|
sessionCols,
|
|
423
|
+
// derivazioni del pane inbox
|
|
424
|
+
rightPane,
|
|
425
|
+
inboxScanned: inbox.scanned,
|
|
426
|
+
inboxOk: inbox.ok,
|
|
427
|
+
inboxViewId,
|
|
428
|
+
inboxCounts,
|
|
429
|
+
inboxStale,
|
|
430
|
+
inboxFiles,
|
|
431
|
+
selInboxPath,
|
|
432
|
+
selInbox,
|
|
341
433
|
// mutatori di navigazione
|
|
342
434
|
selectTaskRow,
|
|
343
435
|
moveTaskSel,
|
|
344
436
|
selectTaskView,
|
|
345
437
|
selectSessionView,
|
|
438
|
+
selectInboxView,
|
|
439
|
+
selectInboxRow,
|
|
440
|
+
moveInboxSel,
|
|
441
|
+
toggleInboxPane,
|
|
442
|
+
closeInboxPane,
|
|
346
443
|
cycleView,
|
|
347
444
|
};
|
|
348
445
|
}
|
package/dist/frame.js
CHANGED
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
// leggeva dichiarata seicento righe più in basso. Il costo è calcolare il
|
|
16
16
|
// budget anche quando il render esce presto su una schermata sostitutiva:
|
|
17
17
|
// aritmetica su array già in memoria, e `layoutBudget` accetta ogni `Mode`.
|
|
18
|
-
import { headerItems, sessionHeaderParts, taskHeaderParts } from './pane-header.js';
|
|
19
|
-
import { FRAME_TEXT_COL, inlineRegions, LAUNCH_ROW, PANE_TEXT_PAD, paneSpans, rowRegions, } from './mouse.js';
|
|
18
|
+
import { headerItems, inboxHeaderParts, sessionHeaderParts, taskHeaderParts } from './pane-header.js';
|
|
19
|
+
import { FRAME_TEXT_COL, HINT_ROW, inlineRegions, LAUNCH_ROW, PANE_TEXT_PAD, paneSpans, rowRegions, } from './mouse.js';
|
|
20
20
|
import { cellWidth, launchLegend, LAUNCH_SEP } from './config.js';
|
|
21
21
|
import { layoutBudget, windowRange } from './viewport.js';
|
|
22
22
|
import { sanitize, termWidth } from './width.js';
|
|
23
|
+
import { WARN } from './glyphs.js';
|
|
24
|
+
import { STATUS_MISSING } from './project-status.js';
|
|
23
25
|
import { META_ROWS } from './model.js';
|
|
24
26
|
import { rowIndexOf } from './session-list.js';
|
|
25
27
|
import { VERSION } from './version.js';
|
|
@@ -53,11 +55,18 @@ export const SURFACE_SEGMENTS = [
|
|
|
53
55
|
export function deckLegend(state) {
|
|
54
56
|
const canSpawn = state.focus === 'tasks' && state.hasTask;
|
|
55
57
|
const canResume = state.focus === 'sessions' && state.hasSession;
|
|
58
|
+
const canOpenInbox = state.focus === 'inbox' && state.hasInbox;
|
|
56
59
|
// T50 — il pin agisce su qualunque riga selezionata (anche stale, per
|
|
57
60
|
// spinnarla); basta il focus sul pane e una selezione.
|
|
58
61
|
const canPin = state.focus === 'sessions' && state.hasSessionId;
|
|
59
62
|
return sanitize([
|
|
60
|
-
...(canSpawn
|
|
63
|
+
...(canSpawn
|
|
64
|
+
? ['⏎ detail', '^K/^P/^R spawn']
|
|
65
|
+
: canResume
|
|
66
|
+
? ['⏎ resume']
|
|
67
|
+
: canOpenInbox
|
|
68
|
+
? ['⏎ apri']
|
|
69
|
+
: []),
|
|
61
70
|
// T112 — la voce nomina il BERSAGLIO, che cambia di taglia senza che
|
|
62
71
|
// cambi il tasto. Legge `purgeBulk`, la stessa condizione del ramo di
|
|
63
72
|
// apertura: una legenda che annunciasse «tutte» dove il tasto ne pota una
|
|
@@ -74,6 +83,11 @@ export function deckLegend(state) {
|
|
|
74
83
|
// cosa e sta a due voci di distanza.
|
|
75
84
|
'^G genera status',
|
|
76
85
|
'^O apri status',
|
|
86
|
+
// T134 — la voce nomina il pane che il tasto MONTA, non quello montato:
|
|
87
|
+
// `^B` scambia i due, e annunciare quello che si sta già guardando
|
|
88
|
+
// direbbe il contrario di ciò che il tasto fa. Stessa regola di `CANC
|
|
89
|
+
// elimina tutte`, che nomina il bersaglio e non il tasto.
|
|
90
|
+
state.inboxPane ? '^B sessioni' : '^B inbox',
|
|
77
91
|
'^F cerca',
|
|
78
92
|
'C nuova',
|
|
79
93
|
'E edit',
|
|
@@ -105,6 +119,87 @@ export function launchRow(launch, columns) {
|
|
|
105
119
|
unreachable: legend.unreachable,
|
|
106
120
|
};
|
|
107
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* T134 — gli INDICATORI ancorati a destra della riga legenda: dato, non
|
|
124
|
+
* stringa, per la stessa ragione della riga launch (`launchRow`). Gli stessi
|
|
125
|
+
* segmenti compongono il testo disegnato e le colonne dell'hit-test, e chi
|
|
126
|
+
* derivasse le seconde ri-splittando il primo terrebbe due conti che divergono
|
|
127
|
+
* alla prima etichetta che contiene il separatore.
|
|
128
|
+
*
|
|
129
|
+
* `key` è la COMBO che l'indicatore rappresenta, col prefisso `^`: il click non
|
|
130
|
+
* chiama l'azione, rientra dalla porta della tastiera premendola. È lo stesso
|
|
131
|
+
* schema delle superfici launch, esteso ai `ctrl` — senza il prefisso il click
|
|
132
|
+
* sintetizzerebbe una lettera nuda, che su questa tastiera fa un'altra cosa.
|
|
133
|
+
*/
|
|
134
|
+
export const INDICATOR_SEP = ' ';
|
|
135
|
+
/**
|
|
136
|
+
* Il bottone che monta il pane inbox: `[ Inbox 📄 3/1/2 🚨2 ]`.
|
|
137
|
+
*
|
|
138
|
+
* SEMPRE ACCESO, mai grigio (D7): è un pane, non un sottoinsieme che può essere
|
|
139
|
+
* vuoto. I tre numeri sono le tre nature nell'ordine del catalogo; la sirena e
|
|
140
|
+
* il suo contatore compaiono solo sopra la soglia.
|
|
141
|
+
*
|
|
142
|
+
* Prima del primo scan i numeri non esistono ancora, e stampare `0/0/0`
|
|
143
|
+
* direbbe «ho misurato e non c'è niente» — cioè la cosa sbagliata (D2
|
|
144
|
+
* preflight: `missing` e guasto sono stati distinti). Il glifo di allerta si
|
|
145
|
+
* AGGIUNGE ai numeri invece di sostituirli: un tentativo fallito lascia in
|
|
146
|
+
* piedi l'esito dell'ultimo riuscito, che resta vero e ancora apribile.
|
|
147
|
+
*/
|
|
148
|
+
export function inboxButton(state) {
|
|
149
|
+
const numbers = state.scanned
|
|
150
|
+
? `${state.counts.nozioni}/${state.counts.derivazione}/${state.counts.sweep}`
|
|
151
|
+
: STATUS_MISSING;
|
|
152
|
+
const siren = state.stale > 0 ? ` 🚨${state.stale}` : '';
|
|
153
|
+
const warn = state.ok ? '' : ` ${WARN}`;
|
|
154
|
+
return sanitize(`[ Inbox 📄 ${numbers}${siren}${warn} ]`);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* L'indicatore hard-wrap: `wrap 77`.
|
|
158
|
+
*
|
|
159
|
+
* Etichetta a PAROLA e non un glifo, dopo una misura sotto pty: `〰` (U+3030)
|
|
160
|
+
* passa il controllo di concordanza di `width.ts` — `string-width` e la nostra
|
|
161
|
+
* tabella EAW dicono entrambe 2 — ma dentro il frame Ink emette una colonna in
|
|
162
|
+
* meno di quante ne conta, e la riga esce larga `columns - 1` col bordo
|
|
163
|
+
* mangiato. È un terzo caso oltre alle due invarianti note: `agrees()` confronta
|
|
164
|
+
* due LIBRERIE, e un glifo può ingannarle entrambe insieme. Chi volesse
|
|
165
|
+
* rimettere un simbolo qui lo misuri sul frame vero, non su `agrees`.
|
|
166
|
+
*
|
|
167
|
+
* Conta i soli `WRAP` (D4). `misto` compare in lista con un marker proprio ma
|
|
168
|
+
* resta fuori: è la classe che ospita il falso allarme noto — i file di note
|
|
169
|
+
* scritte una riga per pensiero, che nessuno vuole srotolare — e sommarlo
|
|
170
|
+
* darebbe un numero che non si può portare a zero. Un contatore che non arriva
|
|
171
|
+
* mai a zero smette di essere letto.
|
|
172
|
+
*
|
|
173
|
+
* Tre stati distinti, come per il project status: `missing` quando lo scan non
|
|
174
|
+
* è mai stato eseguito (non parte all'avvio, D4), `scan…` mentre cammina
|
|
175
|
+
* l'albero, il numero altrimenti. Il glifo di allerta si AGGIUNGE al numero
|
|
176
|
+
* invece di sostituirlo — un tentativo fallito non riscrive la cache, quindi
|
|
177
|
+
* l'esito dell'ultimo riuscito resta vero e ancora apribile.
|
|
178
|
+
*/
|
|
179
|
+
export function wrapIndicator(state) {
|
|
180
|
+
const body = state.scanning
|
|
181
|
+
? 'scan…'
|
|
182
|
+
: state.mtime === null
|
|
183
|
+
? STATUS_MISSING
|
|
184
|
+
: String(state.count);
|
|
185
|
+
return sanitize(`wrap ${body}${state.ok ? '' : ` ${WARN}`}`);
|
|
186
|
+
}
|
|
187
|
+
export function indicatorRow(state, columns) {
|
|
188
|
+
// Ordine di lettura: l'indicatore, poi il bottone. Il bottone sta all'estremo
|
|
189
|
+
// destro perché è l'unico dei due che MONTA qualcosa restando in vista
|
|
190
|
+
// normale — la lista del wrap è una schermata che si apre e si chiude.
|
|
191
|
+
const segments = [
|
|
192
|
+
{ key: '^w', text: wrapIndicator(state.wrap) },
|
|
193
|
+
{ key: '^b', text: inboxButton(state.inbox) },
|
|
194
|
+
];
|
|
195
|
+
const text = segments.map((s) => s.text).join(INDICATOR_SEP);
|
|
196
|
+
const width = termWidth(text);
|
|
197
|
+
// Ancorato al bordo destro del box esterno: il blocco è renderizzato da un
|
|
198
|
+
// `justifyContent="space-between"`, quindi la sua prima colonna si ricava per
|
|
199
|
+
// sottrazione e non da un accumulo da sinistra.
|
|
200
|
+
const start = FRAME_TEXT_COL + Math.max(0, (columns || 80) - 4) - width;
|
|
201
|
+
return { segments, text, regions: rowRegions(segments, INDICATOR_SEP, start), width };
|
|
202
|
+
}
|
|
108
203
|
/**
|
|
109
204
|
* Il budget d'altezza e le finestre di rendering.
|
|
110
205
|
*
|
|
@@ -139,6 +234,11 @@ export function frameGeometry(input) {
|
|
|
139
234
|
const selRowIndex = rowIndexOf(input.sessionRows, input.selSessionId);
|
|
140
235
|
const sessionWin = windowRange(input.sessionRows.length, selRowIndex, budget.sessionRows);
|
|
141
236
|
const windowRows = input.sessionRows.slice(sessionWin.start, sessionWin.end);
|
|
237
|
+
// Stessa capienza del pane sessioni: i due hanno la stessa cornice e uno solo
|
|
238
|
+
// dei due è montato (vedi SESSIONS_PANE_CHROME in viewport.ts).
|
|
239
|
+
const selInboxIndex = input.inboxFiles.findIndex((f) => f.path === input.selInboxPath);
|
|
240
|
+
const inboxWin = windowRange(input.inboxFiles.length, selInboxIndex, budget.sessionRows);
|
|
241
|
+
const windowInbox = input.inboxFiles.slice(inboxWin.start, inboxWin.end);
|
|
142
242
|
// T21 — geometria delle liste per l'hit-test del click, dalla STESSA
|
|
143
243
|
// aritmetica che disegna i pane: le parti degli header escono dal modulo che
|
|
144
244
|
// `ui/panes.tsx` consuma per renderle, le righe cliccabili sono le finestre
|
|
@@ -150,10 +250,13 @@ export function frameGeometry(input) {
|
|
|
150
250
|
columns: input.columns,
|
|
151
251
|
taskHeader: inlineRegions(headerItems(taskHeaderParts(input.taskCounts, input.taskViewId, taskWin.start, input.paneTasks.length - taskWin.end, input.columns)), spans.tasks.start + PANE_TEXT_PAD),
|
|
152
252
|
sessionHeader: inlineRegions(headerItems(sessionHeaderParts(input.parentLabel, input.sessionCounts, input.sessionViewId, sessionWin.start, input.sessionRows.length - sessionWin.end, input.columns)), spans.sessions.start + PANE_TEXT_PAD),
|
|
253
|
+
inboxHeader: inlineRegions(headerItems(inboxHeaderParts(input.inboxCounts, input.inboxViewId, inboxWin.start, input.inboxFiles.length - inboxWin.end, input.columns)), spans.sessions.start + PANE_TEXT_PAD),
|
|
153
254
|
// Con un errore di caricamento al posto delle task c'è la riga rossa:
|
|
154
255
|
// restano cliccabili le sole righe meta.
|
|
155
256
|
taskRows: META_ROWS + (input.hasLoadError ? 0 : windowTasks.length),
|
|
156
257
|
sessionRows: windowRows.length,
|
|
258
|
+
inboxRows: windowInbox.length,
|
|
259
|
+
rightPane: input.rightPane,
|
|
157
260
|
};
|
|
158
261
|
return {
|
|
159
262
|
budget,
|
|
@@ -161,6 +264,8 @@ export function frameGeometry(input) {
|
|
|
161
264
|
windowTasks,
|
|
162
265
|
sessionWin,
|
|
163
266
|
windowRows,
|
|
267
|
+
inboxWin,
|
|
268
|
+
windowInbox,
|
|
164
269
|
listGeometry,
|
|
165
270
|
// Dimensione del terminale in CELLE (colonne×righe, mai pixel — un processo
|
|
166
271
|
// dentro un terminale vede solo la griglia di caratteri) e versione. La
|
|
@@ -175,4 +280,17 @@ export function frameGeometry(input) {
|
|
|
175
280
|
export function headlineWidth(columns, headerRight) {
|
|
176
281
|
return Math.max(4, columns - 4 - termWidth(headerRight) - 1);
|
|
177
282
|
}
|
|
178
|
-
|
|
283
|
+
/**
|
|
284
|
+
* T134 — il budget della legenda tasti, per sottrazione del blocco indicatori
|
|
285
|
+
* che le sta a destra sulla stessa riga.
|
|
286
|
+
*
|
|
287
|
+
* La legenda cede per prima (D5 preflight): è già troncabile per costruzione e
|
|
288
|
+
* ciò che perde si ricorda, mentre un contatore troncato mente. Pavimento a 4
|
|
289
|
+
* come la testata — sotto quella soglia non resta comunque niente di leggibile,
|
|
290
|
+
* e un budget negativo farebbe tornare `cut` una stringa vuota per il tramite
|
|
291
|
+
* di un numero che nessuno ha scelto.
|
|
292
|
+
*/
|
|
293
|
+
export function legendWidth(columns, indicatorW) {
|
|
294
|
+
return Math.max(4, (columns || 80) - 4 - indicatorW - termWidth(INDICATOR_SEP));
|
|
295
|
+
}
|
|
296
|
+
export { HINT_ROW, LAUNCH_ROW };
|
package/dist/glyphs.js
CHANGED
|
@@ -71,6 +71,30 @@ export function modelShort(id) {
|
|
|
71
71
|
export const LIVE_IDLE = '●';
|
|
72
72
|
export const LIVE_BUSY = '◍';
|
|
73
73
|
export const LIVE_NONE = ' ';
|
|
74
|
+
/**
|
|
75
|
+
* T134 — colonna di stato della riga inbox, larga 2 come la cella marker della
|
|
76
|
+
* lista sessioni.
|
|
77
|
+
*
|
|
78
|
+
* `held` non ha glifo, e non è una dimenticanza: un file senza il token
|
|
79
|
+
* `drainable` resta eseguibile se qualcuno lo nomina, quindi ogni simbolo di
|
|
80
|
+
* divieto direbbe una cosa più forte del vero. L'assenza dice «non è in coda»
|
|
81
|
+
* senza promettere altro, e la colonna natura accanto continua a nominarlo.
|
|
82
|
+
*
|
|
83
|
+
* Nessun glifo è riusato dalla lista sessioni con un significato spostato: là
|
|
84
|
+
* `●`/`◍` dicono la liveness di un processo e `📌`/`🔗`/`○` l'appartenenza,
|
|
85
|
+
* qui si parla di una coda di lavoro. Un falso amico costa più di un simbolo
|
|
86
|
+
* nuovo.
|
|
87
|
+
*/
|
|
88
|
+
export const INBOX_MARK = {
|
|
89
|
+
broken: WARN,
|
|
90
|
+
branched: sanitize('🔒'),
|
|
91
|
+
held: '',
|
|
92
|
+
queued: sanitize('⏳'),
|
|
93
|
+
};
|
|
94
|
+
/** Larghezza della cella marker della riga inbox, gemella dei 2 della lista
|
|
95
|
+
* sessioni: la cella si riempie di spazi anche quando è vuota, o le righe
|
|
96
|
+
* `held` sposterebbero a sinistra tutto ciò che segue. */
|
|
97
|
+
export const INBOX_MARK_W = 2;
|
|
74
98
|
// Marker Done per il DISPLAY. `task.prog` resta il `✔️` letto da tasks.md —
|
|
75
99
|
// `isDone()` e le lookup di `view.ts` ci confrontano sopra, e `task-edit` lo
|
|
76
100
|
// riscrive sul file: è una chiave semantica, non testo. Qui `sanitize` lo
|