topchester-ai 0.47.0 → 0.49.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/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as runTopchesterCli } from "./cli-BHctQstt.mjs";
2
+ import { t as runTopchesterCli } from "./cli-Dwc4y4H4.mjs";
3
3
  //#region src/bin.ts
4
4
  await runTopchesterCli();
5
5
  //#endregion
@@ -8620,6 +8620,48 @@ async function loadSession(workspaceRoot, sessionIdOrLatest) {
8620
8620
  events
8621
8621
  };
8622
8622
  }
8623
+ async function listSessionSummaries(workspaceRoot, options = {}) {
8624
+ if (options.excludeSessionId !== void 0) validateSessionId(options.excludeSessionId);
8625
+ const sessionsPath = getTopchesterSessionsPath(workspaceRoot);
8626
+ let entries;
8627
+ try {
8628
+ entries = await readdir(sessionsPath);
8629
+ } catch {
8630
+ return [];
8631
+ }
8632
+ const summaries = [];
8633
+ for (const sessionId of entries.filter((entry) => SESSION_ID_PATTERN.test(entry)).sort()) {
8634
+ if (options.excludeSessionId === sessionId) continue;
8635
+ const sessionDir = join(sessionsPath, sessionId);
8636
+ const metadataPath = join(sessionDir, "metadata.json");
8637
+ const eventsPath = join(sessionDir, "events.jsonl");
8638
+ let metadata;
8639
+ let events;
8640
+ try {
8641
+ metadata = await readMetadata(metadataPath);
8642
+ validateMetadataConsistency(metadata, sessionId, workspaceRoot, metadataPath);
8643
+ if (metadata.source === "subagent" && options.includeSubagents !== true) continue;
8644
+ events = await readEvents(eventsPath);
8645
+ validateEventConsistency(metadata, events, eventsPath);
8646
+ } catch {
8647
+ continue;
8648
+ }
8649
+ const prompt = firstUserPrompt(events);
8650
+ summaries.push({
8651
+ sessionId,
8652
+ createdAt: metadata.createdAt,
8653
+ updatedAt: metadata.updatedAt,
8654
+ ...metadata.title === void 0 ? {} : { title: metadata.title },
8655
+ ...metadata.forkedFromSessionId === void 0 ? {} : { forkedFromSessionId: metadata.forkedFromSessionId },
8656
+ ...prompt === void 0 ? {} : { firstUserPrompt: prompt }
8657
+ });
8658
+ }
8659
+ const sorted = summaries.sort((left, right) => {
8660
+ const byUpdatedAt = right.updatedAt.localeCompare(left.updatedAt);
8661
+ return byUpdatedAt === 0 ? right.sessionId.localeCompare(left.sessionId) : byUpdatedAt;
8662
+ });
8663
+ return options.limit === void 0 ? sorted : sorted.slice(0, Math.max(0, options.limit));
8664
+ }
8623
8665
  async function resolveLatestSessionId(workspaceRoot) {
8624
8666
  const sessionsPath = getTopchesterSessionsPath(workspaceRoot);
8625
8667
  let entries;
@@ -8796,6 +8838,12 @@ function formatZodError(error) {
8796
8838
  function isVisibleOnlyMessage(meta) {
8797
8839
  return typeof meta === "object" && meta !== null && "visibleOnly" in meta && meta.visibleOnly === true;
8798
8840
  }
8841
+ function firstUserPrompt(events) {
8842
+ for (const event of events) if (event.kind === "message" && event.role === "user" && !isVisibleOnlySlashCommandMessage(event.meta)) return event.text;
8843
+ }
8844
+ function isVisibleOnlySlashCommandMessage(meta) {
8845
+ return typeof meta === "object" && meta !== null && "source" in meta && meta.source === "slash_command" && "visibleOnly" in meta && meta.visibleOnly === true;
8846
+ }
8799
8847
  function isFileNotFoundError(error) {
8800
8848
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
8801
8849
  }
@@ -9093,6 +9141,10 @@ const slashCommandSuggestions = [
9093
9141
  {
9094
9142
  value: "/fork",
9095
9143
  description: "fork the current session"
9144
+ },
9145
+ {
9146
+ value: "/restore",
9147
+ description: "restore a previous session"
9096
9148
  }
9097
9149
  ];
9098
9150
  const slashCommands = [
@@ -9145,6 +9197,11 @@ const slashCommands = [
9145
9197
  name: "fork",
9146
9198
  description: "fork the current interactive TUI session",
9147
9199
  execute: executeForkCommand
9200
+ },
9201
+ {
9202
+ name: "restore",
9203
+ description: "restore a previous interactive TUI session",
9204
+ execute: executeRestoreCommand
9148
9205
  }
9149
9206
  ];
9150
9207
  function parseSlashCommand(input) {
@@ -9165,7 +9222,7 @@ async function executeSlashCommand(input, context) {
9165
9222
  if (!command) {
9166
9223
  const shortcutResult = await executeSkillShortcutCommand(parsed.name, parsed.args, context);
9167
9224
  if (shortcutResult) return shortcutResult;
9168
- return { messages: [`Unknown command: /${parsed.name}`, "Try /kb status, /new, or /fork."] };
9225
+ return { messages: [`Unknown command: /${parsed.name}`, "Try /kb status, /new, /fork, or /restore."] };
9169
9226
  }
9170
9227
  return command.execute(parsed.args, context);
9171
9228
  }
@@ -9265,6 +9322,9 @@ function executeNewCommand() {
9265
9322
  function executeForkCommand() {
9266
9323
  return { messages: ["/fork clones the current session in the interactive TUI."] };
9267
9324
  }
9325
+ function executeRestoreCommand() {
9326
+ return { messages: ["/restore opens a previous-session picker in the interactive TUI."] };
9327
+ }
9268
9328
  function executeInteractiveOnlyCommand(command) {
9269
9329
  return () => ({ messages: [`${command} is available in the interactive TUI.`] });
9270
9330
  }
@@ -9631,6 +9691,9 @@ var ChatLayout = class {
9631
9691
  submitMessage;
9632
9692
  submitCommand;
9633
9693
  modalActionHandler;
9694
+ sessionPicker;
9695
+ sessionPickerSelectionHandler;
9696
+ sessionPickerCancelHandler;
9634
9697
  activeModalActionIndex = 0;
9635
9698
  activeSlashSuggestionIndex = 0;
9636
9699
  threadScrollOffset = 0;
@@ -9722,12 +9785,34 @@ var ChatLayout = class {
9722
9785
  setModalActionHandler(handler) {
9723
9786
  this.modalActionHandler = handler;
9724
9787
  }
9788
+ setSessionPickerHandlers(handlers) {
9789
+ this.sessionPickerSelectionHandler = handlers.select;
9790
+ this.sessionPickerCancelHandler = handlers.cancel;
9791
+ }
9792
+ openSessionPicker(items) {
9793
+ this.sessionPicker = {
9794
+ items,
9795
+ selectedIndex: 0,
9796
+ scrollOffset: 0
9797
+ };
9798
+ this.threadScrollOffset = 0;
9799
+ }
9800
+ closeSessionPicker() {
9801
+ this.sessionPicker = void 0;
9802
+ }
9803
+ isSessionPickerOpen() {
9804
+ return this.sessionPicker !== void 0;
9805
+ }
9725
9806
  setInputValue(value) {
9726
9807
  this.promptValue = value;
9727
9808
  this.promptCursor = value.length;
9728
9809
  this.pastedContent.clear();
9729
9810
  this.pasteCounter = 0;
9730
9811
  }
9812
+ discardLastUserMessage(text) {
9813
+ const last = this.messages.at(-1);
9814
+ if (last?.kind === "user" && last.text === text) this.messages.pop();
9815
+ }
9731
9816
  resetForNewSession(messages) {
9732
9817
  this.messages.splice(0, this.messages.length, ...messages);
9733
9818
  this.promptValue = "";
@@ -9744,6 +9829,9 @@ var ChatLayout = class {
9744
9829
  this.taskPlan = void 0;
9745
9830
  this.cancelPending = void 0;
9746
9831
  this.modalActionHandler = void 0;
9832
+ this.sessionPicker = void 0;
9833
+ this.sessionPickerSelectionHandler = void 0;
9834
+ this.sessionPickerCancelHandler = void 0;
9747
9835
  this.activeModalActionIndex = 0;
9748
9836
  this.activeSlashSuggestionIndex = 0;
9749
9837
  this.threadScrollOffset = 0;
@@ -9780,6 +9868,7 @@ var ChatLayout = class {
9780
9868
  this.inputFocused = value;
9781
9869
  }
9782
9870
  handleInput(data) {
9871
+ if (this.handleSessionPickerInput(data)) return;
9783
9872
  if (this.handleModalInput(data)) return;
9784
9873
  if (this.cancelPending && matchesKey(data, "escape")) {
9785
9874
  this.cancelPending();
@@ -9797,7 +9886,7 @@ var ChatLayout = class {
9797
9886
  invalidate() {}
9798
9887
  render(width) {
9799
9888
  const safeWidth = Math.max(20, width);
9800
- const footerLines = this.getActiveModal() ? this.renderModalHelp(safeWidth) : this.renderPrompt(safeWidth);
9889
+ const footerLines = this.getActiveModal() && !this.sessionPicker ? this.renderModalHelp(safeWidth) : this.renderPrompt(safeWidth);
9801
9890
  const threadHeight = Math.max(1, this.terminal.rows - footerLines.length);
9802
9891
  const allThreadLines = this.renderThread(safeWidth, threadHeight);
9803
9892
  if (this.transcriptMode === "inline") {
@@ -9817,6 +9906,7 @@ var ChatLayout = class {
9817
9906
  }
9818
9907
  }
9819
9908
  renderThread(width, threadHeight) {
9909
+ if (this.sessionPicker) return this.renderSessionPicker(width, threadHeight);
9820
9910
  const innerWidth = Math.max(1, width);
9821
9911
  const activeModalIndex = this.getActiveModalIndex();
9822
9912
  const lines = this.messages.flatMap((message, index) => {
@@ -9843,7 +9933,7 @@ var ChatLayout = class {
9843
9933
  });
9844
9934
  }
9845
9935
  renderPrompt(width) {
9846
- const slashSuggestions = this.getSlashSuggestions();
9936
+ const slashSuggestions = this.sessionPicker ? [] : this.getSlashSuggestions();
9847
9937
  const top = `┌${"─".repeat(Math.max(0, width - 2))}┐`;
9848
9938
  const bottom = `└${"─".repeat(Math.max(0, width - 2))}┘`;
9849
9939
  const prefix = "> ";
@@ -9940,6 +10030,74 @@ var ChatLayout = class {
9940
10030
  const status = formatStatusLine(this.folderName, this.modelLabel, this.status, this.knowledgeStatus, statusInnerWidth);
9941
10031
  return [truncateToWidth(` ${help}`, width, "…", true), truncateToWidth(` ${status}`, width, "…", true)];
9942
10032
  }
10033
+ renderSessionPicker(width, threadHeight) {
10034
+ const picker = this.sessionPicker;
10035
+ if (!picker) return [];
10036
+ const innerWidth = Math.max(1, width);
10037
+ const header = truncateToWidth(" Restore previous session", innerWidth, "…", true);
10038
+ const help = truncateToWidth(" Up/Down move Enter restore Esc cancel", innerWidth, "…", true);
10039
+ const availableRows = Math.max(1, threadHeight - 3);
10040
+ const items = picker.items;
10041
+ if (items.length === 0) return padLines([
10042
+ padThreadLine(ui.label(header), width),
10043
+ padThreadLine("", width),
10044
+ padThreadLine(ui.muted(" No previous sessions for this workspace."), width),
10045
+ padThreadLine(ui.muted(help), width)
10046
+ ], threadHeight, width);
10047
+ picker.selectedIndex = Math.max(0, Math.min(picker.selectedIndex, items.length - 1));
10048
+ picker.scrollOffset = getVisibleSuggestionWindowStart(items.length, picker.selectedIndex, availableRows);
10049
+ const visibleItems = items.slice(picker.scrollOffset, picker.scrollOffset + availableRows);
10050
+ return padLines([
10051
+ padThreadLine(ui.label(header), width),
10052
+ padThreadLine(ui.muted(help), width),
10053
+ padThreadLine("", width),
10054
+ ...visibleItems.map((item, index) => {
10055
+ const selected = picker.scrollOffset + index === picker.selectedIndex;
10056
+ const row = formatSessionPickerRow(item, selected, innerWidth);
10057
+ return selected ? ui.softBackground(padThreadLine(row, width)) : padThreadLine(row, width);
10058
+ })
10059
+ ], threadHeight, width);
10060
+ }
10061
+ handleSessionPickerInput(data) {
10062
+ const picker = this.sessionPicker;
10063
+ if (!picker) return false;
10064
+ if (matchesKey(data, "escape")) {
10065
+ this.closeSessionPicker();
10066
+ this.sessionPickerCancelHandler?.();
10067
+ return true;
10068
+ }
10069
+ if (picker.items.length === 0) return true;
10070
+ if (isUpKey(data)) {
10071
+ picker.selectedIndex = Math.max(0, picker.selectedIndex - 1);
10072
+ return true;
10073
+ }
10074
+ if (isDownKey(data)) {
10075
+ picker.selectedIndex = Math.min(picker.items.length - 1, picker.selectedIndex + 1);
10076
+ return true;
10077
+ }
10078
+ if (isPageUpKey(data)) {
10079
+ picker.selectedIndex = Math.max(0, picker.selectedIndex - Math.max(1, Math.floor(this.terminal.rows / 2)));
10080
+ return true;
10081
+ }
10082
+ if (isPageDownKey(data)) {
10083
+ picker.selectedIndex = Math.min(picker.items.length - 1, picker.selectedIndex + Math.max(1, Math.floor(this.terminal.rows / 2)));
10084
+ return true;
10085
+ }
10086
+ if (isHomeKey(data)) {
10087
+ picker.selectedIndex = 0;
10088
+ return true;
10089
+ }
10090
+ if (isEndKey(data)) {
10091
+ picker.selectedIndex = picker.items.length - 1;
10092
+ return true;
10093
+ }
10094
+ if (matchesKey(data, "enter") || data === "\n" || data === "\r") {
10095
+ const item = picker.items[picker.selectedIndex];
10096
+ if (item) this.sessionPickerSelectionHandler?.(item.sessionId);
10097
+ return true;
10098
+ }
10099
+ return true;
10100
+ }
9943
10101
  handleModalInput(data) {
9944
10102
  const activeModal = this.getActiveModal();
9945
10103
  if (!activeModal) return false;
@@ -10274,6 +10432,17 @@ function getVisibleSuggestionWindowStart(total, activeIndex, visibleRows) {
10274
10432
  if (total <= visibleRows) return 0;
10275
10433
  return Math.max(0, Math.min(activeIndex - visibleRows + 1, total - visibleRows));
10276
10434
  }
10435
+ function formatSessionPickerRow(item, selected, width) {
10436
+ return truncateToWidth(`${selected ? ">" : " "} ${ui.ok(formatSessionPickerDate(item.updatedAt))} ${ui.muted(item.sessionId.slice(0, 8))} ${formatSessionPickerPrompt(item.firstUserPrompt)}`, width, "…", true);
10437
+ }
10438
+ function formatSessionPickerDate(updatedAt) {
10439
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/u.exec(updatedAt);
10440
+ return match ? `${match[1]} ${match[2]}` : updatedAt.slice(0, 16).replace("T", " ");
10441
+ }
10442
+ function formatSessionPickerPrompt(prompt) {
10443
+ const normalized = prompt?.replace(/\s+/gu, " ").trim();
10444
+ return normalized && normalized.length > 0 ? normalized : "(no user prompt)";
10445
+ }
10277
10446
  function colorUserMessageBorder(line) {
10278
10447
  return line.replace("▌", ui.modelInline("▌"));
10279
10448
  }
@@ -12992,6 +13161,9 @@ function isNewSessionCommand(command) {
12992
13161
  function isForkSessionCommand(command) {
12993
13162
  return command.trim() === "/fork";
12994
13163
  }
13164
+ function isRestoreSessionCommand(command) {
13165
+ return command.trim() === "/restore";
13166
+ }
12995
13167
  function isConnectCommand(command) {
12996
13168
  const name = getSlashCommandName(command);
12997
13169
  return name === "connect" || name === "provider" || name === "providers";
@@ -13361,6 +13533,10 @@ var TopchesterTuiShell = class {
13361
13533
  await this.forkCurrentSession(app, tui);
13362
13534
  return;
13363
13535
  }
13536
+ if (isRestoreSessionCommand(command)) {
13537
+ await this.openRestoreSessionPicker(app, tui, command);
13538
+ return;
13539
+ }
13364
13540
  if (isConnectCommand(command)) {
13365
13541
  await this.submitConnectCommand(app, tui, command);
13366
13542
  return;
@@ -13805,6 +13981,75 @@ var TopchesterTuiShell = class {
13805
13981
  if (rehydrated.status) app.setStatus(rehydrated.status);
13806
13982
  tui.requestRender();
13807
13983
  }
13984
+ async openRestoreSessionPicker(app, tui, command) {
13985
+ app.discardLastUserMessage(command);
13986
+ const activeSession = this.session;
13987
+ if (!activeSession) {
13988
+ app.addMessage(systemMessage("Restore failed: no active session."));
13989
+ tui.requestRender();
13990
+ return;
13991
+ }
13992
+ if (!app.isReady()) {
13993
+ app.addMessage(systemMessage("Restore is unavailable while another operation is running."));
13994
+ tui.requestRender();
13995
+ return;
13996
+ }
13997
+ const summaries = await listSessionSummaries(this.context.workspaceRoot, { excludeSessionId: activeSession.sessionId });
13998
+ app.setSessionPickerHandlers({
13999
+ select: (sessionId) => {
14000
+ this.startBackgroundTask(app, tui, "Restore", () => this.restoreSelectedSession(app, tui, sessionId));
14001
+ },
14002
+ cancel: () => {
14003
+ tui.requestRender();
14004
+ }
14005
+ });
14006
+ app.openSessionPicker(summaries.map((summary) => ({
14007
+ sessionId: summary.sessionId,
14008
+ updatedAt: summary.updatedAt,
14009
+ ...summary.firstUserPrompt === void 0 ? {} : { firstUserPrompt: summary.firstUserPrompt }
14010
+ })));
14011
+ tui.requestRender();
14012
+ }
14013
+ async restoreSelectedSession(app, tui, sessionId) {
14014
+ let restoredSession;
14015
+ let restoredMessages;
14016
+ let restoredTaskPlan;
14017
+ let restoredStatus;
14018
+ try {
14019
+ restoredSession = await loadSessionForAppend(this.context.workspaceRoot, sessionId);
14020
+ const rehydrated = rehydrateSession((await loadSession(this.context.workspaceRoot, sessionId)).events);
14021
+ restoredTaskPlan = rehydrated.taskPlan;
14022
+ restoredStatus = rehydrated.status;
14023
+ const noticeText = `Restored session ${sessionId.slice(0, 8)}.`;
14024
+ restoredMessages = [...rehydrated.messages, systemMessage(noticeText)];
14025
+ this.session = restoredSession;
14026
+ this.sessionStartedAt = Date.now();
14027
+ this.pendingSkillActivations = [];
14028
+ if (this.taskPlanNoticeTimer) {
14029
+ clearTimeout(this.taskPlanNoticeTimer);
14030
+ this.taskPlanNoticeTimer = void 0;
14031
+ }
14032
+ try {
14033
+ await restoredSession.append({
14034
+ kind: "message",
14035
+ role: "system",
14036
+ text: noticeText
14037
+ });
14038
+ } catch (error) {
14039
+ restoredMessages.push(systemMessage(`Session save failed: ${formatPlainError(error)}`));
14040
+ }
14041
+ } catch (error) {
14042
+ app.closeSessionPicker();
14043
+ app.addMessage(systemMessage(`Restore failed: ${formatPlainError(error)}`));
14044
+ tui.requestRender();
14045
+ return;
14046
+ }
14047
+ app.closeSessionPicker();
14048
+ app.resetForNewSession(restoredMessages);
14049
+ app.setTaskPlan(restoredTaskPlan);
14050
+ if (restoredStatus) app.setStatus(restoredStatus);
14051
+ tui.requestRender();
14052
+ }
13808
14053
  async appendStartupRuntimeEvents(session, messages, events) {
13809
14054
  for (const event of events) {
13810
14055
  messages.push(...renderRuntimeEvent(event));
@@ -14553,4 +14798,4 @@ function formatDryRunSyncStatus(status) {
14553
14798
  //#endregion
14554
14799
  export { runTopchesterCli as t };
14555
14800
 
14556
- //# sourceMappingURL=cli-BHctQstt.mjs.map
14801
+ //# sourceMappingURL=cli-Dwc4y4H4.mjs.map