@sayknow-cli/coding-agent 0.6.5 → 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.
@@ -112,6 +112,9 @@ export class InputController {
112
112
  "app.session.tree": () => this.ctx.showTreeSelector(),
113
113
  "app.session.fork": () => this.ctx.showUserMessageSelector(),
114
114
  "app.session.resume": () => this.ctx.showSessionSelector(),
115
+ "app.session.continue": async () => {
116
+ await this.ctx.continueRecentSession();
117
+ },
115
118
  "app.session.observe": async () => {
116
119
  await this.ctx.showSessionObserver();
117
120
  },
@@ -710,6 +713,16 @@ export class InputController {
710
713
  return true;
711
714
  });
712
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
+ }
713
726
  this.#registerCommandPaletteAction(
714
727
  "app.message.followUp",
715
728
  () => this.#executeAction("app.message.followUp"),
@@ -1,3 +1,4 @@
1
+ import * as fs from "node:fs";
1
2
  import * as path from "node:path";
2
3
  import { type Agent, type AgentMessage, ThinkingLevel } from "@sayknow-cli/agent-core";
3
4
  import type { CompactionOutcome } from "@sayknow-cli/agent-core/compaction";
@@ -25,7 +26,6 @@ import {
25
26
  KeybindingsManager,
26
27
  type KeyDisplayContext,
27
28
  } from "../config/keybindings";
28
- import { getModelProfilePresentation } from "../config/model-profiles";
29
29
  import { isSettingsInitialized, type Settings, settings } from "../config/settings";
30
30
  import { DEFAULT_SKC_DEFINITION_NAMES } from "../defaults/skc-defaults";
31
31
  import type {
@@ -48,7 +48,6 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
48
48
  import { HistoryStorage } from "../session/history-storage";
49
49
  import type { SessionContext, SessionManager } from "../session/session-manager";
50
50
  import { getRecentSessions, getSessionMessageEntryId } from "../session/session-manager";
51
- import { loadProjectContextFiles } from "../system-prompt";
52
51
  import type { LspStartupServerInfo } from "../tools";
53
52
  import { shortenPath } from "../tools/render-utils";
54
53
  import { formatPhaseDisplayName } from "../tools/todo-write";
@@ -72,19 +71,12 @@ import {
72
71
  isPetCapabilityProbePending,
73
72
  warnWhenPetCapabilitySettled,
74
73
  } from "./components/pet-capability";
75
- import { type PetMode, SayknowPetWidget } from "./components/sayknow-pet-widget";
74
+ import { type PetMode, type PetSkinId, SayknowPetWidget } from "./components/sayknow-pet-widget";
76
75
  import { resolveCurrentBranch } from "./components/status-line/git-utils";
77
76
  import type { ToolExecutionHandle } from "./components/tool-execution";
78
77
  import { StatusLineComponent } from "./components/tool-status-header";
79
78
  import { composeToolText } from "./components/tool-transcript-format";
80
- import {
81
- type RecentSession,
82
- WelcomeComponent,
83
- type WelcomeLogoMode,
84
- type LspServerInfo as WelcomeLspServerInfo,
85
- type WelcomeRoleBinding,
86
- type WelcomeSnapshot,
87
- } from "./components/welcome";
79
+ import { type RecentSession, WelcomeComponent, type WelcomeLogoMode, type WelcomeSnapshot } from "./components/welcome";
88
80
  import { BtwController } from "./controllers/btw-controller";
89
81
  import { CommandController } from "./controllers/command-controller";
90
82
  import { EventController } from "./controllers/event-controller";
@@ -107,7 +99,14 @@ import { shouldShowExtensionCommand } from "./slash-command-visibility";
107
99
  import { TasksAggregator } from "./tasks-aggregator";
108
100
  import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
109
101
  import type { Theme } from "./theme/theme";
110
- 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";
111
110
  import { type RegisterTranscriptItem, TranscriptItemRegistry, transcriptItemId } from "./transcript-item-registry";
112
111
  import {
113
112
  type CompactionQueuedMessage,
@@ -211,6 +210,15 @@ function getShellInputPrefix(isNoContext: boolean): string {
211
210
  return `${shellLabel} `;
212
211
  }
213
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";
220
+ }
221
+
214
222
  /**
215
223
  * The composer is an open rail, not a box: every input row starts with the same
216
224
  * rail glyph a submitted prompt keeps in the transcript, colored by the editor's
@@ -743,7 +751,6 @@ export class InteractiveMode implements InteractiveModeContext {
743
751
  modelName,
744
752
  providerName,
745
753
  recentSessions,
746
- this.#getWelcomeLspServers(),
747
754
  welcomeLogoMode,
748
755
  {
749
756
  getViewportRows: () => this.ui.terminal.rows,
@@ -755,6 +762,8 @@ export class InteractiveMode implements InteractiveModeContext {
755
762
  skipLogoAnimation,
756
763
  snapshot: this.#buildWelcomeSnapshot(),
757
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()),
758
767
  },
759
768
  );
760
769
 
@@ -1611,36 +1620,13 @@ export class InteractiveMode implements InteractiveModeContext {
1611
1620
  }
1612
1621
 
1613
1622
  #handleLspStartupEvent(event: LspStartupEvent): void {
1614
- this.#updateWelcomeLspServers();
1615
-
1616
1623
  const warningMessage = getLspStartupWarningMessage(event);
1617
1624
  if (warningMessage) {
1618
1625
  this.showWarning(warningMessage);
1619
1626
  }
1620
1627
  }
1621
1628
 
1622
- #getWelcomeLspServers(): WelcomeLspServerInfo[] {
1623
- return (
1624
- this.lspServers?.map(server => ({
1625
- name: server.name,
1626
- status: server.status,
1627
- fileTypes: server.fileTypes,
1628
- })) ?? []
1629
- );
1630
- }
1631
-
1632
- #updateWelcomeLspServers(): void {
1633
- if (!this.#welcomeComponent) {
1634
- return;
1635
- }
1636
-
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() });
1640
- this.ui.requestRender();
1641
- }
1642
-
1643
- /** Facts the launch ledger can show synchronously, before any probe runs. */
1629
+ /** Facts the launch card can show synchronously, before the git probe runs. */
1644
1630
  #buildWelcomeSnapshot(): WelcomeSnapshot {
1645
1631
  const projectDir = getProjectDir();
1646
1632
  let branch: string | null = null;
@@ -1649,68 +1635,19 @@ export class InteractiveMode implements InteractiveModeContext {
1649
1635
  } catch {
1650
1636
  branch = null;
1651
1637
  }
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 };
1638
+ return { cwd: shortenPath(projectDir), branch, thinkingLevel: this.session.thinkingLevel };
1682
1639
  }
