@lamemind/loom-deck 0.70.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.js +55 -0
- package/dist/cli.js +39 -13
- package/dist/deck-model.js +113 -31
- package/dist/doc-tree.js +468 -0
- package/dist/doc-views.js +117 -0
- package/dist/frame.js +51 -23
- package/dist/hooks.js +45 -0
- package/dist/inbox.js +10 -1
- package/dist/input-modes.js +38 -1
- package/dist/input.js +90 -21
- package/dist/model.js +11 -0
- package/dist/mouse.js +18 -8
- package/dist/overlays/file-sheet.js +101 -0
- package/dist/pane-header.js +42 -0
- package/dist/sane-note.js +4 -4
- package/dist/spawn-catalog.js +40 -4
- package/dist/ui/doc-pane.js +85 -0
- package/dist/ui/file-sheet-screen.js +58 -0
- package/dist/ui/header-line.js +19 -0
- package/dist/ui/inbox-pane.js +83 -0
- package/dist/ui/panes.js +13 -74
- package/dist/ui/preview.js +25 -1
- package/dist/ui/screens.js +16 -4
- package/dist/viewport.js +41 -18
- package/package.json +1 -1
- package/scripts/deck-run +6 -5
package/dist/actions.js
CHANGED
|
@@ -26,6 +26,7 @@ import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spaw
|
|
|
26
26
|
import { resolveSpawnId } from './spawn-config.js';
|
|
27
27
|
import { useTaskOps } from './task-ops.js';
|
|
28
28
|
import { inboxWords } from './inbox.js';
|
|
29
|
+
import { docWords } from './doc-tree.js';
|
|
29
30
|
import { wrapWords } from './wrap-scan.js';
|
|
30
31
|
export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overrides, catalog, setNote, }) {
|
|
31
32
|
// La riga di stato di OGNI spawn di sessione Claude: il comando esatto, come
|
|
@@ -386,6 +387,58 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
386
387
|
spawned.child.on('error', () => setNote(`⚠ drain ${file.basename} fallito (${DECK_RUN})`));
|
|
387
388
|
noteSpawn(spawned);
|
|
388
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* T160 — la terna della riga `rebalance` sul bersaglio selezionato.
|
|
392
|
+
*
|
|
393
|
+
* Separata dallo spawn perché ha due chiamanti che la vogliono in due momenti
|
|
394
|
+
* diversi: la schermata, che MOSTRA il comando in fondo prima che `⏎` lo
|
|
395
|
+
* lanci, e l'attuatore, che lo lancia. Comporlo due volte darebbe due comandi
|
|
396
|
+
* capaci di divergere — e quello mostrato è la sola cosa che rende `⏎` una
|
|
397
|
+
* scelta invece di un salto nel buio.
|
|
398
|
+
*
|
|
399
|
+
* P9 — su una riga SENZA FLAG il prompt è vuoto, e il deck non spawna. Il
|
|
400
|
+
* flag resta un invito a guardare e non un ordine, quindi la skill su un
|
|
401
|
+
* bersaglio in equilibrio produrrebbe un report e nient'altro: aprire una
|
|
402
|
+
* conversazione per farselo dire è un giro di modello pagato per niente. La
|
|
403
|
+
* ragione viaggia accanto al vuoto (`hint`), o la riga muta si leggerebbe come
|
|
404
|
+
* un comando che non si è riusciti a comporre.
|
|
405
|
+
*/
|
|
406
|
+
function rebalanceSpawn(row) {
|
|
407
|
+
if (row.flags.length === 0) {
|
|
408
|
+
return {
|
|
409
|
+
prompt: '',
|
|
410
|
+
hint: `${row.path} non porta flag: rebalance-doc non ha niente da fare qui`,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const d = spawnDefaults('rebalance', { target: row.path, words: docWords(row.path) });
|
|
414
|
+
return { prompt: d.prompt ?? '', hint: '' };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* T160 — apre la sessione che RIORGANIZZA la topologia attorno a un bersaglio.
|
|
418
|
+
*
|
|
419
|
+
* Gemella di `drainInbox` e `unwrapPath` nella forma: sessione NUDA (il
|
|
420
|
+
* rebalance lavora sulla doc, non su una task), `sessionId` pinnato per poter
|
|
421
|
+
* scrivere la nota nel sidecar, titolo dalla riga di catalogo.
|
|
422
|
+
*
|
|
423
|
+
* Il modello della riga è `sonnet` ed è una cella FISSA, non una preferenza:
|
|
424
|
+
* `rebalance-doc` dichiara `model: sonnet` nel frontmatter e quel campo
|
|
425
|
+
* ri-timbra il modello della sessione qualunque `--model` arrivi dallo spawn.
|
|
426
|
+
* Passarlo comunque nell'argv resta giusto — lo spawn è deterministico e la
|
|
427
|
+
* riga di stato mostra il comando vero — ma configurarlo sarebbe una cella che
|
|
428
|
+
* mente.
|
|
429
|
+
*/
|
|
430
|
+
function rebalanceDoc(row, prompt) {
|
|
431
|
+
if (!prompt)
|
|
432
|
+
return;
|
|
433
|
+
const sid = randomUUID();
|
|
434
|
+
const d = spawnDefaults('rebalance', { target: row.path, words: docWords(row.path) });
|
|
435
|
+
const title = d.title ?? '';
|
|
436
|
+
if (title)
|
|
437
|
+
appendNote(cwd, sid, title);
|
|
438
|
+
const spawned = spawnBare(cwd, prompt, d.model, sid, title);
|
|
439
|
+
spawned.child.on('error', () => setNote(`⚠ rebalance ${row.path} fallito (${DECK_RUN})`));
|
|
440
|
+
noteSpawn(spawned);
|
|
441
|
+
}
|
|
389
442
|
/**
|
|
390
443
|
* T134 — apre la sessione che SROTOLA l'hard-wrap di un path.
|
|
391
444
|
*
|
|
@@ -463,6 +516,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
463
516
|
togglePin,
|
|
464
517
|
togglePriority,
|
|
465
518
|
drainInbox,
|
|
519
|
+
rebalanceSpawn,
|
|
520
|
+
rebalanceDoc,
|
|
466
521
|
unwrapPath,
|
|
467
522
|
openTerminal,
|
|
468
523
|
openClaude,
|
package/dist/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ import { useSearchOverlay } from './overlays/search.js';
|
|
|
32
32
|
import { useSheetOverlay } from './overlays/sheet.js';
|
|
33
33
|
import { useAssignOverlay } from './overlays/assign.js';
|
|
34
34
|
import { useProjectStatus } from './overlays/status.js';
|
|
35
|
-
import {
|
|
35
|
+
import { useFileSheet } from './overlays/file-sheet.js';
|
|
36
36
|
import { useWrapOverlay } from './overlays/wrap.js';
|
|
37
37
|
import { useSpawnPage } from './overlays/spawn.js';
|
|
38
38
|
import { useGitlink } from './overlays/gitlink.js';
|
|
@@ -41,7 +41,9 @@ import { useTextModals, useViewModals } from './overlays/modals.js';
|
|
|
41
41
|
import { useTerminalSize, useSpawnConfig } from './hooks.js';
|
|
42
42
|
import { EditModal, FilterModal, PurgeModal, SortModal } from './ui/modals.js';
|
|
43
43
|
import { StatusHeadline } from './ui/status-screen.js';
|
|
44
|
-
import {
|
|
44
|
+
import { SessionsPane, TasksPane } from './ui/panes.js';
|
|
45
|
+
import { InboxPane } from './ui/inbox-pane.js';
|
|
46
|
+
import { DocPane } from './ui/doc-pane.js';
|
|
45
47
|
import { PreviewPane, detailMetaOf } from './ui/preview.js';
|
|
46
48
|
import { CompactNotice, HintBar, TextBox, screenFor } from './ui/screens.js';
|
|
47
49
|
import { VERSION } from './version.js';
|
|
@@ -123,14 +125,27 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
123
125
|
setMode,
|
|
124
126
|
setNote,
|
|
125
127
|
});
|
|
126
|
-
// T134 —
|
|
127
|
-
//
|
|
128
|
-
|
|
128
|
+
// T134/T160 — le DUE istanze dello sheet di file: la coda inbox e il bersaglio
|
|
129
|
+
// doc. Stesso hook (`useFileSheet`, P11), due modi distinti — `esc` da una
|
|
130
|
+
// torna al pane inbox, dall'altra al pane doc, e un modo condiviso
|
|
131
|
+
// obbligherebbe a ricordare altrove da dove si era entrati. Come lo sheet
|
|
132
|
+
// della task ricevono lo spawn come callback: un hook di overlay non esce dal
|
|
133
|
+
// deck.
|
|
134
|
+
const inbox = useFileSheet({
|
|
135
|
+
mode: 'inbox',
|
|
129
136
|
rows,
|
|
130
137
|
columns,
|
|
131
138
|
setMode,
|
|
132
139
|
setNote,
|
|
133
|
-
|
|
140
|
+
onEnter: actions.drainInbox,
|
|
141
|
+
});
|
|
142
|
+
const docSheet = useFileSheet({
|
|
143
|
+
mode: 'doc',
|
|
144
|
+
rows,
|
|
145
|
+
columns,
|
|
146
|
+
setMode,
|
|
147
|
+
setNote,
|
|
148
|
+
onEnter: actions.rebalanceDoc,
|
|
134
149
|
});
|
|
135
150
|
// T134 — l'hard-wrap. Come il project status non è solo un overlay: i suoi
|
|
136
151
|
// numeri si leggono in testata a schermata chiusa, e la lista è una delle sue
|
|
@@ -188,6 +203,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
188
203
|
search,
|
|
189
204
|
status,
|
|
190
205
|
inbox,
|
|
206
|
+
doc: docSheet,
|
|
191
207
|
wrap,
|
|
192
208
|
spawn,
|
|
193
209
|
gitlink,
|
|
@@ -210,9 +226,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
210
226
|
? model.selInbox
|
|
211
227
|
? 'inbox'
|
|
212
228
|
: 'none'
|
|
213
|
-
: model.
|
|
214
|
-
?
|
|
215
|
-
|
|
229
|
+
: model.focus === 'doctree'
|
|
230
|
+
? model.selDoc
|
|
231
|
+
? 'doc'
|
|
232
|
+
: 'none'
|
|
233
|
+
: model.selSessionObj
|
|
234
|
+
? 'session'
|
|
235
|
+
: 'none';
|
|
216
236
|
const detailParts = model.detail ? detailMetaOf(model.detail) : null;
|
|
217
237
|
const launch = launchRow(model.launch, columns);
|
|
218
238
|
// T154 — i bottoni del blocco selettore della nuda: stessa primitiva
|
|
@@ -262,11 +282,16 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
262
282
|
sessionViewId: model.sessionViewId,
|
|
263
283
|
parentLabel: model.parentLabel,
|
|
264
284
|
hasLoadError: Boolean(model.loadError),
|
|
265
|
-
|
|
285
|
+
deckMode: model.deckMode,
|
|
266
286
|
inboxFiles: model.inboxFiles,
|
|
267
287
|
selInboxPath: model.selInboxPath,
|
|
268
288
|
inboxCounts: model.inboxCounts,
|
|
269
289
|
inboxViewId: model.inboxViewId,
|
|
290
|
+
docRows: model.docRows,
|
|
291
|
+
selDocPath: model.selDocPath,
|
|
292
|
+
docCounts: model.docCounts,
|
|
293
|
+
docViewId: model.docViewId,
|
|
294
|
+
docHasDirs: model.docHasDirs,
|
|
270
295
|
});
|
|
271
296
|
useDeckInput({
|
|
272
297
|
cwd,
|
|
@@ -296,7 +321,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
296
321
|
projectName: model.projectName,
|
|
297
322
|
taskRowData: model.taskRowData,
|
|
298
323
|
hiddenTasks: model.hiddenTasks,
|
|
299
|
-
overlays: { assign, sheet, search, status, inbox, wrap, spawn },
|
|
324
|
+
overlays: { assign, sheet, search, status, inbox, doc: docSheet, wrap, spawn },
|
|
300
325
|
});
|
|
301
326
|
if (screen)
|
|
302
327
|
return screen;
|
|
@@ -312,10 +337,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
312
337
|
hasSession: model.selSessionObj !== null,
|
|
313
338
|
hasSessionId: model.selSessionId !== null,
|
|
314
339
|
purgeBulk: model.purgeBulk,
|
|
315
|
-
|
|
340
|
+
docMode: model.deckMode === 'doc',
|
|
316
341
|
hasInbox: model.selInbox !== null,
|
|
342
|
+
hasDoc: model.selDoc !== null,
|
|
317
343
|
gitlink: gitlink.enabled,
|
|
318
|
-
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark }), model.
|
|
344
|
+
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [model.deckMode === 'doc' ? (_jsx(DocPane, { rows: frame.windowDoc, counts: model.docCounts, activeView: model.docViewId, paneCount: model.docRows.length, selectedPath: model.selDocPath, focused: model.focus === 'doctree', above: frame.docWin.start, below: model.docRows.length - frame.docWin.end, columns: columns, ok: model.docOk, scanned: model.docScanned, scanning: model.docScanning, hasDirs: model.docHasDirs })) : (_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark })), model.deckMode === 'doc' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, sessionPriority: model.sessionPriority, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'doc' && model.selDoc ? (_jsx(PreviewPane, { kind: "doc", row: model.selDoc, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
319
345
|
}
|
|
320
346
|
const cwd = process.cwd();
|
|
321
347
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
package/dist/deck-model.js
CHANGED
|
@@ -14,15 +14,16 @@
|
|
|
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, useCommitTimes, useDirtyFolders, useEpicHierarchy, useInboxScan, useSessions, useTaskDetail, useTasks, } from './hooks.js';
|
|
17
|
+
import { useArchivable, useCommitTimes, useDirtyFolders, useDocScan, useEpicHierarchy, useInboxScan, useSessions, useTaskDetail, useTasks, } from './hooks.js';
|
|
18
18
|
import { DEFAULT_INBOX_STALE_HOURS, staleCount } from './inbox.js';
|
|
19
|
+
import { cycleDocView, docCounts as deriveDocCounts, docView, selectDocRows, } from './doc-views.js';
|
|
19
20
|
import { cycleInboxView, inboxCounts as deriveInboxCounts, inboxView, selectInboxRows, } from './inbox-views.js';
|
|
20
21
|
import { docsRootName } from './tasks.js';
|
|
21
22
|
import { isDone } from './layout.js';
|
|
22
23
|
import { TASK_EMPTY, modelAlias, relTime } from './glyphs.js';
|
|
23
24
|
import { assembleSessionList, firstSelectableId, rowIndexOf, selectedSession, } from './session-list.js';
|
|
24
25
|
import { cycleSessionView, cycleTaskView, selectSessionRows, selectTasks, sessionView, taskView, } from './pane-views.js';
|
|
25
|
-
import { ALL, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, ROW_ALL, ROW_SPOT, SPOT, } from './model.js';
|
|
26
|
+
import { ALL, LEFT_PANE, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, RIGHT_PANE, ROW_ALL, ROW_SPOT, SPOT, isLeftPane, } from './model.js';
|
|
26
27
|
import { applyView, epicRollup, taskColumns } from './view.js';
|
|
27
28
|
import { loadView } from './view-store.js';
|
|
28
29
|
import { termWidth } from './width.js';
|
|
@@ -166,15 +167,21 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
166
167
|
// riaperta a freddo si legge come la lista intera.
|
|
167
168
|
const [taskViewId, setTaskViewId] = useState('tasks');
|
|
168
169
|
const [sessionViewId, setSessionViewId] = useState('context');
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
170
|
+
// T160 — il MODO del deck (quale coppia di pane è montata) e la vista attiva
|
|
171
|
+
// dei due pane che il modo doc porta con sé. Volatili tutti e tre: il deck
|
|
172
|
+
// riapre sempre in modo task, sulle viste di default.
|
|
173
|
+
const [deckMode, setDeckMode] = useState('task');
|
|
172
174
|
const [inboxViewId, setInboxViewId] = useState('all');
|
|
175
|
+
const [docViewId, setDocViewId] = useState('all');
|
|
173
176
|
// Selezione KEYED SUL PATH, mai su indice: la lista si riordina sotto le
|
|
174
177
|
// viste e si accorcia a ogni scan, e un indice grezzo punterebbe alla riga
|
|
175
178
|
// sbagliata in silenzio (stessa trappola di T39 sulle task e T50 sulle
|
|
176
179
|
// conversazioni).
|
|
177
180
|
const [selInboxPath, setSelInboxPath] = useState(null);
|
|
181
|
+
// T160 — gemella della precedente, sul pane doc: keyed sul PATH e mai
|
|
182
|
+
// sull'indice, perché la lista cambia forma fra le viste (albero con le
|
|
183
|
+
// cartelle, liste piatte per flag) e si riordina a ogni scan.
|
|
184
|
+
const [selDocPath, setSelDocPath] = useState(null);
|
|
178
185
|
// T136 — data dell'ultimo commit di ogni task file, per la chiave `commit`
|
|
179
186
|
// della chain di sort.
|
|
180
187
|
const commitAt = useCommitTimes(tasksDir, cwd);
|
|
@@ -218,6 +225,21 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
218
225
|
const inboxStale = staleCount(inbox.files, DEFAULT_INBOX_STALE_HOURS, Date.now());
|
|
219
226
|
const inboxFiles = useMemo(() => selectInboxRows(inboxViewId, inbox.files), [inboxViewId, inbox.files]);
|
|
220
227
|
const selInbox = inboxFiles.find((f) => f.path === selInboxPath) ?? null;
|
|
228
|
+
// T160 — l'albero della doc, quinto scan della famiglia e l'unico con un
|
|
229
|
+
// interruttore: gira solo mentre il modo doc è montato (P3 preflight). Il
|
|
230
|
+
// primo giro lo fa partire l'effect di `useDocScan` quando `enabled` passa a
|
|
231
|
+
// vero, quindi il montaggio rimisura da sé — `toggleDeckMode` sotto non deve
|
|
232
|
+
// chiamare `doc.scan()` come fa il gemello inbox, e chiamarlo darebbe due
|
|
233
|
+
// spawn da quattro secondi per un gesto solo.
|
|
234
|
+
const doc = useDocScan(cwd, docsRoot, deckMode === 'doc');
|
|
235
|
+
// La misura GREZZA, memoizzata a parte: la consumano tre derivazioni (i
|
|
236
|
+
// contatori, le righe della vista, l'elenco dei file di una cartella nello
|
|
237
|
+
// sheet) e ricomporre l'oggetto a ogni render darebbe una identità nuova a
|
|
238
|
+
// ogni giro, cioè tre memo che non memoizzano più niente.
|
|
239
|
+
const docData = useMemo(() => ({ files: doc.files, dirs: doc.dirs, hasDirs: doc.hasDirs }), [doc.files, doc.dirs, doc.hasDirs]);
|
|
240
|
+
const docCounts = useMemo(() => deriveDocCounts(docData), [docData]);
|
|
241
|
+
const docRows = useMemo(() => selectDocRows(docViewId, docData), [docViewId, docData]);
|
|
242
|
+
const selDoc = docRows.find((r) => r.path === selDocPath) ?? null;
|
|
221
243
|
// T100 — le task effettivamente a schermo: la vista principale coincide con
|
|
222
244
|
// `viewTasks` (nessun ricalcolo sul cammino di default), le altre due passano
|
|
223
245
|
// dal predicato del catalogo. I CONTATORI restano misurati sulla vista di
|
|
@@ -344,6 +366,17 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
344
366
|
setSelInboxPath(inboxFiles[0].path);
|
|
345
367
|
}
|
|
346
368
|
}, [inboxFiles, selInboxPath]);
|
|
369
|
+
// T160 — gemello del precedente sul pane doc: un file splittato sparisce dalla
|
|
370
|
+
// vista `SPLIT` al primo scan dopo l'operazione, e la selezione cade sulla
|
|
371
|
+
// prima riga invece che su una posizione a caso.
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (selDocPath !== null && !docRows.some((r) => r.path === selDocPath)) {
|
|
374
|
+
setSelDocPath(docRows[0]?.path ?? null);
|
|
375
|
+
}
|
|
376
|
+
else if (selDocPath === null && docRows.length > 0) {
|
|
377
|
+
setSelDocPath(docRows[0].path);
|
|
378
|
+
}
|
|
379
|
+
}, [docRows, selDocPath]);
|
|
347
380
|
/** Selezione per INDICE nella vista, riconvertita subito in sentinella o id.
|
|
348
381
|
* T21 — la chiama anche il click su una riga del pane task. */
|
|
349
382
|
function selectTaskRow(index) {
|
|
@@ -391,11 +424,20 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
391
424
|
setSelInboxPath(null);
|
|
392
425
|
setNote(`vista inbox: ${inboxView(next).label(inboxCounts)}`);
|
|
393
426
|
}
|
|
427
|
+
function selectDocView(next) {
|
|
428
|
+
if (next === docViewId)
|
|
429
|
+
return;
|
|
430
|
+
setDocViewId(next);
|
|
431
|
+
setSelDocPath(null);
|
|
432
|
+
setNote(`vista doc: ${docView(next).label(docCounts)}`);
|
|
433
|
+
}
|
|
394
434
|
function cycleView(delta) {
|
|
395
435
|
if (focus === 'tasks')
|
|
396
436
|
selectTaskView(cycleTaskView(taskViewId, delta));
|
|
397
437
|
else if (focus === 'inbox')
|
|
398
438
|
selectInboxView(cycleInboxView(inboxViewId, delta));
|
|
439
|
+
else if (focus === 'doctree')
|
|
440
|
+
selectDocView(cycleDocView(docViewId, delta));
|
|
399
441
|
else
|
|
400
442
|
selectSessionView(cycleSessionView(sessionViewId, delta));
|
|
401
443
|
}
|
|
@@ -412,6 +454,20 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
412
454
|
const next = Math.max(0, Math.min(inboxFiles.length - 1, (at < 0 ? 0 : at) + delta));
|
|
413
455
|
setSelInboxPath(inboxFiles[next].path);
|
|
414
456
|
}
|
|
457
|
+
/** Gemelle delle due sopra, sul pane doc. Nessuna riga è non-selezionabile:
|
|
458
|
+
* una cartella è un bersaglio di `rebalance-doc` quanto un file. */
|
|
459
|
+
function selectDocRow(index) {
|
|
460
|
+
const r = docRows[index];
|
|
461
|
+
if (r)
|
|
462
|
+
setSelDocPath(r.path);
|
|
463
|
+
}
|
|
464
|
+
function moveDocSel(delta) {
|
|
465
|
+
const at = docRows.findIndex((r) => r.path === selDocPath);
|
|
466
|
+
if (docRows.length === 0)
|
|
467
|
+
return;
|
|
468
|
+
const next = Math.max(0, Math.min(docRows.length - 1, (at < 0 ? 0 : at) + delta));
|
|
469
|
+
setSelDocPath(docRows[next].path);
|
|
470
|
+
}
|
|
415
471
|
/** T148 — `m` scorre il selettore modello della riga sessione selezionata al
|
|
416
472
|
* valore SUCCESSIVO del catalogo (ciclico), gemella di `cycleView` ma su un
|
|
417
473
|
* asse diverso: qui non c'è una vista da cambiare, solo un valore da far
|
|
@@ -434,34 +490,45 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
434
490
|
return next;
|
|
435
491
|
}
|
|
436
492
|
/**
|
|
437
|
-
*
|
|
493
|
+
* T160 — `^B` scambia il MODO, cioè ENTRAMBI gli slot insieme.
|
|
494
|
+
*
|
|
495
|
+
* Erede del `toggleInboxPane` di T134, che scambiava il solo pane destro. Un
|
|
496
|
+
* tasto per slot avrebbe reso raggiungibili le due combinazioni che l'asse
|
|
497
|
+
* unico esiste per non avere (albero doc accanto alle conversazioni, lista
|
|
498
|
+
* task accanto alla coda inbox), e ne avrebbe richiesto un secondo che nessuna
|
|
499
|
+
* legenda ha spazio per annunciare.
|
|
438
500
|
*
|
|
439
|
-
* Il FOCUS segue il
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
501
|
+
* Il FOCUS segue la POSIZIONE, non il contenuto: chi stava guardando a
|
|
502
|
+
* sinistra continua a guardare a sinistra, e quello che c'è adesso è l'altro
|
|
503
|
+
* pane. È la stessa regola di T134 (lì con una posizione sola su cui valeva) e
|
|
504
|
+
* la stessa di `←→`, che nominano un lato e non un pane.
|
|
505
|
+
*
|
|
506
|
+
* Nessuno scan esplicito qui, a differenza del gemello di T134: il montaggio
|
|
507
|
+
* lo rimisura da sé, perché l'effect di `useDocScan` è keyed sull'interruttore
|
|
508
|
+
* e riparte quando passa a vero. Chiamarlo anche di qui darebbe due spawn da
|
|
509
|
+
* quattro secondi per un gesto solo. Lo scan inbox invece resta chiesto a
|
|
510
|
+
* mano — il suo hook gira sempre, e non ha un interruttore che gli dica che
|
|
511
|
+
* qualcuno ha appena aperto il pane.
|
|
443
512
|
*/
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
// questa chiamata chi apre il pane guarda l'ultima misura, che può essere
|
|
451
|
-
// vecchia quanto l'intero intervallo. Non serve il verso opposto — tornare
|
|
452
|
-
// alle sessioni non guarda la coda, e uno scan lì si pagherebbe per nessuno.
|
|
453
|
-
if (next === 'inbox')
|
|
513
|
+
function switchMode(next) {
|
|
514
|
+
if (next === deckMode)
|
|
515
|
+
return;
|
|
516
|
+
setDeckMode(next);
|
|
517
|
+
setFocus((f) => (isLeftPane(f) ? LEFT_PANE[next] : RIGHT_PANE[next]));
|
|
518
|
+
if (next === 'doc')
|
|
454
519
|
inbox.scan();
|
|
455
|
-
setNote(next === '
|
|
520
|
+
setNote(next === 'doc' ? '^B → modo doc' : '^B → modo task');
|
|
521
|
+
}
|
|
522
|
+
function toggleDeckMode() {
|
|
523
|
+
switchMode(deckMode === 'doc' ? 'task' : 'doc');
|
|
456
524
|
}
|
|
457
|
-
/** `esc` in vista normale: torna
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
function
|
|
461
|
-
if (
|
|
525
|
+
/** `esc` in vista normale: torna al modo task. Solo in quel verso — `esc` dice
|
|
526
|
+
* «esci da dove sei», e dal modo task non c'è più niente da cui uscire (il
|
|
527
|
+
* tasto resta inerte, come è sempre stato). */
|
|
528
|
+
function closeDocMode() {
|
|
529
|
+
if (deckMode !== 'doc')
|
|
462
530
|
return false;
|
|
463
|
-
|
|
464
|
-
setFocus((f) => (f === 'inbox' ? 'sessions' : f));
|
|
531
|
+
switchMode('task');
|
|
465
532
|
return true;
|
|
466
533
|
}
|
|
467
534
|
return {
|
|
@@ -517,8 +584,9 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
517
584
|
cycleResumeModel,
|
|
518
585
|
bareModel,
|
|
519
586
|
cycleBareModel,
|
|
587
|
+
// modo del deck
|
|
588
|
+
deckMode,
|
|
520
589
|
// derivazioni del pane inbox
|
|
521
|
-
rightPane,
|
|
522
590
|
inboxScanned: inbox.scanned,
|
|
523
591
|
inboxOk: inbox.ok,
|
|
524
592
|
inboxViewId,
|
|
@@ -527,6 +595,17 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
527
595
|
inboxFiles,
|
|
528
596
|
selInboxPath,
|
|
529
597
|
selInbox,
|
|
598
|
+
// derivazioni del pane doc
|
|
599
|
+
docData,
|
|
600
|
+
docScanned: doc.scanned,
|
|
601
|
+
docOk: doc.ok,
|
|
602
|
+
docScanning: doc.scanning,
|
|
603
|
+
docHasDirs: doc.hasDirs,
|
|
604
|
+
docViewId,
|
|
605
|
+
docCounts,
|
|
606
|
+
docRows,
|
|
607
|
+
selDocPath,
|
|
608
|
+
selDoc,
|
|
530
609
|
// mutatori di navigazione
|
|
531
610
|
selectTaskRow,
|
|
532
611
|
moveTaskSel,
|
|
@@ -535,8 +614,11 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
535
614
|
selectInboxView,
|
|
536
615
|
selectInboxRow,
|
|
537
616
|
moveInboxSel,
|
|
538
|
-
|
|
539
|
-
|
|
617
|
+
selectDocView,
|
|
618
|
+
selectDocRow,
|
|
619
|
+
moveDocSel,
|
|
620
|
+
toggleDeckMode,
|
|
621
|
+
closeDocMode,
|
|
540
622
|
cycleView,
|
|
541
623
|
};
|
|
542
624
|
}
|