@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 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
  );