@sayknow-cli/coding-agent 0.6.4 → 0.6.5

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 (81) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +2 -2
  3. package/dist/types/config/keybindings.d.ts +1 -1
  4. package/dist/types/config/settings-schema.d.ts +7 -3
  5. package/dist/types/i18n/messages/en.d.ts +16 -0
  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 +49 -6
  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 +7 -1
  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 +1 -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/tui/output-block.d.ts +12 -1
  20. package/dist/types/tui/utils.d.ts +1 -3
  21. package/dist/types/utils/session-title-generation.d.ts +6 -0
  22. package/dist/types/utils/title-generator.d.ts +11 -2
  23. package/package.json +7 -7
  24. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  25. package/src/cli/session-picker.ts +4 -0
  26. package/src/config/keybindings.ts +1 -1
  27. package/src/config/settings-schema.ts +4 -3
  28. package/src/i18n/messages/de.messages.ts +4 -1
  29. package/src/i18n/messages/de.settings.ts +2 -0
  30. package/src/i18n/messages/de.ts +16 -0
  31. package/src/i18n/messages/en.ts +16 -0
  32. package/src/i18n/messages/es.messages.ts +4 -1
  33. package/src/i18n/messages/es.settings.ts +2 -0
  34. package/src/i18n/messages/es.ts +16 -0
  35. package/src/i18n/messages/fr.messages.ts +4 -1
  36. package/src/i18n/messages/fr.settings.ts +2 -0
  37. package/src/i18n/messages/fr.ts +16 -0
  38. package/src/i18n/messages/ja.messages.ts +4 -1
  39. package/src/i18n/messages/ja.settings.ts +2 -0
  40. package/src/i18n/messages/ja.ts +16 -0
  41. package/src/i18n/messages/ko.messages.ts +4 -1
  42. package/src/i18n/messages/ko.settings.ts +2 -0
  43. package/src/i18n/messages/ko.ts +16 -0
  44. package/src/i18n/messages/zh.messages.ts +2 -1
  45. package/src/i18n/messages/zh.settings.ts +2 -0
  46. package/src/i18n/messages/zh.ts +16 -0
  47. package/src/internal-urls/docs-index.generated.ts +10 -10
  48. package/src/lsp/render.ts +0 -1
  49. package/src/modes/components/session-selector.ts +71 -21
  50. package/src/modes/components/sessions-dashboard.ts +8 -5
  51. package/src/modes/components/settings-selector.ts +1 -0
  52. package/src/modes/components/status-line/presets.ts +5 -5
  53. package/src/modes/components/status-line/separators.ts +4 -0
  54. package/src/modes/components/tool-execution.ts +6 -19
  55. package/src/modes/components/tool-status-header.ts +103 -12
  56. package/src/modes/components/user-message-selector.ts +57 -15
  57. package/src/modes/components/user-message.ts +25 -15
  58. package/src/modes/components/welcome.ts +374 -478
  59. package/src/modes/controllers/command-controller.ts +56 -7
  60. package/src/modes/controllers/input-controller.ts +4 -0
  61. package/src/modes/controllers/selector-controller.ts +91 -10
  62. package/src/modes/interactive-mode.ts +101 -12
  63. package/src/modes/theme/defaults/glow-octopus.json +114 -0
  64. package/src/modes/theme/defaults/index.ts +6 -0
  65. package/src/modes/theme/defaults/ink-octopus.json +114 -0
  66. package/src/modes/theme/defaults/violet-octopus.json +114 -0
  67. package/src/modes/theme/theme.ts +18 -2
  68. package/src/modes/types.ts +1 -1
  69. package/src/prompts/system/title-system.md +1 -1
  70. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  71. package/src/session/agent-session.ts +12 -0
  72. package/src/session/internal/managed-session-scope.ts +10 -0
  73. package/src/session/session-manager.ts +26 -1
  74. package/src/session/session-stars.ts +104 -0
  75. package/src/slash-commands/builtin-registry.ts +104 -4
  76. package/src/tools/debug.ts +0 -1
  77. package/src/tools/fetch.ts +0 -1
  78. package/src/tui/output-block.ts +29 -61
  79. package/src/tui/utils.ts +1 -8
  80. package/src/utils/session-title-generation.ts +29 -0
  81. 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";
