@sayknow-cli/coding-agent 0.6.4 → 0.6.6

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +2 -2
  3. package/dist/types/config/keybindings.d.ts +6 -1
  4. package/dist/types/config/settings-schema.d.ts +7 -3
  5. package/dist/types/i18n/messages/en.d.ts +5 -14
  6. package/dist/types/modes/components/session-selector.d.ts +7 -3
  7. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  8. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  9. package/dist/types/modes/components/user-message.d.ts +3 -1
  10. package/dist/types/modes/components/welcome.d.ts +34 -14
  11. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  12. package/dist/types/modes/controllers/selector-controller.d.ts +5 -0
  13. package/dist/types/modes/interactive-mode.d.ts +14 -2
  14. package/dist/types/modes/theme/defaults/index.d.ts +342 -0
  15. package/dist/types/modes/theme/theme.d.ts +5 -1
  16. package/dist/types/modes/types.d.ts +3 -1
  17. package/dist/types/session/session-manager.d.ts +9 -0
  18. package/dist/types/session/session-stars.d.ts +15 -0
  19. package/dist/types/tools/debug.d.ts +2 -2
  20. package/dist/types/tui/output-block.d.ts +12 -1
  21. package/dist/types/tui/utils.d.ts +1 -3
  22. package/dist/types/utils/session-title-generation.d.ts +6 -0
  23. package/dist/types/utils/title-generator.d.ts +11 -2
  24. package/package.json +7 -7
  25. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  26. package/src/cli/session-picker.ts +4 -0
  27. package/src/config/keybindings.ts +8 -1
  28. package/src/config/settings-schema.ts +4 -3
  29. package/src/i18n/messages/de.messages.ts +4 -1
  30. package/src/i18n/messages/de.settings.ts +2 -0
  31. package/src/i18n/messages/de.ts +3 -12
  32. package/src/i18n/messages/en.ts +5 -14
  33. package/src/i18n/messages/es.messages.ts +4 -1
  34. package/src/i18n/messages/es.settings.ts +2 -0
  35. package/src/i18n/messages/es.ts +3 -12
  36. package/src/i18n/messages/fr.messages.ts +4 -1
  37. package/src/i18n/messages/fr.settings.ts +2 -0
  38. package/src/i18n/messages/fr.ts +3 -12
  39. package/src/i18n/messages/ja.messages.ts +4 -1
  40. package/src/i18n/messages/ja.settings.ts +2 -0
  41. package/src/i18n/messages/ja.ts +3 -12
  42. package/src/i18n/messages/ko.messages.ts +4 -1
  43. package/src/i18n/messages/ko.settings.ts +2 -0
  44. package/src/i18n/messages/ko.ts +4 -13
  45. package/src/i18n/messages/zh.messages.ts +2 -1
  46. package/src/i18n/messages/zh.settings.ts +2 -0
  47. package/src/i18n/messages/zh.ts +3 -12
  48. package/src/internal-urls/docs-index.generated.ts +10 -10
  49. package/src/lsp/render.ts +0 -1
  50. package/src/modes/action-registry.ts +1 -0
  51. package/src/modes/components/session-selector.ts +71 -21
  52. package/src/modes/components/sessions-dashboard.ts +8 -5
  53. package/src/modes/components/settings-selector.ts +1 -0
  54. package/src/modes/components/status-line/presets.ts +5 -5
  55. package/src/modes/components/status-line/separators.ts +4 -0
  56. package/src/modes/components/tool-execution.ts +6 -19
  57. package/src/modes/components/tool-status-header.ts +103 -12
  58. package/src/modes/components/user-message-selector.ts +57 -15
  59. package/src/modes/components/user-message.ts +25 -15
  60. package/src/modes/components/welcome.ts +298 -576
  61. package/src/modes/controllers/command-controller.ts +56 -7
  62. package/src/modes/controllers/input-controller.ts +17 -0
  63. package/src/modes/controllers/selector-controller.ts +91 -10
  64. package/src/modes/interactive-mode.ts +85 -38
  65. package/src/modes/theme/defaults/glow-octopus.json +114 -0
  66. package/src/modes/theme/defaults/index.ts +6 -0
  67. package/src/modes/theme/defaults/ink-octopus.json +114 -0
  68. package/src/modes/theme/defaults/violet-octopus.json +114 -0
  69. package/src/modes/theme/theme.ts +18 -2
  70. package/src/modes/types.ts +3 -1
  71. package/src/prompts/system/title-system.md +1 -1
  72. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  73. package/src/session/agent-session.ts +12 -0
  74. package/src/session/internal/managed-session-scope.ts +10 -0
  75. package/src/session/session-manager.ts +26 -1
  76. package/src/session/session-stars.ts +104 -0
  77. package/src/slash-commands/builtin-registry.ts +104 -4
  78. package/src/tools/debug.ts +0 -1
  79. package/src/tools/fetch.ts +0 -1
  80. package/src/tui/output-block.ts +29 -61
  81. package/src/tui/utils.ts +1 -8
  82. package/src/utils/session-title-generation.ts +29 -0
  83. package/src/utils/title-generator.ts +46 -2
