@robota-sdk/agent-cli 3.0.0-beta.44 → 3.0.0-beta.46

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 CHANGED
@@ -181,6 +181,34 @@ After the Edit tool runs, a `DiffBlock` component renders the change inline:
181
181
 
182
182
  Removed lines appear in red with `-`, added lines in green with `+`. Diffs longer than 10 lines show the first 8 + a `... and N more lines` summary.
183
183
 
184
+ ## Session Management
185
+
186
+ The CLI supports continuing, resuming, forking, and naming sessions.
187
+
188
+ ### CLI Flags
189
+
190
+ | Flag | Description |
191
+ | --------------------- | ------------------------------------------------ |
192
+ | `-c`, `--continue` | Continue the most recent session |
193
+ | `-r`, `--resume <id>` | Resume a specific session by ID |
194
+ | `--fork-session <id>` | Fork a session (new session with copied history) |
195
+ | `--name <name>` | Assign a name to the session at startup |
196
+
197
+ ### TUI Commands
198
+
199
+ | Command | Description |
200
+ | ---------------- | ----------------------------------- |
201
+ | `/resume` | List recent sessions and resume one |
202
+ | `/rename <name>` | Rename the current session |
203
+
204
+ ### Session Name Display
205
+
206
+ When a session has a name, it appears in three places:
207
+
208
+ - **Input border** — session name shown in the input area border
209
+ - **Terminal title** — updated via ANSI escape sequences
210
+ - **StatusBar** — displayed alongside mode, model, and context usage
211
+
184
212
  ## Slash Commands
185
213
 
186
214
  | Command | Description |
@@ -195,6 +223,8 @@ Removed lines appear in red with `-`, added lines in green with `+`. Diffs longe
195
223
  | `/context` | Context window details |
196
224
  | `/permissions` | Show permission rules |
197
225
  | `/plugin [subcommand]` | Plugin management TUI |
226
+ | `/resume` | List recent sessions and resume one |
227
+ | `/rename <name>` | Rename the current session |
198
228
  | `/exit` | Exit CLI |
199
229
 
200
230
  Typing `/` triggers an autocomplete popup with arrow-key navigation, Tab completion, and Esc to dismiss. Commands with subcommands (e.g., `/mode`, `/model`) show a nested submenu. Skill commands discovered from `.agents/skills/` and `.claude/commands/` appear alongside built-in commands.
package/dist/node/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startCli
4
- } from "./chunk-Y6VSMUKG.js";
4
+ } from "./chunk-L5G6WFOL.js";
5
5
 
6
6
  // src/bin.ts