1683
1640
 
1684
- /** Fill in the ledger facts that need I/O: git change counts and project instruction files. */
1641
+ /** Fill in the git change counts, which need I/O. */
1685
1642
  #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
1643
  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 }))
1644
+ .summary(getProjectDir())
1645
+ .then(gitChanges => {
1646
+ if (this.#welcomeComponent !== welcomeComponent) return;
1647
+ welcomeComponent.setSnapshot({ gitChanges });
1648
+ this.ui.requestRender();
1649
+ })
1699
1650
  .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
1651
  }
1715
1652
 
1716
1653
  #getWorkingMessageAccent(): WorkingMessageAccent | undefined {
@@ -2246,6 +2183,27 @@ export class InteractiveMode implements InteractiveModeContext {
2246
2183
  this.#selectorController.showSessionSelector();
2247
2184
  }
2248
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
+
2249
2207
  handleResumeSession(sessionPath: string, options?: { requireIdle?: boolean }): Promise<boolean> {
2250
2208
  return this.#selectorController.handleResumeSession(sessionPath, options, () => {
2251
2209
  this.#btwController.dispose();
@@ -327,6 +327,8 @@ export interface InteractiveModeContext {
327
327
  showUserMessageSelector(): void;
328
328
  showTreeSelector(): void;
329
329
  showSessionSelector(): void;
330
+ /** Resume the most recent saved session other than this one (the launch card's first row). */
331
+ continueRecentSession(): Promise<void>;
330
332
  showSessionsDashboard(): void;
331
333
  handleResumeSession(sessionPath: string, options?: { requireIdle?: boolean }): Promise<boolean>;
332
334
  handleSessionDeleteCommand(): Promise<void>;