@@ -48,7 +48,16 @@ import { replaceTabs } from "../../tools/render-utils";
48
48
  import { getDisplayChangelogEntries } from "../../utils/changelog";
49
49
  import { copyToClipboard } from "../../utils/clipboard";
50
50
  import { openPath } from "../../utils/open";
51
- import { setSessionTerminalTitle } from "../../utils/title-generator";
51
+ import {
52
+ beginSessionTitleGeneration,
53
+ invalidateSessionTitleGeneration,
54
+ isSessionTitleGenerationCurrent,
55
+ } from "../../utils/session-title-generation";
56
+ import {
57
+ buildConversationTitleInput,
58
+ generateSessionTitle,
59
+ setSessionTerminalTitle,
60
+ } from "../../utils/title-generator";
52
61
  import { prepareTranscriptRebuild } from "../utils/ui-helpers";
53
62
 
54
63
  function showMarkdownPanel(ctx: InteractiveModeContext, title: string, markdown: string): void {
@@ -1122,23 +1131,63 @@ export class CommandController {
1122
1131
  }
1123
1132
  }
1124
1133
 
1125
- async handleRenameCommand(title: string): Promise<void> {
1134
+ async handleRenameCommand(title?: string): Promise<void> {
1126
1135
  try {
1136
+ if (!title?.trim()) {
1137
+ const generation = beginSessionTitleGeneration(this.ctx.sessionManager);
1138
+ const input = buildConversationTitleInput(this.ctx.session.messages);
1139
+ if (!input) {
1140
+ this.ctx.showError("Nothing to summarize yet — pass a title: /rename <title>");
1141
+ return;
1142
+ }
1143
+
1144
+ const generated = await generateSessionTitle(
1145
+ input,
1146
+ this.ctx.session.modelRegistry,
1147
+ this.ctx.settings,
1148
+ this.ctx.session.sessionId,
1149
+ this.ctx.session.model,
1150
+ provider => this.ctx.session.agent.metadataForProvider(provider),
1151
+ );
1152
+ if (
1153
+ !isSessionTitleGenerationCurrent(this.ctx.sessionManager, generation) ||
1154
+ buildConversationTitleInput(this.ctx.session.messages) !== input
1155
+ )
1156
+ return;
1157
+ if (!generated) {
1158
+ this.ctx.showError("Could not generate a session title — pass one: /rename <title>");
1159
+ return;
1160
+ }
1161
+
1162
+ const stored = await this.ctx.sessionManager.setSessionName(generated, "user");
1163
+ if (!stored) {
1164
+ this.ctx.showError("Session name cannot be empty.");
1165
+ return;
1166
+ }
1167
+ this.#finishRename();
1168
+ return;
1169
+ }
1170
+
1171
+ invalidateSessionTitleGeneration(this.ctx.sessionManager);
1127
1172
  const stored = await this.ctx.sessionManager.setSessionName(title, "user");
1128
1173
  if (!stored) {
1129
1174
  this.ctx.showError("Session name cannot be empty.");
1130
1175
  return;
1131
1176
  }
1132
- const name = this.ctx.sessionManager.getSessionName()!;
1133
- setSessionTerminalTitle(name, this.ctx.sessionManager.getCwd());
1134
- this.ctx.statusLine.invalidate();
1135
- this.ctx.updateEditorBorderColor();
1136
- this.ctx.showStatus(`Session renamed to "${name}".`);
1177
+ this.#finishRename();
1137
1178
  } catch (err) {
1138
1179
  this.ctx.showError(`Rename failed: ${err instanceof Error ? err.message : String(err)}`);
1139
1180
  }
1140
1181
  }
1141
1182
 
1183
+ #finishRename(): void {
1184
+ const name = this.ctx.sessionManager.getSessionName()!;
1185
+ setSessionTerminalTitle(name, this.ctx.sessionManager.getCwd());
1186
+ this.ctx.statusLine.invalidate();
1187
+ this.ctx.updateEditorBorderColor();
1188
+ this.ctx.showStatus(`Session renamed to "${name}".`);
1189
+ }
1190
+
1142
1191
  async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