7
7
  process.on("uncaughtException", (err) => {
@@ -3,6 +3,7 @@ import { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as
3
3
  import { join as join5, dirname as dirname2 } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { InteractiveSession as InteractiveSession2 } from "@robota-sdk/agent-sdk";
6
+ import { SessionStore } from "@robota-sdk/agent-sessions";
6
7
 
7
8
  // src/utils/cli-args.ts
8
9
  import { parseArgs } from "util";
@@ -31,12 +32,14 @@ function parseCliArgs() {
31
32
  allowPositionals: true,
32
33
  options: {
33
34
  p: { type: "boolean", short: "p", default: false },
34
- c: { type: "boolean", short: "c", default: false },
35
- r: { type: "string", short: "r" },
35
+ continue: { type: "boolean", short: "c", default: false },
36
+ resume: { type: "string", short: "r" },
36
37
  model: { type: "string" },
37
38
  language: { type: "string" },
38
39
  "permission-mode": { type: "string" },
39
40
  "max-turns": { type: "string" },
41
+ "fork-session": { type: "boolean", default: false },
42
+ name: { type: "string", short: "n" },
40
43
  version: { type: "boolean", default: false },
41
44
  reset: { type: "boolean", default: false }
42
45
  }
@@ -44,12 +47,14 @@ function parseCliArgs() {
44
47
  return {
45
48
  positional: positionals,
46
49
  printMode: values["p"] ?? false,
47
- continueMode: values["c"] ?? false,
48
- resumeId: values["r"],
50
+ continueMode: values["continue"] ?? false,
51
+ resumeId: values["resume"],
49
52
  model: values["model"],
50
53
  language: values["language"],
51
54
  permissionMode: parsePermissionMode(values["permission-mode"]),
52
55
  maxTurns: parseMaxTurns(values["max-turns"]),
56
+ forkSession: values["fork-session"] ?? false,
57
+ sessionName: values["name"],
53
58
  version: values["version"] ?? false,
54
59
  reset: values["reset"] ?? false
55
60
  };
@@ -133,8 +138,8 @@ function createProviderFromSettings(cwd, modelOverride) {
133
138
  import { render } from "ink";
134
139
 
135
140
  // src/ui/App.tsx
136
- import { useState as useState9, useRef as useRef7 } from "react";
137
- import { Box as Box11, Text as Text13, useApp, useInput as useInput7 } from "ink";
141
+ import { useState as useState10, useRef as useRef8, useEffect as useEffect4 } from "react";
142
+ import { Box as Box12, Text as Text14, useApp, useInput as useInput8 } from "ink";
138
143
  import { getModelName, createSystemMessage as createSystemMessage2, messageToHistoryEntry as messageToHistoryEntry2 } from "@robota-sdk/agent-core";
139
144
 
140
145
  // src/ui/hooks/useInteractiveSession.ts
@@ -258,7 +263,11 @@ function initializeSession(props, permissionHandler) {
258
263
  provider: props.provider,
259
264
  permissionMode: props.permissionMode,
260
265
  maxTurns: props.maxTurns,
261
- permissionHandler
266
+ permissionHandler,
267
+ sessionStore: props.sessionStore,
268
+ resumeSessionId: props.resumeSessionId,
269
+ forkSession: props.forkSession,
270
+ sessionName: props.sessionName
262
271
  });
263
272
  const registry = new CommandRegistry();
264
273
  registry.addSource(new BuiltinCommandSource());
@@ -313,6 +322,12 @@ function useInteractiveSession(props) {
313
322
  }
314
323
  const { interactiveSession, registry, manager } = stateRef.current;
315
324
  manager.onChange = () => forceRender((n) => n + 1);
325
+ if (manager.history.length === 0) {
326
+ const restored = interactiveSession.getFullHistory();
327
+ if (restored.length > 0) {
328
+ manager.syncHistory(restored);
329
+ }
330
+ }
316
331
  useEffect(() => {
317
332
  interactiveSession.on("text_delta", manager.onTextDelta);
318
333
  interactiveSession.on("tool_start", manager.onToolStart);
@@ -329,6 +344,10 @@ function useInteractiveSession(props) {
329
344
  usedTokens: ctx.usedTokens,
330
345
  maxTokens: ctx.maxTokens
331
346
  });
347
+ const restored = interactiveSession.getFullHistory();
348
+ if (restored.length > 0) {
349
+ manager.syncHistory(restored);
350
+ }
332
351
  clearInterval(initCheck);
333
352
  } catch {
334
353
  }
@@ -372,6 +391,14 @@ function useInteractiveSession(props) {
372
391
  effects._resetRequested = true;
373
392
  return;
374
393
  }
394
+ if (result.data?.triggerResumePicker) {
395
+ effects._triggerResumePicker = true;
396
+ return;
397
+ }
398
+ if (result.data?.name) {
399
+ effects._sessionName = result.data.name;
400
+ return;
401
+ }
375
402
  const ctx = interactiveSession.getContextState();
376
403
  manager.setContextState({
377
404
  percentage: ctx.usedPercentage,
@@ -764,7 +791,7 @@ function MessageList({ history }) {
764
791
  // src/ui/StatusBar.tsx
765
792
  import { Box as Box3, Text as Text3 } from "ink";
766
793
  import { formatTokenCount } from "@robota-sdk/agent-core";
767
- import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
794
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
768
795
  var CONTEXT_YELLOW_THRESHOLD = 70;
769
796
  var CONTEXT_RED_THRESHOLD = 90;
770
797
  function getContextColor(percentage) {
@@ -780,7 +807,8 @@ function StatusBar({
780
807
  isThinking,
781
808
  contextPercentage,
782
809
  contextUsedTokens,
783
- contextMaxTokens
810
+ contextMaxTokens,
811
+ sessionName
784
812
  }) {
785
813
  const contextColor = getContextColor(contextPercentage);
786
814
  return /* @__PURE__ */ jsxs3(
@@ -796,6 +824,10 @@ function StatusBar({
796
824
  /* @__PURE__ */ jsx2(Text3, { color: "cyan", bold: true, children: "Mode:" }),
797
825
  " ",
798
826
  /* @__PURE__ */ jsx2(Text3, { children: permissionMode }),
827
+ sessionName && /* @__PURE__ */ jsxs3(Fragment2, { children: [
828
+ " | ",
829
+ /* @__PURE__ */ jsx2(Text3, { color: "magenta", children: sessionName })
830
+ ] }),
799
831
  " | ",
800
832
  /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: modelName }),
801
833
  " | ",
@@ -1137,7 +1169,8 @@ function InputArea({
1137
1169
  isDisabled,
1138
1170
  isAborting,
1139
1171
  pendingPrompt,
1140
- registry
1172
+ registry,
1173
+ sessionName
1141
1174
  }) {
1142
1175
  const [value, setValue] = useState4("");
1143
1176
  const pasteStore = useRef3(/* @__PURE__ */ new Map());
@@ -1220,6 +1253,17 @@ function InputArea({
1220
1253
  },
1221
1254
  { isActive: !!pendingPrompt }
1222
1255
  );
1256
+ const borderColor = isAborting ? "yellow" : pendingPrompt ? "cyan" : isDisabled ? "gray" : "green";
1257
+ const innerWidth = Math.max(1, terminalColumns - BORDER_HORIZONTAL);
1258
+ const topBorder = (() => {
1259
+ if (sessionName) {
1260
+ const label = ` "${sessionName}" `;
1261
+ const rightPad = 2;
1262
+ const leftLen = Math.max(0, innerWidth - label.length - rightPad);
1263
+ return { left: "\u250C" + "\u2500".repeat(leftLen), label, right: "\u2500".repeat(rightPad) + "\u2510" };
1264
+ }
1265
+ return { left: "\u250C" + "\u2500".repeat(innerWidth), label: "", right: "\u2510" };
1266
+ })();
1223
1267
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1224
1268
  showPopup && /* @__PURE__ */ jsx6(
1225
1269
  SlashAutocomplete,
@@ -1230,34 +1274,31 @@ function InputArea({
1230
1274
  isSubcommandMode
1231
1275
  }
1232
1276
  ),
1233
- /* @__PURE__ */ jsx6(
1234
- Box5,
1235
- {
1236
- borderStyle: "single",
1237
- borderColor: isAborting ? "yellow" : pendingPrompt ? "cyan" : isDisabled ? "gray" : "green",
1238
- paddingLeft: 1,
1239
- children: isAborting ? /* @__PURE__ */ jsx6(Text7, { color: "yellow", children: " Interrupting..." }) : pendingPrompt ? /* @__PURE__ */ jsxs5(Text7, { color: "cyan", children: [
1240
- " ",
1241
- "Queued: ",
1242
- pendingPrompt.length > 50 ? pendingPrompt.slice(0, 47) + "..." : pendingPrompt,
1243
- " ",
1244
- /* @__PURE__ */ jsx6(Text7, { dimColor: true, children: "(Backspace to cancel)" })
1245
- ] }) : isDisabled ? /* @__PURE__ */ jsx6(WaveText, { text: " Waiting for response... (ESC to interrupt)" }) : /* @__PURE__ */ jsxs5(Box5, { children: [
1246
- /* @__PURE__ */ jsx6(Text7, { color: "green", bold: true, children: "> " }),
1247
- /* @__PURE__ */ jsx6(
1248
- CjkTextInput,
1249
- {
1250
- value,
1251
- onChange: setValue,
1252
- onSubmit: handleSubmit,
1253
- onPaste: handlePaste,
1254
- placeholder: "Type a message or /help",
1255
- availableWidth
1256
- }
1257
- )
1258
- ] })
1259
- }
1260
- )
1277
+ /* @__PURE__ */ jsxs5(Text7, { color: borderColor, children: [
1278
+ topBorder.left,
1279
+ topBorder.label ? /* @__PURE__ */ jsx6(Text7, { backgroundColor: borderColor, color: "black", bold: true, children: topBorder.label }) : null,
1280
+ topBorder.right
1281
+ ] }),
1282
+ /* @__PURE__ */ jsx6(Box5, { borderStyle: "single", borderTop: false, borderColor, paddingLeft: 1, children: isAborting ? /* @__PURE__ */ jsx6(Text7, { color: "yellow", children: " Interrupting..." }) : pendingPrompt ? /* @__PURE__ */ jsxs5(Text7, { color: "cyan", children: [
1283
+ " ",
1284
+ "Queued: ",
1285
+ pendingPrompt.length > 50 ? pendingPrompt.slice(0, 47) + "..." : pendingPrompt,
1286
+ " ",
1287
+ /* @__PURE__ */ jsx6(Text7, { dimColor: true, children: "(Backspace to cancel)" })
1288
+ ] }) : isDisabled ? /* @__PURE__ */ jsx6(WaveText, { text: " Waiting for response... (ESC to interrupt)" }) : /* @__PURE__ */ jsxs5(Box5, { children: [
1289
+ /* @__PURE__ */ jsx6(Text7, { color: "green", bold: true, children: "> " }),
1290
+ /* @__PURE__ */ jsx6(
1291
+ CjkTextInput,
1292
+ {
1293
+ value,
1294
+ onChange: setValue,
1295
+ onSubmit: handleSubmit,
1296
+ onPaste: handlePaste,
1297
+ placeholder: "Type a message or /help",
1298
+ availableWidth
1299
+ }
1300
+ )
1301
+ ] }) })
1261
1302
  ] });
1262
1303
  }
1263
1304
 
@@ -1370,7 +1411,7 @@ function PermissionPrompt({ request }) {
1370
1411
 
1371
1412
  // src/ui/StreamingIndicator.tsx
1372
1413
  import { Box as Box8, Text as Text10 } from "ink";
1373
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1414
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1374
1415
  function getToolStyle(t) {
1375
1416
  if (t.isRunning) return { color: "yellow", icon: "\u27F3", strikethrough: false };
1376
1417
  if (t.result === "error") return { color: "red", icon: "\u2717", strikethrough: true };
@@ -1381,7 +1422,7 @@ function StreamingIndicator({ text, activeTools }) {
1381
1422
  const hasTools = activeTools.length > 0;
1382
1423
  const hasText = text.length > 0;
1383
1424
  if (!hasTools && !hasText) {
1384
- return /* @__PURE__ */ jsx9(Fragment2, {});
1425
+ return /* @__PURE__ */ jsx9(Fragment3, {});
1385
1426
  }
1386
1427
  return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1387
1428
  hasTools && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
@@ -1862,10 +1903,90 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
1862
1903
  );
1863
1904
  }
1864
1905
 
1865
- // src/ui/App.tsx
1906
+ // src/ui/ListPicker.tsx
1907
+ import { useState as useState9, useRef as useRef7 } from "react";
1908
+ import { Box as Box11, Text as Text13, useInput as useInput7 } from "ink";
1866
1909
  import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1910
+ var DEFAULT_MAX_VISIBLE = 3;
1911
+ function ListPicker({
1912
+ items,
1913
+ renderItem,
1914
+ onSelect,
1915
+ onCancel,
1916
+ maxVisible = DEFAULT_MAX_VISIBLE
1917
+ }) {
1918
+ const [selectedIndex, setSelectedIndex] = useState9(0);
1919
+ const [scrollOffset, setScrollOffset] = useState9(0);
1920
+ const selectedRef = useRef7(0);
1921
+ const resolvedRef = useRef7(false);
1922
+ useInput7((_input, key) => {
1923
+ if (resolvedRef.current) return;
1924
+ if (key.escape) {
1925
+ resolvedRef.current = true;
1926
+ onCancel();
1927
+ return;
1928
+ }
1929
+ if (items.length === 0) return;
1930
+ if (key.upArrow) {
1931
+ const next = selectedRef.current > 0 ? selectedRef.current - 1 : selectedRef.current;
1932
+ selectedRef.current = next;
1933
+ setSelectedIndex(next);
1934
+ if (next < scrollOffset) {
1935
+ setScrollOffset(next);
1936
+ }
1937
+ } else if (key.downArrow) {
1938
+ const next = selectedRef.current < items.length - 1 ? selectedRef.current + 1 : selectedRef.current;
1939
+ selectedRef.current = next;
1940
+ setSelectedIndex(next);
1941
+ if (next >= scrollOffset + maxVisible) {
1942
+ setScrollOffset(next - maxVisible + 1);
1943
+ }
1944
+ } else if (key.return) {
1945
+ const item = items[selectedRef.current];
1946
+ if (item !== void 0) {
1947
+ resolvedRef.current = true;
1948
+ onSelect(item);
1949
+ }
1950
+ }
1951
+ });
1952
+ if (items.length === 0) {
1953
+ return /* @__PURE__ */ jsx13(Box11, {});
1954
+ }
1955
+ const visibleItems = items.slice(scrollOffset, scrollOffset + maxVisible);
1956
+ const hasMore = scrollOffset + maxVisible < items.length;
1957
+ const hasLess = scrollOffset > 0;
1958
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
1959
+ hasLess && /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
1960
+ " \u2191 ",
1961
+ scrollOffset,
1962
+ " more above"
1963
+ ] }),
1964
+ visibleItems.map((item, index) => /* @__PURE__ */ jsx13(Box11, { marginBottom: 1, children: renderItem(item, scrollOffset + index === selectedIndex) }, scrollOffset + index)),
1965
+ hasMore && /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
1966
+ " \u2193 ",
1967
+ items.length - scrollOffset - maxVisible,
1968
+ " more below"
1969
+ ] })
1970
+ ] });
1971
+ }
1972
+
1973
+ // src/ui/App.tsx
1974
+ import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1867
1975
  var EXIT_DELAY_MS = 500;
1976
+ var SESSION_ID_DISPLAY_LENGTH = 8;
1868
1977
  function App(props) {
1978
+ const [activeSessionId, setActiveSessionId] = useState10(props.resumeSessionId);
1979
+ return /* @__PURE__ */ jsx14(
1980
+ AppInner,
1981
+ {
1982
+ ...props,
1983
+ resumeSessionId: activeSessionId,
1984
+ onSessionSwitch: (sessionId) => setActiveSessionId(sessionId)
1985
+ },
1986
+ activeSessionId ?? "__new__"
1987
+ );
1988
+ }
1989
+ function AppInner(props) {
1869
1990
  const { exit } = useApp();
1870
1991
  const cwd = props.cwd;
1871
1992
  const {
@@ -1887,12 +2008,28 @@ function App(props) {
1887
2008
  cwd,
1888
2009
  provider: props.provider,
1889
2010
  permissionMode: props.permissionMode,
1890
- maxTurns: props.maxTurns
2011
+ maxTurns: props.maxTurns,
2012
+ sessionStore: props.sessionStore,
2013
+ resumeSessionId: props.resumeSessionId,
2014
+ forkSession: props.forkSession,
2015
+ sessionName: props.sessionName
1891
2016
  });
1892
2017
  const pluginCallbacks = usePluginCallbacks(cwd);
1893
- const [pendingModelId, setPendingModelId] = useState9(null);
1894
- const pendingModelChangeRef = useRef7(null);
1895
- const [showPluginTUI, setShowPluginTUI] = useState9(false);
2018
+ const [pendingModelId, setPendingModelId] = useState10(null);
2019
+ const pendingModelChangeRef = useRef8(null);
2020
+ const [showPluginTUI, setShowPluginTUI] = useState10(false);
2021
+ const [showSessionPicker, setShowSessionPicker] = useState10(
2022
+ props.resumeSessionId === "__picker__"
2023
+ );
2024
+ const [sessionName, setSessionName] = useState10(props.sessionName);
2025
+ useEffect4(() => {
2026
+ const name = interactiveSession?.getName?.();
2027
+ if (name && !sessionName) setSessionName(name);
2028
+ }, [interactiveSession, sessionName]);
2029
+ useEffect4(() => {
2030
+ const title = sessionName ? `Robota \u2014 ${sessionName}` : "Robota";
2031
+ process.stdout.write(`\x1B]0;${title}\x07`);
2032
+ }, [sessionName]);
1896
2033
  const handleSubmit = async (input) => {
1897
2034
  await baseHandleSubmit(input);
1898
2035
  const sideEffects = interactiveSession;
@@ -1937,14 +2074,26 @@ function App(props) {
1937
2074
  setShowPluginTUI(true);
1938
2075
  return;
1939
2076
  }
2077
+ if (sideEffects._triggerResumePicker) {
2078
+ delete sideEffects._triggerResumePicker;
2079
+ setShowSessionPicker(true);
2080
+ return;
2081
+ }
2082
+ if (sideEffects._sessionName) {
2083
+ const name = sideEffects._sessionName;
2084
+ delete sideEffects._sessionName;
2085
+ interactiveSession.setName(name);
2086
+ setSessionName(name);
2087
+ return;
2088
+ }
1940
2089
  };
1941
- useInput7(
2090
+ useInput8(
1942
2091
  (_input, key) => {
1943
2092
  if (key.escape && isThinking) {
1944
2093
  handleAbort();
1945
2094
  }
1946
2095
  },
1947
- { isActive: !permissionRequest && !showPluginTUI }
2096
+ { isActive: !permissionRequest && !showPluginTUI && !showSessionPicker }
1948
2097
  );
1949
2098
  let permissionMode = props.permissionMode ?? "default";
1950
2099
  let sessionId = "";
@@ -1954,26 +2103,26 @@ function App(props) {
1954
2103
  sessionId = session.getSessionId();
1955
2104
  } catch {
1956
2105
  }
1957
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
1958
- /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1959
- /* @__PURE__ */ jsx13(Text13, { color: "cyan", bold: true, children: `
2106
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
2107
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
2108
+ /* @__PURE__ */ jsx14(Text14, { color: "cyan", bold: true, children: `
1960
2109
  ____ ___ ____ ___ _____ _
1961
2110
  | _ \\ / _ \\| __ ) / _ \\_ _|/ \\
1962
2111
  | |_) | | | | _ \\| | | || | / _ \\
1963
2112
  | _ <| |_| | |_) | |_| || |/ ___ \\
1964
2113
  |_| \\_\\\\___/|____/ \\___/ |_/_/ \\_\\
1965
2114
  ` }),
1966
- /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
2115
+ /* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
1967
2116
  " v",
1968
2117
  props.version ?? "0.0.0"
1969
2118
  ] })
1970
2119
  ] }),
1971
- /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
1972
- /* @__PURE__ */ jsx13(MessageList, { history }),
1973
- (isThinking || activeTools.length > 0) && /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx13(StreamingIndicator, { text: streamingText, activeTools }) })
2120
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
2121
+ /* @__PURE__ */ jsx14(MessageList, { history }),
2122
+ (isThinking || activeTools.length > 0) && /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx14(StreamingIndicator, { text: streamingText, activeTools }) })
1974
2123
  ] }),
1975
- permissionRequest && /* @__PURE__ */ jsx13(PermissionPrompt, { request: permissionRequest }),
1976
- pendingModelId && /* @__PURE__ */ jsx13(
2124
+ permissionRequest && /* @__PURE__ */ jsx14(PermissionPrompt, { request: permissionRequest }),
2125
+ pendingModelId && /* @__PURE__ */ jsx14(
1977
2126
  ConfirmPrompt,
1978
2127
  {
1979
2128
  message: `Change model to ${getModelName(pendingModelId)}? This will restart the session.`,
@@ -2007,7 +2156,7 @@ function App(props) {
2007
2156
  }
2008
2157
  }
2009
2158
  ),
2010
- showPluginTUI && /* @__PURE__ */ jsx13(
2159
+ showPluginTUI && /* @__PURE__ */ jsx14(
2011
2160
  PluginTUI,
2012
2161
  {
2013
2162
  callbacks: pluginCallbacks,
@@ -2015,7 +2164,52 @@ function App(props) {
2015
2164
  addMessage: (msg) => addEntry(messageToHistoryEntry2(createSystemMessage2(msg.content)))
2016
2165
  }
2017
2166
  ),
2018
- /* @__PURE__ */ jsx13(
2167
+ showSessionPicker && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
2168
+ /* @__PURE__ */ jsx14(Text14, { bold: true, color: "cyan", children: "Select a session to resume (ESC to cancel):" }),
2169
+ /* @__PURE__ */ jsx14(
2170
+ ListPicker,
2171
+ {
2172
+ items: (props.sessionStore?.list() ?? []).filter((s) => s.cwd === props.cwd),
2173
+ renderItem: (session, isSelected) => {
2174
+ const lastMsg = session.messages.slice().reverse().find((m) => {
2175
+ const msg = m;
2176
+ return msg.role === "assistant" && msg.content;
2177
+ });
2178
+ const rawPreview = lastMsg?.content?.replace(/[\n\r]+/g, " ").trim() ?? "";
2179
+ const preview = rawPreview ? rawPreview.slice(0, 60) + (rawPreview.length > 60 ? "..." : "") : "";
2180
+ return /* @__PURE__ */ jsxs12(Text14, { children: [
2181
+ isSelected ? "> " : " ",
2182
+ /* @__PURE__ */ jsx14(Text14, { bold: true, children: session.name ?? session.id.slice(0, SESSION_ID_DISPLAY_LENGTH) }),
2183
+ " ",
2184
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: new Date(session.updatedAt).toLocaleString(void 0, {
2185
+ month: "short",
2186
+ day: "numeric",
2187
+ hour: "2-digit",
2188
+ minute: "2-digit"
2189
+ }) }),
2190
+ " ",
2191
+ /* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
2192
+ "msgs: ",
2193
+ session.messages.length
2194
+ ] }),
2195
+ preview ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
2196
+ "\n ",
2197
+ /* @__PURE__ */ jsx14(Text14, { color: "gray", children: preview })
2198
+ ] }) : null
2199
+ ] });
2200
+ },
2201
+ onSelect: (session) => {
2202
+ setShowSessionPicker(false);
2203
+ props.onSessionSwitch(session.id);
2204
+ },
2205
+ onCancel: () => {
2206
+ setShowSessionPicker(false);
2207
+ addEntry(messageToHistoryEntry2(createSystemMessage2("Session resume cancelled.")));
2208
+ }
2209
+ }
2210
+ )
2211
+ ] }),
2212
+ /* @__PURE__ */ jsx14(
2019
2213
  StatusBar,
2020
2214
  {
2021
2215
  permissionMode,
@@ -2025,26 +2219,28 @@ function App(props) {
2025
2219
  isThinking,
2026
2220
  contextPercentage: contextState.percentage,
2027
2221
  contextUsedTokens: contextState.usedTokens,
2028
- contextMaxTokens: contextState.maxTokens
2222
+ contextMaxTokens: contextState.maxTokens,
2223
+ sessionName
2029
2224
  }
2030
2225
  ),
2031
- /* @__PURE__ */ jsx13(
2226
+ /* @__PURE__ */ jsx14(
2032
2227
  InputArea,
2033
2228
  {
2034
2229
  onSubmit: handleSubmit,
2035
2230
  onCancelQueue: handleCancelQueue,
2036
- isDisabled: !!permissionRequest || showPluginTUI || isThinking && !!pendingPrompt,
2231
+ isDisabled: !!permissionRequest || showPluginTUI || showSessionPicker || isThinking && !!pendingPrompt,
2037
2232
  isAborting,
2038
2233
  pendingPrompt,
2039
- registry
2234
+ registry,
2235
+ sessionName
2040
2236
  }
2041
2237
  ),
2042
- /* @__PURE__ */ jsx13(Text13, { children: " " })
2238
+ /* @__PURE__ */ jsx14(Text14, { children: " " })
2043
2239
  ] });
2044
2240
  }
2045
2241
 
2046
2242
  // src/ui/render.tsx
2047
- import { jsx as jsx14 } from "react/jsx-runtime";
2243
+ import { jsx as jsx15 } from "react/jsx-runtime";
2048
2244
  function renderApp(options) {
2049
2245
  process.on("unhandledRejection", (reason) => {
2050
2246
  process.stderr.write(`
@@ -2058,7 +2254,7 @@ function renderApp(options) {
2058
2254
  if (process.stdin.isTTY && process.stdout.isTTY) {
2059
2255
  process.stdout.write("\x1B[?2004h");
2060
2256
  }
2061
- const instance = render(/* @__PURE__ */ jsx14(App, { ...options }), {
2257
+ const instance = render(/* @__PURE__ */ jsx15(App, { ...options }), {
2062
2258
  exitOnCtrlC: true
2063
2259
  });
2064
2260
  instance.waitUntilExit().then(() => {
@@ -2227,6 +2423,28 @@ async function startCli() {
2227
2423
  const providerSettings = readProviderSettings(cwd);
2228
2424
  const modelId = args.model ?? providerSettings.model;
2229
2425
  const provider = createProviderFromSettings(cwd, args.model);
2426
+ const sessionStore = new SessionStore();
2427
+ let resumeSessionId;
2428
+ if (args.continueMode) {
2429
+ const sessions = sessionStore.list().filter((s) => s.cwd === cwd);
2430
+ if (sessions.length > 0) {
2431
+ resumeSessionId = sessions[0].id;
2432
+ }
2433
+ } else if (args.resumeId !== void 0) {
2434
+ if (args.resumeId === "") {
2435
+ resumeSessionId = "__picker__";
2436
+ } else {
2437
+ const sessions = sessionStore.list();
2438
+ const match = sessions.find((s) => s.id === args.resumeId || s.name === args.resumeId);
2439
+ if (match) {
2440
+ resumeSessionId = match.id;
2441
+ } else {
2442
+ process.stderr.write(`Session not found: ${args.resumeId}
2443
+ `);
2444
+ process.exit(1);
2445
+ }
2446
+ }
2447
+ }
2230
2448
  if (args.printMode) {
2231
2449
  const prompt = args.positional.join(" ").trim();
2232
2450
  if (prompt.length === 0) {
@@ -2237,7 +2455,9 @@ async function startCli() {
2237
2455
  cwd,
2238
2456
  provider,
2239
2457
  permissionMode: args.permissionMode ?? "bypassPermissions",
2240
- maxTurns: args.maxTurns
2458
+ maxTurns: args.maxTurns,
2459
+ sessionStore,
2460
+ sessionName: args.sessionName
2241
2461
  });
2242
2462
  await new Promise((resolve, reject) => {
2243
2463
  session.on("complete", (result) => {
@@ -2260,7 +2480,11 @@ async function startCli() {
2260
2480
  language: args.language,
2261
2481
  permissionMode: args.permissionMode,
2262
2482
  maxTurns: args.maxTurns,
2263
- version: readVersion()
2483
+ version: readVersion(),
2484
+ sessionStore,
2485
+ resumeSessionId,
2486
+ forkSession: args.forkSession,
2487
+ sessionName: args.sessionName
2264
2488
  });
2265
2489
  }
2266
2490
 
@@ -39,6 +39,7 @@ var import_node_fs3 = require("fs");
39
39
  var import_node_path5 = require("path");
40
40
  var import_node_url = require("url");
41
41
  var import_agent_sdk3 = require("@robota-sdk/agent-sdk");
42
+ var import_agent_sessions = require("@robota-sdk/agent-sessions");
42
43
 
43
44
  // src/utils/cli-args.ts
44
45
  var import_node_util = require("util");
@@ -67,12 +68,14 @@ function parseCliArgs() {
67
68
  allowPositionals: true,
68
69
  options: {
69
70
  p: { type: "boolean", short: "p", default: false },
70
- c: { type: "boolean", short: "c", default: false },
71
- r: { type: "string", short: "r" },
71
+ continue: { type: "boolean", short: "c", default: false },
72
+ resume: { type: "string", short: "r" },
72
73
  model: { type: "string" },
73
74
  language: { type: "string" },
74
75
  "permission-mode": { type: "string" },
75
76
  "max-turns": { type: "string" },
77
+ "fork-session": { type: "boolean", default: false },
78
+ name: { type: "string", short: "n" },
76
79
  version: { type: "boolean", default: false },
77
80
  reset: { type: "boolean", default: false }
78
81
  }
@@ -80,12 +83,14 @@ function parseCliArgs() {
80
83
  return {
81
84
  positional: positionals,
82
85
  printMode: values["p"] ?? false,
83
- continueMode: values["c"] ?? false,
84
- resumeId: values["r"],
86
+ continueMode: values["continue"] ?? false,
87
+ resumeId: values["resume"],
85
88
  model: values["model"],
86
89
  language: values["language"],
87
90
  permissionMode: parsePermissionMode(values["permission-mode"]),
88
91
  maxTurns: parseMaxTurns(values["max-turns"]),
92
+ forkSession: values["fork-session"] ?? false,
93
+ sessionName: values["name"],
89
94
  version: values["version"] ?? false,
90
95
  reset: values["reset"] ?? false
91
96
  };
@@ -166,11 +171,11 @@ function createProviderFromSettings(cwd, modelOverride) {
166
171
  }
167
172
 
168
173
  // src/ui/render.tsx
169
- var import_ink14 = require("ink");
174
+ var import_ink15 = require("ink");
170
175
 
171
176
  // src/ui/App.tsx
172
- var import_react12 = require("react");
173
- var import_ink13 = require("ink");
177
+ var import_react13 = require("react");
178
+ var import_ink14 = require("ink");
174
179
  var import_agent_core4 = require("@robota-sdk/agent-core");
175
180
 
176
181
  // src/ui/hooks/useInteractiveSession.ts
@@ -286,7 +291,11 @@ function initializeSession(props, permissionHandler) {
286
291
  provider: props.provider,
287
292
  permissionMode: props.permissionMode,
288
293
  maxTurns: props.maxTurns,
289
- permissionHandler
294
+ permissionHandler,
295
+ sessionStore: props.sessionStore,
296
+ resumeSessionId: props.resumeSessionId,
297
+ forkSession: props.forkSession,
298
+ sessionName: props.sessionName
290
299
  });
291
300
  const registry = new import_agent_sdk.CommandRegistry();
292
301
  registry.addSource(new import_agent_sdk.BuiltinCommandSource());
@@ -341,6 +350,12 @@ function useInteractiveSession(props) {
341
350
  }
342
351
  const { interactiveSession, registry, manager } = stateRef.current;
343
352
  manager.onChange = () => forceRender((n) => n + 1);
353
+ if (manager.history.length === 0) {
354
+ const restored = interactiveSession.getFullHistory();
355
+ if (restored.length > 0) {
356
+ manager.syncHistory(restored);
357
+ }
358
+ }
344
359
  (0, import_react.useEffect)(() => {
345
360
  interactiveSession.on("text_delta", manager.onTextDelta);
346
361
  interactiveSession.on("tool_start", manager.onToolStart);
@@ -357,6 +372,10 @@ function useInteractiveSession(props) {
357
372
  usedTokens: ctx.usedTokens,
358
373
  maxTokens: ctx.maxTokens
359
374
  });
375
+ const restored = interactiveSession.getFullHistory();
376
+ if (restored.length > 0) {
377
+ manager.syncHistory(restored);
378
+ }
360
379
  clearInterval(initCheck);
361
380
  } catch {
362
381
  }
@@ -400,6 +419,14 @@ function useInteractiveSession(props) {
400
419
  effects._resetRequested = true;
401
420
  return;
402
421
  }
422
+ if (result.data?.triggerResumePicker) {
423
+ effects._triggerResumePicker = true;
424
+ return;
425
+ }
426
+ if (result.data?.name) {
427
+ effects._sessionName = result.data.name;
428
+ return;
429
+ }
403
430
  const ctx = interactiveSession.getContextState();
404
431
  manager.setContextState({
405
432
  percentage: ctx.usedPercentage,
@@ -803,7 +830,8 @@ function StatusBar({
803
830
  isThinking,
804
831
  contextPercentage,
805
832
  contextUsedTokens,
806
- contextMaxTokens
833
+ contextMaxTokens,
834
+ sessionName
807
835
  }) {
808
836
  const contextColor = getContextColor(contextPercentage);
809
837
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -819,6 +847,10 @@ function StatusBar({
819
847
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { color: "cyan", bold: true, children: "Mode:" }),
820
848
  " ",
821
849
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { children: permissionMode }),
850
+ sessionName && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
851
+ " | ",
852
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { color: "magenta", children: sessionName })
853
+ ] }),
822
854
  " | ",
823
855
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ink3.Text, { dimColor: true, children: modelName }),
824
856
  " | ",
@@ -1160,7 +1192,8 @@ function InputArea({
1160
1192
  isDisabled,
1161
1193
  isAborting,
1162
1194
  pendingPrompt,
1163
- registry
1195
+ registry,
1196
+ sessionName
1164
1197
  }) {
1165
1198
  const [value, setValue] = (0, import_react6.useState)("");
1166
1199
  const pasteStore = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
@@ -1243,6 +1276,17 @@ function InputArea({
1243
1276
  },
1244
1277
  { isActive: !!pendingPrompt }
1245
1278
  );
1279
+ const borderColor = isAborting ? "yellow" : pendingPrompt ? "cyan" : isDisabled ? "gray" : "green";
1280
+ const innerWidth = Math.max(1, terminalColumns - BORDER_HORIZONTAL);
1281
+ const topBorder = (() => {
1282
+ if (sessionName) {
1283
+ const label = ` "${sessionName}" `;
1284
+ const rightPad = 2;
1285
+ const leftLen = Math.max(0, innerWidth - label.length - rightPad);
1286
+ return { left: "\u250C" + "\u2500".repeat(leftLen), label, right: "\u2500".repeat(rightPad) + "\u2510" };
1287
+ }
1288
+ return { left: "\u250C" + "\u2500".repeat(innerWidth), label: "", right: "\u2510" };
1289
+ })();
1246
1290
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink7.Box, { flexDirection: "column", children: [
1247
1291
  showPopup && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1248
1292
  SlashAutocomplete,
@@ -1253,34 +1297,31 @@ function InputArea({
1253
1297
  isSubcommandMode
1254
1298
  }
1255
1299
  ),
1256
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1257
- import_ink7.Box,
1258
- {
1259
- borderStyle: "single",
1260
- borderColor: isAborting ? "yellow" : pendingPrompt ? "cyan" : isDisabled ? "gray" : "green",
1261
- paddingLeft: 1,
1262
- children: isAborting ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink7.Text, { color: "yellow", children: " Interrupting..." }) : pendingPrompt ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink7.Text, { color: "cyan", children: [
1263
- " ",
1264
- "Queued: ",
1265
- pendingPrompt.length > 50 ? pendingPrompt.slice(0, 47) + "..." : pendingPrompt,
1266
- " ",
1267
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink7.Text, { dimColor: true, children: "(Backspace to cancel)" })
1268
- ] }) : isDisabled ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(WaveText, { text: " Waiting for response... (ESC to interrupt)" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink7.Box, { children: [
1269
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink7.Text, { color: "green", bold: true, children: "> " }),
1270
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1271
- CjkTextInput,
1272
- {
1273
- value,
1274
- onChange: setValue,
1275
- onSubmit: handleSubmit,
1276
- onPaste: handlePaste,
1277
- placeholder: "Type a message or /help",
1278
- availableWidth
1279
- }
1280
- )
1281
- ] })
1282
- }
1283
- )
1300
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink7.Text, { color: borderColor, children: [
1301
+ topBorder.left,
1302
+ topBorder.label ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink7.Text, { backgroundColor: borderColor, color: "black", bold: true, children: topBorder.label }) : null,
1303
+ topBorder.right
1304
+ ] }),
1305
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink7.Box, { borderStyle: "single", borderTop: false, borderColor, paddingLeft: 1, children: isAborting ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink7.Text, { color: "yellow", children: " Interrupting..." }) : pendingPrompt ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink7.Text, { color: "cyan", children: [
1306
+ " ",
1307
+ "Queued: ",
1308
+ pendingPrompt.length > 50 ? pendingPrompt.slice(0, 47) + "..." : pendingPrompt,
1309
+ " ",
1310
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink7.Text, { dimColor: true, children: "(Backspace to cancel)" })
1311
+ ] }) : isDisabled ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(WaveText, { text: " Waiting for response... (ESC to interrupt)" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink7.Box, { children: [
1312
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink7.Text, { color: "green", bold: true, children: "> " }),
1313
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1314
+ CjkTextInput,
1315
+ {
1316
+ value,
1317
+ onChange: setValue,
1318
+ onSubmit: handleSubmit,
1319
+ onPaste: handlePaste,
1320
+ placeholder: "Type a message or /help",
1321
+ availableWidth
1322
+ }
1323
+ )
1324
+ ] }) })
1284
1325
  ] });
