@sayknow-cli/coding-agent 0.6.5 → 0.6.7

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 (69) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/config/keybindings.d.ts +5 -0
  3. package/dist/types/config/settings-schema.d.ts +79 -0
  4. package/dist/types/i18n/messages/en.d.ts +6 -29
  5. package/dist/types/modes/components/welcome.d.ts +34 -40
  6. package/dist/types/modes/interactive-mode.d.ts +7 -4
  7. package/dist/types/modes/types.d.ts +4 -0
  8. package/dist/types/sdk/broker/broker.d.ts +22 -0
  9. package/dist/types/sdk/broker/process-guard.d.ts +71 -0
  10. package/dist/types/sdk/broker/transport.d.ts +2 -0
  11. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  12. package/dist/types/session/agent-session.d.ts +9 -0
  13. package/dist/types/session/auth-storage-discovery.d.ts +15 -0
  14. package/dist/types/session/auto-fallback.d.ts +27 -0
  15. package/dist/types/session/fallback-chain-controller.d.ts +5 -0
  16. package/dist/types/session/response-language.d.ts +25 -0
  17. package/dist/types/setup/model-onboarding-guidance.d.ts +8 -1
  18. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  19. package/dist/types/tools/debug.d.ts +2 -2
  20. package/dist/types/tools/index.d.ts +1 -0
  21. package/dist/types/tools/locate-core.d.ts +97 -0
  22. package/dist/types/tools/locate.d.ts +38 -0
  23. package/package.json +7 -7
  24. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  25. package/src/cli/setup-cli.ts +7 -4
  26. package/src/commands/sdk.ts +3 -0
  27. package/src/commands/setup.ts +4 -1
  28. package/src/config/keybindings.ts +7 -0
  29. package/src/config/settings-schema.ts +82 -0
  30. package/src/decisions/typesafe-backend.ts +38 -4
  31. package/src/i18n/messages/de.settings.ts +26 -0
  32. package/src/i18n/messages/de.ts +4 -27
  33. package/src/i18n/messages/en.ts +6 -29
  34. package/src/i18n/messages/es.settings.ts +26 -0
  35. package/src/i18n/messages/es.ts +4 -27
  36. package/src/i18n/messages/fr.settings.ts +26 -0
  37. package/src/i18n/messages/fr.ts +4 -27
  38. package/src/i18n/messages/ja.settings.ts +26 -0
  39. package/src/i18n/messages/ja.ts +4 -27
  40. package/src/i18n/messages/ko.settings.ts +25 -0
  41. package/src/i18n/messages/ko.ts +5 -28
  42. package/src/i18n/messages/zh.settings.ts +23 -0
  43. package/src/i18n/messages/zh.ts +4 -27
  44. package/src/internal-urls/docs-index.generated.ts +8 -7
  45. package/src/modes/action-registry.ts +1 -0
  46. package/src/modes/components/welcome.ts +385 -387
  47. package/src/modes/controllers/input-controller.ts +15 -0
  48. package/src/modes/controllers/selector-controller.ts +13 -0
  49. package/src/modes/interactive-mode.ts +160 -98
  50. package/src/modes/types.ts +4 -0
  51. package/src/prompts/system/system-prompt.md +7 -2
  52. package/src/prompts/tools/locate.md +12 -0
  53. package/src/sdk/broker/broker.ts +75 -1
  54. package/src/sdk/broker/process-guard.ts +160 -0
  55. package/src/sdk/broker/transport.ts +15 -1
  56. package/src/sdk/bus/chat-daemon-runtime.ts +13 -1
  57. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  58. package/src/sdk/session.ts +4 -1
  59. package/src/session/agent-session.ts +118 -2
  60. package/src/session/auth-storage-discovery.ts +22 -7
  61. package/src/session/auto-fallback.ts +59 -0
  62. package/src/session/fallback-chain-controller.ts +5 -0
  63. package/src/session/response-language.ts +71 -0
  64. package/src/setup/model-onboarding-guidance.ts +29 -14
  65. package/src/setup/provider-onboarding.ts +5 -0
  66. package/src/slash-commands/builtin-registry.ts +112 -2
  67. package/src/tools/index.ts +3 -0
  68. package/src/tools/locate-core.ts +720 -0
  69. package/src/tools/locate.ts +197 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.7] - 2026-09-28