1143
1192
  const isDeferred = this.ctx.session.isStreaming;
1144
1193
  this.ctx.bashComponent = new BashExecutionComponent(command, this.ctx.ui, excludeFromContext);
@@ -26,6 +26,7 @@ import { ensureSupportedImageInput, ImageInputTooLargeError } from "../../utils/
26
26
  import { resizeImage } from "../../utils/image-resize";
27
27
  import { loadPastedImageBatch, PastedImageBatchError } from "../../utils/pasted-image-loading";
28
28
  import { formatPastedImageReference, parsePastedImagePaths } from "../../utils/pasted-image-path";
29
+ import { invalidateSessionTitleGeneration } from "../../utils/session-title-generation";
29
30
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
30
31
  import { ActionRegistry, APP_ACTION_METADATA } from "../action-registry";
31
32
  import { CommandPalette, type CommandPaletteAction, type CommandPaletteEntry } from "../components/command-palette";
@@ -111,6 +112,9 @@ export class InputController {
111
112
  "app.session.tree": () => this.ctx.showTreeSelector(),
112
113
  "app.session.fork": () => this.ctx.showUserMessageSelector(),
113
114
  "app.session.resume": () => this.ctx.showSessionSelector(),
115
+ "app.session.continue": async () => {
116
+ await this.ctx.continueRecentSession();
117
+ },
114
118
  "app.session.observe": async () => {
115
119
  await this.ctx.showSessionObserver();
116
120
  },
@@ -709,6 +713,16 @@ export class InputController {
709
713
  return true;
710
714
  });
711
715
  }
716
+ this.#registerCommandPaletteAction("app.session.continue", () => {
717
+ void this.ctx.continueRecentSession();
718
+ return undefined;
719
+ });
720
+ for (const key of this.ctx.keybindings.getKeys("app.session.continue")) {
721
+ this.ctx.editor.setCustomKeyHandler(key, () => {
722
+ this.#executeAction("app.session.continue");
723
+ return true;
724
+ });
725
+ }
712
726
  this.#registerCommandPaletteAction(
713
727
  "app.message.followUp",
714
728
  () => this.#executeAction("app.message.followUp"),
