@aiwayds/dsh-tui-pi 0.28.0 → 1.0.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/README.md +3 -0
- package/README.zh-CN.md +3 -0
- package/lib/ask-user.d.ts +72 -0
- package/lib/ask-user.js +212 -0
- package/lib/ask-user.js.map +1 -1
- package/lib/clipboard.d.ts +114 -0
- package/lib/clipboard.js +236 -0
- package/lib/clipboard.js.map +1 -0
- package/lib/index.js +139 -41
- package/lib/index.js.map +1 -1
- package/lib/log-repair.d.ts +161 -0
- package/lib/log-repair.js +273 -0
- package/lib/log-repair.js.map +1 -0
- package/lib/remote-tail.d.ts +49 -0
- package/lib/remote-tail.js +114 -0
- package/lib/remote-tail.js.map +1 -0
- package/lib/repair-dialog.d.ts +69 -0
- package/lib/repair-dialog.js +153 -0
- package/lib/repair-dialog.js.map +1 -0
- package/lib/session.d.ts +96 -1
- package/lib/session.js +346 -39
- package/lib/session.js.map +1 -1
- package/lib/sessions.d.ts +21 -0
- package/lib/sessions.js +30 -8
- package/lib/sessions.js.map +1 -1
- package/lib/tui.js +17 -1
- package/lib/tui.js.map +1 -1
- package/lib/writer-lock.d.ts +70 -0
- package/lib/writer-lock.js +206 -0
- package/lib/writer-lock.js.map +1 -0
- package/package.json +1 -1
- package/scripts/repair-session-log.mjs +246 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The /resume repair confirmation dialog — shown when a picked session's
|
|
3
|
+
* log fails to load with a corrupt-log error (src/log-repair.ts
|
|
4
|
+
* `isCorruptLogError`). Repairing rewrites user data on disk, so it is
|
|
5
|
+
* NEVER done implicitly: this dialog is the explicit confirmation gate.
|
|
6
|
+
*
|
|
7
|
+
* Two options, in the spec's button order:
|
|
8
|
+
* 1. Repair & resume — run the guarded in-place repair, then re-enter the
|
|
9
|
+
* selected row's resume path.
|
|
10
|
+
* 2. Cancel — leave the log untouched; /resume reports the original error.
|
|
11
|
+
* ↑↓ moves the selection, `1`/`2` select directly, Enter confirms, Esc
|
|
12
|
+
* cancels — the exact keymap of the submit routing dialog (route-dialog.ts),
|
|
13
|
+
* whose pure-reducer split this file mirrors so the decision matrix stays
|
|
14
|
+
* unit-testable without a terminal. Framing/focus follow the shared overlay
|
|
15
|
+
* contract: PanelHost framing, close re-focuses the CURRENT editor instance
|
|
16
|
+
* through `restoreFocus`.
|
|
17
|
+
*/
|
|
18
|
+
import { getKeybindings } from '@earendil-works/pi-tui';
|
|
19
|
+
import { PanelHost, panelThemeFns } from "./panels.js";
|
|
20
|
+
import { BOLD, RESET, ansiFg } from "./theme/index.js";
|
|
21
|
+
import { clipToWidth, wrapText } from "./text.js";
|
|
22
|
+
/** The two choices in display order; index 0 is preselected. */
|
|
23
|
+
export const REPAIR_CONFIRM_OPTIONS = [
|
|
24
|
+
{ id: 'repair', title: 'Repair & resume', hint: 'rebuild the log in place, then enter the session' },
|
|
25
|
+
{ id: 'cancel', title: 'Cancel', hint: 'leave the log untouched' },
|
|
26
|
+
];
|
|
27
|
+
/** The fixed body copy (spec wording): what happened, what repair does, what is kept. */
|
|
28
|
+
export const REPAIR_CONFIRM_MESSAGE = 'Resume log is corrupted (likely from a historical double-writer). '
|
|
29
|
+
+ 'Repair it in place? The original is kept as .corrupt-bak.';
|
|
30
|
+
/** Footer hint — hardcoded like every other panel footer (English-only). */
|
|
31
|
+
export const REPAIR_CONFIRM_FOOTER = '↑↓ select · 1/2 pick · Enter confirm · Esc cancel';
|
|
32
|
+
export function initialRepairConfirmState() {
|
|
33
|
+
return { selected: 0 };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Apply one raw key sequence to the dialog state. Unknown keys are no-ops;
|
|
37
|
+
* anything after a settle is ignored (single terminal outcome guard).
|
|
38
|
+
*/
|
|
39
|
+
export function updateRepairConfirm(state, data) {
|
|
40
|
+
if (state.settled !== undefined)
|
|
41
|
+
return state;
|
|
42
|
+
const kb = getKeybindings();
|
|
43
|
+
if (kb.matches(data, 'tui.select.cancel'))
|
|
44
|
+
return { ...state, settled: 'cancel' };
|
|
45
|
+
if (kb.matches(data, 'tui.input.submit'))
|
|
46
|
+
return { ...state, settled: 'confirm' };
|
|
47
|
+
if (kb.matches(data, 'tui.select.up')) {
|
|
48
|
+
return { ...state, selected: Math.max(0, state.selected - 1) };
|
|
49
|
+
}
|
|
50
|
+
if (kb.matches(data, 'tui.select.down')) {
|
|
51
|
+
return { ...state, selected: Math.min(REPAIR_CONFIRM_OPTIONS.length - 1, state.selected + 1) };
|
|
52
|
+
}
|
|
53
|
+
// Digit direct-select (1-based): selects the row, Enter still confirms —
|
|
54
|
+
// same select-then-confirm split as the routing dialog.
|
|
55
|
+
const digit = /^([1-9])$/.exec(data);
|
|
56
|
+
if (digit !== null) {
|
|
57
|
+
const index = Number(digit[1]) - 1;
|
|
58
|
+
if (index < REPAIR_CONFIRM_OPTIONS.length)
|
|
59
|
+
return { ...state, selected: index };
|
|
60
|
+
}
|
|
61
|
+
return state;
|
|
62
|
+
}
|
|
63
|
+
/** Resolved dialog outcome: the chosen action, or undefined on cancel. */
|
|
64
|
+
export function repairConfirmOutcome(state) {
|
|
65
|
+
if (state.settled !== 'confirm')
|
|
66
|
+
return undefined;
|
|
67
|
+
return REPAIR_CONFIRM_OPTIONS[state.selected]?.id;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The framed overlay component. Renders the fixed body copy plus the two
|
|
71
|
+
* option rows; every key goes through {@link updateRepairConfirm}, and the
|
|
72
|
+
* first terminal key fires `onFinish` exactly once.
|
|
73
|
+
*/
|
|
74
|
+
export class RepairConfirmPanel {
|
|
75
|
+
theme;
|
|
76
|
+
onFinish;
|
|
77
|
+
requestRenderFn;
|
|
78
|
+
state = initialRepairConfirmState();
|
|
79
|
+
constructor(theme, onFinish, requestRender) {
|
|
80
|
+
this.theme = theme;
|
|
81
|
+
this.onFinish = onFinish;
|
|
82
|
+
this.requestRenderFn = requestRender;
|
|
83
|
+
}
|
|
84
|
+
invalidate() { }
|
|
85
|
+
render(width) {
|
|
86
|
+
const fns = panelThemeFns(this.theme);
|
|
87
|
+
const wrap = Math.max(2, width - 2);
|
|
88
|
+
// Word-wrap the body FIRST, then paint (iron rule: width math runs on
|
|
89
|
+
// plain text, ANSI goes on after clipping).
|
|
90
|
+
const lines = [
|
|
91
|
+
fns.accent(BOLD + clipToWidth('● Corrupted resume log', wrap) + RESET),
|
|
92
|
+
...wrapText(REPAIR_CONFIRM_MESSAGE, wrap).map(segment => fns.muted(clipToWidth(segment, wrap))),
|
|
93
|
+
'',
|
|
94
|
+
];
|
|
95
|
+
for (let i = 0; i < REPAIR_CONFIRM_OPTIONS.length; i++) {
|
|
96
|
+
const option = REPAIR_CONFIRM_OPTIONS[i];
|
|
97
|
+
const marker = i === this.state.selected ? '▸' : ' ';
|
|
98
|
+
const row = clipToWidth(`${marker} ${i + 1}. ${option.title} — ${option.hint}`, wrap);
|
|
99
|
+
lines.push(i === this.state.selected
|
|
100
|
+
? ansiFg(this.theme.palette.accent) + BOLD + row + RESET
|
|
101
|
+
: fns.muted(row));
|
|
102
|
+
}
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push(fns.subtle(clipToWidth(REPAIR_CONFIRM_FOOTER, wrap)));
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
handleInput(data) {
|
|
108
|
+
const previous = this.state;
|
|
109
|
+
this.state = updateRepairConfirm(this.state, data);
|
|
110
|
+
if (this.state === previous)
|
|
111
|
+
return;
|
|
112
|
+
if (this.state.settled === undefined) {
|
|
113
|
+
this.requestRenderFn();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.onFinish(repairConfirmOutcome(this.state));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Open the repair confirmation dialog for one corrupt resume target.
|
|
121
|
+
* Resolves `'repair'` when the user explicitly confirmed, `'cancelled'`
|
|
122
|
+
* otherwise (Esc, or an overlay that failed to mount — treated as cancel so
|
|
123
|
+
* a half-mounted dialog can never imply consent). Closing always hands
|
|
124
|
+
* focus back through `restoreFocus` before the promise settles.
|
|
125
|
+
*/
|
|
126
|
+
export function openRepairConfirmDialog(tui, theme, restoreFocus) {
|
|
127
|
+
return new Promise(resolve => {
|
|
128
|
+
let settled = false;
|
|
129
|
+
const settle = (outcome) => {
|
|
130
|
+
if (settled)
|
|
131
|
+
return;
|
|
132
|
+
settled = true;
|
|
133
|
+
resolve(outcome);
|
|
134
|
+
};
|
|
135
|
+
// A half-mounted overlay must not strand the keyboard: PanelHost's
|
|
136
|
+
// onError closes + calls restoreFocus, then we settle as cancelled.
|
|
137
|
+
const host = new PanelHost(tui, theme, () => {
|
|
138
|
+
restoreFocus();
|
|
139
|
+
settle('cancelled');
|
|
140
|
+
});
|
|
141
|
+
const finish = (outcome) => {
|
|
142
|
+
host.close();
|
|
143
|
+
restoreFocus();
|
|
144
|
+
settle(outcome === 'repair' ? 'repair' : 'cancelled');
|
|
145
|
+
};
|
|
146
|
+
const panel = new RepairConfirmPanel(theme, outcome => finish(outcome), () => tui.requestRender());
|
|
147
|
+
// maxHeight is a hard slice in pi-tui: ~9 content rows + 5 frame rows
|
|
148
|
+
// must fit or the bottom border (and footer) is cut. 75% of the 24-row
|
|
149
|
+
// e2e floor is 18 — the same headroom the /resume picker reserves.
|
|
150
|
+
host.open(panel, '70%', '75%');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=repair-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repair-dialog.js","sourceRoot":"","sources":["../src/repair-dialog.ts"],"names":[],"mappings":"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"}
|
package/lib/session.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ export interface BridgeCallbacks {
|
|
|
19
19
|
onEvent(event: SessionEvent): void;
|
|
20
20
|
/** Whole-agent lifecycle transition. */
|
|
21
21
|
onStatus(status: 'idle' | 'running'): void;
|
|
22
|
+
/**
|
|
23
|
+
* Watched remote session is idle and queued follow-ups await: the bridge
|
|
24
|
+
* already took the writer lock; rebind locally (like a manual /resume)
|
|
25
|
+
* then drain takePendingRemoteFollowups(). Throw ⇒ back to watching;
|
|
26
|
+
* the poll retries. Absent ⇒ queuing still works, waits for manual resume.
|
|
27
|
+
*/
|
|
28
|
+
onRemotePromotable?(sessionId: string): Promise<void>;
|
|
22
29
|
/** Live snapshot of tracked subagent (child session) rows, on any change. */
|
|
23
30
|
onLive(agents: readonly AgentView[]): void;
|
|
24
31
|
/**
|
|
@@ -58,6 +65,30 @@ export declare class DshSessionBridge {
|
|
|
58
65
|
private resuming;
|
|
59
66
|
/** Target id of the in-flight resume — see `resume()`/`getResumingSessionId()`. */
|
|
60
67
|
private resumeTargetId;
|
|
68
|
+
/**
|
|
69
|
+
* Session dirs whose cross-process writer lock THIS bridge established via
|
|
70
|
+
* a cold arm. Release is bound to the owning handle's disposal
|
|
71
|
+
* (`ownWithLock`), so the set itself only tracks double-release safety.
|
|
72
|
+
*/
|
|
73
|
+
private readonly heldLockDirs;
|
|
74
|
+
/** Active cross-process read-only view (see watchRemote). */
|
|
75
|
+
private remoteTail;
|
|
76
|
+
/** Session watched read-only right now. */
|
|
77
|
+
private remoteWatchId;
|
|
78
|
+
/** Follow-ups typed while watching — sent once promotion wins the lock. */
|
|
79
|
+
private readonly remoteFollowups;
|
|
80
|
+
private promotingRemote;
|
|
81
|
+
private promoteTimer;
|
|
82
|
+
private idleReleaseTimer;
|
|
83
|
+
/** Writer lock taken for the CURRENTLY bound local agent (see drive arms). */
|
|
84
|
+
private driveLock;
|
|
85
|
+
/** Test seam + tuning for the watcher (interval/decoder injection). */
|
|
86
|
+
private readonly remoteTailOptions;
|
|
87
|
+
private readonly idleReleaseDelayMs;
|
|
88
|
+
private readonly promoteIntervalMs;
|
|
89
|
+
/** Serializes watch-lifecycle ops against the promotion poll. */
|
|
90
|
+
private opChain;
|
|
91
|
+
private runExclusive;
|
|
61
92
|
private readonly disposers;
|
|
62
93
|
private sessionId;
|
|
63
94
|
/** Agent preset id to pass to `meta.agentPreset` on the next `createSession`. */
|
|
@@ -153,7 +184,14 @@ export declare class DshSessionBridge {
|
|
|
153
184
|
private readonly trackedSessions;
|
|
154
185
|
/** `${runId}:${seq}` → childId, for `tool-workflow/agent-end` pairing. */
|
|
155
186
|
private readonly runSeqToChild;
|
|
156
|
-
constructor(ctx: Context, callbacks: BridgeCallbacks
|
|
187
|
+
constructor(ctx: Context, callbacks: BridgeCallbacks, options?: {
|
|
188
|
+
remoteTailOptions?: {
|
|
189
|
+
intervalMs?: number;
|
|
190
|
+
decode?(file: string): Promise<string>;
|
|
191
|
+
};
|
|
192
|
+
idleReleaseDelayMs?: number;
|
|
193
|
+
promoteIntervalMs?: number;
|
|
194
|
+
});
|
|
157
195
|
/**
|
|
158
196
|
* Re-derive each tracked child's round count from its own session log
|
|
159
197
|
* (`ctx.sessions`), the authoritative append-only facts. The firehose
|
|
@@ -198,6 +236,38 @@ export declare class DshSessionBridge {
|
|
|
198
236
|
isChildLogTruncated(childId: string): boolean;
|
|
199
237
|
/** Queue one user prompt, creating the session lazily on first use. */
|
|
200
238
|
prompt(text: string): Promise<void>;
|
|
239
|
+
/** While a remote watch is active input QUEUES instead of driving. */
|
|
240
|
+
isReadOnlyView(): boolean;
|
|
241
|
+
/** Follow-ups typed while watching, oldest first; takes them all. */
|
|
242
|
+
takePendingRemoteFollowups(): string[];
|
|
243
|
+
/**
|
|
244
|
+
* READ-ONLY cross-process view: another dsh process drives this session
|
|
245
|
+
* (the single-writer guard refused our cold resume), so sync DISPLAY from
|
|
246
|
+
* its persisted log instead — poll-decode, emit durable events through
|
|
247
|
+
* the same render pipeline a live session uses. Contract: latency ≈ poll
|
|
248
|
+
* interval, streaming detail omitted, every turn's final assistant
|
|
249
|
+
* message arrives. Replaces any prior binding; ends via stopRemoteWatch
|
|
250
|
+
* on dispose or the next resume.
|
|
251
|
+
*/
|
|
252
|
+
watchRemote(sessionId: string): Promise<void>;
|
|
253
|
+
private watchRemoteInner;
|
|
254
|
+
private stopRemoteWatch;
|
|
255
|
+
/**
|
|
256
|
+
* Contract "idle releases the write; next input takes it": when OUR agent
|
|
257
|
+
* settles with nothing pending locally (own re-wake included), drop the
|
|
258
|
+
* writer lock so another surface can take over. Delayed once and
|
|
259
|
+
* re-checked because upstream may latch idle only to immediately replay
|
|
260
|
+
* a queued wake as a new turn.
|
|
261
|
+
*/
|
|
262
|
+
private scheduleIdleRelease;
|
|
263
|
+
/**
|
|
264
|
+
* Promote poll tick: viewer + queued follow-ups + watched driver idle →
|
|
265
|
+
* race for the writer lock; winning hands to onRemotePromotable (UI
|
|
266
|
+
* rebuild via resume + queue drain), losing retries next tick. All async
|
|
267
|
+
* sequencing stays in THIS loop — never inside event callbacks.
|
|
268
|
+
*/
|
|
269
|
+
private maybePromoteRemote;
|
|
270
|
+
private maybePromoteRemoteInner;
|
|
201
271
|
/** The live agent, creating the session lazily when needed. */
|
|
202
272
|
ensureAgent(): Promise<Agent>;
|
|
203
273
|
/** The live agent when a session already exists (no creation side effect). */
|
|
@@ -239,6 +309,31 @@ export declare class DshSessionBridge {
|
|
|
239
309
|
* an empty `onLive` is emitted so the renderer drops the agents block.
|
|
240
310
|
*/
|
|
241
311
|
detachCurrent(): Promise<void>;
|
|
312
|
+
/**
|
|
313
|
+
* Historical cwd for a session, from the persistence header list — the
|
|
314
|
+
* log lives under THAT project key. All access best-effort: an absent
|
|
315
|
+
* service or odd host yields undefined and the arm stays UNGUARDED
|
|
316
|
+
* (fail-open, never locks a decoy), never breaks the resume.
|
|
317
|
+
*/
|
|
318
|
+
private headerCwdOf;
|
|
319
|
+
/**
|
|
320
|
+
* Single-writer guard for COLD arms (create / cold resume) only — the
|
|
321
|
+
* adopt arm above shares the live instance in-process and must stay
|
|
322
|
+
* lock-free. Locks sit beside the jsonl log so every process that could
|
|
323
|
+
* append to it competes on one file; a live foreign holder throws
|
|
324
|
+
* WriterLockedError (surfaces as "Cannot resume <id>: session is locked
|
|
325
|
+
* by a live process …"). `knownCwd` skips the header scan for creations,
|
|
326
|
+
* which always target process.cwd().
|
|
327
|
+
*/
|
|
328
|
+
private acquireSessionLock;
|
|
329
|
+
/** Undo a guard whose arm then failed — transient errors must not pin the session. */
|
|
330
|
+
private releaseColdLock;
|
|
331
|
+
/**
|
|
332
|
+
* Bind lock release to an owned handle's disposal — every teardown path
|
|
333
|
+
* already goes through handle.dispose(), so this is the single choke
|
|
334
|
+
* point; switching away or exiting releases what we established.
|
|
335
|
+
*/
|
|
336
|
+
private ownWithLock;
|
|
242
337
|
/**
|
|
243
338
|
* Resume a persisted session: tear down the live agent (disposers are kept —
|
|
244
339
|
* the session-id filter re-binds to the resumed id) and load the persisted
|