6
+
7
+ ### Added
8
+
9
+ - Launch card sessions open from the card: press `↓` in the empty composer to highlight a recent session, `↑`/`↓` to move, `Enter` to open it (`Esc` returns to typing), or click it. Until the first prompt SKC captures the mouse so the click reaches the card, then hands it back to the terminal; `startup.welcomeMouse` (Settings → Interaction → Clickable Launch Card) turns that off.
10
+ - `locate` tool: find code by describing what it does. Jev (TypeSafe) judges relevance while walking the tree: large folders are split and only plausible subfolders are entered, then candidate files are ranked from their outlines. Only folder paths, file names and declaration lines are sent (no bodies, comments, imports or literal values; initialisers are cut). Offered only when a TypeSafe key is stored; `locate.enabled` turns it off. The six best files are then compared in one question to settle their order. A local keyword score over each file's full text (read on this machine, never sent) is blended in. On 154 held-out commit questions from 8 public repositories (Python, TypeScript, Go, Rust), an answer file ranked first 51% of the time, in the top five 78% and in the top ten 86%, against 27%, 58% and 69% for keyword grep; a search returns about 5 KB. Block comments, docstrings, multi-line strings, trailing comments and string-literal lines are kept out of what is sent. See `docs/tools/locate.md`.
11
+ - Automatic model fallback. When the default model is blocked — no credentials, quota, auth or server errors — the session now continues on a model from another provider you are logged in to instead of stopping. The chain is the configured default (a single model or a `modelRoles` array), then `fallback.models`, then one model per other logged-in provider (the one you used most recently there, else the provider's curated default; up to three). A missing login is skipped before any request; failures during a turn retry `fallback.maxAttempts` times, rotate to the provider's next logged-in account, then move on, and the status line shows each switch. Automatic picks are never added under an `enabledModels` allow-list, for keyless local providers, for an explicit `--model`, or for subagent chains, and they are not written into the session's configured chain. Turn it off with `fallback.auto` (Settings → Model → Automatic Model Fallback).
12
+ - `/fallback` shows the chain the next prompt walks — where each model came from (`default`, `chain`, `added`, `auto`) and which is in use — and edits it: `/fallback add <model>`, `remove <n|model>`, `clear`, `auto on|off`. Added models are kept when you pick another default in `/model`.
13
+ - Settings → Providers → **Multi-Account Order** (`auth.credentialRankingMode`: `balanced` or `earliest-reset`) chooses which logged-in account of a provider a new session uses; it applies immediately. The existing `SKC_CREDENTIAL_RANKING_MODE` env var still overrides it per machine. Settings → Model also shows **Fallback Attempts per Model** (`fallback.maxAttempts`).
14
+
15
+ ### Fixed
16
+
17
+ - Answers no longer drift into English after long tool work. The system prompt now always carries a reply-language rule (the language the user writes in, or the one they asked for, for the whole turn including the final report; English tool output, logs, skills and summaries never change it); `reasoningLanguage: english` only changes reasoning. When a message is clearly written in a non-Latin script (Korean, Japanese, Chinese, Russian, Arabic, Hebrew, Thai, Hindi, Greek), the turn starts with a one-line reminder naming that language. Code blocks, inline code, quotes and links are ignored when detecting it, so a pasted English log with one Korean word does not count. Compaction and handoff summaries now record the reply language, so it survives a context compaction.
18
+ - Typed decisions (Jev) kept failing with 401 in every session that was already open when the TypeSafe key was replaced, because a session reads stored credentials once at startup and kept sending the revoked key; routing then fell back to an uncalibrated model. On 401/403 the TypeSafe backend now reloads stored credentials and retries once if the key changed; an unchanged key is not retried.
19
+ - SDK background processes no longer live forever. A broker now stops after 30 minutes with no client connection, no request in flight and no live session host (the next client starts a fresh one; chat daemons now start one when the discovery file is missing). A session host stops once its ownership marker or worktree has been gone for three checks in a row. A broker or host run from TypeScript source kills itself as soon as its entry script disappears, so ejecting the drive that holds a checkout no longer leaves a process spinning at 100% CPU in Bun's fault handler on the unmapped native addon. Found after brokers and hosts left behind by SDK tests stayed up for two days and one pinned a core when the external drive disconnected.
20
+ - A missing-credential error no longer opens with "For MiniMax/GLM presets…" whatever provider was called. It names the provider that lacks credentials and its `/login <provider>` (or env var) first, then lists other ways to add a provider. Preset lists in these messages, `/provider`, and `skc setup provider` come from the bundled preset catalog instead of a hand-kept `minimax|minimax-cn|glm`, so `alibaba-token-plan` is no longer missing from them.
21
+
22
+ ## [0.6.6] - 2026-09-28
23
+
24
+ ### Added
25
+
26
+ - `ctrl+q` (`app.session.continue`) continues the most recent saved session in one keystroke, from the launch card or mid-session; it never resumes the live session into itself and refuses while a turn is running. The launch card marks that session with `›` and names the key next to `alt+r` for the full list. It is a Control chord on purpose: many macOS terminals turn Option into composed text, so Option shortcuts such as `alt+r` only work where the terminal sends Option as Meta.
27
+
28
+ ### Changed
29
+
30
+ - Launch card redesign: the SAYKNOW CLI wordmark with a small version of the pet (8×4, drawn for the size) beside it, tagline and version under the wordmark, model and project/branch aligned in the same column (the place moves to its own row instead of being cut off). The release line and the recent-sessions heading are rule headers (`label ──── hints`) that divide the card, and the keys row uses accent keys with `·` separators.
31
+ - The composer's input rows sit on a background band (`userMessageBg`) with the rail, so the line being typed is distinct from the transcript.
32
+ - The launch screen is a compact card instead of the two-column ledger. The Sayknow pet — the same octopus sprite as the composer pet, drawn in half blocks so it needs no image protocol — sits beside four lines — version, tagline, model with reasoning level, and project path with branch and change counts — followed by one line after an update with the new version and its first note (pointing at `/changelog`), the three most recent sessions (with the key that opens the full list), and a single row of keys. Commits, preset, role models, MCP/skill/rule/LSP status, workflows, and the full keymap are no longer on it; they are one key or command away. The card stays within 76 columns on wide terminals and, when rows run short, drops the keys row first, then the release line, then the sessions. The pet is the one you keep beside the composer (`pet.mode`), or, with the pet off, blue on `blue-octopus` and red elsewhere. The intro has the pet sway its tentacles and lets color spread for about half a second; every fact is readable from the first frame, and `startup.skipLogoAnimation` still turns it off.
33
+
5
34
  ## [0.6.5] - 2026-09-26
6
35
 
7
36
  ### Added
@@ -30,6 +30,7 @@ interface AppKeybindings {
30
30
  "app.session.tree": true;
31
31
  "app.session.fork": true;
32
32
  "app.session.resume": true;
33
+ "app.session.continue": true;
33
34
  "app.session.observe": true;
34
35
  "app.session.dashboard": true;
35
36
  "app.jobs.open": true;
@@ -167,6 +168,10 @@ export declare const KEYBINDINGS: {
167
168
  readonly defaultKeys: "alt+r";
168
169
  readonly description: "Resume session";
169
170
  };
171
+ readonly "app.session.continue": {
172
+ readonly defaultKeys: "ctrl+q";
173
+ readonly description: "Continue the most recent session";
174
+ };
170
175
  readonly "app.session.observe": {
171
176
  readonly defaultKeys: "ctrl+s";
172
177
  readonly description: "Observe subagent sessions";
@@ -1456,6 +1456,43 @@ export declare const SETTINGS_SCHEMA: {
1456
1456
  readonly type: "number";
1457
1457
  readonly default: 3;
1458
1458
  readonly validate: (value: number) => boolean;
1459
+ readonly ui: {
1460
+ readonly tab: "model";
1461
+ readonly label: "Fallback Attempts per Model";
1462
+ readonly description: "Tries on each model in the fallback chain (including the first) before moving to the next one. Missing credentials skip a model immediately.";
1463
+ readonly options: readonly [{
1464
+ readonly value: "1";
1465
+ readonly label: "1 try";
1466
+ }, {
1467
+ readonly value: "2";
1468
+ readonly label: "2 tries";
1469
+ }, {
1470
+ readonly value: "3";
1471
+ readonly label: "3 tries";
1472
+ }, {
1473
+ readonly value: "5";
1474
+ readonly label: "5 tries";
1475
+ }];
1476
+ };
1477
+ };
1478
+ /**
1479
+ * When the default model is blocked, keep going on another logged-in provider
1480
+ * after the configured chain and `fallback.models` run out. The extra entries
1481
+ * are computed per session and never written into the configured chain.
1482
+ */
1483
+ readonly "fallback.auto": {
1484
+ readonly type: "boolean";
1485
+ readonly default: true;
1486
+ readonly ui: {
1487
+ readonly tab: "model";
1488
+ readonly label: "Automatic Model Fallback";
1489
+ readonly description: "When the default model is blocked (no login, quota, auth or server errors), continue on a model from another logged-in provider. /fallback shows the chain.";
1490
+ };
1491
+ };
1492
+ /** Models to try, in order, after the default chain. Edited with `/fallback`. */
1493
+ readonly "fallback.models": {
1494
+ readonly type: "array";
1495
+ readonly default: string[];
1459
1496
  };
1460
1497
  readonly "retry.enabled": {
1461
1498
  readonly type: "boolean";
@@ -1691,6 +1728,15 @@ export declare const SETTINGS_SCHEMA: {
1691
1728
  }];
1692
1729
  };
1693
1730
  };
1731
+ readonly "startup.welcomeMouse": {
1732
+ readonly type: "boolean";
1733
+ readonly default: true;
1734
+ readonly ui: {
1735
+ readonly tab: "interaction";
1736
+ readonly label: "Clickable Launch Card";
1737
+ readonly description: "Until the first prompt, capture the mouse so a recent session on the launch card opens with one click. Terminal text selection needs Shift held meanwhile. Off: open sessions with ↓ and Enter.";
1738
+ };
1739
+ };
1694
1740
  readonly "startup.skipLogoAnimation": {
1695
1741
  readonly type: "boolean";
1696
1742
  readonly default: false;
@@ -3587,6 +3633,20 @@ export declare const SETTINGS_SCHEMA: {
3587
3633
  readonly description: "Hide non-essential built-in tools behind a search tool to save tokens.";
3588
3634
  };
3589
3635
  };
3636
+ /**
3637
+ * The `locate` tool: find code by describing what it does, judged by Jev. Offered only
3638
+ * when a TypeSafe key is stored. Sends folder paths, file names and declaration lines
3639
+ * (never bodies or comments) to TypeSafe.
3640
+ */
3641
+ readonly "locate.enabled": {
3642
+ readonly type: "boolean";
3643
+ readonly default: true;
3644
+ readonly ui: {
3645
+ readonly tab: "tools";
3646
+ readonly label: "Locate (Jev code search)";
3647
+ readonly description: "Offer the locate tool when a TypeSafe key is stored. It sends folder paths, file names and declaration lines (no function bodies or comments) to TypeSafe to judge relevance.";
3648
+ };
3649
+ };
3590
3650
  readonly "tools.essentialOverride": {
3591
3651
  readonly type: "array";
3592
3652
  readonly default: string[];
@@ -4002,6 +4062,25 @@ export declare const SETTINGS_SCHEMA: {
4002
4062
  readonly type: "boolean";
4003
4063
  readonly default: false;
4004
4064
  };
4065
+ readonly "auth.credentialRankingMode": {
4066
+ readonly type: "enum";
4067
+ readonly values: readonly ["balanced", "earliest-reset"];
4068
+ readonly default: "balanced";
4069
+ readonly ui: {
4070
+ readonly tab: "providers";
4071
+ readonly label: "Multi-Account Order";
4072
+ readonly description: "Which logged-in account of a provider a new session uses. A blocked or exhausted account is always skipped for the next one.";
4073
+ readonly options: readonly [{
4074
+ readonly value: "balanced";
4075
+ readonly label: "Balanced";
4076
+ readonly description: "Least-used account first; spreads load and keeps headroom on every account";
4077
+ }, {
4078
+ readonly value: "earliest-reset";
4079
+ readonly label: "Earliest reset";
4080
+ readonly description: "Account whose usage window resets soonest first, so quota is not lost at reset";
4081
+ }];
4082
+ };
4083
+ };
4005
4084
  readonly "secrets.enabled": {
4006
4085
  readonly type: "boolean";
4007
4086
  readonly default: false;
@@ -8,41 +8,18 @@ export declare const en: {
8
8
  readonly "lang.fr": "Français";
9
9
  readonly "lang.de": "Deutsch";
10
10
  readonly "welcome.tagline": "Coding should feel like thinking.";
11
- readonly "welcome.workflows": "Workflows";
12
- readonly "welcome.wf.deepInterview": "scope · interview → spec";
13
- readonly "welcome.wf.ralplan": "consensus plan";
14
- readonly "welcome.wf.ultragoal": "autonomous build";
15
- readonly "welcome.wf.team": "parallel agents";
16
- readonly "welcome.flowKeys": "Flow keys";
17
11
  readonly "welcome.commands": "commands";
18
- readonly "welcome.actions": "actions";
19
- readonly "welcome.shell": "shell";
20
- readonly "welcome.python": "python";
21
12
  readonly "welcome.keymap": "keymap";
22
13
  readonly "welcome.model": "model";
23
- readonly "welcome.reasoning": "reasoning";
24
- readonly "welcome.projectPulse": "Project pulse";
25
- readonly "welcome.noLsp": "No LSP servers";
26
- readonly "welcome.sessionTrail": "Session trail";
27
- readonly "welcome.noSessions": "No saved trails";
14
+ readonly "welcome.sessionTrail": "Recent sessions";
15
+ readonly "welcome.noSessions": "No saved sessions";
28
16
  readonly "welcome.allSessions": "{key} all sessions";
17
+ readonly "welcome.continue": "{key} continue";
18
+ readonly "welcome.pick": "{key} pick";
19
+ readonly "welcome.pickActive": "↑↓ move · ⏎ open · esc back";
20
+ readonly "welcome.sessions": "sessions";
29
21
  readonly "welcome.chooseModel": "choose a model";
30
22
  readonly "welcome.modelHint": "ctrl+l to pick · / for commands";
31
- readonly "welcome.label.workspace": "workspace";
32
- readonly "welcome.label.branch": "branch";
33
- readonly "welcome.label.commits": "commits";
34
- readonly "welcome.label.model": "model";
35
- readonly "welcome.label.reasoning": "reasoning";
36
- readonly "welcome.label.preset": "preset";
37
- readonly "welcome.label.roles": "roles";
38
- readonly "welcome.label.tools": "tools";
39
- readonly "welcome.noGit": "not a git repository";
40
- readonly "welcome.rolesInherit": "roles follow the default model";
41
- readonly "welcome.skills": "skills";
42
- readonly "welcome.rules": "rules";
43
- readonly "welcome.clean": "clean";
44
- readonly "welcome.whatsNew": "What's new";
45
- readonly "welcome.readyPrompt": "Ready for your next prompt";
46
23
  readonly "settings.tab.appearance": "Appearance";
47
24
  readonly "settings.tab.model": "Model";
48
25
  readonly "settings.tab.interaction": "Interaction";
@@ -1,23 +1,15 @@
1
1
  import type { ThinkingLevel } from "@sayknow-cli/agent-core";
2
- import { type Component } from "@sayknow-cli/tui";
2
+ import { type Component, type PetSkinId } from "@sayknow-cli/tui";
3
3
  import { type KeyDisplayContext } from "../../config/keybindings";
4
4
  export interface RecentSession {
5
5
  name: string;
6
6
  timeAgo: string;
7
- }
8
- export interface LspServerInfo {
9
- name: string;
10
- status: "idle" | "ready" | "error" | "connecting";
11
- fileTypes: string[];
12
- }
13
- export interface WelcomeRoleBinding {
14
- role: string;
15
- model: string;
7
+ /** Session file; present when the row can be opened from the card. */
8
+ path?: string;
16
9
  }
17
10
  /**
18
- * Workspace and runtime facts shown in the launch ledger. Every field is
19
- * optional: the ledger renders what is known and fills the rest in as the
20
- * asynchronous probes (git status, context files, MCP) settle.
11
+ * Workspace facts on the launch card. Every field is optional: the card renders
12
+ * what is known and fills in git state once the asynchronous probe settles.
21
13
  */
22
14
  export interface WelcomeSnapshot {
23
15
  /** Display path of the project directory (already home-shortened). */
@@ -29,19 +21,7 @@ export interface WelcomeSnapshot {
29
21
  unstaged: number;
30
22
  untracked: number;
31
23
  } | null;
32
- /** Latest commits as `<short-sha> <subject>` onelines, newest first. */
33
- recentCommits?: readonly string[];
34
24
  thinkingLevel?: ThinkingLevel | string;
35
- /** Display name of the active model preset. */
36
- profile?: string;
37
- roles?: readonly WelcomeRoleBinding[];
38
- mcp?: {
39
- connected: number;
40
- total: number;
41
- };
42
- skills?: number;
43
- /** Display names of the loaded project instruction files (AGENTS.md, …). */
44
- contextFiles?: readonly string[];
45
25
  }
46
26
  export type WelcomeLogoMode = "unicode" | "square" | "ascii";
47
27
  export interface WelcomeComponentOptions {
@@ -49,21 +29,26 @@ export interface WelcomeComponentOptions {
49
29
  getReservedBottomRows?: (termWidth: number) => number;
50
30
  changelogMarkdown?: string;
51
31
  rightGutterWidth?: number;
32
+ /** Show only the version on the release line, without its first note. */
52
33
  collapseChangelog?: boolean;
53
34
  buildLabel?: string;
54
35
  keyDisplayContext?: KeyDisplayContext;
55
36
  skipLogoAnimation?: boolean;
56
37
  snapshot?: WelcomeSnapshot;
57
- /** Key bound to the resume picker (`app.session.resume`); the sessions heading names it. */
38
+ /** Key bound to the resume picker (`app.session.resume`); the card names it. */
58
39
  resumeKey?: string;
40
+ /** Key bound to `app.session.continue`, which resumes the first session on the card. */
41
+ continueKey?: string;
42
+ /** Which Sayknow pet stands on the card (default red). */
43
+ petSkin?: PetSkinId;
44
+ /** Called when a session row is opened by click or Enter. */
45
+ onOpenSession?: (session: RecentSession) => void;
59
46
  }
60
47
  /**
61
- * Sayknow-CLI launch surface: an open, borderless ledger. The left column is
62
- * the state of this workspace — path, branch, model, reasoning, preset, role
63
- * agents, tooling — so the first screen answers "what am I about to run with".
64
- * The right column carries activity: what changed, recent sessions, workflows
65
- * and keys. No enclosing box and no hero wordmark: the octopus mark and the
66
- * facts carry the identity.
48
+ * Sayknow-CLI launch card: the wordmark with the pet beside it, who and where you are
49
+ * (version, model and reasoning, project and branch), the release line after an update,
50
+ * the three most recent sessions — which can be opened from the card with a click or
51
+ * ↓ then Enter — and a single row of keys.
67
52
  */
68
53
  export declare class WelcomeComponent implements Component {
69
54
  #private;
@@ -71,24 +56,33 @@ export declare class WelcomeComponent implements Component {
71
56
  private modelName;
72
57
  private providerName;
73
58
  private recentSessions;
74
- private lspServers;
75
59
  private readonly logoMode;
76
60
  private readonly options;
77
- constructor(version: string, modelName: string, providerName: string, recentSessions?: RecentSession[], lspServers?: LspServerInfo[], logoMode?: WelcomeLogoMode, options?: WelcomeComponentOptions);
61
+ constructor(version: string, modelName: string, providerName: string, recentSessions?: RecentSession[], logoMode?: WelcomeLogoMode, options?: WelcomeComponentOptions);
78
62
  invalidate(): void;
79
63
  /**
80
- * Play a short one-shot reveal: sections appear top to bottom a few frames
81
- * apart, each settling from dim into its colors. Launch happens once per
82
- * session, so a sub-second reveal is affordable; it never blocks input.
83
- * Safe to call multiple times — subsequent calls reset and replay.
64
+ * Play the short launch intro. Content is fully readable from the first frame;
65
+ * only color and the tentacles move. Safe to call again — it restarts.
84
66
  */
85
67
  playIntro(requestRender: () => void): void;
86
68
  dispose(): void;
87
69
  setModel(modelName: string, providerName: string): void;
88
70
  setRecentSessions(sessions: RecentSession[]): void;
89
- setLspServers(servers: LspServerInfo[]): void;
90
- /** Merge newly probed workspace facts into the ledger. */
71
+ /** Merge newly probed workspace facts into the card. */
91
72
  setSnapshot(patch: WelcomeSnapshot): void;
73
+ /** Rows that can be opened right now. Zero once the card stops taking input. */
74
+ get openableCount(): number;
75
+ get selectedIndex(): number | undefined;
76
+ /** Stop offering the session rows (after the first prompt or a session switch). */
77
+ endInteraction(): void;
78
+ get interactive(): boolean;
79
+ /** Highlight a row, or clear the highlight with undefined. Returns false when there is nothing to select. */
80
+ select(index: number | undefined): boolean;
81
+ /** Move the highlight; returns false when it would leave the list upward (the caller hands focus back). */
82
+ moveSelection(delta: number): boolean;
83
+ /** Open the highlighted row. */
84
+ openSelected(): boolean;
85
+ handleClick(line: number): boolean;
92
86
  render(termWidth: number): string[];
93
87
  }
94
88
  /** Resolve the intro cadence without making tests mutate global process state. */
@@ -22,7 +22,7 @@ import type { EvalExecutionComponent } from "./components/eval-execution";
22
22
  import type { HookEditorComponent } from "./components/hook-editor";
23
23
  import type { HookInputComponent } from "./components/hook-input";
24
24
  import type { HookSelectorComponent } from "./components/hook-selector";
25
- import { type PetMode, SayknowPetWidget } from "./components/sayknow-pet-widget";
25
+ import { type PetMode, type PetSkinId, SayknowPetWidget } from "./components/sayknow-pet-widget";
26
26
  import type { ToolExecutionHandle } from "./components/tool-execution";
27
27
  import { StatusLineComponent } from "./components/tool-status-header";
28
28
  import { type WelcomeLogoMode } from "./components/welcome";
@@ -42,10 +42,10 @@ export declare function getComposerPlaceholder(keybindings: Pick<KeybindingsMana
42
42
  }): string;
43
43
  export declare function getWelcomeTranscriptReservedRows(chatContainer: Container, width: number): number;
44
44
  /**
45
- * The composer is an open rail, not a box: every input row starts with the same
46
- * rail glyph a submitted prompt keeps in the transcript, colored by the editor's
47
- * border color (session accent, thinking level, shell/python mode).
45
+ * The pet on the launch card: the one the user keeps beside the composer, or —
46
+ * with the pet off — the skin that matches the theme (blue for blue-octopus).
48
47
  */
48
+ export declare function resolveWelcomePetSkin(petMode: PetMode, themeName: string | undefined): PetSkinId;
49
49
  export declare function configureDefaultComposerChrome(editor: CustomEditor): void;
50
50
  export type WelcomeBannerSettingMode = "auto" | "unicode" | "square" | "ascii";
51
51
  export declare function resolveWelcomeLogoMode(mode: WelcomeBannerSettingMode, env?: Record<string, string | undefined>, platform?: NodeJS.Platform): WelcomeLogoMode;
@@ -276,6 +276,9 @@ export declare class InteractiveMode implements InteractiveModeContext {
276
276
  showUserMessageSelector(): void;
277
277
  showTreeSelector(): void;
278
278
  showSessionSelector(): void;
279
+ /** The card stops taking input: first prompt sent, a session opened, or the UI rebuilt. */
280
+ endWelcomeInteraction(): void;
281
+ continueRecentSession(): Promise<void>;
279
282
  handleResumeSession(sessionPath: string, options?: {
280
283
  requireIdle?: boolean;
281
284
  }): Promise<boolean>;
@@ -286,6 +286,10 @@ export interface InteractiveModeContext {
286
286
  showUserMessageSelector(): void;
287
287
  showTreeSelector(): void;
288
288
  showSessionSelector(): void;
289
+ /** Resume the most recent saved session other than this one (the launch card's first row). */
290
+ continueRecentSession(): Promise<void>;
291
+ /** The launch card stops taking ↓/Enter/clicks and releases mouse capture (first prompt sent). */
292
+ endWelcomeInteraction?(): void;
289
293
  showSessionsDashboard(): void;
290
294
  handleResumeSession(sessionPath: string, options?: {
291
295
  requireIdle?: boolean;
@@ -9,6 +9,18 @@ export interface BrokerSettings {
9
9
  packageGeneration?: string;
10
10
  port?: number;
11
11
  heartbeatTtlMs?: number;
12
+ /**
13
+ * Stop after this long with no connection, no request in flight and no live
14
+ * session. Clients call `ensureBroker` before use, so the next one starts a fresh
15
+ * broker. 0 disables. Default {@link BROKER_IDLE_SHUTDOWN_MS}.
16
+ */
17
+ idleShutdownMs?: number;
18
+ /**
19
+ * Runs at the start of every publication tick, before the tick calls into the
20
+ * native addon. The broker process uses it to stop at once when its source
21
+ * checkout has vanished (see `process-guard.ts`); return true to skip the tick.
22
+ */
23
+ beforePublicationTick?: () => boolean;
12
24
  /** Broker-owned migration policy. Client lifecycle frames cannot select it. */
13
25
  resolveDirectoryMigration?: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
14
26
  }
@@ -17,6 +29,8 @@ type ResolvedBrokerSettings = {
17
29
  packageGeneration: string;
18
30
  port: number;
19
31
  heartbeatTtlMs: number;
32
+ idleShutdownMs: number;
33
+ beforePublicationTick: () => boolean;
20
34
  resolveDirectoryMigration: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
21
35
  };
22
36
  export type BrokerErrorCode = "idempotency_conflict" | "terminal_uncertain" | "broker_restarting" | "unavailable" | "endpoint_stale" | "resource_gone" | "invalid_input" | "spawn_failed" | "readiness_timeout" | "close_refused" | "not_found" | "live_session" | "cleanup_pending" | (string & {});
@@ -102,6 +116,12 @@ export type BrokerResponse = {
102
116
  durableEffects?: LifecycleDurableEffectsReceipt;
103
117
  startupFailure?: LifecycleStartupFailureReceipt;
104
118
  };
119
+ /**
120
+ * A broker with nothing to serve stops after this long. Without it a broker lived
121
+ * until killed: one started by a test (or a crashed client) whose caller never came
122
+ * back stayed up for days holding a port, memory and a mapped native addon.
123
+ */
124
+ export declare const BROKER_IDLE_SHUTDOWN_MS: number;
105
125
  export declare class Broker {
106
126
  #private;
107
127
  readonly settings: ResolvedBrokerSettings;
@@ -113,6 +133,8 @@ export declare class Broker {
113
133
  get ownsDiscovery(): boolean;
114
134
  get completion(): Promise<void>;
115
135
  status(): RedactedBrokerDiscovery | null;
136
+ /** Record client activity; an active broker never idles out. */
137
+ noteActivity(): void;
116
138
  heartbeat(): Promise<void>;
117
139
  stop(): Promise<void>;
118
140
  handleRequest(operation: string, input: Record<string, unknown>, idempotencyKey?: string): Promise<BrokerResponse>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Guards for the SDK's long-lived background processes (`sdk broker-internal`,
3
+ * `sdk session-host-internal`). They are spawned detached and outlive whoever
4
+ * started them, so each one has to notice on its own when it has no reason, or no
5
+ * way, to keep running.
6
+ */
7
+ /** How often the guards run. Cheap: one `stat`, plus a small JSON read for hosts. */
8
+ export declare const PROCESS_GUARD_INTERVAL_MS = 5000;
9
+ /**
10
+ * The entry script of a process run from source (`bun …/src/cli.ts sdk …`), or
11
+ * undefined for a compiled binary, whose entry lives in Bun's virtual filesystem
12
+ * and cannot disappear from under it.
13
+ */
14
+ export declare function sourceEntryPath(main?: string | undefined): string | undefined;
15
+ /** Whether the source entry is still reachable. A compiled binary (no entry) always is. */
16
+ export declare function sourceEntryAvailable(entry: string | undefined, stat?: (file: string) => unknown): boolean;
17
+ /**
18
+ * End this process at once when the source it runs from has vanished — typically
19
+ * an external drive with the checkout was unplugged. The native addon is mapped
20
+ * from that checkout and paged in lazily, so the next call into it faults on a
21
+ * page that can no longer be read, and Bun's fault handler then spins on the same
22
+ * unreadable mapping at 100% CPU forever. No cleanup is safe at that point (it
23
+ * would call into the same addon), so the process kills itself; readers already
24
+ * treat its discovery and markers as stale once the pid is gone.
25
+ *
26
+ * Call this before any native call in a periodic task. Returns true if it fired.
27
+ */
28
+ export declare function exitIfSourceGone(entry?: string | undefined, options?: {
29
+ stat?: (file: string) => unknown;
30
+ kill?: () => void;
31
+ }): boolean;
32
+ /** What a session host needs to recognise that its session is still its own. */
33
+ export interface SessionHostAuthority {
34
+ /** `<stateRoot>/sdk/<sessionId>.lifecycle.json`, written once by the broker when it spawned this host. */
35
+ markerPath: string;
36
+ pid: number;
37
+ effectMarker: string;
38
+ incarnation: string;
39
+ /** The host's worktree. */
40
+ cwd: string;
41
+ }
42
+ /**
43
+ * `held`: the marker still names this process and the worktree exists. `lost`: the
44
+ * marker or worktree is gone, or the marker names another process — the session was
45
+ * deleted, taken over, or its whole state root was removed (a finished test). `unknown`:
46
+ * something could not be read for another reason; that is not evidence either way.
47
+ */
48
+ export type SessionHostAuthorityState = "held" | "lost" | "unknown";
49
+ export declare function checkSessionHostAuthority(authority: SessionHostAuthority, io?: {
50
+ readFile?: (file: string) => string;
51
+ stat?: (file: string) => unknown;
52
+ }): SessionHostAuthorityState;
53
+ /** Consecutive `lost` checks before a host gives up its session (≈15s at the default interval). */
54
+ export declare const SESSION_HOST_LOST_CHECKS = 3;
55
+ /**
56
+ * Watch a running session host. It stops (through `onLost`, the host's normal
57
+ * shutdown) once its ownership marker or worktree has been gone for
58
+ * {@link SESSION_HOST_LOST_CHECKS} checks in a row, and kills itself at once if the
59
+ * source checkout it runs from disappears. Before this, a host whose session state
60
+ * had been deleted kept running until someone killed it — for days, after tests.
61
+ * Returns a function that stops the watch.
62
+ */
63
+ export declare function startSessionHostGuard(options: SessionHostAuthority & {
64
+ onLost: () => void;
65
+ intervalMs?: number;
66
+ lostChecks?: number;
67
+ check?: (authority: SessionHostAuthority) => SessionHostAuthorityState;
68
+ sourceGuard?: () => boolean;
69
+ setInterval?: typeof setInterval;
70
+ clearInterval?: typeof clearInterval;
71
+ }): () => void;
@@ -3,6 +3,8 @@ import type { Broker } from "./broker";
3
3
  export declare class BrokerTransport {
4
4
  #private;
5
5
  constructor(broker: Broker, token: string, port?: number);
6
+ /** Client WebSockets currently open. The broker does not idle out while any is. */
7
+ get openConnections(): number;
6
8
  get port(): number;
7
9
  start(): Promise<number>;
8
10
  stop(): Promise<void>;
@@ -46,6 +46,10 @@ export interface ChatDaemonRuntimeDeps {
46
46
  url: string;
47
47
  token: string;
48
48
  }) => Promise<ChatDaemonSdkClient>;
49
+ /** Starts the agent broker when none is running (default: `ensureBroker`). */
50
+ ensureBroker?: (settings: {
51
+ agentDir: string;
52
+ }) => Promise<unknown>;
49
53
  onReconciled?: () => void;
50
54
  setInterval?: typeof setInterval;
51
55
  clearInterval?: typeof clearInterval;
@@ -1221,6 +1221,15 @@ export declare class AgentSession {
1221
1221
  setAutoCompactionEnabled(enabled: boolean): void;
1222
1222
  /** Whether auto-compaction is enabled */
1223
1223
  get autoCompactionEnabled(): boolean;
1224
+ /**
1225
+ * The default fallback chain the next prompt walks, automatic entries included,
1226
+ * and the position currently in use.
1227
+ */
1228
+ getDefaultFallbackChain(): {
1229
+ entries: readonly string[];
1230
+ activeIndex: number;
1231
+ appendedFrom: number;
1232
+ };
1224
1233
  /**
1225
1234
  * Cancel in-progress retry.
1226
1235
  */
@@ -1,3 +1,4 @@
1
+ import type { Settings } from "../config/settings";
1
2
  import { AuthStorage } from "./auth-storage";
2
3
  /**
3
4
  * Create an AuthStorage instance.
@@ -11,3 +12,17 @@ import { AuthStorage } from "./auth-storage";
11
12
  * override to re-mint access tokens when needed.
12
13
  */
13
14
  export declare function discoverAuthStorage(agentDir?: string): Promise<AuthStorage>;
15
+ /**
16
+ * Per-machine multi-account ranking override from `SKC_CREDENTIAL_RANKING_MODE`.
17
+ * Unset/unknown → `undefined`, so the `auth.credentialRankingMode` setting (or
18
+ * {@link AuthStorage}'s `balanced` default) decides. `earliest-reset` switches to
19
+ * earliest-expiry-first selection so soon-to-reset tumbling-window quota is
20
+ * drained before it is lost.
21
+ */
22
+ export declare function credentialRankingModeFromEnv(): "balanced" | "earliest-reset" | undefined;
23
+ /**
24
+ * Apply the `auth.credentialRankingMode` setting to a credential store. The env
25
+ * var still wins, so a machine that pins a mode keeps it whatever config says.
26
+ * Returns the mode now in effect.
27
+ */
28
+ export declare function applyCredentialRankingModeSetting(storage: Pick<AuthStorage, "setCredentialRankingMode">, settings: Pick<Settings, "get">): "balanced" | "earliest-reset";
@@ -0,0 +1,27 @@
1
+ import type { Api, Model } from "@sayknow-cli/ai";
2
+ /**
3
+ * How many other providers the automatic tail may add. Each entry can cost up to
4
+ * `fallback.maxAttempts` tries before the chain moves on, so the tail stays short.
5
+ */
6
+ export declare const AUTO_FALLBACK_PROVIDER_LIMIT = 3;
7
+ /** Provider of a `provider/model[:level]` selector, or undefined for bare aliases. */
8
+ export declare function selectorProvider(selector: string): string | undefined;
9
+ export interface AutoFallbackInput {
10
+ /** Models whose provider has auth configured (`ModelRegistry.getAvailable()`). */
11
+ available: readonly Model<Api>[];
12
+ /** True when the provider has a stored credential or API key (keyless local providers do not count). */
13
+ hasCredentials: (provider: string) => boolean;
14
+ /** Providers already in the chain; the tail adds a different provider or nothing. */
15
+ excludeProviders: ReadonlySet<string>;
16
+ /** Most-recently-used `provider/id` keys, newest first. */
17
+ usageOrder?: readonly string[];
18
+ limit?: number;
19
+ }
20
+ /**
21
+ * One model per other logged-in provider, for the automatic tail of the default
22
+ * fallback chain. Within a provider the model the user used most recently wins,
23
+ * then the provider's curated default; a provider with neither is skipped rather
24
+ * than guessed, because catalog order would land on an old model. Providers used
25
+ * recently come first, then the rest in curated-default order.
26
+ */
27
+ export declare function autoFallbackSelectors(input: AutoFallbackInput): string[];
@@ -6,6 +6,11 @@ export interface ConfiguredFallbackChain {
6
6
  origin: string;
7
7
  identity?: string;
8
8
  explicitHead: boolean;
9
+ /**
10
+ * Index of the first entry appended at runtime after the configured intent
11
+ * (`fallback.models`, then automatic picks). Absent when nothing was appended.
12
+ */
13
+ appendedFrom?: number;
9
14
  }
10
15
  export interface FallbackFailure {
11
16
  selector: string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Which language to answer in, detected from the script of the user's message.
3
+ *
4
+ * Long tool-heavy turns drift into English: tool output, logs, skill text and context
5
+ * summaries are English, and the final report follows them. The system prompt states
6
+ * the rule once; this adds a one-line reminder at the start of each turn, naming the
7
+ * language, when the message is clearly written in a non-Latin script. Latin-script
8
+ * languages (English, Spanish, French, German, …) cannot be told apart by script alone,
9
+ * so they get no reminder and rely on the system prompt rule.
10
+ */
11
+ export interface DetectedLanguage {
12
+ /** BCP 47 code. */
13
+ code: string;
14
+ /** English name, as used in the reminder. */
15
+ name: string;
16
+ }
17
+ /**
18
+ * The language of `text` when its prose is clearly in a non-Latin script, else
19
+ * undefined. "Clearly": at least two characters of the script, and — since one CJK
20
+ * character carries about a word — at least a quarter as many as Latin letters, so a
21
+ * pasted English log with one Korean word does not count.
22
+ */
23
+ export declare function detectResponseLanguage(text: string): DetectedLanguage | undefined;
24
+ /** The per-turn reminder for a detected language. */
25
+ export declare function buildResponseLanguageReminder(language: DetectedLanguage): string;