@@ -971,6 +985,7 @@ export class InputController {
971
985
 
972
986
  // Queue input during compaction
973
987
  if (this.ctx.session.isCompacting) {
988
+ invalidateSessionTitleGeneration(this.ctx.sessionManager);
974
989
  if ((inputImages?.length ?? 0) > 0) {
975
990
  this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
976
991
  return;
@@ -984,6 +999,7 @@ export class InputController {
984
999
  // prompt to run after the active turn completes (in submission order).
985
1000
  // This handles extension commands (execute immediately), prompt template expansion, and queueing
986
1001
  if (this.ctx.session.isStreaming) {
1002
+ invalidateSessionTitleGeneration(this.ctx.sessionManager);
987
1003
  if (this.#canModifyComposer(composer)) {
988
1004
  this.ctx.editor.addToHistory(text);
989
1005
  this.ctx.editor.setText("");
@@ -1008,6 +1024,7 @@ export class InputController {
1008
1024
  }
1009
1025
 
1010
1026
  // Normal message submission
1027
+ invalidateSessionTitleGeneration(this.ctx.sessionManager);
1011
1028
  // First, move any pending bash components to chat
1012
1029
  this.ctx.flushPendingBashComponents();
1013
1030
 
@@ -1895,36 +1895,114 @@ export class SelectorController {
1895
1895
  });
1896
1896
  }
1897
1897
 
1898
+ #forkBusyReason(): string | undefined {
1899
+ if (this.ctx.session.isStreaming) return "Wait for the current response to finish or abort it before forking.";
1900
+ if (this.ctx.session.isCompacting) return "Wait for compaction to finish or abort it before forking.";
1901
+ if (this.ctx.bashComponent || this.ctx.pythonComponent)
1902
+ return "Wait for the foreground command to finish or cancel it before forking.";
1903
+ return undefined;
1904
+ }
1905
+
1906
+ /**
1907
+ * Pick an earlier prompt and continue from it in a new session. The original
1908
+ * transcript is kept; the selected prompt comes back as an editable draft,
1909
+ * never auto-submitted.
1910
+ */
1898
1911
  showUserMessageSelector(): void {
1899
- const userMessages = this.ctx.session.getUserMessagesForBranching();
1912
+ const openedSessionId = this.ctx.sessionManager.getSessionId();
1913
+ const openedSessionFile = this.ctx.sessionManager.getSessionFile();
1914
+ if (!openedSessionFile) {
1915
+ this.ctx.showError("Fork requires a persisted session");
1916
+ return;
1917
+ }
1918
+ const busyReason = this.#forkBusyReason();
1919
+ if (busyReason) {
1920
+ this.ctx.showWarning(busyReason);
1921
+ return;
1922
+ }
1900
1923
 
1924
+ const userMessages = this.ctx.session.getUserMessagesForBranching();
1901
1925
  if (userMessages.length === 0) {
1902
1926
  this.ctx.showStatus("No messages to branch from");
1903
1927
  return;
1904
1928
  }
1905
1929
 
1930
+ // Close the side-chat composer before the picker takes the editor area.
1931
+ if (this.ctx.hasActiveBtw()) this.ctx.handleBtwEscape();
1932
+
1906
1933
  this.showSelector(done => {
1934
+ let selectionInFlight = false;
1935
+ const closeWithoutBranch = (message: string, error = false): void => {
1936
+ done();
1937
+ if (error) this.ctx.showError(message);
1938
+ else this.ctx.showStatus(message);
1939
+ this.ctx.ui.requestRender();
1940
+ };
1941
+ const sessionChanged = (): boolean =>
1942
+ this.ctx.sessionManager.getSessionId() !== openedSessionId ||
1943
+ this.ctx.sessionManager.getSessionFile() !== openedSessionFile;
1907
1944
  const selector = new UserMessageSelectorComponent(
1908
1945
  userMessages.map(m => ({ id: m.entryId, text: m.text })),
1909
1946
  async entryId => {
1910
- const result = await this.ctx.session.branch(entryId);
1911
- if (result.cancelled) {
1912
- // Hook cancelled the branch
1913
- done();
1914
- this.ctx.ui.requestRender();
1947
+ if (selectionInFlight) return;
1948
+ selectionInFlight = true;
1949
+ if (sessionChanged()) {
1950
+ closeWithoutBranch("Fork cancelled because the active session changed", true);
1951
+ return;
1952
+ }
1953
+ const busyReason = this.#forkBusyReason();
1954
+ if (busyReason) {
1955
+ closeWithoutBranch(busyReason);
1915
1956
  return;
1916
1957
  }
1917
- this.ctx.resetIrcSidebarSession();
1918
1958
 
1919
- this.ctx.rebuildInitialMessages("replace-identity");
1920
- this.ctx.editor.setText(result.selectedText);
1959
+ let selectedText = userMessages.find(message => message.entryId === entryId)?.text ?? "";
1960
+ let setupFailure: unknown;
1961
+ try {
1962
+ const result = await this.ctx.session.branch(entryId);
1963
+ if (result.cancelled) {
1964
+ closeWithoutBranch("Fork cancelled");
1965
+ return;
1966
+ }
1967
+ selectedText = result.selectedText;
1968
+ } catch (error) {
1969
+ // Only a failure before the successor was committed is a failed fork.
1970
+ if (!sessionChanged()) {
1971
+ closeWithoutBranch(`Fork failed: ${error instanceof Error ? error.message : String(error)}`, true);
1972
+ return;
1973
+ }
1974
+ setupFailure = error;
1975
+ }
1976
+
1977
+ try {
1978
+ this.ctx.resetIrcSidebarSession();
1979
+ this.ctx.resetObserverRegistry();
1980
+ this.#refreshSessionTerminalTitle();
1981
+ this.ctx.statusLine.invalidate();
1982
+ this.ctx.statusLine.setSessionStartTime(Date.now());
1983
+ this.ctx.updateEditorTopBorder();
1984
+ this.ctx.updateEditorBorderColor();
1985
+ this.ctx.rebuildInitialMessages("replace-identity");
1986
+ await this.ctx.reloadTodos();
1987
+ } catch (error) {
1988
+ setupFailure ??= error;
1989
+ }
1990
+ this.ctx.editor.setText(selectedText);
1921
1991
  done();
1922
- this.ctx.showStatus("Branched to new session");
1992
+ if (setupFailure !== undefined) {
1993
+ this.ctx.showError(
1994
+ `Fork created, but session setup failed: ${setupFailure instanceof Error ? setupFailure.message : String(setupFailure)}`,
1995
+ );
1996
+ } else {
1997
+ this.ctx.showStatus("Forked to a new session; edit the selected prompt to continue");
1998
+ }
1999
+ this.ctx.ui.requestRender();
1923
2000
  },
1924
2001
  () => {
1925
2002
  done();
1926
2003
  this.ctx.ui.requestRender();
1927
2004
  },
2005
+ () => this.ctx.ui.terminal.rows,
1928
2006
  );
1929
2007
  return { component: selector, focus: selector.getMessageList() };
1930
2008
  });
@@ -2086,6 +2164,9 @@ export class SelectorController {
2086
2164
  });
2087
2165
  }
2088
2166
  },
2167
+ undefined,
2168
+ undefined,
2169
+ (session, starred) => this.ctx.sessionManager.setSessionStarredForPicker(session, starred),
2089
2170
  );
2090
2171
  selector.setOnRequestRender(() => this.ctx.ui.requestRender());
2091
2172
  return { component: selector, focus: selector };
@@ -1,3 +1,5 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
1
3
  import { type Agent, type AgentMessage, ThinkingLevel } from "@sayknow-cli/agent-core";
2
4
  import type { CompactionOutcome } from "@sayknow-cli/agent-core/compaction";
3
5
  import type { AssistantMessage, ImageContent, Message, UsageReport } from "@sayknow-cli/ai";
@@ -47,9 +49,11 @@ import { HistoryStorage } from "../session/history-storage";
47
49
  import type { SessionContext, SessionManager } from "../session/session-manager";
48
50
  import { getRecentSessions, getSessionMessageEntryId } from "../session/session-manager";
49
51
  import type { LspStartupServerInfo } from "../tools";
52
+ import { shortenPath } from "../tools/render-utils";
50
53
  import { formatPhaseDisplayName } from "../tools/todo-write";
51
54
  import { copyToClipboard } from "../utils/clipboard";
52
55
  import type { EventBus } from "../utils/event-bus";
56
+ import * as gitUtils from "../utils/git";
53
57
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
54
58
  import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
55
59
  import type { AssistantMessageComponent } from "./components/assistant-message";
@@ -67,16 +71,12 @@ import {
67
71
  isPetCapabilityProbePending,
68
72
  warnWhenPetCapabilitySettled,
69
73
  } from "./components/pet-capability";
70
- import { type PetMode, SayknowPetWidget } from "./components/sayknow-pet-widget";
74
+ import { type PetMode, type PetSkinId, SayknowPetWidget } from "./components/sayknow-pet-widget";
75
+ import { resolveCurrentBranch } from "./components/status-line/git-utils";
71
76
  import type { ToolExecutionHandle } from "./components/tool-execution";
72
77
  import { StatusLineComponent } from "./components/tool-status-header";
73
78
  import { composeToolText } from "./components/tool-transcript-format";
74
- import {
75
- type RecentSession,
76
- WelcomeComponent,
77
- type WelcomeLogoMode,
78
- type LspServerInfo as WelcomeLspServerInfo,
79
- } from "./components/welcome";
79
+ import { type RecentSession, WelcomeComponent, type WelcomeLogoMode, type WelcomeSnapshot } from "./components/welcome";
80
80
  import { BtwController } from "./controllers/btw-controller";
81
81
  import { CommandController } from "./controllers/command-controller";
82
82
  import { EventController } from "./controllers/event-controller";
@@ -99,7 +99,14 @@ import { shouldShowExtensionCommand } from "./slash-command-visibility";
99
99
  import { TasksAggregator } from "./tasks-aggregator";
100
100
  import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
101
101
  import type { Theme } from "./theme/theme";
102
- import { getEditorTheme, getSymbolTheme, onTerminalAppearanceChange, onThemeChange, theme } from "./theme/theme";
102
+ import {
103
+ getCurrentThemeName,
104
+ getEditorTheme,
105
+ getSymbolTheme,
106
+ onTerminalAppearanceChange,
107
+ onThemeChange,
108
+ theme,
109
+ } from "./theme/theme";
103
110
  import { type RegisterTranscriptItem, TranscriptItemRegistry, transcriptItemId } from "./transcript-item-registry";
104
111
  import {
105
112
  type CompactionQueuedMessage,
@@ -196,23 +203,33 @@ const HINT_SHIMMER_PALETTE: ShimmerPalette = {
196
203
  high: "borderAccent",
197
204
  };
198
205
 
199
- function getDefaultInputPrefix(): string {
200
- return `${theme.fg("accent", ">")} `;
201
- }
202
-
203
206
  function getShellInputPrefix(isNoContext: boolean): string {
204
207
  const shellLabel = isNoContext
205
208
  ? theme.fg("warning", theme.bold("shell no-context"))
206
209
  : theme.fg("bashMode", theme.bold("shell"));
207
- return `${shellLabel} ${getDefaultInputPrefix()}`;
210
+ return `${shellLabel} `;
211
+ }
212
+
213
+ /**
214
+ * The pet on the launch card: the one the user keeps beside the composer, or —
215
+ * with the pet off — the skin that matches the theme (blue for blue-octopus).
216
+ */
217
+ export function resolveWelcomePetSkin(petMode: PetMode, themeName: string | undefined): PetSkinId {
218
+ if (petMode !== "off") return petMode;
219
+ return themeName?.startsWith("blue") ? "blue" : "red";
208
220
  }
209
221
 
210
- function configureDefaultComposerChrome(editor: CustomEditor): void {
211
- editor.setBorderVisible(true);
212
- editor.setBorderStyle("round");
213
- editor.setClosedBorderBox(true);
222
+ /**
223
+ * The composer is an open rail, not a box: every input row starts with the same
224
+ * rail glyph a submitted prompt keeps in the transcript, colored by the editor's
225
+ * border color (session accent, thinking level, shell/python mode).
226
+ */
227
+ export function configureDefaultComposerChrome(editor: CustomEditor): void {
228
+ editor.setBorderVisible(false);
229
+ editor.setClosedBorderBox(false);
214
230
  editor.setPromptGutter(undefined);
215
- editor.setInputPrefix(getDefaultInputPrefix());
231
+ editor.setRailGutter(theme.rail.user);
232
+ editor.setInputPrefix(undefined);
216
233
  editor.setPlaceholder(getDefaultComposerPlaceholder());
217
234
  editor.setPaddingX(1);
218
235
  editor.setRightGutterWidth(COMPOSER_RIGHT_GUTTER_WIDTH);
@@ -734,7 +751,6 @@ export class InteractiveMode implements InteractiveModeContext {
734
751
  modelName,
735
752
  providerName,
736
753
  recentSessions,
737
- this.#getWelcomeLspServers(),
738
754
  welcomeLogoMode,
739
755
  {
740
756
  getViewportRows: () => this.ui.terminal.rows,
@@ -744,6 +760,10 @@ export class InteractiveMode implements InteractiveModeContext {
744
760
  collapseChangelog: settings.get("collapseChangelog"),
745
761
  keyDisplayContext: this.#keyDisplayContext,
746
762
  skipLogoAnimation,
763
+ snapshot: this.#buildWelcomeSnapshot(),
764
+ resumeKey: this.keybindings.getKeys("app.session.resume")[0],
765
+ continueKey: this.keybindings.getKeys("app.session.continue")[0],
766
+ petSkin: resolveWelcomePetSkin(settings.get("pet.mode"), getCurrentThemeName()),
747
767
  },
748
768
  );
749
769
 
@@ -873,6 +893,7 @@ export class InteractiveMode implements InteractiveModeContext {
873
893
  error: error instanceof Error ? error.message : String(error),
874
894
  });
875
895
  });
896
+ this.#probeWelcomeWorkspace(welcomeComponent);
876
897
  }, 0);