@@ -971,6 +972,7 @@ export class InputController {
971
972
 
972
973
  // Queue input during compaction
973
974
  if (this.ctx.session.isCompacting) {
975
+ invalidateSessionTitleGeneration(this.ctx.sessionManager);
974
976
  if ((inputImages?.length ?? 0) > 0) {
975
977
  this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
976
978
  return;
@@ -984,6 +986,7 @@ export class InputController {
984
986
  // prompt to run after the active turn completes (in submission order).
985
987
  // This handles extension commands (execute immediately), prompt template expansion, and queueing
986
988
  if (this.ctx.session.isStreaming) {
989
+ invalidateSessionTitleGeneration(this.ctx.sessionManager);
987
990
  if (this.#canModifyComposer(composer)) {
988
991
  this.ctx.editor.addToHistory(text);
989
992
  this.ctx.editor.setText("");
@@ -1008,6 +1011,7 @@ export class InputController {
1008
1011
  }
1009
1012
 
1010
1013
  // Normal message submission
1014
+ invalidateSessionTitleGeneration(this.ctx.sessionManager);
1011
1015
  // First, move any pending bash components to chat
1012
1016
  this.ctx.flushPendingBashComponents();
1013
1017
 
@@ -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,4 @@
1
+ import * as path from "node:path";
1
2
  import { type Agent, type AgentMessage, ThinkingLevel } from "@sayknow-cli/agent-core";
2
3
  import type { CompactionOutcome } from "@sayknow-cli/agent-core/compaction";
3
4
  import type { AssistantMessage, ImageContent, Message, UsageReport } from "@sayknow-cli/ai";
@@ -24,6 +25,7 @@ import {
24
25
  KeybindingsManager,
25
26
  type KeyDisplayContext,
26
27
  } from "../config/keybindings";
28
+ import { getModelProfilePresentation } from "../config/model-profiles";
27
29
  import { isSettingsInitialized, type Settings, settings } from "../config/settings";
28
30
  import { DEFAULT_SKC_DEFINITION_NAMES } from "../defaults/skc-defaults";
29
31
  import type {
@@ -46,10 +48,13 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
46
48
  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";
51
+ import { loadProjectContextFiles } from "../system-prompt";
49
52
  import type { LspStartupServerInfo } from "../tools";
53
+ import { shortenPath } from "../tools/render-utils";
50
54
  import { formatPhaseDisplayName } from "../tools/todo-write";
51
55
  import { copyToClipboard } from "../utils/clipboard";
52
56
  import type { EventBus } from "../utils/event-bus";
57
+ import * as gitUtils from "../utils/git";
53
58
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
54
59
  import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
55
60
  import type { AssistantMessageComponent } from "./components/assistant-message";
@@ -68,6 +73,7 @@ import {
68
73
  warnWhenPetCapabilitySettled,
69
74
  } from "./components/pet-capability";
70
75
  import { type PetMode, SayknowPetWidget } from "./components/sayknow-pet-widget";
76
+ import { resolveCurrentBranch } from "./components/status-line/git-utils";
71
77
  import type { ToolExecutionHandle } from "./components/tool-execution";
72
78
  import { StatusLineComponent } from "./components/tool-status-header";
73
79
  import { composeToolText } from "./components/tool-transcript-format";
@@ -76,6 +82,8 @@ import {
76
82
  WelcomeComponent,
77
83
  type WelcomeLogoMode,
78
84
  type LspServerInfo as WelcomeLspServerInfo,
85
+ type WelcomeRoleBinding,
86
+ type WelcomeSnapshot,
79
87
  } from "./components/welcome";
80
88
  import { BtwController } from "./controllers/btw-controller";
81
89
  import { CommandController } from "./controllers/command-controller";
@@ -196,23 +204,24 @@ const HINT_SHIMMER_PALETTE: ShimmerPalette = {
196
204
  high: "borderAccent",
197
205
  };
198
206
 
199
- function getDefaultInputPrefix(): string {
200
- return `${theme.fg("accent", ">")} `;
201
- }
202
-
203
207
  function getShellInputPrefix(isNoContext: boolean): string {
204
208
  const shellLabel = isNoContext
205
209
  ? theme.fg("warning", theme.bold("shell no-context"))
206
210
  : theme.fg("bashMode", theme.bold("shell"));
207
- return `${shellLabel} ${getDefaultInputPrefix()}`;
211
+ return `${shellLabel} `;
208
212
  }
209
213
 
210
- function configureDefaultComposerChrome(editor: CustomEditor): void {
211
- editor.setBorderVisible(true);
212
- editor.setBorderStyle("round");
213
- editor.setClosedBorderBox(true);
214
+ /**
215
+ * The composer is an open rail, not a box: every input row starts with the same
216
+ * rail glyph a submitted prompt keeps in the transcript, colored by the editor's
217
+ * border color (session accent, thinking level, shell/python mode).
218
+ */
219
+ export function configureDefaultComposerChrome(editor: CustomEditor): void {
220
+ editor.setBorderVisible(false);
221
+ editor.setClosedBorderBox(false);
214
222
  editor.setPromptGutter(undefined);
215
- editor.setInputPrefix(getDefaultInputPrefix());
223
+ editor.setRailGutter(theme.rail.user);
224
+ editor.setInputPrefix(undefined);
216
225
  editor.setPlaceholder(getDefaultComposerPlaceholder());
217
226
  editor.setPaddingX(1);
218
227
  editor.setRightGutterWidth(COMPOSER_RIGHT_GUTTER_WIDTH);
@@ -744,6 +753,8 @@ export class InteractiveMode implements InteractiveModeContext {
744
753
  collapseChangelog: settings.get("collapseChangelog"),
745
754
  keyDisplayContext: this.#keyDisplayContext,
746
755
  skipLogoAnimation,
756
+ snapshot: this.#buildWelcomeSnapshot(),
757
+ resumeKey: this.keybindings.getKeys("app.session.resume")[0],
747
758
  },
748
759
  );
749
760
 
@@ -873,6 +884,7 @@ export class InteractiveMode implements InteractiveModeContext {
873
884
  error: error instanceof Error ? error.message : String(error),
874
885
  });
875
886
  });
887
+ this.#probeWelcomeWorkspace(welcomeComponent);
876
888
  }, 0);
877
889
  recentSessionsTimer.unref?.();
878
890
  }
@@ -1206,8 +1218,10 @@ export class InteractiveMode implements InteractiveModeContext {
1206
1218
  }
1207
1219
  }
1208
1220
  if (!this.isBashMode) {
1209
- this.editor.setInputPrefix(getDefaultInputPrefix());
1221
+ this.editor.setInputPrefix(undefined);
1210
1222
  }
1223
+ // Re-read the glyph so a symbol-preset switch (unicode/nerd/ascii) reaches the rail.
1224
+ this.editor.setRailGutter(theme.rail.user);
1211
1225
  this.editor.setPlaceholder(this.#getComposerPlaceholder());
1212
1226
  this.#setComposerTopBorder();
1213
1227
  this.ui.requestRender();
@@ -1621,9 +1635,84 @@ export class InteractiveMode implements InteractiveModeContext {
1621
1635
  }
1622
1636
 
1623
1637
  this.#welcomeComponent.setLspServers(this.#getWelcomeLspServers());
1638
+ // LSP startup settles around the same time the MCP connections do.
1639
+ this.#welcomeComponent.setSnapshot({ mcp: this.#welcomeMcpSummary() });
1624
1640
  this.ui.requestRender();
1625
1641
  }
1626
1642
 
1643
+ /** Facts the launch ledger can show synchronously, before any probe runs. */
1644
+ #buildWelcomeSnapshot(): WelcomeSnapshot {
1645
+ const projectDir = getProjectDir();
1646
+ let branch: string | null = null;
1647
+ try {
1648
+ branch = resolveCurrentBranch(projectDir).branch;
1649
+ } catch {
1650
+ branch = null;
1651
+ }
1652
+ const profileName = this.session.getActiveModelProfile() ?? settings.get("modelProfile.default");
1653
+ return {
1654
+ cwd: shortenPath(projectDir),
1655
+ branch,
1656
+ thinkingLevel: this.session.thinkingLevel,
1657
+ profile: profileName ? getModelProfilePresentation(profileName).displayName : undefined,
1658
+ roles: this.#welcomeRoleBindings(),
1659
+ mcp: this.#welcomeMcpSummary(),
1660
+ skills: this.session.skills.length,
1661
+ };
1662
+ }
1663
+
1664
+ /** Role agents in their canonical order, with the provider prefix dropped for width. */
1665
+ #welcomeRoleBindings(): WelcomeRoleBinding[] {
1666
+ const overrides = settings.get("task.agentModelOverrides") as Record<string, unknown> | undefined;
1667
+ if (!overrides) return [];
1668
+ const bindings: WelcomeRoleBinding[] = [];
1669
+ for (const role of ["executor", "planner", "critic", "architect"]) {
1670
+ const selector = overrides[role];
1671
+ if (typeof selector !== "string" || selector.length === 0) continue;
1672
+ const slash = selector.indexOf("/");
1673
+ bindings.push({ role, model: slash >= 0 ? selector.slice(slash + 1) : selector });
1674
+ }
1675
+ return bindings;
1676
+ }
1677
+
1678
+ #welcomeMcpSummary(): { connected: number; total: number } {
1679
+ const manager = this.mcpManager;
1680
+ if (!manager) return { connected: 0, total: 0 };
1681
+ return { connected: manager.getConnectedServers().length, total: manager.getAllServerNames().length };
1682
+ }
1683
+
1684
+ /** Fill in the ledger facts that need I/O: git change counts and project instruction files. */
1685
+ #probeWelcomeWorkspace(welcomeComponent: WelcomeComponent): void {
1686
+ const projectDir = getProjectDir();
1687
+ const apply = (patch: WelcomeSnapshot): void => {
1688
+ if (this.#welcomeComponent !== welcomeComponent) return;
1689
+ welcomeComponent.setSnapshot(patch);
1690
+ this.ui.requestRender();
1691
+ };
1692
+ void gitUtils.status
1693
+ .summary(projectDir)
1694
+ .then(gitChanges => apply({ gitChanges }))
1695
+ .catch(() => apply({ gitChanges: null }));
1696
+ void gitUtils.log
1697
+ .onelines(projectDir, 3)
1698
+ .then(recentCommits => apply({ recentCommits }))
1699
+ .catch(() => {});
1700
+ void loadProjectContextFiles({ cwd: projectDir })
1701
+ .then(files =>
1702
+ apply({
1703
+ contextFiles: files.map(file => {
1704
+ const relative = path.relative(projectDir, file.path);
1705
+ return relative.startsWith("..") ? shortenPath(file.path) : relative;
1706
+ }),
1707
+ }),
1708
+ )
1709
+ .catch(error => {
1710
+ logger.debug("Failed to load context files for welcome screen", {
1711
+ error: error instanceof Error ? error.message : String(error),
1712
+ });
1713
+ });
1714
+ }
1715
+
1627
1716
  #getWorkingMessageAccent(): WorkingMessageAccent | undefined {
1628
1717
  const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
1629
1718
  const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
@@ -1866,7 +1955,7 @@ export class InteractiveMode implements InteractiveModeContext {
1866
1955
  return this.#commandController.handleMoveCommand(targetPath);
1867
1956
  }
1868
1957
 
1869
- handleRenameCommand(title: string): Promise<void> {
1958
+ handleRenameCommand(title?: string): Promise<void> {
1870
1959
  return this.#commandController.handleRenameCommand(title);
1871
1960
  }
1872
1961
 
@@ -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
  };