@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
@@ -1,5 +1,6 @@
1
1
  export declare const MODEL_ONBOARDING_API_PROVIDER_COMMAND = "/provider add --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <model>";
2
- export declare const MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND = "/provider add --preset <minimax|minimax-cn|glm>";
2
+ /** Built from the bundled preset catalog, so a new preset shows up here without editing copy. */
3
+ export declare const MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND: string;
3
4
  export declare const MODEL_ONBOARDING_SETUP_COMMAND = "skc setup provider";
4
5
  export declare const MODEL_ONBOARDING_OAUTH_COMMAND = "/provider login [provider-id] or /login [provider-id]";
5
6
  /**
@@ -7,8 +8,14 @@ export declare const MODEL_ONBOARDING_OAUTH_COMMAND = "/provider login [provider
7
8
  * user learns it exists is from the surfaces where they go to add credentials.
8
9
  */
9
10
  export declare const MODEL_ONBOARDING_TYPESAFE_COMMAND = "/provider typesafe";
11
+ /** Where the models tried after a blocked one are shown and edited. */
12
+ export declare const MODEL_ONBOARDING_FALLBACK_COMMAND = "/fallback";
10
13
  export declare function formatModelOnboardingGuidance(): string;
11
14
  export declare function formatModelOnboardingInlineHint(): string;
12
15
  export declare function formatNoModelOnboardingError(): string;
16
+ /**
17
+ * Missing credentials for the provider the session tried to call. The fix for that
18
+ * provider comes first; the ways to add a different provider follow as alternatives.
19
+ */
13
20
  export declare function formatNoCredentialOnboardingError(providerId: string): string;
14
21
  export declare function formatNoModelsAvailableFallback(): string;
@@ -46,6 +46,8 @@ export declare function normalizeProviderId(providerId: string): string;
46
46
  export declare function parseProviderCompatibility(value: string): ProviderCompatibility;
47
47
  export declare function findProviderPreset(value: string | undefined): ProviderPreset | undefined;
48
48
  export declare function formatProviderPresetList(): string;
49
+ /** `minimax|minimax-cn|…` — the preset ids for a `--preset <…>` usage line, in catalog order. */
50
+ export declare function formatProviderPresetChoices(): string;
49
51
  export declare function parseModelList(values: readonly string[]): string[];
50
52
  export declare function redactSecret(secret: string): string;
51
53
  export declare function validateModelApi(modelApi: Readonly<Record<string, string>> | undefined, models: readonly string[], presetId: string): void;