877
898
  recentSessionsTimer.unref?.();
878
899
  }
@@ -1206,8 +1227,10 @@ export class InteractiveMode implements InteractiveModeContext {
1206
1227
  }
1207
1228
  }
1208
1229
  if (!this.isBashMode) {
1209
- this.editor.setInputPrefix(getDefaultInputPrefix());
1230
+ this.editor.setInputPrefix(undefined);
1210
1231
  }
1232
+ // Re-read the glyph so a symbol-preset switch (unicode/nerd/ascii) reaches the rail.
1233
+ this.editor.setRailGutter(theme.rail.user);
1211
1234
  this.editor.setPlaceholder(this.#getComposerPlaceholder());
1212
1235
  this.#setComposerTopBorder();
1213
1236
  this.ui.requestRender();
@@ -1597,31 +1620,34 @@ export class InteractiveMode implements InteractiveModeContext {
1597
1620
  }
1598
1621
 
1599
1622
  #handleLspStartupEvent(event: LspStartupEvent): void {
1600
- this.#updateWelcomeLspServers();
1601
-
1602
1623
  const warningMessage = getLspStartupWarningMessage(event);
1603
1624
  if (warningMessage) {
1604
1625
  this.showWarning(warningMessage);
1605
1626
  }
1606
1627
  }
