@wrongstack/tui 0.5.0 → 0.5.3
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/index.d.ts +6 -0
- package/dist/index.js +153 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,12 @@ interface RunTuiOptions {
|
|
|
114
114
|
* Ignored when `initialGoal` is also set.
|
|
115
115
|
*/
|
|
116
116
|
initialAsk?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Directory containing session JSONL files. Required for rewind
|
|
119
|
+
* functionality. When provided the TUI can list checkpoints and
|
|
120
|
+
* trigger a rewind via `/rewind` or Ctrl+R.
|
|
121
|
+
*/
|
|
122
|
+
sessionsDir?: string;
|
|
117
123
|
/**
|
|
118
124
|
* SDD session context getter. When an SDD session is active, returns
|
|
119
125
|
* the AI prompt context to inject into user messages.
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { render, useApp, Box, useStdout, Static, Text, useInput, useStdin } from
|
|
|
2
2
|
import React2, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
|
|
3
3
|
import * as fs2 from 'fs/promises';
|
|
4
4
|
import * as path2 from 'path';
|
|
5
|
-
import { InputBuilder, formatTodosList, buildChildEnv } from '@wrongstack/core';
|
|
5
|
+
import { InputBuilder, DefaultSessionRewinder, formatTodosList, buildChildEnv } from '@wrongstack/core';
|
|
6
6
|
import { routeImagesForModel } from '@wrongstack/runtime/vision';
|
|
7
7
|
import { readClipboardImage } from '@wrongstack/runtime/clipboard';
|
|
8
8
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
@@ -94,6 +94,49 @@ function ConfirmPrompt({
|
|
|
94
94
|
}
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
|
+
function CheckpointTimeline({
|
|
98
|
+
checkpoints,
|
|
99
|
+
selected,
|
|
100
|
+
onSelect,
|
|
101
|
+
onConfirm,
|
|
102
|
+
onClose
|
|
103
|
+
}) {
|
|
104
|
+
useInput((_, key) => {
|
|
105
|
+
if (key.escape) {
|
|
106
|
+
onClose();
|
|
107
|
+
} else if (key.upArrow) {
|
|
108
|
+
onSelect(Math.max(0, selected - 1));
|
|
109
|
+
} else if (key.downArrow) {
|
|
110
|
+
onSelect(Math.min(checkpoints.length - 1, selected + 1));
|
|
111
|
+
} else if (key.return) {
|
|
112
|
+
onConfirm(selected);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
116
|
+
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
117
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u27F2 Session Rewind" }),
|
|
118
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 \u2191/\u2193 navigate \xB7 Enter rewind \xB7 Esc cancel" })
|
|
119
|
+
] }),
|
|
120
|
+
checkpoints.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No checkpoints in this session." }) : checkpoints.map((cp, i) => {
|
|
121
|
+
const isSelected = i === selected;
|
|
122
|
+
const label = `[${cp.promptIndex}] ${cp.promptPreview}`;
|
|
123
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
124
|
+
/* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : void 0, bold: isSelected, children: isSelected ? "\u25B8 " : " " }),
|
|
125
|
+
/* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : void 0, bold: isSelected, children: label }),
|
|
126
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
127
|
+
" ",
|
|
128
|
+
new Date(cp.ts).toLocaleTimeString()
|
|
129
|
+
] }),
|
|
130
|
+
cp.fileCount > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
131
|
+
" \xB7 ",
|
|
132
|
+
cp.fileCount,
|
|
133
|
+
" file",
|
|
134
|
+
cp.fileCount !== 1 ? "s" : ""
|
|
135
|
+
] })
|
|
136
|
+
] }, cp.promptIndex);
|
|
137
|
+
})
|
|
138
|
+
] });
|
|
139
|
+
}
|
|
97
140
|
function FilePicker({ query, matches, selected }) {
|
|
98
141
|
if (matches.length === 0) {
|
|
99
142
|
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
@@ -2470,6 +2513,34 @@ function reducer(state, action) {
|
|
|
2470
2513
|
case "setStreamFleet": {
|
|
2471
2514
|
return { ...state, streamFleet: action.enabled };
|
|
2472
2515
|
}
|
|
2516
|
+
case "checkpointReceived": {
|
|
2517
|
+
const existing = state.checkpoints.find((c) => c.promptIndex === action.cp.promptIndex);
|
|
2518
|
+
if (existing) return state;
|
|
2519
|
+
return { ...state, checkpoints: [...state.checkpoints, action.cp] };
|
|
2520
|
+
}
|
|
2521
|
+
case "rewindOverlayOpen": {
|
|
2522
|
+
return {
|
|
2523
|
+
...state,
|
|
2524
|
+
rewindOverlay: { checkpoints: state.checkpoints, selected: state.checkpoints.length - 1 }
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
case "rewindOverlayClose": {
|
|
2528
|
+
return { ...state, rewindOverlay: null };
|
|
2529
|
+
}
|
|
2530
|
+
case "rewindOverlayMove": {
|
|
2531
|
+
if (!state.rewindOverlay) return state;
|
|
2532
|
+
const len = state.rewindOverlay.checkpoints.length;
|
|
2533
|
+
if (len === 0) return { ...state, rewindOverlay: null };
|
|
2534
|
+
const selected = Math.max(0, Math.min(len - 1, state.rewindOverlay.selected + action.delta));
|
|
2535
|
+
return { ...state, rewindOverlay: { ...state.rewindOverlay, selected } };
|
|
2536
|
+
}
|
|
2537
|
+
case "sessionRewound": {
|
|
2538
|
+
return {
|
|
2539
|
+
...state,
|
|
2540
|
+
checkpoints: state.checkpoints.filter((c) => c.promptIndex <= action.toPromptIndex),
|
|
2541
|
+
rewindOverlay: null
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2473
2544
|
}
|
|
2474
2545
|
}
|
|
2475
2546
|
var PASTE_THRESHOLD_CHARS = 200;
|
|
@@ -2605,7 +2676,8 @@ function App({
|
|
|
2605
2676
|
onClearHistory,
|
|
2606
2677
|
fleetStreamController,
|
|
2607
2678
|
initialGoal,
|
|
2608
|
-
initialAsk
|
|
2679
|
+
initialAsk,
|
|
2680
|
+
sessionsDir
|
|
2609
2681
|
}) {
|
|
2610
2682
|
const { exit } = useApp();
|
|
2611
2683
|
const [liveModel, setLiveModel] = useState(model);
|
|
@@ -2654,7 +2726,9 @@ function App({
|
|
|
2654
2726
|
contextChipVersion: 0,
|
|
2655
2727
|
fleet: {},
|
|
2656
2728
|
fleetCost: 0,
|
|
2657
|
-
streamFleet: true
|
|
2729
|
+
streamFleet: true,
|
|
2730
|
+
checkpoints: [],
|
|
2731
|
+
rewindOverlay: null
|
|
2658
2732
|
});
|
|
2659
2733
|
const builderRef = useRef(null);
|
|
2660
2734
|
if (builderRef.current === null) {
|
|
@@ -2675,6 +2749,13 @@ function App({
|
|
|
2675
2749
|
stateRef.current = state;
|
|
2676
2750
|
const draftRef = useRef({ buffer: state.buffer, cursor: state.cursor });
|
|
2677
2751
|
draftRef.current = { buffer: state.buffer, cursor: state.cursor };
|
|
2752
|
+
const handleRewindTo = React2.useCallback(async (checkpointIndex) => {
|
|
2753
|
+
const sessionId = agent.ctx.session.id;
|
|
2754
|
+
if (!sessionId) return;
|
|
2755
|
+
const rewinder = new DefaultSessionRewinder(sessionsDir ?? "");
|
|
2756
|
+
await rewinder.rewindToCheckpoint(sessionId, checkpointIndex);
|
|
2757
|
+
await agent.ctx.session.truncateToCheckpoint(checkpointIndex);
|
|
2758
|
+
}, [agent.ctx.session, sessionsDir]);
|
|
2678
2759
|
const setDraft = (buffer, cursor) => {
|
|
2679
2760
|
draftRef.current = { buffer, cursor };
|
|
2680
2761
|
dispatch({ type: "setBuffer", buffer, cursor });
|
|
@@ -3071,6 +3152,39 @@ function App({
|
|
|
3071
3152
|
slashRegistry.unregister("steer");
|
|
3072
3153
|
};
|
|
3073
3154
|
}, [slashRegistry, director]);
|
|
3155
|
+
useEffect(() => {
|
|
3156
|
+
const cmd = {
|
|
3157
|
+
name: "rewind",
|
|
3158
|
+
description: "Open checkpoint timeline to rewind session: /rewind [checkpoint-index]",
|
|
3159
|
+
help: [
|
|
3160
|
+
"Usage: /rewind [checkpoint-index]",
|
|
3161
|
+
"",
|
|
3162
|
+
"Opens a checkpoint timeline. Use \u2191/\u2193 to navigate, Enter to rewind,",
|
|
3163
|
+
"Esc to cancel. The session is reverted to the selected checkpoint",
|
|
3164
|
+
"and conversation history is truncated \u2014 LLM continues fresh.",
|
|
3165
|
+
"",
|
|
3166
|
+
"If a checkpoint index is provided the timeline is skipped and",
|
|
3167
|
+
"rewind happens immediately."
|
|
3168
|
+
].join("\n"),
|
|
3169
|
+
async run(args) {
|
|
3170
|
+
const idx = Number.parseInt(args.trim(), 10);
|
|
3171
|
+
if (!Number.isNaN(idx) && idx >= 0) {
|
|
3172
|
+
handleRewindTo(idx);
|
|
3173
|
+
return {};
|
|
3174
|
+
}
|
|
3175
|
+
const s = stateRef.current;
|
|
3176
|
+
if (s.checkpoints.length === 0) {
|
|
3177
|
+
return { message: "No checkpoints in this session yet." };
|
|
3178
|
+
}
|
|
3179
|
+
dispatch({ type: "rewindOverlayOpen" });
|
|
3180
|
+
return {};
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
slashRegistry.register(cmd);
|
|
3184
|
+
return () => {
|
|
3185
|
+
slashRegistry.unregister("rewind");
|
|
3186
|
+
};
|
|
3187
|
+
}, [slashRegistry, handleRewindTo]);
|
|
3074
3188
|
useEffect(() => {
|
|
3075
3189
|
const cmd = {
|
|
3076
3190
|
name: "goal",
|
|
@@ -3341,6 +3455,30 @@ function App({
|
|
|
3341
3455
|
offTool();
|
|
3342
3456
|
};
|
|
3343
3457
|
}, [events, director]);
|
|
3458
|
+
useEffect(() => {
|
|
3459
|
+
const offCheckpoint = events.on("checkpoint.written", (e) => {
|
|
3460
|
+
dispatch({
|
|
3461
|
+
type: "checkpointReceived",
|
|
3462
|
+
cp: {
|
|
3463
|
+
promptIndex: e.promptIndex,
|
|
3464
|
+
promptPreview: e.promptPreview,
|
|
3465
|
+
ts: e.ts,
|
|
3466
|
+
fileCount: e.fileCount
|
|
3467
|
+
}
|
|
3468
|
+
});
|
|
3469
|
+
});
|
|
3470
|
+
const offRewound = events.on("session.rewound", (_e) => {
|
|
3471
|
+
dispatch({ type: "sessionRewound", toPromptIndex: 0 });
|
|
3472
|
+
dispatch({ type: "clearHistory" });
|
|
3473
|
+
if (onClearHistory) {
|
|
3474
|
+
onClearHistory(dispatch);
|
|
3475
|
+
}
|
|
3476
|
+
});
|
|
3477
|
+
return () => {
|
|
3478
|
+
offCheckpoint();
|
|
3479
|
+
offRewound();
|
|
3480
|
+
};
|
|
3481
|
+
}, [events, onClearHistory]);
|
|
3344
3482
|
useEffect(() => {
|
|
3345
3483
|
if (!fleetStreamController) return;
|
|
3346
3484
|
fleetStreamController.enabled = state.streamFleet;
|
|
@@ -4114,6 +4252,16 @@ User message:
|
|
|
4114
4252
|
hint: state.modelPicker.hint
|
|
4115
4253
|
}
|
|
4116
4254
|
) : null,
|
|
4255
|
+
state.rewindOverlay ? /* @__PURE__ */ jsx(
|
|
4256
|
+
CheckpointTimeline,
|
|
4257
|
+
{
|
|
4258
|
+
checkpoints: state.rewindOverlay.checkpoints,
|
|
4259
|
+
selected: state.rewindOverlay.selected,
|
|
4260
|
+
onSelect: (i) => dispatch({ type: "rewindOverlayMove", delta: i - state.rewindOverlay.selected }),
|
|
4261
|
+
onConfirm: (i) => handleRewindTo(state.rewindOverlay.checkpoints[i].promptIndex),
|
|
4262
|
+
onClose: () => dispatch({ type: "rewindOverlayClose" })
|
|
4263
|
+
}
|
|
4264
|
+
) : null,
|
|
4117
4265
|
state.confirmQueue.length > 0 && (() => {
|
|
4118
4266
|
const head = state.confirmQueue[0];
|
|
4119
4267
|
let resolved = false;
|
|
@@ -4293,7 +4441,8 @@ async function runTui(opts) {
|
|
|
4293
4441
|
initialGoal: opts.initialGoal,
|
|
4294
4442
|
initialAsk: opts.initialAsk,
|
|
4295
4443
|
getSDDContext: opts.getSDDContext,
|
|
4296
|
-
onSDDOutput: opts.onSDDOutput
|
|
4444
|
+
onSDDOutput: opts.onSDDOutput,
|
|
4445
|
+
sessionsDir: opts.sessionsDir
|
|
4297
4446
|
}),
|
|
4298
4447
|
{ exitOnCtrlC: false }
|
|
4299
4448
|
);
|