1285
1326
  }
1286
1327
 
@@ -1885,11 +1926,91 @@ function PluginTUI({ callbacks, onClose, addMessage }) {
1885
1926
  );
1886
1927
  }
1887
1928
 
1888
- // src/ui/App.tsx
1929
+ // src/ui/ListPicker.tsx
1930
+ var import_react12 = require("react");
1931
+ var import_ink13 = require("ink");
1889
1932
  var import_jsx_runtime14 = require("react/jsx-runtime");
1933
+ var DEFAULT_MAX_VISIBLE = 3;
1934
+ function ListPicker({
1935
+ items,
1936
+ renderItem,
1937
+ onSelect,
1938
+ onCancel,
1939
+ maxVisible = DEFAULT_MAX_VISIBLE
1940
+ }) {
1941
+ const [selectedIndex, setSelectedIndex] = (0, import_react12.useState)(0);
1942
+ const [scrollOffset, setScrollOffset] = (0, import_react12.useState)(0);
1943
+ const selectedRef = (0, import_react12.useRef)(0);
1944
+ const resolvedRef = (0, import_react12.useRef)(false);
1945
+ (0, import_ink13.useInput)((_input, key) => {
1946
+ if (resolvedRef.current) return;
1947
+ if (key.escape) {
1948
+ resolvedRef.current = true;
1949
+ onCancel();
1950
+ return;
1951
+ }
1952
+ if (items.length === 0) return;
1953
+ if (key.upArrow) {
1954
+ const next = selectedRef.current > 0 ? selectedRef.current - 1 : selectedRef.current;
1955
+ selectedRef.current = next;
1956
+ setSelectedIndex(next);
1957
+ if (next < scrollOffset) {
1958
+ setScrollOffset(next);
1959
+ }
1960
+ } else if (key.downArrow) {
1961
+ const next = selectedRef.current < items.length - 1 ? selectedRef.current + 1 : selectedRef.current;
1962
+ selectedRef.current = next;
1963
+ setSelectedIndex(next);
1964
+ if (next >= scrollOffset + maxVisible) {
1965
+ setScrollOffset(next - maxVisible + 1);
1966
+ }
1967
+ } else if (key.return) {
1968
+ const item = items[selectedRef.current];
1969
+ if (item !== void 0) {
1970
+ resolvedRef.current = true;
1971
+ onSelect(item);
1972
+ }
1973
+ }
1974
+ });
1975
+ if (items.length === 0) {
1976
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ink13.Box, {});
1977
+ }
1978
+ const visibleItems = items.slice(scrollOffset, scrollOffset + maxVisible);
1979
+ const hasMore = scrollOffset + maxVisible < items.length;
1980
+ const hasLess = scrollOffset > 0;
1981
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_ink13.Box, { flexDirection: "column", children: [
1982
+ hasLess && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_ink13.Text, { dimColor: true, children: [
1983
+ " \u2191 ",
1984
+ scrollOffset,
1985
+ " more above"
1986
+ ] }),
1987
+ visibleItems.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ink13.Box, { marginBottom: 1, children: renderItem(item, scrollOffset + index === selectedIndex) }, scrollOffset + index)),
1988
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_ink13.Text, { dimColor: true, children: [
1989
+ " \u2193 ",
1990
+ items.length - scrollOffset - maxVisible,
1991
+ " more below"
1992
+ ] })
1993
+ ] });
1994
+ }
1995
+
1996
+ // src/ui/App.tsx
1997
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1890
1998
  var EXIT_DELAY_MS = 500;