1607
1628
 
1608
- #getWelcomeLspServers(): WelcomeLspServerInfo[] {
1609
- return (
1610
- this.lspServers?.map(server => ({
1611
- name: server.name,
1612
- status: server.status,
1613
- fileTypes: server.fileTypes,
1614
- })) ?? []
1615
- );
1616
- }
1617
-
1618
- #updateWelcomeLspServers(): void {
1619
- if (!this.#welcomeComponent) {
1620
- return;
1629
+ /** Facts the launch card can show synchronously, before the git probe runs. */
1630
+ #buildWelcomeSnapshot(): WelcomeSnapshot {
1631
+ const projectDir = getProjectDir();
1632
+ let branch: string | null = null;
1633
+ try {
1634
+ branch = resolveCurrentBranch(projectDir).branch;
1635
+ } catch {
1636
+ branch = null;
1621
1637
  }
1638
+ return { cwd: shortenPath(projectDir), branch, thinkingLevel: this.session.thinkingLevel };
1639
+ }
1622
1640
 
1623
- this.#welcomeComponent.setLspServers(this.#getWelcomeLspServers());
1624
- this.ui.requestRender();
1641
+ /** Fill in the git change counts, which need I/O. */
1642
+ #probeWelcomeWorkspace(welcomeComponent: WelcomeComponent): void {
1643
+ void gitUtils.status
1644
+ .summary(getProjectDir())
1645
+ .then(gitChanges => {
1646
+ if (this.#welcomeComponent !== welcomeComponent) return;
1647
+ welcomeComponent.setSnapshot({ gitChanges });
1648
+ this.ui.requestRender();
1649
+ })
1650
+ .catch(() => {});
1625
1651
  }
