topchester-ai 0.46.0 → 0.48.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-BgMj4Ifj.mjs";
2
+ import { t as runTopchesterCli } from "./cli-BZmxcvol.mjs";
3
3
  //#region src/bin.ts
4
4
  await runTopchesterCli();
5
5
  //#endregion
@@ -8341,6 +8341,8 @@ const sessionMetadataSchema = z.object({
8341
8341
  rootSessionId: z.string().optional(),
8342
8342
  parentSessionId: z.string().optional(),
8343
8343
  parentToolCallId: z.string().optional(),
8344
+ forkedFromSessionId: z.string().optional(),
8345
+ forkedFromRootSessionId: z.string().optional(),
8344
8346
  source: z.enum(["user", "subagent"]).optional(),
8345
8347
  agentProfileId: z.string().optional(),
8346
8348
  title: z.string().optional(),
@@ -8553,6 +8555,45 @@ async function createChildSession(workspaceRoot, options) {
8553
8555
  }));
8554
8556
  return child;
8555
8557
  }
8558
+ async function forkSession(workspaceRoot, sourceSessionIdOrLatest, options = {}) {
8559
+ const source = await loadSession(workspaceRoot, sourceSessionIdOrLatest);
8560
+ const copiedEvents = await readFile(join(source.sessionDir, "events.jsonl"), "utf8");
8561
+ const sessionsPath = getTopchesterSessionsPath(workspaceRoot);
8562
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
8563
+ for (let attempt = 0; attempt < 10; attempt += 1) {
8564
+ const sessionId = generateSessionId();
8565
+ const sessionDir = join(sessionsPath, sessionId);
8566
+ const metadataPath = join(sessionDir, "metadata.json");
8567
+ const eventsPath = join(sessionDir, "events.jsonl");
8568
+ const metadata = {
8569
+ version: 1,
8570
+ sessionId,
8571
+ rootSessionId: sessionId,
8572
+ forkedFromSessionId: source.sessionId,
8573
+ forkedFromRootSessionId: source.metadata.rootSessionId,
8574
+ source: "user",
8575
+ ...options.title === void 0 ? {} : { title: options.title },
8576
+ workspaceRoot,
8577
+ createdAt,
8578
+ updatedAt: createdAt,
8579
+ lastEventId: source.metadata.lastEventId
8580
+ };
8581
+ try {
8582
+ await mkdir(sessionDir);
8583
+ await writeMetadata(metadataPath, metadata);
8584
+ await writeFile(eventsPath, copiedEvents, { flag: "wx" });
8585
+ return buildHandle(sessionDir, metadata);
8586
+ } catch (error) {
8587
+ if (isFileExistsError(error)) continue;
8588
+ await rm(sessionDir, {
8589
+ recursive: true,
8590
+ force: true
8591
+ });
8592
+ throw error;
8593
+ }
8594
+ }
8595
+ throw new Error("Could not create forked session after repeated session id collisions");
8596
+ }
8556
8597
  async function loadSessionForAppend(workspaceRoot, sessionId) {
8557
8598
  const loaded = await loadSession(workspaceRoot, sessionId);
8558
8599
  return buildHandle(loaded.sessionDir, loaded.metadata);
@@ -8579,6 +8620,48 @@ async function loadSession(workspaceRoot, sessionIdOrLatest) {
8579
8620
  events
8580
8621
  };
8581
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
+ }
8582
8665
  async function resolveLatestSessionId(workspaceRoot) {
8583
8666
  const sessionsPath = getTopchesterSessionsPath(workspaceRoot);
8584
8667
  let entries;
@@ -8755,9 +8838,18 @@ function formatZodError(error) {
8755
8838
  function isVisibleOnlyMessage(meta) {
8756
8839
  return typeof meta === "object" && meta !== null && "visibleOnly" in meta && meta.visibleOnly === true;
8757
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
+ }
8758
8847
  function isFileNotFoundError(error) {
8759
8848
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
8760
8849
  }
8850
+ function isFileExistsError(error) {
8851
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
8852
+ }
8761
8853
  //#endregion
8762
8854
  //#region src/tui/banner.ts
8763
8855
  const ASCII_BANNERS = [
@@ -9045,6 +9137,14 @@ const slashCommandSuggestions = [
9045
9137
  {
9046
9138
  value: "/new",
9047
9139
  description: "start a fresh session"
9140
+ },
9141
+ {
9142
+ value: "/fork",
9143
+ description: "fork the current session"
9144
+ },
9145
+ {
9146
+ value: "/restore",
9147
+ description: "restore a previous session"
9048
9148
  }
9049
9149
  ];
9050
9150
  const slashCommands = [
@@ -9092,6 +9192,16 @@ const slashCommands = [
9092
9192
  name: "new",
9093
9193
  description: "start a fresh interactive TUI session",
9094
9194
  execute: executeNewCommand
9195
+ },
9196
+ {
9197
+ name: "fork",
9198
+ description: "fork the current interactive TUI session",
9199
+ execute: executeForkCommand
9200
+ },
9201
+ {
9202
+ name: "restore",
9203
+ description: "restore a previous interactive TUI session",
9204
+ execute: executeRestoreCommand
9095
9205
  }
9096
9206
  ];
9097
9207
  function parseSlashCommand(input) {
@@ -9112,7 +9222,7 @@ async function executeSlashCommand(input, context) {
9112
9222
  if (!command) {
9113
9223
  const shortcutResult = await executeSkillShortcutCommand(parsed.name, parsed.args, context);
9114
9224
  if (shortcutResult) return shortcutResult;
9115
- return { messages: [`Unknown command: /${parsed.name}`, "Try /kb status or /new."] };
9225
+ return { messages: [`Unknown command: /${parsed.name}`, "Try /kb status, /new, /fork, or /restore."] };
9116
9226
  }
9117
9227
  return command.execute(parsed.args, context);
9118
9228
  }
@@ -9209,6 +9319,12 @@ async function executeKbCommand(args, context) {
9209
9319
  function executeNewCommand() {
9210
9320
  return { messages: ["/new starts a fresh session in the interactive TUI."] };
9211
9321
  }
9322
+ function executeForkCommand() {
9323
+ return { messages: ["/fork clones the current session in the interactive TUI."] };
9324
+ }
9325
+ function executeRestoreCommand() {
9326
+ return { messages: ["/restore opens a previous-session picker in the interactive TUI."] };
9327
+ }
9212
9328
  function executeInteractiveOnlyCommand(command) {
9213
9329
  return () => ({ messages: [`${command} is available in the interactive TUI.`] });
9214
9330
  }
@@ -9575,6 +9691,9 @@ var ChatLayout = class {
9575
9691
  submitMessage;
9576
9692
  submitCommand;
9577
9693
  modalActionHandler;
9694
+ sessionPicker;
9695
+ sessionPickerSelectionHandler;
9696
+ sessionPickerCancelHandler;
9578
9697
  activeModalActionIndex = 0;
9579
9698
  activeSlashSuggestionIndex = 0;
9580
9699
  threadScrollOffset = 0;
@@ -9666,12 +9785,34 @@ var ChatLayout = class {
9666
9785
  setModalActionHandler(handler) {
9667
9786
  this.modalActionHandler = handler;
9668
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
+ }
9669
9806
  setInputValue(value) {
9670
9807
  this.promptValue = value;
9671
9808
  this.promptCursor = value.length;
9672
9809
  this.pastedContent.clear();
9673
9810
  this.pasteCounter = 0;
9674
9811
  }
9812
+ discardLastUserMessage(text) {
9813
+ const last = this.messages.at(-1);
9814
+ if (last?.kind === "user" && last.text === text) this.messages.pop();
9815
+ }
9675
9816
  resetForNewSession(messages) {
9676
9817
  this.messages.splice(0, this.messages.length, ...messages);
9677
9818
  this.promptValue = "";
@@ -9688,6 +9829,9 @@ var ChatLayout = class {
9688
9829
  this.taskPlan = void 0;
9689
9830
  this.cancelPending = void 0;
9690
9831
  this.modalActionHandler = void 0;
9832
+ this.sessionPicker = void 0;
9833
+ this.sessionPickerSelectionHandler = void 0;
9834
+ this.sessionPickerCancelHandler = void 0;
9691
9835
  this.activeModalActionIndex = 0;
9692
9836
  this.activeSlashSuggestionIndex = 0;
9693
9837
  this.threadScrollOffset = 0;
@@ -9724,6 +9868,7 @@ var ChatLayout = class {
9724
9868
  this.inputFocused = value;
9725
9869
  }
9726
9870
  handleInput(data) {
9871
+ if (this.handleSessionPickerInput(data)) return;
9727
9872
  if (this.handleModalInput(data)) return;
9728
9873
  if (this.cancelPending && matchesKey(data, "escape")) {
9729
9874
  this.cancelPending();
@@ -9741,7 +9886,7 @@ var ChatLayout = class {
9741
9886
  invalidate() {}
9742
9887
  render(width) {
9743
9888
  const safeWidth = Math.max(20, width);
9744
- const footerLines = this.getActiveModal() ? this.renderModalHelp(safeWidth) : this.renderPrompt(safeWidth);
9889
+ const footerLines = this.getActiveModal() && !this.sessionPicker ? this.renderModalHelp(safeWidth) : this.renderPrompt(safeWidth);
9745
9890
  const threadHeight = Math.max(1, this.terminal.rows - footerLines.length);
9746
9891
  const allThreadLines = this.renderThread(safeWidth, threadHeight);
9747
9892
  if (this.transcriptMode === "inline") {
@@ -9761,6 +9906,7 @@ var ChatLayout = class {
9761
9906
  }
9762
9907
  }
9763
9908
  renderThread(width, threadHeight) {
9909
+ if (this.sessionPicker) return this.renderSessionPicker(width, threadHeight);
9764
9910
  const innerWidth = Math.max(1, width);
9765
9911
  const activeModalIndex = this.getActiveModalIndex();
9766
9912
  const lines = this.messages.flatMap((message, index) => {
@@ -9787,7 +9933,7 @@ var ChatLayout = class {
9787
9933
  });
9788
9934
  }
9789
9935
  renderPrompt(width) {
9790
- const slashSuggestions = this.getSlashSuggestions();
9936
+ const slashSuggestions = this.sessionPicker ? [] : this.getSlashSuggestions();
9791
9937
  const top = `┌${"─".repeat(Math.max(0, width - 2))}┐`;
9792
9938
  const bottom = `└${"─".repeat(Math.max(0, width - 2))}┘`;
9793
9939
  const prefix = "> ";
@@ -9884,6 +10030,74 @@ var ChatLayout = class {
9884
10030
  const status = formatStatusLine(this.folderName, this.modelLabel, this.status, this.knowledgeStatus, statusInnerWidth);
9885
10031
  return [truncateToWidth(` ${help}`, width, "…", true), truncateToWidth(` ${status}`, width, "…", true)];
9886
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
+ }
9887
10101
  handleModalInput(data) {
9888
10102
  const activeModal = this.getActiveModal();
9889
10103
  if (!activeModal) return false;
@@ -10218,6 +10432,17 @@ function getVisibleSuggestionWindowStart(total, activeIndex, visibleRows) {
10218
10432
  if (total <= visibleRows) return 0;
10219
10433
  return Math.max(0, Math.min(activeIndex - visibleRows + 1, total - visibleRows));
10220
10434
  }
10435
+ function formatSessionPickerRow(item, selected, width) {
10436
+ return truncateToWidth(`${selected ? ">" : " "} ${formatSessionPickerDate(item.updatedAt)} ${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
+ }
10221
10446
  function colorUserMessageBorder(line) {
10222
10447
  return line.replace("▌", ui.modelInline("▌"));
10223
10448
  }
@@ -12933,6 +13158,12 @@ function getSlashCommandActivities(command) {
12933
13158
  function isNewSessionCommand(command) {
12934
13159
  return command.trim() === "/new";
12935
13160
  }
13161
+ function isForkSessionCommand(command) {
13162
+ return command.trim() === "/fork";
13163
+ }
13164
+ function isRestoreSessionCommand(command) {
13165
+ return command.trim() === "/restore";
13166
+ }
12936
13167
  function isConnectCommand(command) {
12937
13168
  const name = getSlashCommandName(command);
12938
13169
  return name === "connect" || name === "provider" || name === "providers";
@@ -13298,6 +13529,14 @@ var TopchesterTuiShell = class {
13298
13529
  await this.startNewSession(app, tui);
13299
13530
  return;
13300
13531
  }
13532
+ if (isForkSessionCommand(command)) {
13533
+ await this.forkCurrentSession(app, tui);
13534
+ return;
13535
+ }
13536
+ if (isRestoreSessionCommand(command)) {
13537
+ await this.openRestoreSessionPicker(app, tui, command);
13538
+ return;
13539
+ }
13301
13540
  if (isConnectCommand(command)) {
13302
13541
  await this.submitConnectCommand(app, tui, command);
13303
13542
  return;
@@ -13709,6 +13948,108 @@ var TopchesterTuiShell = class {
13709
13948
  tui.requestRender();
13710
13949
  await this.checkAgent(app, tui);
13711
13950
  }
13951
+ async forkCurrentSession(app, tui) {
13952
+ if (this.taskPlanNoticeTimer) {
13953
+ clearTimeout(this.taskPlanNoticeTimer);
13954
+ this.taskPlanNoticeTimer = void 0;
13955
+ }
13956
+ const sourceSession = this.session;
13957
+ if (!sourceSession) {
13958
+ app.addMessage(systemMessage("Fork failed: no active session."));
13959
+ tui.requestRender();
13960
+ return;
13961
+ }
13962
+ const fork = await forkSession(this.context.workspaceRoot, sourceSession.sessionId);
13963
+ const rehydrated = rehydrateSession((await loadSession(this.context.workspaceRoot, fork.sessionId)).events);
13964
+ const forkNoticeText = `Forked session from ${sourceSession.sessionId.slice(0, 8)}.`;
13965
+ const forkNotice = systemMessage(forkNoticeText);
13966
+ const resetMessages = [...rehydrated.messages, forkNotice];
13967
+ this.session = fork;
13968
+ this.sessionStartedAt = Date.now();
13969
+ this.pendingSkillActivations = [];
13970
+ try {
13971
+ await fork.append({
13972
+ kind: "message",
13973
+ role: "system",
13974
+ text: forkNoticeText
13975
+ });
13976
+ } catch (error) {
13977
+ resetMessages.push(systemMessage(`Session save failed: ${formatPlainError(error)}`));
13978
+ }
13979
+ app.resetForNewSession(resetMessages);
13980
+ app.setTaskPlan(rehydrated.taskPlan);
13981
+ if (rehydrated.status) app.setStatus(rehydrated.status);
13982
+ tui.requestRender();
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
+ }
13712
14053
  async appendStartupRuntimeEvents(session, messages, events) {
13713
14054
  for (const event of events) {
13714
14055
  messages.push(...renderRuntimeEvent(event));
@@ -14284,6 +14625,18 @@ function createTopchesterProgram() {
14284
14625
  process.exitCode = 1;
14285
14626
  }
14286
14627
  });
14628
+ program.command("fork").description("fork a saved project session").argument("[session]", "session id to fork").option("--last", "fork the latest project session").action(async (sessionId, options) => {
14629
+ const context = createContextFromOptions(program);
14630
+ try {
14631
+ if (options.last && sessionId) throw new Error("Usage: topchester fork [--last] [session-id]");
14632
+ const source = options.last ? "latest" : sessionId;
14633
+ if (!source) throw new Error("topchester fork requires --last or a session id until a saved-session picker exists.");
14634
+ await openForkedSession(context, source);
14635
+ } catch (error) {
14636
+ console.error(formatStartupError(error));
14637
+ process.exitCode = 1;
14638
+ }
14639
+ });
14287
14640
  program.command("search").description("search compiled L1 knowledge entries").argument("<query...>", "search query").option("--limit <count>", "maximum number of matches", parsePositiveInteger).option("--json", "write full JSON search result to stdout").action(async (queryParts, options) => {
14288
14641
  await executeKbSearchCommand(program, queryParts, options);
14289
14642
  });
@@ -14372,6 +14725,17 @@ function printStartupSummary(context) {
14372
14725
  }
14373
14726
  }
14374
14727
  }
14728
+ async function openForkedSession(context, sourceSession) {
14729
+ const fork = await forkSession(context.workspaceRoot, sourceSession);
14730
+ const loaded = await loadSession(context.workspaceRoot, fork.sessionId);
14731
+ const session = await loadSessionForAppend(context.workspaceRoot, loaded.sessionId);
14732
+ const rehydrated = rehydrateSession(loaded.events);
14733
+ await new TopchesterTuiShell(context, void 0, {
14734
+ session,
14735
+ initialMessages: rehydrated.messages,
14736
+ initialTaskPlan: rehydrated.taskPlan
14737
+ }).render();
14738
+ }
14375
14739
  function createContextFromOptions(program) {
14376
14740
  return createAppContext(getContextOptionsFromProgram(program));
14377
14741
  }
@@ -14434,4 +14798,4 @@ function formatDryRunSyncStatus(status) {
14434
14798
  //#endregion
14435
14799
  export { runTopchesterCli as t };
14436
14800
 
14437
- //# sourceMappingURL=cli-BgMj4Ifj.mjs.map
14801
+ //# sourceMappingURL=cli-BZmxcvol.mjs.map