1999
+ var SESSION_ID_DISPLAY_LENGTH = 8;
1891
2000
  function App(props) {
1892
- const { exit } = (0, import_ink13.useApp)();
2001
+ const [activeSessionId, setActiveSessionId] = (0, import_react13.useState)(props.resumeSessionId);
2002
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2003
+ AppInner,
2004
+ {
2005
+ ...props,
2006
+ resumeSessionId: activeSessionId,
2007
+ onSessionSwitch: (sessionId) => setActiveSessionId(sessionId)
2008
+ },
2009
+ activeSessionId ?? "__new__"
2010
+ );
2011
+ }
2012
+ function AppInner(props) {
2013
+ const { exit } = (0, import_ink14.useApp)();
1893
2014
  const cwd = props.cwd;
1894
2015
  const {
1895
2016
  interactiveSession,
@@ -1910,12 +2031,28 @@ function App(props) {
1910
2031
  cwd,
1911
2032
  provider: props.provider,
1912
2033
  permissionMode: props.permissionMode,
1913
- maxTurns: props.maxTurns
2034
+ maxTurns: props.maxTurns,
2035
+ sessionStore: props.sessionStore,
2036
+ resumeSessionId: props.resumeSessionId,
2037
+ forkSession: props.forkSession,
2038
+ sessionName: props.sessionName
1914
2039
  });
1915
2040
  const pluginCallbacks = usePluginCallbacks(cwd);
1916
- const [pendingModelId, setPendingModelId] = (0, import_react12.useState)(null);
1917
- const pendingModelChangeRef = (0, import_react12.useRef)(null);
1918
- const [showPluginTUI, setShowPluginTUI] = (0, import_react12.useState)(false);
2041
+ const [pendingModelId, setPendingModelId] = (0, import_react13.useState)(null);
2042
+ const pendingModelChangeRef = (0, import_react13.useRef)(null);
2043
+ const [showPluginTUI, setShowPluginTUI] = (0, import_react13.useState)(false);
2044
+ const [showSessionPicker, setShowSessionPicker] = (0, import_react13.useState)(
2045
+ props.resumeSessionId === "__picker__"
2046
+ );
2047
+ const [sessionName, setSessionName] = (0, import_react13.useState)(props.sessionName);
2048
+ (0, import_react13.useEffect)(() => {
2049
+ const name = interactiveSession?.getName?.();
2050
+ if (name && !sessionName) setSessionName(name);
2051
+ }, [interactiveSession, sessionName]);
2052
+ (0, import_react13.useEffect)(() => {
2053
+ const title = sessionName ? `Robota \u2014 ${sessionName}` : "Robota";
2054
+ process.stdout.write(`\x1B]0;${title}\x07`);
2055
+ }, [sessionName]);
1919
2056
  const handleSubmit = async (input) => {
1920
2057
  await baseHandleSubmit(input);
1921
2058
  const sideEffects = interactiveSession;
@@ -1960,14 +2097,26 @@ function App(props) {
1960
2097
  setShowPluginTUI(true);
1961
2098
  return;
1962
2099
  }
2100
+ if (sideEffects._triggerResumePicker) {
2101
+ delete sideEffects._triggerResumePicker;
2102
+ setShowSessionPicker(true);
2103
+ return;
2104
+ }
2105
+ if (sideEffects._sessionName) {
2106
+ const name = sideEffects._sessionName;
2107
+ delete sideEffects._sessionName;
2108
+ interactiveSession.setName(name);
2109
+ setSessionName(name);
2110
+ return;
2111
+ }
1963
2112
  };
1964
- (0, import_ink13.useInput)(
2113
+ (0, import_ink14.useInput)(
1965
2114
  (_input, key) => {
1966
2115
  if (key.escape && isThinking) {
1967
2116
  handleAbort();
1968
2117
  }
1969
2118
  },
1970
- { isActive: !permissionRequest && !showPluginTUI }
2119
+ { isActive: !permissionRequest && !showPluginTUI && !showSessionPicker }
1971
2120
  );
1972
2121
  let permissionMode = props.permissionMode ?? "default";
1973
2122
  let sessionId = "";
@@ -1977,26 +2126,26 @@ function App(props) {
1977
2126
  sessionId = session.getSessionId();
1978
2127
  } catch {
1979
2128
  }
1980
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_ink13.Box, { flexDirection: "column", children: [
1981
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_ink13.Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1982
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ink13.Text, { color: "cyan", bold: true, children: `
2129
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_ink14.Box, { flexDirection: "column", children: [
2130
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_ink14.Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
2131
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_ink14.Text, { color: "cyan", bold: true, children: `
1983
2132
  ____ ___ ____ ___ _____ _
1984
2133
  | _ \\ / _ \\| __ ) / _ \\_ _|/ \\
1985
2134
  | |_) | | | | _ \\| | | || | / _ \\
1986
2135
  | _ <| |_| | |_) | |_| || |/ ___ \\
1987
2136
  |_| \\_\\\\___/|____/ \\___/ |_/_/ \\_\\
1988
2137
  ` }),
1989
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_ink13.Text, { dimColor: true, children: [
2138
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_ink14.Text, { dimColor: true, children: [
1990
2139
  " v",
1991
2140
  props.version ?? "0.0.0"
1992
2141
  ] })
1993
2142
  ] }),
1994
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_ink13.Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
1995
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MessageList, { history }),
1996
- (isThinking || activeTools.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ink13.Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(StreamingIndicator, { text: streamingText, activeTools }) })
2143
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_ink14.Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
2144
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(MessageList, { history }),
2145
+ (isThinking || activeTools.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_ink14.Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(StreamingIndicator, { text: streamingText, activeTools }) })
1997
2146
  ] }),
1998
- permissionRequest && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(PermissionPrompt, { request: permissionRequest }),
1999
- pendingModelId && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2147
+ permissionRequest && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PermissionPrompt, { request: permissionRequest }),
2148
+ pendingModelId && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2000
2149
  ConfirmPrompt,
2001
2150
  {
2002
2151
  message: `Change model to ${(0, import_agent_core4.getModelName)(pendingModelId)}? This will restart the session.`,
@@ -2030,7 +2179,7 @@ function App(props) {
2030
2179
  }
2031
2180
  }
2032
2181
  ),
2033
- showPluginTUI && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2182
+ showPluginTUI && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2034
2183
  PluginTUI,
2035
2184
  {
2036
2185
  callbacks: pluginCallbacks,
@@ -2038,7 +2187,52 @@ function App(props) {
2038
2187
  addMessage: (msg) => addEntry((0, import_agent_core4.messageToHistoryEntry)((0, import_agent_core4.createSystemMessage)(msg.content)))
2039
2188
  }
2040
2189
  ),
2041
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2190
+ showSessionPicker && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_ink14.Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
2191
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_ink14.Text, { bold: true, color: "cyan", children: "Select a session to resume (ESC to cancel):" }),
2192
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2193
+ ListPicker,
2194
+ {
2195
+ items: (props.sessionStore?.list() ?? []).filter((s) => s.cwd === props.cwd),
2196
+ renderItem: (session, isSelected) => {
2197
+ const lastMsg = session.messages.slice().reverse().find((m) => {
2198
+ const msg = m;
2199
+ return msg.role === "assistant" && msg.content;
2200
+ });
2201
+ const rawPreview = lastMsg?.content?.replace(/[\n\r]+/g, " ").trim() ?? "";
2202
+ const preview = rawPreview ? rawPreview.slice(0, 60) + (rawPreview.length > 60 ? "..." : "") : "";
2203
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_ink14.Text, { children: [
2204
+ isSelected ? "> " : " ",
2205
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_ink14.Text, { bold: true, children: session.name ?? session.id.slice(0, SESSION_ID_DISPLAY_LENGTH) }),
2206
+ " ",
2207
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_ink14.Text, { dimColor: true, children: new Date(session.updatedAt).toLocaleString(void 0, {
2208
+ month: "short",
2209
+ day: "numeric",
2210
+ hour: "2-digit",
2211
+ minute: "2-digit"
2212
+ }) }),
2213
+ " ",
2214
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_ink14.Text, { dimColor: true, children: [
2215
+ "msgs: ",
2216
+ session.messages.length
2217
+ ] }),
2218
+ preview ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
2219
+ "\n ",
2220
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_ink14.Text, { color: "gray", children: preview })
2221
+ ] }) : null
2222
+ ] });
2223
+ },
2224
+ onSelect: (session) => {
2225
+ setShowSessionPicker(false);
2226
+ props.onSessionSwitch(session.id);
2227
+ },
2228
+ onCancel: () => {
2229
+ setShowSessionPicker(false);
2230
+ addEntry((0, import_agent_core4.messageToHistoryEntry)((0, import_agent_core4.createSystemMessage)("Session resume cancelled.")));
2231
+ }
2232
+ }
2233
+ )
2234
+ ] }),
2235
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2042
2236
  StatusBar,
2043
2237
  {
2044
2238
  permissionMode,
@@ -2048,26 +2242,28 @@ function App(props) {
2048
2242
  isThinking,
2049
2243
  contextPercentage: contextState.percentage,
2050
2244
  contextUsedTokens: contextState.usedTokens,
2051
- contextMaxTokens: contextState.maxTokens
2245
+ contextMaxTokens: contextState.maxTokens,
2246
+ sessionName
2052
2247
  }
2053
2248
  ),
2054
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2249
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2055
2250
  InputArea,
2056
2251
  {
2057
2252
  onSubmit: handleSubmit,
2058
2253
  onCancelQueue: handleCancelQueue,
2059
- isDisabled: !!permissionRequest || showPluginTUI || isThinking && !!pendingPrompt,
2254
+ isDisabled: !!permissionRequest || showPluginTUI || showSessionPicker || isThinking && !!pendingPrompt,
2060
2255
  isAborting,
2061
2256
  pendingPrompt,
2062
- registry
2257
+ registry,
2258
+ sessionName
2063
2259
  }
2064
2260
  ),
2065
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_ink13.Text, { children: " " })
2261
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_ink14.Text, { children: " " })
2066
2262
  ] });
2067
2263
  }
2068
2264
 
2069
2265
  // src/ui/render.tsx
2070
- var import_jsx_runtime15 = require("react/jsx-runtime");
2266
+ var import_jsx_runtime16 = require("react/jsx-runtime");
2071
2267
  function renderApp(options) {
2072
2268
  process.on("unhandledRejection", (reason) => {
2073
2269
  process.stderr.write(`
@@ -2081,7 +2277,7 @@ function renderApp(options) {
2081
2277
  if (process.stdin.isTTY && process.stdout.isTTY) {
2082
2278
  process.stdout.write("\x1B[?2004h");
2083
2279
  }
2084
- const instance = (0, import_ink14.render)(/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(App, { ...options }), {
2280
+ const instance = (0, import_ink15.render)(/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(App, { ...options }), {
2085
2281
  exitOnCtrlC: true
2086
2282
  });
2087
2283
  instance.waitUntilExit().then(() => {
@@ -2251,6 +2447,28 @@ async function startCli() {
2251
2447
  const providerSettings = readProviderSettings(cwd);
2252
2448
  const modelId = args.model ?? providerSettings.model;
2253
2449
  const provider = createProviderFromSettings(cwd, args.model);
2450
+ const sessionStore = new import_agent_sessions.SessionStore();
2451
+ let resumeSessionId;
2452
+ if (args.continueMode) {
2453
+ const sessions = sessionStore.list().filter((s) => s.cwd === cwd);
2454
+ if (sessions.length > 0) {
2455
+ resumeSessionId = sessions[0].id;
2456
+ }
2457
+ } else if (args.resumeId !== void 0) {
2458
+ if (args.resumeId === "") {
2459
+ resumeSessionId = "__picker__";
2460
+ } else {
2461
+ const sessions = sessionStore.list();
2462
+ const match = sessions.find((s) => s.id === args.resumeId || s.name === args.resumeId);
2463
+ if (match) {
2464
+ resumeSessionId = match.id;
2465
+ } else {
2466
+ process.stderr.write(`Session not found: ${args.resumeId}
2467
+ `);
2468
+ process.exit(1);
2469
+ }
2470
+ }
2471
+ }
2254
2472
  if (args.printMode) {
2255
2473
  const prompt = args.positional.join(" ").trim();
2256
2474
  if (prompt.length === 0) {
@@ -2261,7 +2479,9 @@ async function startCli() {
2261
2479
  cwd,
2262
2480
  provider,
2263
2481
  permissionMode: args.permissionMode ?? "bypassPermissions",
2264
- maxTurns: args.maxTurns
2482
+ maxTurns: args.maxTurns,
2483
+ sessionStore,
2484
+ sessionName: args.sessionName
2265
2485
  });
2266
2486
  await new Promise((resolve, reject) => {
2267
2487
  session.on("complete", (result) => {
@@ -2284,7 +2504,11 @@ async function startCli() {
2284
2504
  language: args.language,
2285
2505
  permissionMode: args.permissionMode,
2286
2506
  maxTurns: args.maxTurns,
2287
- version: readVersion()
2507
+ version: readVersion(),
2508
+ sessionStore,
2509
+ resumeSessionId,
2510
+ forkSession: args.forkSession,
2511
+ sessionName: args.sessionName
2288
2512
  });
2289
2513
  }
2290
2514
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startCli
3
- } from "./chunk-Y6VSMUKG.js";
3
+ } from "./chunk-L5G6WFOL.js";
4
4
  export {
5
5
  startCli
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robota-sdk/agent-cli",
3
- "version": "3.0.0-beta.44",
3
+ "version": "3.0.0-beta.46",
4
4
  "description": "AI coding assistant CLI built on Robota SDK",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,7 @@
25
25
  "dist"
26
26
  ],
27
27
  "dependencies": {
28
+ "@robota-sdk/agent-sessions": "3.0.0-beta.46",
28
29
  "chalk": "^5.3.0",
29
30
  "cli-highlight": "^2.1.0",
30
31
  "ink": "^6.8.0",
@@ -35,9 +36,9 @@
35
36
  "marked-terminal": "^7.3.0",
36
37
  "react": "19.2.4",
37
38
  "string-width": "^8.2.0",
38
- "@robota-sdk/agent-core": "3.0.0-beta.44",
39
- "@robota-sdk/agent-sdk": "3.0.0-beta.44",
40
- "@robota-sdk/agent-provider-anthropic": "3.0.0-beta.44"
39
+ "@robota-sdk/agent-core": "3.0.0-beta.46",
40
+ "@robota-sdk/agent-sdk": "3.0.0-beta.46",
41
+ "@robota-sdk/agent-provider-anthropic": "3.0.0-beta.46"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/marked": "^6.0.0",