1626
1652
 
1627
1653
  #getWorkingMessageAccent(): WorkingMessageAccent | undefined {
@@ -1866,7 +1892,7 @@ export class InteractiveMode implements InteractiveModeContext {
1866
1892
  return this.#commandController.handleMoveCommand(targetPath);
1867
1893
  }
1868
1894
 
1869
- handleRenameCommand(title: string): Promise<void> {
1895
+ handleRenameCommand(title?: string): Promise<void> {
1870
1896
  return this.#commandController.handleRenameCommand(title);
1871
1897
  }
1872
1898
 
@@ -2157,6 +2183,27 @@ export class InteractiveMode implements InteractiveModeContext {
2157
2183
  this.#selectorController.showSessionSelector();
2158
2184
  }
2159
2185
 
2186
+ async continueRecentSession(): Promise<void> {
2187
+ // Listing returns canonical paths (/private/var on macOS); compare canonically so the
2188
+ // live session is never "continued" into itself.
2189
+ const canonical = (file: string): string => {
2190
+ try {
2191
+ return fs.realpathSync(file);
2192
+ } catch {
2193
+ return path.resolve(file);
2194
+ }
2195
+ };
2196
+ const currentFile = this.sessionManager.getSessionFile();
2197
+ const current = currentFile ? canonical(currentFile) : undefined;
2198
+ const recent = await getRecentSessions(this.sessionManager.getSessionDir());
2199
+ const target = recent.find(session => canonical(session.path) !== current);
2200
+ if (!target) {
2201
+ this.showStatus("No earlier session to continue");
2202
+ return;
2203
+ }
2204
+ await this.handleResumeSession(target.path, { requireIdle: true });
2205
+ }
2206
+
2160
2207
  handleResumeSession(sessionPath: string, options?: { requireIdle?: boolean }): Promise<boolean> {
2161
2208
  return this.#selectorController.handleResumeSession(sessionPath, options, () => {
2162
2209
  this.#btwController.dispose();
@@ -0,0 +1,114 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/jaybeyond/Sayknow_CLI/main/packages/coding-agent/theme-schema.json",
3
+ "name": "glow-octopus",
4
+ "vars": {
5
+ "ink": "#031210",
6
+ "mantle": "#061c19",
7
+ "surface": "#0a2723",
8
+ "surfaceBright": "#10362f",
9
+ "text": "#dff7ef",
10
+ "muted": "#9cc2b6",
11
+ "dim": "#4f7a70",
12
+ "accent": "#3df5b0",
13
+ "secondary": "#a98bff",
14
+ "success": "#5ee3a0",
15
+ "error": "#ff6b8b",
16
+ "warning": "#ffd166",
17
+ "link": "#8fe7ff",
18
+ "keyword": "#a98bff",
19
+ "function": "#ffd166",
20
+ "string": "#6ef0c0",
21
+ "number": "#8fe7ff",
22
+ "type": "#c9b3ff",
23
+ "diffRemoved": "#ff6b8b",
24
+ "toolSuccessBg": "#0b2a20",
25
+ "toolErrorBg": "#2c1320",
26
+ "think1": "#8fe7ff",
27
+ "think2": "#6ef0c0",
28
+ "think3": "#3df5b0",
29
+ "think4": "#a98bff"
30
+ },
31
+ "colors": {
32
+ "accent": "accent",
33
+ "border": "surfaceBright",
34
+ "borderAccent": "accent",
35
+ "borderMuted": "dim",
36
+ "success": "success",
37
+ "error": "error",
38
+ "warning": "warning",
39
+ "muted": "muted",
40
+ "dim": "dim",
41
+ "text": "text",
42
+ "thinkingText": "muted",
43
+ "selectedBg": "surfaceBright",
44
+ "userMessageBg": "surface",
45
+ "userMessageText": "text",
46
+ "customMessageBg": "mantle",
47
+ "customMessageText": "text",
48
+ "customMessageLabel": "accent",
49
+ "toolPendingBg": "mantle",
50
+ "toolSuccessBg": "toolSuccessBg",
51
+ "toolErrorBg": "toolErrorBg",
52
+ "toolTitle": "text",
53
+ "toolOutput": "muted",
54
+ "mdHeading": "accent",
55
+ "mdLink": "link",
56
+ "mdLinkUrl": "dim",
57
+ "mdCode": "secondary",
58
+ "mdCodeBlock": "string",
59
+ "mdCodeBlockBorder": "surfaceBright",
60
+ "mdQuote": "muted",
61
+ "mdQuoteBorder": "surfaceBright",
62
+ "mdHr": "dim",
63
+ "mdListBullet": "accent",
64
+ "toolDiffAdded": "success",
65
+ "toolDiffRemoved": "diffRemoved",
66
+ "toolDiffContext": "muted",
67
+ "link": "link",
68
+ "syntaxComment": "dim",
69
+ "syntaxKeyword": "keyword",
70
+ "syntaxFunction": "function",
71
+ "syntaxVariable": "text",
72
+ "syntaxString": "string",
73
+ "syntaxNumber": "number",
74
+ "syntaxType": "type",
75
+ "syntaxOperator": "muted",
76
+ "syntaxPunctuation": "dim",
77
+ "thinkingOff": "dim",
78
+ "thinkingMinimal": "muted",
79
+ "thinkingLow": "think1",
80
+ "thinkingMedium": "think2",
81
+ "thinkingHigh": "think3",
82
+ "thinkingXhigh": "think4",
83
+ "bashMode": "accent",
84
+ "statusLineBg": "surfaceBright",
85
+ "statusLineSep": "dim",
86
+ "statusLineModel": "accent",
87
+ "statusLinePath": "secondary",
88
+ "statusLineGitClean": "success",
89
+ "statusLineGitDirty": "warning",
90
+ "statusLineContext": "string",
91
+ "statusLineSpend": "warning",
92
+ "statusLineStaged": "success",
93
+ "statusLineDirty": "warning",
94
+ "statusLineUntracked": "diffRemoved",
95
+ "statusLineOutput": "text",
96
+ "statusLineCost": "accent",
97
+ "statusLineSubagents": "secondary",
98
+ "pythonMode": "function"
99
+ },
100
+ "export": {
101
+ "pageBg": "#031210",
102
+ "cardBg": "#061c19",
103
+ "infoBg": "#0a2723"
104
+ },
105
+ "symbols": {
106
+ "preset": "unicode",
107
+ "overrides": {
108
+ "icon.pi": "🐙",
109
+ "icon.agents": "🦑",
110
+ "status.running": "◌",
111
+ "md.bullet": "▸"
112
+ }
113
+ }
114
+ }
@@ -1,15 +1,21 @@
1
1
  import blue_octopus from "./blue-octopus.json" with { type: "json" };
2
2
  import claude_code from "./claude-code.json" with { type: "json" };
3
3
  import codex from "./codex.json" with { type: "json" };
4
+ import glow_octopus from "./glow-octopus.json" with { type: "json" };
4
5
  import gruvbox_dark from "./gruvbox-dark.json" with { type: "json" };
6
+ import ink_octopus from "./ink-octopus.json" with { type: "json" };
5
7
  import opencode from "./opencode.json" with { type: "json" };
6
8
  import red_octopus from "./red-octopus.json" with { type: "json" };
9
+ import violet_octopus from "./violet-octopus.json" with { type: "json" };
7
10
 
8
11
  export const defaultThemes = {
9
12
  "blue-octopus": blue_octopus,
10
13
  "claude-code": claude_code,
11
14
  codex,
15
+ "glow-octopus": glow_octopus,
12
16
  "gruvbox-dark": gruvbox_dark,
17
+ "ink-octopus": ink_octopus,
13
18
  opencode,
14
19
  "red-octopus": red_octopus,
20
+ "violet-octopus": violet_octopus,
15
21
  };