@@ -8,6 +8,7 @@ import type { OutputMeta } from "./output-meta";
8
8
  declare const debugSchema: z.ZodObject<{
9
9
  action: z.ZodEnum<{
10
10
  output: "output";
11
+ sessions: "sessions";
11
12
  pause: "pause";
12
13
  scopes: "scopes";
13
14
  variables: "variables";
@@ -34,7 +35,6 @@ declare const debugSchema: z.ZodObject<{
34
35
  loaded_sources: "loaded_sources";
35
36
  custom_request: "custom_request";
36
37
  terminate: "terminate";
37
- sessions: "sessions";
38
38
  }>;
39
39
  program: z.ZodOptional<z.ZodString>;
40
40
  args: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -131,6 +131,7 @@ export declare class DebugTool implements AgentTool<typeof debugSchema, DebugToo
131
131
  readonly parameters: z.ZodObject<{
132
132
  action: z.ZodEnum<{
133
133
  output: "output";
134
+ sessions: "sessions";
134
135
  pause: "pause";
135
136
  scopes: "scopes";
136
137
  variables: "variables";
@@ -157,7 +158,6 @@ export declare class DebugTool implements AgentTool<typeof debugSchema, DebugToo
157
158
  loaded_sources: "loaded_sources";
158
159
  custom_request: "custom_request";
159
160
  terminate: "terminate";
160
- sessions: "sessions";
161
161
  }>;
162
162
  program: z.ZodOptional<z.ZodString>;
163
163
  args: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -44,6 +44,7 @@ export * from "./gh";
44
44
  export * from "./image-gen";
45
45
  export * from "./irc";
46
46
  export * from "./job";
47
+ export * from "./locate";
47
48
  export * from "./monitor";
48
49
  export * from "./read";
49
50
  export * from "./recipe";
@@ -0,0 +1,97 @@
1
+ import type { DecisionRequest, DecisionResult } from "../decisions/types";
2
+ /**
3
+ * Find where behaviour lives by asking Jev, the way `jevgrep` does, but inside SKC and
4
+ * without shipping source bodies: a folder is judged from its path and the names of the
5
+ * files in it; a file is judged from its path and its declaration lines (signatures),
6
+ * never its function bodies or comments.
7
+ *
8
+ * The walk narrows before it widens: large folders are split and only the subfolders Jev
9
+ * rates plausible are entered, so a question about the broker never uploads the TUI.
10
+ */
11
+ export interface LocateParams {
12
+ query: string;
13
+ /** Absolute search root. */
14
+ root: string;
15
+ /** Most files to return. */
16
+ limit: number;
17
+ }
18
+ export interface LocateDeps {
19
+ /** Source files under the root, relative, `/`-separated. */
20
+ listFiles(root: string, signal?: AbortSignal): Promise<string[]>;
21
+ /** Declaration lines of one file (`{ line, text }`), empty when none can be extracted. */
22
+ outline(absolutePath: string, signal?: AbortSignal): Promise<OutlineLine[]>;
23
+ /** One Jev request. Null means the request failed or no key is configured. */
24
+ decide(request: DecisionRequest): Promise<DecisionResult | null>;
25
+ /**
26
+ * Full text of a file for the local keyword score. Read and scored on this machine only;
27
+ * nothing from it is sent. Undefined when a file cannot or should not be read.
28
+ */
29
+ readText?(absolutePath: string): Promise<string | undefined>;
30
+ signal?: AbortSignal;
31
+ }
32
+ export interface OutlineLine {
33
+ line: number;
34
+ text: string;
35
+ }
36
+ export interface LocatedFile {
37
+ path: string;
38
+ score: number;
39
+ outline: OutlineLine[];
40
+ /** Below {@link FILE_BAR}: a lead worth a glance, not a confident match. */
41
+ weak?: boolean;
42
+ }
43
+ export interface LocateResult {
44
+ files: LocatedFile[];
45
+ /** Best-scoring files when none cleared the bar, so the caller still has leads. */
46
+ weakLeads: LocatedFile[];
47
+ foldersJudged: number;
48
+ filesJudged: number;
49
+ requests: number;
50
+ failedRequests: number;
51
+ /** Files considered at all after the folder walk (before any cap). */
52
+ candidates: number;
53
+ truncated: boolean;
54
+ durationMs: number;
55
+ }
56
+ /** A folder with at most this many files underneath is not split further; its files are judged directly. */
57
+ export declare const LEAF_FOLDER_FILES = 40;
58
+ export declare const FILE_BAR = 0.5;
59
+ /** Files between this and {@link FILE_BAR} fill the remaining slots, marked weak. */
60
+ export declare const WEAK_FILE_BAR = 0.3;
61
+ /** Hard ceilings so one question can never turn into an unbounded bill. */
62
+ export declare const MAX_REQUESTS = 100;
63
+ /** Query terms for lexical hints: lowercase words of 4+ letters, cut to a 5-letter stem. */
64
+ export declare function queryTerms(query: string): string[];
65
+ /** How many query terms appear in `text`, with camelCase and snake_case identifiers split into words. */
66
+ export declare function termHits(text: string, terms: readonly string[]): number;
67
+ export declare function locateCode(params: LocateParams, deps: LocateDeps): Promise<LocateResult>;
68
+ /**
69
+ * Declaration lines from a structural summary whose bodies and comments were all elided
70
+ * (`minBodyLines: 1`, `minCommentLines: 1`), plus the member lines of elided class-like
71
+ * bodies taken from `sourceLines`: the summariser folds a whole class body, so its methods
72
+ * would otherwise vanish. Only lines at the body's own member indentation are taken;
73
+ * method bodies sit deeper and never come along. Initialisers are cut, so values do not
74
+ * leave the machine. Over {@link MAX_OUTLINE_LINES}, top-level declarations and methods
75
+ * are kept before fields, so a long run of type members never hides what follows.
76
+ */
77
+ export declare function outlineFromSegments(segments: ReadonlyArray<{
78
+ kind: string;
79
+ startLine: number;
80
+ endLine?: number;
81
+ text?: string | null;
82
+ }>, sourceLines?: readonly string[]): OutlineLine[];
83
+ /**
84
+ * 1-based numbers of lines that belong to a multi-line comment or string: `/* … *\/`
85
+ * blocks (including `/**` and Rust's `/*!`), Python triple-quoted docstrings and strings,
86
+ * and multi-line template literals. A declaration line that opens a string value
87
+ * (`x = """`) is not masked itself — its initialiser is cut later — but the lines after
88
+ * it are.
89
+ */
90
+ export declare function maskedLines(lines: readonly string[]): Set<number>;
91
+ /**
92
+ * Fallback outline for a file the structural parser rejects (1-2% of TypeScript files,
93
+ * often large central ones). Keeps top-level declarations and members one indent level
94
+ * in, under the same rules as {@link outlineFromSegments}: no comments, imports, bodies,
95
+ * or initialiser values.
96
+ */
97
+ export declare function outlineFromSource(sourceLines: readonly string[]): OutlineLine[];
@@ -0,0 +1,38 @@
1
+ import type { AgentTool, AgentToolResult } from "@sayknow-cli/agent-core";
2
+ import * as z from "zod/v4";
3
+ import type { ToolSession } from ".";
4
+ import { type LocateResult } from "./locate-core";
5
+ declare const locateSchema: z.ZodObject<{
6
+ query: z.ZodString;
7
+ path: z.ZodOptional<z.ZodString>;
8
+ limit: z.ZodOptional<z.ZodNumber>;
9
+ }, z.core.$strip>;
10
+ type LocateParams = z.infer<typeof locateSchema>;
11
+ export interface LocateToolDetails {
12
+ result: Omit<LocateResult, "files" | "weakLeads"> & {
13
+ files: string[];
14
+ };
15
+ }
16
+ export declare class LocateTool implements AgentTool<typeof locateSchema, LocateToolDetails> {
17
+ private readonly session;
18
+ readonly name = "locate";
19
+ readonly label = "Locate";
20
+ readonly summary = "Find code by describing what it does (Jev relevance judgement)";
21
+ readonly description: string;
22
+ readonly parameters: z.ZodObject<{
23
+ query: z.ZodString;
24
+ path: z.ZodOptional<z.ZodString>;
25
+ limit: z.ZodOptional<z.ZodNumber>;
26
+ }, z.core.$strip>;
27
+ readonly strict = true;
28
+ /**
29
+ * Offered only when it can work: a TypeSafe key is stored and `locate.enabled` is on.
30
+ * Without the hosted model every judgement would fall to an uncalibrated LLM, one
31
+ * request per folder batch, which costs more than the search saves.
32
+ */
33
+ static createIf(session: ToolSession): LocateTool | null;
34
+ constructor(session: ToolSession);
35
+ execute(_toolCallId: string, params: LocateParams, signal?: AbortSignal): Promise<AgentToolResult<LocateToolDetails>>;
36
+ }
37
+ export declare function formatLocateResult(query: string, root: string, cwd: string, result: LocateResult): string;
38
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/coding-agent",
4
- "version": "0.6.5",
4
+ "version": "0.6.7",
5
5
  "description": "Sayknow-CLI CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -54,12 +54,12 @@
54
54
  "@agentclientprotocol/sdk": "1.3.0",
55
55
  "@babel/parser": "^7.29.3",
56
56
  "@mozilla/readability": "^0.6.0",
57
- "@sayknow-cli/stats": "0.6.5",
58
- "@sayknow-cli/agent-core": "0.6.5",
59
- "@sayknow-cli/ai": "0.6.5",
60
- "@sayknow-cli/natives": "0.6.5",
61
- "@sayknow-cli/tui": "0.6.5",
62
- "@sayknow-cli/utils": "0.6.5",
57
+ "@sayknow-cli/stats": "0.6.7",
58
+ "@sayknow-cli/agent-core": "0.6.7",
59
+ "@sayknow-cli/ai": "0.6.7",
60
+ "@sayknow-cli/natives": "0.6.7",
61
+ "@sayknow-cli/tui": "0.6.7",
62
+ "@sayknow-cli/utils": "0.6.7",
63
63
  "@puppeteer/browsers": "^2.13.0",
64
64
  "@types/turndown": "5.0.6",
65
65
  "@xterm/headless": "^6.0.0",
@@ -38,6 +38,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
38
38
  "local session-discovery metadata command; no SDK operation counterpart or runtime/retention authority",
39
39
  "slash_command:unstar":
40
40
  "local session-discovery metadata command; no SDK operation counterpart or runtime/retention authority",
41
+ "slash_command:fallback":
42
+ "local fallback-chain configuration (fallback.models / fallback.auto settings); no SDK operation counterpart, and SDK clients observe switches via model_fallback_switched",
41
43
  "slash_command:import-session":
42
44
  "local transcript-file import into a new session; ACP is explicitly disabled and no SDK operation reads caller-selected host paths",
43
45
  "agent_session:constructor": "internal accessor/plumbing, not a user-facing control seam",
@@ -193,6 +195,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
193
195
  "internal profile and fallback-chain state, not a user-facing SDK control seam",
194
196
  "agent_session:setConfiguredModelChain":
195
197
  "internal profile and fallback-chain state, not a user-facing SDK control seam",
198
+ "agent_session:getDefaultFallbackChain":
199
+ "internal profile and fallback-chain state, not a user-facing SDK control seam",
196
200
  "agent_session:setDefaultFallbackRuntimeModel":
197
201
  "internal fallback runtime bookkeeping, not a user-facing SDK control seam",
198
202
  "agent_session:seedDefaultFallbackResolution":
@@ -33,8 +33,10 @@ import {
33
33
  import { buildHostPluginSetup, formatHostPluginSetup, type HostPluginKind } from "../setup/host-plugin-setup";
34
34
  import {
35
35
  addApiCompatibleProvider,
36
+ formatProviderPresetChoices,
36
37
  formatProviderPresetList,
37
38
  formatProviderSetupResult,
39
+ PROVIDER_PRESETS,
38
40
  parseProviderCompatibility,
39
41
  } from "../setup/provider-onboarding";
40
42
 
@@ -119,7 +121,7 @@ function rejectProviderFlagsOutsideProvider(component: SetupComponent, flags: Se
119
121
  console.error(chalk.red("Provider setup flags require the explicit `provider` component."));
120
122
  console.error(
121
123
  chalk.dim(
122
- `Run: ${APP_NAME} setup provider --preset <minimax|glm> or ${APP_NAME} setup provider --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <id>`,
124
+ `Run: ${APP_NAME} setup provider --preset <${formatProviderPresetChoices()}> or ${APP_NAME} setup provider --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <id>`,
123
125
  ),
124
126
  );
125
127
  process.exit(1);
@@ -761,8 +763,9 @@ ${chalk.bold("Components:")}
761
763
 
762
764
 
763
765
  ${chalk.bold("Provider example:")}
764
- ${APP_NAME} setup provider --preset minimax
765
- ${APP_NAME} setup provider --preset glm
766
+ ${PROVIDER_PRESETS.slice(0, 2)
767
+ .map(preset => ` ${APP_NAME} setup provider --preset ${preset.id}`)
768
+ .join("\n")}
766
769
  MY_PROVIDER_KEY=sk-... ${APP_NAME} setup provider --compat openai --provider my-oai --base-url https://api.example.com/v1 --api-key-env MY_PROVIDER_KEY --model gpt-example
767
770
 
768
771
  ${chalk.bold("Hermes example:")}
@@ -775,7 +778,7 @@ ${chalk.bold("Options:")}
775
778
  -c, --check Check if dependencies are installed without installing
776
779
  -f, --force Overwrite existing default workflow skill files
777
780
  --json Output status as JSON
778
- --preset Provider preset: minimax, minimax-cn, or glm (aliases include minimax-code and zai)
781
+ --preset Provider preset: ${PROVIDER_PRESETS.map(preset => preset.id).join(", ")} (aliases: ${PROVIDER_PRESETS.flatMap(preset => preset.aliases).join(", ")})
779
782
  --compat Provider compatibility: openai or anthropic
780
783
  --provider Provider id to add to models.yml
781
784
  --base-url Provider API base URL
@@ -18,6 +18,7 @@ import {
18
18
  writeSessionLifecycleFailure,
19
19
  writeSessionLifecycleReady,
20
20
  } from "../sdk/broker/lifecycle";
21
+ import { exitIfSourceGone, startSessionHostGuard } from "../sdk/broker/process-guard";
21
22
  import { processIncarnation } from "../sdk/broker/process-incarnation";
22
23
  import { type CreateLifecycleAgentSessionResult, createLifecycleAgentSession } from "../sdk/lifecycle-session";
23
24
  import { listManagedSessionCandidates, resolveManagedSessionScope } from "../sdk/session-directory";
@@ -505,6 +506,7 @@ export async function runSessionHost(
505
506
  }
506
507
  process.once("SIGTERM", stop);
507
508
  process.once("SIGINT", stop);
509
+ startSessionHostGuard({ markerPath, pid: process.pid, effectMarker, incarnation, cwd, onLost: stop });
508
510
  await new Promise<void>(() => {});
509
511
  }
510
512
 
@@ -559,6 +561,7 @@ export default class Sdk extends Command {
559
561
  const policy = (await Settings.loadForScope({ cwd, agentDir })).get("session.directoryMigration");
560
562
  return policy === "disabled" ? "disabled" : "copy-retain";
561
563
  },
564
+ beforePublicationTick: () => exitIfSourceGone(),
562
565
  });
563
566
  await broker.start();
564
567
  if (!broker.ownsDiscovery) return;
@@ -4,6 +4,7 @@
4
4
  import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
5
5
  import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
+ import { PROVIDER_PRESETS } from "../setup/provider-onboarding";
7
8
 
8
9
  const COMPONENTS: SetupComponent[] = [
9
10
  "claude",
@@ -52,7 +53,9 @@ export default class Setup extends Command {
52
53
  "skc-command": Flags.string({ description: "Command used to start `skc mcp-serve coordinator`" }),
53
54
  target: Flags.string({ description: "Hermes config file target for config-only install" }),
54
55
  "profile-dir": Flags.string({ description: "Hermes profile directory for full setup install" }),
55
- preset: Flags.string({ description: "Provider preset: minimax, minimax-cn, or glm" }),
56
+ preset: Flags.string({
57
+ description: `Provider preset: ${PROVIDER_PRESETS.map(preset => preset.id).join(", ")}`,
58
+ }),
56
59
  compat: Flags.string({ description: "Provider compatibility: openai or anthropic" }),
57
60
  provider: Flags.string({ description: "Provider id to add to models.yml" }),
58
61
  "base-url": Flags.string({ description: "Provider API base URL" }),
@@ -43,6 +43,7 @@ interface AppKeybindings {
43
43
  "app.session.tree": true;
44
44
  "app.session.fork": true;
45
45
  "app.session.resume": true;
46
+ "app.session.continue": true;
46
47
  "app.session.observe": true;
47
48
  "app.session.dashboard": true;
48
49
  "app.jobs.open": true;
@@ -189,6 +190,12 @@ export const KEYBINDINGS = {
189
190
  defaultKeys: "alt+r",
190
191
  description: "Resume session",
191
192
  },
193
+ // A Control chord on purpose: macOS terminals often turn Option into composed text,
194
+ // and continuing the last session is the one launch action that must always work.
195
+ "app.session.continue": {
196
+ defaultKeys: "ctrl+q",
197
+ description: "Continue the most recent session",
198
+ },
192
199
  "app.session.observe": {
193
200
  defaultKeys: "ctrl+s",
194
201
  description: "Observe subagent sessions",
@@ -1223,8 +1223,39 @@ export const SETTINGS_SCHEMA = {
1223
1223
  type: "number",
1224
1224
  default: 3,
1225
1225
  validate: (value: number) => Number.isInteger(value) && value > 0,
1226
+ ui: {
1227
+ tab: "model",
1228
+ label: "Fallback Attempts per Model",
1229
+ description:
1230
+ "Tries on each model in the fallback chain (including the first) before moving to the next one. Missing credentials skip a model immediately.",
1231
+ options: [
1232
+ { value: "1", label: "1 try" },
1233
+ { value: "2", label: "2 tries" },
1234
+ { value: "3", label: "3 tries" },
1235
+ { value: "5", label: "5 tries" },
1236
+ ],
1237
+ },
1238
+ },
1239
+
1240
+ /**
1241
+ * When the default model is blocked, keep going on another logged-in provider
1242
+ * after the configured chain and `fallback.models` run out. The extra entries
1243
+ * are computed per session and never written into the configured chain.
1244
+ */
1245
+ "fallback.auto": {
1246
+ type: "boolean",
1247
+ default: true,
1248
+ ui: {
1249
+ tab: "model",
1250
+ label: "Automatic Model Fallback",
1251
+ description:
1252
+ "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.",
1253
+ },
1226
1254
  },
1227
1255
 
1256
+ /** Models to try, in order, after the default chain. Edited with `/fallback`. */
1257
+ "fallback.models": { type: "array", default: EMPTY_STRING_ARRAY },
1258
+
1228
1259
  // Retries
1229
1260
  "retry.enabled": { type: "boolean", default: true },
1230
1261
 
@@ -1443,6 +1474,16 @@ export const SETTINGS_SCHEMA = {
1443
1474
  ],
1444
1475
  },
1445
1476
  },
1477
+ "startup.welcomeMouse": {
1478
+ type: "boolean",
1479
+ default: true,
1480
+ ui: {
1481
+ tab: "interaction",
1482
+ label: "Clickable Launch Card",
1483
+ description:
1484
+ "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.",
1485
+ },
1486
+ },
1446
1487
  "startup.skipLogoAnimation": {
1447
1488
  type: "boolean",
1448
1489
  default: false,
@@ -3107,6 +3148,22 @@ export const SETTINGS_SCHEMA = {
3107
3148
  },
3108
3149
  },
3109
3150
 
3151
+ /**
3152
+ * The `locate` tool: find code by describing what it does, judged by Jev. Offered only
3153
+ * when a TypeSafe key is stored. Sends folder paths, file names and declaration lines
3154
+ * (never bodies or comments) to TypeSafe.
3155
+ */
3156
+ "locate.enabled": {
3157
+ type: "boolean",
3158
+ default: true,
3159
+ ui: {
3160
+ tab: "tools",
3161
+ label: "Locate (Jev code search)",
3162
+ description:
3163
+ "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.",
3164
+ },
3165
+ },
3166
+
3110
3167
  "tools.essentialOverride": {
3111
3168
  type: "array",
3112
3169
  default: [] as string[],
@@ -3492,6 +3549,31 @@ export const SETTINGS_SCHEMA = {
3492
3549
  // Providers
3493
3550
  // ────────────────────────────────────────────────────────────────────────
3494
3551
 
3552
+ // Multi-account order. `SKC_CREDENTIAL_RANKING_MODE` overrides it per machine.
3553
+ "auth.credentialRankingMode": {
3554
+ type: "enum",
3555
+ values: ["balanced", "earliest-reset"] as const,
3556
+ default: "balanced",
3557
+ ui: {
3558
+ tab: "providers",
3559
+ label: "Multi-Account Order",
3560
+ description:
3561
+ "Which logged-in account of a provider a new session uses. A blocked or exhausted account is always skipped for the next one.",
3562
+ options: [
3563
+ {
3564
+ value: "balanced",
3565
+ label: "Balanced",
3566
+ description: "Least-used account first; spreads load and keeps headroom on every account",
3567
+ },
3568
+ {
3569
+ value: "earliest-reset",
3570
+ label: "Earliest reset",
3571
+ description: "Account whose usage window resets soonest first, so quota is not lost at reset",
3572
+ },
3573
+ ],
3574
+ },
3575
+ },
3576
+
3495
3577
  // Secret handling
3496
3578
  "secrets.enabled": {
3497
3579
  type: "boolean",
@@ -119,7 +119,7 @@ export function createTypeSafeDecisionBackend(deps: TypeSafeBackendDeps): Decisi
119
119
  name: "typesafe",
120
120
  async decide(request: DecisionRequest): Promise<DecisionResult | null> {
121
121
  validateQuestions(request.questions);
122
- const apiKey = await deps.registry.getApiKeyForProvider(TYPESAFE_PROVIDER, deps.sessionId);
122
+ let apiKey = await deps.registry.getApiKeyForProvider(TYPESAFE_PROVIDER, deps.sessionId);
123
123
  // No key means the user never added TypeSafe. That is not an error — the next
124
124
  // backend (their logged-in model) handles it.
125
125
  if (!apiKey) return null;
@@ -132,13 +132,26 @@ export function createTypeSafeDecisionBackend(deps: TypeSafeBackendDeps): Decisi
132
132
  request.signal?.addEventListener("abort", abortOnCaller, { once: true });
133
133
  const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
134
134
  const started = Date.now();
135
- try {
136
- const response = await doFetch(`${baseUrl}/v1/systemone`, {
135
+ const send = (key: string) =>
136
+ doFetch(`${baseUrl}/v1/systemone`, {
137
137
  method: "POST",
138
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
138
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
139
139
  body: JSON.stringify({ state: request.state, model, questions: toWireQuestions(request.questions) }),
140
140
  signal: controller.signal,
141
141
  });
142
+ try {
143
+ let response = await send(apiKey);
144
+ if (response.status === 401 || response.status === 403) {
145
+ // A session reads the credential store once, at startup. A key replaced from
146
+ // another session (or `skc setup typesafe`) stays invisible here, so every
147
+ // request kept sending the revoked key. Reload once; retry only if it changed.
148
+ const refreshed = await reloadedKey(deps, apiKey);
149
+ if (refreshed && !controller.signal.aborted) {
150
+ await response.body?.cancel().catch(() => {});
151
+ apiKey = refreshed;
152
+ response = await send(apiKey);
153
+ }
154
+ }
142
155
  if (!response.ok) {
143
156
  logger.debug("decisions/typesafe: request failed", {
144
157
  status: response.status,
@@ -169,3 +182,24 @@ export function createTypeSafeDecisionBackend(deps: TypeSafeBackendDeps): Decisi
169
182
  },
170
183
  };
171
184
  }
185
+
186
+ /**
187
+ * Re-read stored credentials and return the TypeSafe key if it differs from the one
188
+ * that was just rejected. Undefined when nothing changed (the key really is bad) or the
189
+ * store cannot be reloaded, so a genuinely revoked key costs one extra read, not a retry.
190
+ */
191
+ async function reloadedKey(deps: TypeSafeBackendDeps, rejected: string): Promise<string | undefined> {
192
+ const storage = (deps.registry as { authStorage?: { reload?: () => Promise<void> } }).authStorage;
193
+ if (typeof storage?.reload !== "function") return undefined;
194
+ try {
195
+ await storage.reload();
196
+ const current = await deps.registry.getApiKeyForProvider(TYPESAFE_PROVIDER, deps.sessionId);
197
+ if (current && current !== rejected) {
198
+ logger.debug("decisions/typesafe: picked up a key replaced since this session started");
199
+ return current;
200
+ }
201
+ } catch (error) {
202
+ logger.debug("decisions/typesafe: credential reload failed", { error: String(error) });
203
+ }
204
+ return undefined;
205
+ }
@@ -235,6 +235,31 @@ export const deSettings: Record<string, string> = {
235
235
  "set:task.serviceTier:o:claude-only:l": "Priorität (nur Claude)",
236
236
  "set:task.serviceTier:o:claude-only:d":
237
237
  "Anthropic-Schnellmodus bei direkten Claude-Anfragen; sonst ignoriert (inkl. Bedrock/Vertex)",
238
+ "set:fallback.maxAttempts:l": "Fallback-Versuche pro Modell",
239
+ "set:fallback.maxAttempts:d":
240
+ "Versuche je Modell der Fallback-Kette (einschließlich des ersten), bevor das nächste drankommt. Modelle ohne Anmeldedaten werden sofort übersprungen.",
241
+ "set:fallback.maxAttempts:o:1:l": "1 Versuch",
242
+ "set:fallback.maxAttempts:o:2:l": "2 Versuche",
243
+ "set:fallback.maxAttempts:o:3:l": "3 Versuche",
244
+ "set:fallback.maxAttempts:o:5:l": "5 Versuche",
245
+ "set:fallback.auto:l": "Automatischer Modell-Fallback",
246
+ "set:fallback.auto:d":
247
+ "Ist das Standardmodell blockiert (keine Anmeldung, Kontingent, Auth- oder Serverfehler), mit einem Modell eines anderen angemeldeten Anbieters weitermachen. /fallback zeigt die Kette.",
248
+ "set:auth.credentialRankingMode:l": "Reihenfolge mehrerer Konten",
249
+ "set:auth.credentialRankingMode:d":
250
+ "Welches angemeldete Konto eines Anbieters eine neue Sitzung verwendet. Ein blockiertes oder erschöpftes Konto übergibt immer an das nächste.",
251
+ "set:auth.credentialRankingMode:o:balanced:l": "Ausgewogen",
252
+ "set:auth.credentialRankingMode:o:balanced:d":
253
+ "Am wenigsten genutztes Konto zuerst; verteilt die Last und lässt jedem Konto Reserve",
254
+ "set:auth.credentialRankingMode:o:earliest-reset:l": "Früheste Zurücksetzung",
255
+ "set:auth.credentialRankingMode:o:earliest-reset:d":
256
+ "Konto, dessen Nutzungsfenster zuerst zurückgesetzt wird, zuerst, damit kein Kontingent verfällt",
257
+ "set:locate.enabled:l": "Locate (Jev-Codesuche)",
258
+ "set:locate.enabled:d":
259
+ "Bietet das locate-Werkzeug an, wenn ein TypeSafe-Schlüssel gespeichert ist. Zur Relevanzbewertung werden Ordnerpfade, Dateinamen und Deklarationszeilen (ohne Funktionsrümpfe und Kommentare) an TypeSafe gesendet.",
260
+ "set:startup.welcomeMouse:l": "Klickbare Startkarte",
261
+ "set:startup.welcomeMouse:d":
262
+ "Bis zum ersten Prompt die Maus übernehmen, damit eine letzte Sitzung auf der Startkarte mit einem Klick öffnet. Terminaltext währenddessen mit gedrückter Umschalttaste markieren. Aus: mit ↓ und Enter öffnen.",
238
263
  "set:retry.maxRetries:l": "Wiederholungsversuche",
239
264
  "set:retry.maxRetries:d": "Maximale Wiederholungsversuche bei API-Fehlern",
240
265
  "set:retry.maxRetries:o:1:l": "1 Wiederholung",
@@ -773,6 +798,7 @@ export const deSettings: Record<string, string> = {
773
798
  "set:searxng.endpoint:l": "SearXNG-Endpunkt",
774
799
  "set:searxng.endpoint:d": "Basis-URL für selbst gehostete Suche",
775
800
  "cmd:model:d": "Modell auswählen (öffnet Auswahloberfläche)",
801
+ "cmd:fallback:d": "Modelle anzeigen oder bearbeiten, die bei blockiertem Standardmodell versucht werden",
776
802
  "cmd:fast:d": "Prioritäts-Service-Tier umschalten (OpenAI service_tier=priority, Anthropic speed=fast)",
777
803
  "cmd:export:d": "Sitzung in HTML-Datei exportieren",
778
804
  "cmd:dump:d": "Sitzungstranskript in die Zwischenablage kopieren",
@@ -8,41 +8,18 @@ export const de: Partial<Record<MsgKey, string>> = {
8
8
  "lang.zh": "中文 (简体)",
9
9
  "lang.ja": "日本語",
10
10
  "welcome.tagline": "Programmieren sollte sich wie Denken anfühlen.",
11
- "welcome.workflows": "Workflows",
12
- "welcome.wf.deepInterview": "Umfang · Interview → Spec",
13
- "welcome.wf.ralplan": "Konsensplan",
14
- "welcome.wf.ultragoal": "autonomer Build",
15
- "welcome.wf.team": "parallele Agenten",
16
- "welcome.flowKeys": "Flow-Tasten",
17
11
  "welcome.commands": "Befehle",
18
- "welcome.actions": "Aktionen",
19
- "welcome.shell": "Shell",
20
- "welcome.python": "Python",
21
12
  "welcome.keymap": "Keymap",
22
13
  "welcome.model": "Modell",
23
- "welcome.reasoning": "Reasoning",
24
- "welcome.projectPulse": "Projektpuls",
25
- "welcome.noLsp": "Keine LSP-Server",
26
14
  "welcome.sessionTrail": "Sitzungsverlauf",
27
15
  "welcome.noSessions": "Keine gespeicherten Verläufe",
28
16
  "welcome.allSessions": "{key} alle Sitzungen",
17
+ "welcome.continue": "{key} fortsetzen",
18
+ "welcome.pick": "{key} wählen",
19
+ "welcome.pickActive": "↑↓ bewegen · ⏎ öffnen · esc zurück",
20
+ "welcome.sessions": "Sitzungen",
29
21
  "welcome.chooseModel": "Modell auswählen",
30
22
  "welcome.modelHint": "ctrl+l zum Auswählen · / für Befehle",
31
- "welcome.label.workspace": "Arbeitsbereich",
32
- "welcome.label.branch": "Branch",
33
- "welcome.label.commits": "Commits",
34
- "welcome.label.model": "Modell",
35
- "welcome.label.reasoning": "Reasoning",
36
- "welcome.label.preset": "Preset",
37
- "welcome.label.roles": "Rollen",
38
- "welcome.label.tools": "Werkzeuge",
39
- "welcome.noGit": "kein Git-Repository",
40
- "welcome.rolesInherit": "Rollen folgen dem Standardmodell",
41
- "welcome.skills": "Skills",
42
- "welcome.rules": "Regeln",
43
- "welcome.clean": "sauber",
44
- "welcome.whatsNew": "Neu",
45
- "welcome.readyPrompt": "Bereit für den nächsten Prompt",
46
23
  "settings.tab.appearance": "Darstellung",
47
24
  "settings.tab.model": "Modell",
48
25
  "settings.tab.interaction": "Interaktion",