@lamemind/loom-deck 0.71.2 → 0.72.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 +57 -0
- package/dist/cli.js +10 -2
- package/dist/frame.js +4 -1
- package/dist/input-modes.js +3 -1
- package/dist/input.js +10 -1
- package/dist/overlays/drop.js +36 -0
- package/dist/session-drop.js +35 -0
- package/dist/spawn-catalog.js +8 -6
- package/dist/ui/modals.js +14 -1
- package/dist/ui/screens.js +3 -0
- package/dist/viewport.js +5 -0
- package/package.json +1 -1
- package/scripts/prompt-catalog +7 -8
package/dist/actions.js
CHANGED
|
@@ -21,6 +21,7 @@ import { taskSlug } from './tasks.js';
|
|
|
21
21
|
import { appendNote, appendPin, appendPriority, appendSessionRecord, appendTaskBinding, } from './task-index.js';
|
|
22
22
|
import { neighborId } from './session-list.js';
|
|
23
23
|
import { cut, cutMiddle } from './width.js';
|
|
24
|
+
import { dropSessionFiles, sessionArtifacts } from './session-drop.js';
|
|
24
25
|
import { saveView, viewFilePath } from './view-store.js';
|
|
25
26
|
import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, DECK_RUN, } from './spawn.js';
|
|
26
27
|
import { resolveSpawnId } from './spawn-config.js';
|
|
@@ -350,6 +351,60 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
350
351
|
model.reloadSessions();
|
|
351
352
|
setNote(`${on ? 'priorità via' : '🚨 prioritaria'} ${sid.slice(0, 8)}`);
|
|
352
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* La bozza di conferma per `CANC` sul pane sessioni, o `null` se non c'è
|
|
356
|
+
* niente da confermare (la nota dice perché). Tre rifiuti, in ordine:
|
|
357
|
+
* nessuna riga selezionata; riga stale (il transcript non c'è già più — da
|
|
358
|
+
* lì si esce spinnando, non eliminando); conversazione VIVA — un processo
|
|
359
|
+
* `claude` ci sta scrivendo, e togliergli il file da sotto lascia un
|
|
360
|
+
* processo che continua a scrivere su un inode sganciato, invisibile a tutti.
|
|
361
|
+
*/
|
|
362
|
+
function dropDraftFor() {
|
|
363
|
+
if (model.focus !== 'sessions') {
|
|
364
|
+
setNote('CANC → eliminare: seleziona una conversazione (→ per il pane)');
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const sid = model.selSessionId;
|
|
368
|
+
if (!sid) {
|
|
369
|
+
setNote('CANC → nessuna conversazione da eliminare');
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const s = model.selSessionObj;
|
|
373
|
+
if (!s) {
|
|
374
|
+
setNote(`CANC → ${sid.slice(0, 8)}: transcript già assente (p per spinnare)`);
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const alive = model.live.get(sid);
|
|
378
|
+
if (alive) {
|
|
379
|
+
setNote(`CANC → ${sid.slice(0, 8)} è viva (pid ${alive.pid}): chiudila prima`);
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return { sessionId: sid, title: s.title, path: s.path, hasFolder: sessionArtifacts(s.path).length > 1 };
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* L'eliminazione confermata. Il vicino si calcola PRIMA di toccare il disco,
|
|
386
|
+
* come nel pin: dopo la riga non c'è più e non ha vicini. Se la conversazione
|
|
387
|
+
* era pinnata si spinna nello stesso gesto — altrimenti il sidecar la terrebbe
|
|
388
|
+
* in lista come pinnata stale, cioè una riga che dice «c'era» di una cosa
|
|
389
|
+
* appena eliminata apposta.
|
|
390
|
+
*/
|
|
391
|
+
function dropSession(draft) {
|
|
392
|
+
const short = draft.sessionId.slice(0, 8);
|
|
393
|
+
const landing = neighborId(model.sessionRows, draft.sessionId);
|
|
394
|
+
try {
|
|
395
|
+
dropSessionFiles(draft.path);
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
setNote(`⚠ ${short}: eliminazione fallita: ${e instanceof Error ? e.message : String(e)}`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (model.pinned.has(draft.sessionId))
|
|
402
|
+
appendPin(cwd, draft.sessionId, false);
|
|
403
|
+
model.reloadSessions();
|
|
404
|
+
if (landing)
|
|
405
|
+
model.setSelSessionId(landing);
|
|
406
|
+
setNote(`✔ eliminata ${short} «${cut(draft.title, 40)}»`);
|
|
407
|
+
}
|
|
353
408
|
/** `t` — terminale a project root, con un titolo che il matcher di compass riconosce. */
|
|
354
409
|
function openTerminal() {
|
|
355
410
|
const title = model.identity ? `🖥️ ${model.identity.name} [term]` : null;
|
|
@@ -548,6 +603,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
548
603
|
forkSession,
|
|
549
604
|
togglePin,
|
|
550
605
|
togglePriority,
|
|
606
|
+
dropDraftFor,
|
|
607
|
+
dropSession,
|
|
551
608
|
drainInbox,
|
|
552
609
|
rebalanceSpawn,
|
|
553
610
|
rebalanceDoc,
|
package/dist/cli.js
CHANGED
|
@@ -37,9 +37,10 @@ import { useWrapOverlay } from './overlays/wrap.js';
|
|
|
37
37
|
import { useSpawnPage } from './overlays/spawn.js';
|
|
38
38
|
import { useGitlink } from './overlays/gitlink.js';
|
|
39
39
|
import { usePurgeOverlay } from './overlays/purge.js';
|
|
40
|
+
import { useDropOverlay } from './overlays/drop.js';
|
|
40
41
|
import { useTextModals, useViewModals } from './overlays/modals.js';
|
|
41
42
|
import { useTerminalSize, useSpawnConfig } from './hooks.js';
|
|
42
|
-
import { EditModal, FilterModal, PurgeModal, SortModal } from './ui/modals.js';
|
|
43
|
+
import { DropModal, EditModal, FilterModal, PurgeModal, SortModal } from './ui/modals.js';
|
|
43
44
|
import { StatusHeadline } from './ui/status-screen.js';
|
|
44
45
|
import { SessionsPane, TasksPane } from './ui/panes.js';
|
|
45
46
|
import { InboxPane } from './ui/inbox-pane.js';
|
|
@@ -181,6 +182,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
181
182
|
draftFor: actions.purgeDraftFor,
|
|
182
183
|
onSubmit: actions.purgeTasks,
|
|
183
184
|
});
|
|
185
|
+
const drop = useDropOverlay({
|
|
186
|
+
setMode,
|
|
187
|
+
setNote,
|
|
188
|
+
draftFor: actions.dropDraftFor,
|
|
189
|
+
onSubmit: actions.dropSession,
|
|
190
|
+
});
|
|
184
191
|
const viewModals = useViewModals({
|
|
185
192
|
view: model.view,
|
|
186
193
|
setView: model.setView,
|
|
@@ -208,6 +215,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
208
215
|
spawn,
|
|
209
216
|
gitlink,
|
|
210
217
|
purge,
|
|
218
|
+
drop,
|
|
211
219
|
view: viewModals,
|
|
212
220
|
text: textModals,
|
|
213
221
|
};
|
|
@@ -341,7 +349,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
341
349
|
hasInbox: model.selInbox !== null,
|
|
342
350
|
hasDoc: model.selDoc !== null,
|
|
343
351
|
gitlink: gitlink.enabled,
|
|
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] }));
|
|
352
|
+
}), 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, mode === 'drop' && drop.draft ? _jsx(DropModal, { draft: drop.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] }));
|
|
345
353
|
}
|
|
346
354
|
const cwd = process.cwd();
|
|
347
355
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
package/dist/frame.js
CHANGED
|
@@ -97,7 +97,10 @@ export function deckLegend(state) {
|
|
|
97
97
|
// apertura: una legenda che annunciasse «tutte» dove il tasto ne pota una
|
|
98
98
|
// sola sarebbe peggio di nessuna legenda.
|
|
99
99
|
...(state.focus === 'tasks' ? [state.purgeBulk ? 'CANC elimina tutte' : 'CANC elimina'] : []),
|
|
100
|
-
|
|
100
|
+
// `CANC` sul pane sessioni: stessa condizione del fork (una conversazione
|
|
101
|
+
// VERA sotto il cursore), perché una stale non ha un transcript da
|
|
102
|
+
// eliminare — da lì si esce con `p`.
|
|
103
|
+
...(canResume ? ['f fork', 'm modello', 'CANC elimina'] : []),
|
|
101
104
|
// T117 · D3 — «titolo» anche qui: è lo stesso valore che il detail chiede
|
|
102
105
|
// allo spawn, e due nomi per una cosa sola li paga chi legge le due
|
|
103
106
|
// schermate. Rename di sola ETICHETTA: il dato resta `note` nel sidecar e
|
package/dist/input-modes.js
CHANGED
|
@@ -37,6 +37,7 @@ export const CAPTURING_MODES = [
|
|
|
37
37
|
'filter',
|
|
38
38
|
'edit',
|
|
39
39
|
'purge',
|
|
40
|
+
'drop',
|
|
40
41
|
];
|
|
41
42
|
const _MODES_ARE_EXHAUSTIVE = true;
|
|
42
43
|
void _MODES_ARE_EXHAUSTIVE;
|
|
@@ -56,7 +57,8 @@ export function captures(mode) {
|
|
|
56
57
|
* e un'eccezione che vive solo dentro un `if` annidato è come non averla.
|
|
57
58
|
*
|
|
58
59
|
* `purge` (T112) non ne ha e non può averne: è una domanda binaria, e dentro
|
|
59
|
-
* una domanda non c'è nessun acceleratore da salvare.
|
|
60
|
+
* una domanda non c'è nessun acceleratore da salvare. Lo stesso vale per
|
|
61
|
+
* `drop`, la conferma gemella sulle conversazioni.
|
|
60
62
|
*/
|
|
61
63
|
export const CTRL_DEROGATIONS = {
|
|
62
64
|
detail: ['f'],
|
package/dist/input.js
CHANGED
|
@@ -91,6 +91,7 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
91
91
|
filter: overlays.view.onFilterKey,
|
|
92
92
|
edit: overlays.text.onEditKey,
|
|
93
93
|
purge: overlays.purge.onKey,
|
|
94
|
+
drop: overlays.drop.onKey,
|
|
94
95
|
};
|
|
95
96
|
// T21 (mandata 2) — la ROTELLA, per i soli modi che scorrono un contenuto
|
|
96
97
|
// lungo (`SCROLLING_MODES`). Stessa forma di `MODE_KEYS`: un `Record` sul
|
|
@@ -525,7 +526,15 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
525
526
|
// c'è nessun campo di testo da cui Backspace possa rubare un significato,
|
|
526
527
|
// e ogni modo che ne ha uno è capturing e non vede questo ramo. Il modale
|
|
527
528
|
// di conferma è la rete contro chi usa Backspace come «indietro».
|
|
528
|
-
|
|
529
|
+
//
|
|
530
|
+
// Il bersaglio lo decide il FOCUS, come per `⏎`: sul pane sessioni il
|
|
531
|
+
// tasto elimina la conversazione selezionata (transcript su disco),
|
|
532
|
+
// altrove pota una task via skill. Due overlay e non uno perché i due
|
|
533
|
+
// effetti non hanno niente in comune oltre al tasto.
|
|
534
|
+
if (model.focus === 'sessions')
|
|
535
|
+
overlays.drop.open();
|
|
536
|
+
else
|
|
537
|
+
overlays.purge.open();
|
|
529
538
|
}
|
|
530
539
|
else if (input === 'C') {
|
|
531
540
|
overlays.text.openCreate();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// L'overlay di conferma dell'eliminazione di una CONVERSAZIONE (`CANC` sul pane
|
|
2
|
+
// sessioni). Gemello di `purge.ts` nella forma — un modo in `CAPTURING_MODES`,
|
|
3
|
+
// una bozza, un handler binario ⏎/esc — e separato da lui nell'oggetto: la
|
|
4
|
+
// task si pota via skill del plugin con commit, la conversazione si rimuove
|
|
5
|
+
// dal disco e basta, e una conferma sola per i due bersagli direbbe l'effetto
|
|
6
|
+
// di uno mentre esegue l'altro.
|
|
7
|
+
//
|
|
8
|
+
// La deroga di T104 vale anche qui: l'hook non esce dal deck. La bozza arriva
|
|
9
|
+
// da `draftFor`, l'esecuzione parte da `onSubmit`, entrambe in `actions.ts`.
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
export function useDropOverlay({ setMode, setNote, draftFor, onSubmit, }) {
|
|
12
|
+
const [draft, setDraft] = useState(null);
|
|
13
|
+
function open() {
|
|
14
|
+
const next = draftFor();
|
|
15
|
+
if (!next)
|
|
16
|
+
return;
|
|
17
|
+
setDraft(next);
|
|
18
|
+
setNote('');
|
|
19
|
+
setMode('drop');
|
|
20
|
+
}
|
|
21
|
+
function onKey(_input, key) {
|
|
22
|
+
if (key.escape) {
|
|
23
|
+
setMode('normal');
|
|
24
|
+
setDraft(null);
|
|
25
|
+
setNote('CANC → eliminazione annullata');
|
|
26
|
+
}
|
|
27
|
+
else if (key.return) {
|
|
28
|
+
const current = draft;
|
|
29
|
+
setMode('normal');
|
|
30
|
+
setDraft(null);
|
|
31
|
+
if (current)
|
|
32
|
+
onSubmit(current);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { draft, open, onKey };
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Eliminazione di una conversazione dal disco — il pezzo PURO, senza React e
|
|
2
|
+
// senza selezione: riceve il path del transcript e rimuove ciò che CC tiene per
|
|
3
|
+
// quella conversazione. La decisione (quale, se viva, se pinnata) sta in
|
|
4
|
+
// `actions.ts`; la conferma nell'overlay `drop`.
|
|
5
|
+
import { existsSync, rmSync, statSync } from 'node:fs';
|
|
6
|
+
import { basename, dirname, join } from 'node:path';
|
|
7
|
+
/**
|
|
8
|
+
* Gli artefatti su disco di una conversazione: il transcript `<sid>.jsonl` e,
|
|
9
|
+
* quando esiste, la cartella omonima accanto (`<sid>/`) dove CC scrive i
|
|
10
|
+
* transcript dei subagent e gli output dei tool. Il sidecar del deck
|
|
11
|
+
* (`session-tasks.jsonl`) NON è fra questi: è nostro, non di CC, e i suoi
|
|
12
|
+
* record restano — chi elimina spinna da sé, così non nasce una riga stale.
|
|
13
|
+
*
|
|
14
|
+
* Il path deve finire in `.jsonl`: è l'unica forma che questo modulo accetta di
|
|
15
|
+
* rimuovere, e un path qualunque passato per errore non deve diventare un
|
|
16
|
+
* `rm -rf`.
|
|
17
|
+
*/
|
|
18
|
+
export function sessionArtifacts(transcriptPath) {
|
|
19
|
+
if (!transcriptPath.endsWith('.jsonl')) {
|
|
20
|
+
throw new Error(`non è un transcript: ${transcriptPath}`);
|
|
21
|
+
}
|
|
22
|
+
const sibling = join(dirname(transcriptPath), basename(transcriptPath, '.jsonl'));
|
|
23
|
+
const out = [transcriptPath];
|
|
24
|
+
if (existsSync(sibling) && statSync(sibling).isDirectory())
|
|
25
|
+
out.push(sibling);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/** Rimuove gli artefatti e restituisce i path toccati. Irreversibile: i
|
|
29
|
+
* transcript non stanno in git, e non esiste un cestino. */
|
|
30
|
+
export function dropSessionFiles(transcriptPath) {
|
|
31
|
+
const targets = sessionArtifacts(transcriptPath);
|
|
32
|
+
for (const t of targets)
|
|
33
|
+
rmSync(t, { recursive: true, force: true });
|
|
34
|
+
return targets;
|
|
35
|
+
}
|
package/dist/spawn-catalog.js
CHANGED
|
@@ -65,10 +65,9 @@ export const MODEL_DEFAULT = 'fable';
|
|
|
65
65
|
// T152 — `recap-status-project` non è una chiave di `prompt-catalog` (le chiavi
|
|
66
66
|
// lì sono i valori di `--prompt-kind`, e lo spawn headless non ne passa mai
|
|
67
67
|
// uno): il suo default vive nella riga `project-status` di questo catalogo.
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// anche giorni dopo.
|
|
68
|
+
// È l'unico recap che gira in headless (`-p`), senza nessuno che ne legga
|
|
69
|
+
// l'esito mentre si forma e possa rilanciarlo — il testo prodotto è il
|
|
70
|
+
// deliverable, e viene riletto da disco anche giorni dopo.
|
|
72
71
|
export const PROJECT_STATUS_MODEL = 'opus';
|
|
73
72
|
// T66 — le azioni del detail. Non sono un catalogo a parte: ognuna nomina una
|
|
74
73
|
// riga di questo catalogo, per kind, e tutte passano dallo stesso `spawnForTask`
|
|
@@ -137,7 +136,10 @@ export const SPAWN_ACTIONS = [
|
|
|
137
136
|
// D2 di T150 — nessun prefisso: `open` è "nessuna azione", e inventargliene
|
|
138
137
|
// uno contraddirebbe l'intenzione di chi la sceglie.
|
|
139
138
|
title: '{slug}',
|
|
140
|
-
|
|
139
|
+
// Cablato qui e non nel catalogo dati: `none` non ha una riga in
|
|
140
|
+
// `prompt-catalog` (nessun prompt non è un template vuoto), quindi senza
|
|
141
|
+
// un valore proprio cadrebbe su `MODEL_DEFAULT`.
|
|
142
|
+
model: 'opus',
|
|
141
143
|
prompt: '',
|
|
142
144
|
holes: ['TASK', 'slug'],
|
|
143
145
|
fixed: {},
|
|
@@ -220,7 +222,7 @@ export const SPAWN_ACTIONS = [
|
|
|
220
222
|
surface: 'tasto c',
|
|
221
223
|
kind: null,
|
|
222
224
|
title: null,
|
|
223
|
-
model:
|
|
225
|
+
model: 'opus',
|
|
224
226
|
prompt: '',
|
|
225
227
|
holes: [],
|
|
226
228
|
fixed: {
|
package/dist/ui/modals.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Box, Text } from 'ink';
|
|
|
6
6
|
import { cut, cutParts, sanitize } from '../width.js';
|
|
7
7
|
import { FieldText } from './fields.js';
|
|
8
8
|
import { CARET, CARET_OFF, WARN } from '../glyphs.js';
|
|
9
|
-
import { EDIT_PRI, EDIT_PROG } from '../model.js';
|
|
9
|
+
import { EDIT_PRI, EDIT_PROG, } from '../model.js';
|
|
10
10
|
import { PRI_ENTRIES, PROG_ENTRIES } from '../view.js';
|
|
11
11
|
import { progressText, PRI_GLYPH, PRI_LABEL, PROG_GLYPH } from '../task-edit.js';
|
|
12
12
|
export const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id', commit: 'commit' };
|
|
@@ -60,6 +60,19 @@ export function PurgeModal({ draft, columns }) {
|
|
|
60
60
|
? `${WARN} ${draft.skipped.length} scartate (folder con file non tracciati): ${idList(draft.skipped)}`
|
|
61
61
|
: 'nessuna esclusa · ⏎ conferma · esc annulla', width) }))] }));
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Conferma di eliminazione di una CONVERSAZIONE, in flusso come `PurgeModal`
|
|
65
|
+
* e con la stessa regola: la conferma dice cosa sparisce (id e titolo) e
|
|
66
|
+
* l'effetto sul disco, non un nome di vista. Tre righe fisse, costo dichiarato
|
|
67
|
+
* in `MODAL_HEIGHT.drop`.
|
|
68
|
+
*/
|
|
69
|
+
export function DropModal({ draft, columns }) {
|
|
70
|
+
const width = Math.max(8, columns - 8);
|
|
71
|
+
const effect = draft.hasFolder
|
|
72
|
+
? 'transcript + cartella subagent rimossi da ~/.claude/projects · irreversibile'
|
|
73
|
+
: 'transcript rimosso da ~/.claude/projects · irreversibile';
|
|
74
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "red", children: ["CANC \u203A eliminare la conversazione ", draft.sessionId.slice(0, 8), "?"] }), _jsx(Text, { wrap: "truncate-end", children: cut(`«${draft.title}»`, width) }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: cut(`${effect} · ⏎ conferma · esc annulla`, width) })] }));
|
|
75
|
+
}
|
|
63
76
|
/** Gli ID del bersaglio, troncati con `+N` quando non ci stanno tutti. Il
|
|
64
77
|
* troncamento lo fa questa funzione e non `cut`, perché tagliare a metà un id
|
|
65
78
|
* (`T1…`) direbbe una cosa falsa invece di dire che manca. */
|
package/dist/ui/screens.js
CHANGED
|
@@ -83,6 +83,9 @@ export function HintBar({ mode, purge, keyLegend, indicators, columns, }) {
|
|
|
83
83
|
if (mode === 'purge') {
|
|
84
84
|
return (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] }));
|
|
85
85
|
}
|
|
86
|
+
if (mode === 'drop') {
|
|
87
|
+
return (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] }));
|
|
88
|
+
}
|
|
86
89
|
if (mode === 'edit') {
|
|
87
90
|
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"] }));
|
|
88
91
|
}
|
package/dist/viewport.js
CHANGED
|
@@ -76,6 +76,11 @@ export const MODAL_HEIGHT = {
|
|
|
76
76
|
// condizionali mutuamente esclusivi obbligherebbero il budget a sapere QUALE
|
|
77
77
|
// dei due è a schermo (stessa ragione della riga nota in DETAIL_CHROME, T111).
|
|
78
78
|
purge: 7,
|
|
79
|
+
// Conferma di eliminazione di una conversazione: marginTop + 2 bordi + 3
|
|
80
|
+
// righe (domanda con l'id, titolo, effetto sul disco + tasti). Nessuna riga
|
|
81
|
+
// condizionale: la terza nomina la cartella subagent solo quando c'è, ma la
|
|
82
|
+
// riga c'è sempre.
|
|
83
|
+
drop: 6,
|
|
79
84
|
// T52 — search e reader sono gli unici modali NON in flusso: sostituiscono i
|
|
80
85
|
// due pane invece di spingerli giù (una lista di occorrenze non entra in un
|
|
81
86
|
// box sopra il deck). Costo 0 nel budget dei pane perché quel budget non
|
package/package.json
CHANGED
package/scripts/prompt-catalog
CHANGED
|
@@ -34,19 +34,18 @@
|
|
|
34
34
|
# nel comando passato a `bash -lc`, e questo file è committato — un apice qui
|
|
35
35
|
# sarebbe un errore di scrittura, non un input da quotare.
|
|
36
36
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# non è una preferenza di questo progetto, è il default di famiglia.
|
|
37
|
+
# La colonna modello: opus su ogni kind, tranne il preflight che resta su
|
|
38
|
+
# fable. Cambiare una parola qui cambia il costo di OGNI sessione aperta su
|
|
39
|
+
# quel kind, da ogni superficie del deck — non è una preferenza di questo
|
|
40
|
+
# progetto, è il default di famiglia.
|
|
42
41
|
# `recap` è il DISPATCHER: risolve la task, ne legge il Size e passa da sé alla
|
|
43
42
|
# sotto-skill. Lo usa chi non sa se la task è un cappello — gli acceleratori
|
|
44
43
|
# della lista, dove il deck ha in mano solo tasks.md, che il Size non ce l'ha.
|
|
45
44
|
# Le due voci `recap-task`/`recap-epic` saltano quel giro: le sceglie il DETAIL,
|
|
46
45
|
# che il task file l'ha già aperto e quindi il Size lo conosce.
|
|
47
|
-
recap /loom-works:recap-status {TASK}
|
|
48
|
-
recap-task /loom-works:recap-status-task {TASK}
|
|
49
|
-
recap-epic /loom-works:recap-status-epic {TASK}
|
|
46
|
+
recap /loom-works:recap-status {TASK} opus
|
|
47
|
+
recap-task /loom-works:recap-status-task {TASK} opus
|
|
48
|
+
recap-epic /loom-works:recap-status-epic {TASK} opus
|
|
50
49
|
preflight /loom-works:preflight-task {TASK} fable
|
|
51
50
|
run /loom-works:run-task {TASK} opus
|
|
52
51
|
checkpoint /loom-works:checkpoint-task {TASK} opus
|