@sayknow-cli/coding-agent 0.2.7 → 0.3.1

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 (216) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/notify-cli.d.ts +25 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +136 -3
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  18. package/dist/types/extensibility/shared-events.d.ts +1 -0
  19. package/dist/types/i18n/messages/en.d.ts +1 -0
  20. package/dist/types/lsp/types.d.ts +2 -0
  21. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  22. package/dist/types/modes/components/settings-defs.d.ts +1 -0
  23. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  24. package/dist/types/modes/interactive-mode.d.ts +1 -1
  25. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  26. package/dist/types/modes/theme/theme.d.ts +1 -1
  27. package/dist/types/modes/types.d.ts +7 -1
  28. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  29. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  30. package/dist/types/notifications/config-commands.d.ts +26 -0
  31. package/dist/types/notifications/config.d.ts +69 -0
  32. package/dist/types/notifications/engine.d.ts +59 -0
  33. package/dist/types/notifications/helpers.d.ts +55 -0
  34. package/dist/types/notifications/html-format.d.ts +62 -0
  35. package/dist/types/notifications/index.d.ts +28 -0
  36. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  37. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  38. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  39. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  40. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  41. package/dist/types/notifications/telegram-daemon.d.ts +295 -0
  42. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  43. package/dist/types/notifications/threaded-inbound.d.ts +77 -0
  44. package/dist/types/notifications/threaded-render.d.ts +71 -0
  45. package/dist/types/notifications/topic-registry.d.ts +67 -0
  46. package/dist/types/rlm/index.d.ts +12 -0
  47. package/dist/types/session/agent-session.d.ts +41 -2
  48. package/dist/types/session/auth-storage.d.ts +1 -1
  49. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  50. package/dist/types/setup/credential-import.d.ts +3 -0
  51. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  52. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  53. package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
  54. package/dist/types/skc-runtime/state-writer.d.ts +2 -0
  55. package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
  56. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  57. package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
  58. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
  59. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  60. package/dist/types/tools/fetch.d.ts +23 -0
  61. package/dist/types/tools/index.d.ts +19 -0
  62. package/dist/types/tools/subagent.d.ts +3 -0
  63. package/dist/types/tools/telegram-send.d.ts +32 -0
  64. package/dist/types/web/insane/bridge.d.ts +103 -0
  65. package/dist/types/web/insane/url-guard.d.ts +22 -0
  66. package/dist/types/web/search/provider.d.ts +18 -1
  67. package/dist/types/web/search/providers/insane.d.ts +53 -0
  68. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  69. package/dist/types/web/search/types.d.ts +12 -4
  70. package/package.json +10 -8
  71. package/scripts/build-binary.ts +3 -0
  72. package/scripts/verify-insane-vendor.ts +132 -0
  73. package/src/async/job-manager.ts +5 -1
  74. package/src/cli/args.ts +1 -1
  75. package/src/cli/daemon-cli.ts +122 -0
  76. package/src/cli/fast-help.ts +1 -1
  77. package/src/cli/notify-cli.ts +421 -0
  78. package/src/cli/setup-cli.ts +173 -84
  79. package/src/cli.ts +3 -3
  80. package/src/commands/daemon.ts +47 -0
  81. package/src/commands/notify.ts +61 -0
  82. package/src/commands/setup.ts +11 -1
  83. package/src/commands/team.ts +1 -1
  84. package/src/config/model-profile-activation.ts +74 -5
  85. package/src/config/model-profiles.ts +7 -4
  86. package/src/config/model-registry.ts +6 -3
  87. package/src/config/models-config-schema.ts +1 -1
  88. package/src/config/settings-schema.ts +143 -0
  89. package/src/coordinator/contract.ts +3 -0
  90. package/src/coordinator-mcp/server.ts +270 -1
  91. package/src/daemon/builtin.ts +46 -0
  92. package/src/daemon/control-types.ts +65 -0
  93. package/src/daemon/runtime.ts +51 -0
  94. package/src/defaults/skc/rules/ponytail.md +68 -0
  95. package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
  96. package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
  97. package/src/edit/modes/replace.ts +1 -1
  98. package/src/extensibility/extensions/runner.ts +4 -0
  99. package/src/extensibility/extensions/types.ts +8 -0
  100. package/src/extensibility/shared-events.ts +1 -0
  101. package/src/goals/tools/goal-tool.ts +11 -2
  102. package/src/hashline/hash.ts +1 -1
  103. package/src/i18n/messages/de.ts +1 -0
  104. package/src/i18n/messages/en.ts +1 -0
  105. package/src/i18n/messages/es.ts +1 -0
  106. package/src/i18n/messages/fr.ts +1 -0
  107. package/src/i18n/messages/ja.ts +1 -0
  108. package/src/i18n/messages/ko.ts +1 -0
  109. package/src/i18n/messages/zh.ts +1 -0
  110. package/src/internal-urls/docs-index.generated.ts +12 -10
  111. package/src/lsp/config.ts +16 -3
  112. package/src/lsp/defaults.json +7 -0
  113. package/src/lsp/types.ts +2 -0
  114. package/src/main.ts +30 -0
  115. package/src/modes/acp/acp-event-mapper.ts +1 -0
  116. package/src/modes/components/hook-editor.ts +7 -2
  117. package/src/modes/components/oauth-selector.ts +19 -0
  118. package/src/modes/components/settings-defs.ts +2 -1
  119. package/src/modes/components/settings-selector.ts +7 -2
  120. package/src/modes/controllers/event-controller.ts +35 -0
  121. package/src/modes/controllers/selector-controller.ts +80 -17
  122. package/src/modes/interactive-mode.ts +52 -4
  123. package/src/modes/runtime-init.ts +1 -0
  124. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  125. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  126. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  127. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  128. package/src/modes/theme/theme.ts +4 -0
  129. package/src/modes/types.ts +7 -1
  130. package/src/modes/utils/context-usage.ts +2 -2
  131. package/src/modes/utils/ui-helpers.ts +23 -0
  132. package/src/notifications/attachment-registry.ts +23 -0
  133. package/src/notifications/chat-adapters.ts +147 -0
  134. package/src/notifications/config-commands.ts +50 -0
  135. package/src/notifications/config.ts +128 -0
  136. package/src/notifications/engine.ts +100 -0
  137. package/src/notifications/helpers.ts +135 -0
  138. package/src/notifications/html-format.ts +389 -0
  139. package/src/notifications/index.ts +842 -0
  140. package/src/notifications/managed-daemon.ts +163 -0
  141. package/src/notifications/rate-limit-pool.ts +179 -0
  142. package/src/notifications/telegram-cli.ts +194 -0
  143. package/src/notifications/telegram-daemon-cli.ts +74 -0
  144. package/src/notifications/telegram-daemon-control.ts +370 -0
  145. package/src/notifications/telegram-daemon.ts +1591 -0
  146. package/src/notifications/telegram-reference.ts +335 -0
  147. package/src/notifications/threaded-inbound.ts +136 -0
  148. package/src/notifications/threaded-render.ts +173 -0
  149. package/src/notifications/topic-registry.ts +133 -0
  150. package/src/rlm/index.ts +19 -0
  151. package/src/sdk.ts +16 -0
  152. package/src/session/agent-session.ts +195 -54
  153. package/src/session/auth-storage.ts +3 -0
  154. package/src/session/session-dump-format.ts +43 -2
  155. package/src/session/session-manager.ts +39 -5
  156. package/src/setup/credential-auto-import.ts +258 -0
  157. package/src/setup/credential-import.ts +17 -0
  158. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  159. package/src/setup/host-plugin-setup.ts +142 -0
  160. package/src/skc-runtime/deep-interview-recorder.ts +2 -2
  161. package/src/skc-runtime/launch-tmux.ts +27 -5
  162. package/src/skc-runtime/ledger-event-renderer.ts +1 -0
  163. package/src/skc-runtime/ralplan-runtime.ts +2 -2
  164. package/src/skc-runtime/state-runtime.ts +18 -10
  165. package/src/skc-runtime/state-writer.ts +8 -8
  166. package/src/skc-runtime/tmux-common.ts +8 -0
  167. package/src/skc-runtime/tmux-sessions.ts +8 -1
  168. package/src/skc-runtime/ultragoal-guard.ts +57 -2
  169. package/src/skc-runtime/ultragoal-runtime.ts +105 -19
  170. package/src/skc-runtime/workflow-manifest.generated.json +56 -2
  171. package/src/skc-runtime/workflow-manifest.ts +18 -3
  172. package/src/slash-commands/builtin-registry.ts +4 -1
  173. package/src/task/executor.ts +5 -1
  174. package/src/tools/ask-answer-registry.ts +25 -0
  175. package/src/tools/ask.ts +77 -6
  176. package/src/tools/fetch.ts +78 -1
  177. package/src/tools/image-gen.ts +5 -8
  178. package/src/tools/index.ts +22 -0
  179. package/src/tools/inspect-image.ts +16 -11
  180. package/src/tools/subagent-render.ts +7 -0
  181. package/src/tools/subagent.ts +38 -7
  182. package/src/tools/telegram-send.ts +137 -0
  183. package/src/web/insane/bridge.ts +350 -0
  184. package/src/web/insane/url-guard.ts +155 -0
  185. package/src/web/search/provider.ts +77 -18
  186. package/src/web/search/providers/anthropic.ts +70 -3
  187. package/src/web/search/providers/codex.ts +1 -119
  188. package/src/web/search/providers/gemini.ts +99 -0
  189. package/src/web/search/providers/insane.ts +551 -0
  190. package/src/web/search/providers/openai-compatible.ts +66 -32
  191. package/src/web/search/providers/text-citations.ts +111 -0
  192. package/src/web/search/types.ts +13 -2
  193. package/vendor/insane-search/LICENSE +21 -0
  194. package/vendor/insane-search/MANIFEST.json +24 -0
  195. package/vendor/insane-search/engine/__init__.py +23 -0
  196. package/vendor/insane-search/engine/__main__.py +128 -0
  197. package/vendor/insane-search/engine/bias_check.py +183 -0
  198. package/vendor/insane-search/engine/executor.py +254 -0
  199. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  200. package/vendor/insane-search/engine/learning.py +175 -0
  201. package/vendor/insane-search/engine/phase0.py +214 -0
  202. package/vendor/insane-search/engine/safety.py +91 -0
  203. package/vendor/insane-search/engine/templates/package.json +11 -0
  204. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  205. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  206. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  207. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  208. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  209. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  210. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  211. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  212. package/vendor/insane-search/engine/transport.py +211 -0
  213. package/vendor/insane-search/engine/url_transforms.py +98 -0
  214. package/vendor/insane-search/engine/validators.py +331 -0
  215. package/vendor/insane-search/engine/waf_detector.py +214 -0
  216. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
package/src/lsp/config.ts CHANGED
@@ -88,6 +88,7 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
88
88
  : isRecord(config.initializationOptions)
89
89
  ? config.initializationOptions
90
90
  : undefined;
91
+ const supersedes = normalizeStringArray(config.supersedes);
91
92
 
92
93
  return {
93
94
  ...config,
@@ -96,6 +97,7 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
96
97
  fileTypes,
97
98
  rootMarkers,
98
99
  ...(initOptions ? { initOptions } : {}),
100
+ ...(supersedes ? { supersedes } : {}),
99
101
  };
100
102
  }
101
103
 
@@ -144,6 +146,18 @@ function mergeServers(
144
146
  return merged;
145
147
  }
146
148
 
149
+ function applyServerPrecedence(servers: Record<string, ServerConfig>): Record<string, ServerConfig> {
150
+ const suppressed = new Set<string>();
151
+ for (const config of Object.values(servers)) {
152
+ for (const serverName of config.supersedes ?? []) {
153
+ suppressed.add(serverName);
154
+ }
155
+ }
156
+ if (suppressed.size === 0) return servers;
157
+
158
+ return Object.fromEntries(Object.entries(servers).filter(([name]) => !suppressed.has(name)));
159
+ }
160
+
147
161
  function applyRuntimeDefaults(servers: Record<string, ServerConfig>): Record<string, ServerConfig> {
148
162
  const updated: Record<string, ServerConfig> = { ...servers };
149
163
 
@@ -422,9 +436,8 @@ export function loadConfig(cwd: string): LspConfig {
422
436
  detected[name] = { ...config, resolvedCommand: resolved };
423
437
  }
424
438
 
425
- return { servers: detected, idleTimeoutMs };
439
+ return { servers: applyServerPrecedence(detected), idleTimeoutMs };
426
440
  }
427
-
428
441
  // Merge overrides with defaults and filter to available servers
429
442
  const mergedWithRuntime = applyRuntimeDefaults(mergedServers);
430
443
  const available: Record<string, ServerConfig> = {};
@@ -437,7 +450,7 @@ export function loadConfig(cwd: string): LspConfig {
437
450
  available[name] = { ...config, resolvedCommand: resolved };
438
451
  }
439
452
 
440
- return { servers: available, idleTimeoutMs };
453
+ return { servers: applyServerPrecedence(available), idleTimeoutMs };
441
454
  }
442
455
 
443
456
  // =============================================================================
@@ -342,6 +342,13 @@
342
342
  "fileTypes": [".php"],
343
343
  "rootMarkers": ["composer.json", ".phpactor.json", ".phpactor.yml"]
344
344
  },
345
+ "csharp-ls": {
346
+ "command": "csharp-ls",
347
+ "args": [],
348
+ "fileTypes": [".cs", ".csx"],
349
+ "rootMarkers": ["*.sln", "*.slnx", "*.csproj", "global.json"],
350
+ "supersedes": ["omnisharp"]
351
+ },
345
352
  "omnisharp": {
346
353
  "command": "omnisharp",
347
354
  "args": ["-z", "--hostPID", "$PID", "--encoding", "utf-8", "--languageserver"],
package/src/lsp/types.ts CHANGED
@@ -358,6 +358,8 @@ export interface ServerConfig {
358
358
  /** Per-server warmup timeout in milliseconds. Overrides the global WARMUP_TIMEOUT_MS for this server during startup. */
359
359
  warmupTimeoutMs?: number;
360
360
  capabilities?: ServerCapabilities;
361
+ /** Names of lower-precedence servers to drop when this server is available. */
362
+ supersedes?: string[];
361
363
  /** If true, this is a linter/formatter server (e.g., Biome) - used only for diagnostics/actions, not type intelligence */
362
364
  isLinter?: boolean;
363
365
  /** Resolved absolute path to the command binary (set during config loading) */
package/src/main.ts CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  import type { AgentSession } from "./session/agent-session";
48
48
  import type { AuthStorage } from "./session/auth-storage";
49
49
  import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
50
+ import { runStartupCredentialAutoImportIfNeeded } from "./setup/credential-auto-import";
50
51
  import { formatModelOnboardingGuidance } from "./setup/model-onboarding-guidance";
51
52
  import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
52
53
  import { resolvePromptInput } from "./system-prompt";
@@ -856,6 +857,14 @@ export async function runRootCommand(
856
857
  settingsInstance.get("theme.light"),
857
858
  );
858
859
 
860
+ const credentialAutoImportNotice = isInteractive
861
+ ? await logger.time("credentialAutoImport", runStartupCredentialAutoImportIfNeeded, {
862
+ authStorage,
863
+ modelRegistry,
864
+ agentDir: settingsInstance.getAgentDir(),
865
+ })
866
+ : undefined;
867
+
859
868
  let scopedModels: ScopedModel[] = [];
860
869
  const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
861
870
  const modelMatchPreferences = {
@@ -895,6 +904,24 @@ export async function runRootCommand(
895
904
  sessionManager = await SessionManager.open(selectedPath);
896
905
  }
897
906
 
907
+ // Restore the resumed session's working directory so the HUD branch, the
908
+ // project path, and the agent's tools all match where the session was
909
+ // created. A `--worktree` session lives in a linked worktree whose path
910
+ // differs from where `--continue`/`--resume` is invoked, which would
911
+ // otherwise leave the HUD pinned to the main checkout's branch.
912
+ if (sessionManager && !parsedArgs.cwd) {
913
+ const sessionCwd = sessionManager.getCwd();
914
+ if (sessionCwd && normalizePathForComparison(sessionCwd) !== normalizePathForComparison(getProjectDir())) {
915
+ try {
916
+ if ((await fs.stat(sessionCwd)).isDirectory()) {
917
+ setProjectDir(sessionCwd);
918
+ }
919
+ } catch {
920
+ // Session cwd no longer exists (e.g. worktree removed); keep current dir.
921
+ }
922
+ }
923
+ }
924
+
898
925
  const { options: sessionOptions } = await logger.time(
899
926
  "buildSessionOptions",
900
927
  buildSessionOptions,
@@ -976,6 +1003,9 @@ export async function runRootCommand(
976
1003
  if (modelRegistryError) {
977
1004
  notifs.push({ kind: "error", message: modelRegistryError.message });
978
1005
  }
1006
+ if (credentialAutoImportNotice) {
1007
+ notifs.push({ kind: "info", message: credentialAutoImportNotice });
1008
+ }
979
1009
 
980
1010
  if (isInteractive && !session.model && !modelFallbackMessage) {
981
1011
  notifs.push({
@@ -244,6 +244,7 @@ export function mapAgentSessionEventToAcpSessionUpdates(
244
244
  case "retry_fallback_succeeded":
245
245
  case "ttsr_triggered":
246
246
  case "irc_message":
247
+ case "subagent_steer_message":
247
248
  case "notice":
248
249
  case "thinking_level_changed":
249
250
  case "goal_updated":
@@ -17,6 +17,10 @@ export interface HookEditorOptions {
17
17
  promptStyle?: boolean;
18
18
  }
19
19
 
20
+ function isWindowsRawLfNewlineInput(keyData: string): boolean {
21
+ return process.platform === "win32" && keyData === "\n";
22
+ }
23
+
20
24
  export class HookEditorComponent extends Container {
21
25
  #editor: Editor;
22
26
  #onSubmitCallback: (value: string) => void;
@@ -92,8 +96,9 @@ export class HookEditorComponent extends Container {
92
96
  return;
93
97
  }
94
98
 
95
- // Submit on any plain Enter encoding, including terminals that report unmodified Enter as LF.
96
- if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
99
+ // Submit on plain Enter encodings. On Windows, raw LF is reserved for terminal
100
+ // newline mappings (Shift+Enter/Ctrl+J/Ctrl+Enter); plain Enter reports CR.
101
+ if (!isWindowsRawLfNewlineInput(keyData) && (matchesKey(keyData, "enter") || matchesKey(keyData, "return"))) {
97
102
  this.#onSubmitCallback(this.#editor.getText());
98
103
  return;
99
104
  }
@@ -4,6 +4,7 @@ import { Container, matchesKey, Spacer, TruncatedText } from "@sayknow-cli/tui";
4
4
  import { theme } from "../../modes/theme/theme";
5
5
  import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
6
6
  import type { AuthStorage } from "../../session/auth-storage";
7
+ import type { ImportableCredential } from "../../setup/credential-import";
7
8
  import { DynamicBorder } from "./dynamic-border";
8
9
 
9
10
  const OAUTH_SELECTOR_MAX_VISIBLE = 10;
@@ -22,6 +23,7 @@ export class OAuthSelectorComponent extends Container {
22
23
  #validateAuthCallback?: (providerId: string) => Promise<boolean>;
23
24
  #requestRenderCallback?: () => void;
24
25
  #authState: Map<string, "checking" | "valid" | "invalid"> = new Map();
26
+ #externalCredentialCandidates: ImportableCredential[] = [];
25
27
  #spinnerFrame: number = 0;
26
28
  #spinnerInterval?: NodeJS.Timeout;
27
29
  #validationGeneration: number = 0;
@@ -33,6 +35,7 @@ export class OAuthSelectorComponent extends Container {
33
35
  options?: {
34
36
  validateAuth?: (providerId: string) => Promise<boolean>;
35
37
  requestRender?: () => void;
38
+ externalCredentialCandidates?: ImportableCredential[];
36
39
  },
37
40
  ) {
38
41
  super();
@@ -42,6 +45,7 @@ export class OAuthSelectorComponent extends Container {
42
45
  this.#onCancelCallback = onCancel;
43
46
  this.#validateAuthCallback = options?.validateAuth;
44
47
  this.#requestRenderCallback = options?.requestRender;
48
+ this.#externalCredentialCandidates = options?.externalCredentialCandidates ?? [];
45
49
  // Load all OAuth providers
46
50
  this.#loadProviders();
47
51
  this.addChild(new DynamicBorder());
@@ -195,6 +199,21 @@ export class OAuthSelectorComponent extends Container {
195
199
  this.#listContainer.addChild(new Spacer(1));
196
200
  this.#listContainer.addChild(new TruncatedText(theme.fg("warning", ` ${this.#statusMessage}`), 0, 0));
197
201
  }
202
+ if (this.#mode === "login" && this.#externalCredentialCandidates.length > 0) {
203
+ this.#listContainer.addChild(new Spacer(1));
204
+ for (const credential of this.#externalCredentialCandidates) {
205
+ this.#listContainer.addChild(
206
+ new TruncatedText(
207
+ theme.fg(
208
+ "success",
209
+ ` ${theme.status.success} Imported ${credential.provider} from ${credential.source}`,
210
+ ),
211
+ 0,
212
+ 0,
213
+ ),
214
+ );
215
+ }
216
+ }
198
217
  }
199
218
  handleInput(keyData: string): void {
200
219
  // Up arrow
@@ -62,6 +62,7 @@ export interface SubmenuSettingDef extends BaseSettingDef {
62
62
 
63
63
  export interface TextInputSettingDef extends BaseSettingDef {
64
64
  type: "text";
65
+ secret?: boolean;
65
66
  }
66
67
 
67
68
  export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef | TextInputSettingDef;
@@ -128,7 +129,7 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
128
129
  if (options) {
129
130
  return { ...base, type: "submenu", options };
130
131
  }
131
- return { ...base, type: "text" };
132
+ return { ...base, type: "text", secret: (ui as { secret?: boolean }).secret };
132
133
  }
133
134
 
134
135
  return null;
@@ -14,6 +14,7 @@ import {
14
14
  Text,
15
15
  } from "@sayknow-cli/tui";
16
16
  import { type SettingPath, settings } from "../../config/settings";
17
+ import { maskToken } from "../../notifications/config";
17
18
  import type {
18
19
  SettingTab,
19
20
  StatusLinePreset,
@@ -352,7 +353,9 @@ export class SettingsSelectorComponent extends Container {
352
353
  id: def.path,
353
354
  label,
354
355
  description,
355
- currentValue: (currentValue as string) ?? "",
356
+ currentValue: def.secret
357
+ ? maskToken(currentValue as string | undefined)
358
+ : ((currentValue as string) ?? ""),
356
359
  submenu: (cv, done) => this.#createTextInput(def, cv, done),
357
360
  };
358
361
  }
@@ -480,10 +483,12 @@ export class SettingsSelectorComponent extends Container {
480
483
  this.#textInputActive = false;
481
484
  done(value);
482
485
  };
486
+ // For secret fields, pre-fill with the real stored value rather than the masked display value.
487
+ const prefillValue = def.secret ? ((settings.get(def.path) as string) ?? "") : currentValue;
483
488
  return new TextInputSubmenu(
484
489
  tSettingLabel(def),
485
490
  tSettingDesc(def),
486
- currentValue,
491
+ prefillValue,
487
492
  value => {
488
493
  // Empty string clears the setting; undefined-typed string settings
489
494
  // store "" which the browser.ts expandPath ignores (no-op fallback).
@@ -83,6 +83,7 @@ export class EventController {
83
83
  todo_reminder: e => this.#handleTodoReminder(e),
84
84
  todo_auto_clear: e => this.#handleTodoAutoClear(e),
85
85
  irc_message: e => this.#handleIrcMessage(e),
86
+ subagent_steer_message: e => this.#handleSubagentSteerMessage(e),
86
87
  notice: e => this.#handleNotice(e),
87
88
  thinking_level_changed: async () => {},
88
89
  goal_updated: async () => {},
@@ -200,6 +201,7 @@ export class EventController {
200
201
  this.ctx.statusContainer.clear();
201
202
  }
202
203
  this.#cancelIdleCompaction();
204
+ this.ctx.updateEditorBorderColor();
203
205
  this.ctx.ensureLoadingAnimation();
204
206
  this.ctx.ui.requestRender();
205
207
  }
@@ -284,6 +286,25 @@ export class EventController {
284
286
  this.ctx.ui.requestRender();
285
287
  }
286
288
 
289
+ async #handleSubagentSteerMessage(
290
+ event: Extract<AgentSessionEvent, { type: "subagent_steer_message" }>,
291
+ ): Promise<void> {
292
+ const details = event.message.details as
293
+ | { observationId?: string; from?: string; to?: string; body?: string; state?: string }
294
+ | undefined;
295
+ const obsId = details?.observationId;
296
+ const signature = obsId
297
+ ? `steer:${obsId}`
298
+ : `${event.message.role}:${event.message.customType}:${event.message.timestamp}:${details?.from}:${details?.to}:${details?.state}:${details?.body}`;
299
+ if (this.#renderedCustomMessages.has(signature)) {
300
+ return;
301
+ }
302
+ this.#renderedCustomMessages.add(signature);
303
+ this.#resetReadGroup();
304
+ this.ctx.addMessageToChat(event.message);
305
+ this.ctx.ui.requestRender();
306
+ }
307
+
287
308
  #scheduleIrcExpiry(signature: string, components: Component[]): void {
288
309
  if (components.length === 0 || this.#ircExpiryTimers.has(signature)) return;
289
310
  const timer = setTimeout(() => {
@@ -613,6 +634,7 @@ export class EventController {
613
634
  this.#readToolCallArgs.clear();
614
635
  this.#readToolCallAssistantComponents.clear();
615
636
  this.#lastAssistantComponent = undefined;
637
+ this.ctx.updateEditorBorderColor();
616
638
  this.ctx.ui.requestRender();
617
639
  this.#scheduleIdleCompaction();
618
640
  this.sendCompletionNotification();
@@ -622,6 +644,7 @@ export class EventController {
622
644
  event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
623
645
  ): Promise<void> {
624
646
  this.#cancelIdleCompaction();
647
+ this.ctx.updateEditorBorderColor();
625
648
  this.ctx.autoCompactionEscapeHandler = this.ctx.editor.onEscape;
626
649
  this.ctx.editor.onEscape = () => {
627
650
  this.ctx.session.abortCompaction();
@@ -652,13 +675,20 @@ export class EventController {
652
675
  this.ctx.autoCompactionLoader = undefined;
653
676
  this.ctx.statusContainer.clear();
654
677
  }
678
+ this.ctx.updateEditorBorderColor();
655
679
  const isHandoffAction = event.action === "handoff";
680
+ const continuationDisabled = event.continuationSkipReason === "auto_continue_disabled_non_resumable_tail";
656
681
  if (event.aborted) {
657
682
  this.ctx.showStatus(isHandoffAction ? "Auto-handoff cancelled" : "Auto context-full maintenance cancelled");
658
683
  } else if (event.result) {
659
684
  this.ctx.rebuildChatFromMessages();
660
685
  this.ctx.statusLine.invalidate();
661
686
  this.ctx.updateEditorTopBorder();
687
+ if (continuationDisabled && !isHandoffAction) {
688
+ this.ctx.showStatus("Context overflow recovery skipped: auto_continue_disabled_non_resumable_tail");
689
+ } else if (event.willRetry && !isHandoffAction) {
690
+ this.ctx.showStatus("Context overflow maintenance completed");
691
+ }
662
692
  } else if (event.errorMessage) {
663
693
  this.ctx.showWarning(event.errorMessage);
664
694
  } else if (isHandoffAction) {
@@ -671,6 +701,11 @@ export class EventController {
671
701
  } else if (event.skipped) {
672
702
  // Benign skip: no model selected, no candidate models available, or nothing
673
703
  // to compact yet. Not a failure — suppress the warning.
704
+ if (continuationDisabled) {
705
+ this.ctx.showStatus("Context overflow recovery skipped: auto_continue_disabled_non_resumable_tail");
706
+ } else if (event.willRetry && !isHandoffAction) {
707
+ this.ctx.showStatus("Context overflow maintenance skipped");
708
+ }
674
709
  } else {
675
710
  this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
676
711
  }
@@ -29,10 +29,14 @@ import {
29
29
  setTheme,
30
30
  theme,
31
31
  } from "../../modes/theme/theme";
32
- import type { InteractiveModeContext } from "../../modes/types";
32
+ import type { InteractiveModeContext, OAuthSelectorOptions } from "../../modes/types";
33
33
  import { type SessionInfo, SessionManager } from "../../session/session-manager";
34
34
  import { FileSessionStorage } from "../../session/session-storage";
35
- import { discoverExternalCredentials, formatDiscoverySummary, importCredentials } from "../../setup/credential-import";
35
+ import {
36
+ CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING,
37
+ runExternalCredentialAutoImport,
38
+ } from "../../setup/credential-auto-import";
39
+ import { filterAutoImportOAuthCredentials, formatDiscoverySummary } from "../../setup/credential-import";
36
40
  import {
37
41
  MODEL_ONBOARDING_API_PROVIDER_COMMAND,
38
42
  MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
@@ -160,10 +164,22 @@ export class SelectorController {
160
164
 
161
165
  async #handleCredentialImport(): Promise<void> {
162
166
  this.ctx.showStatus("Scanning for existing Claude Code / Codex CLI credentials…");
163
- const result = await discoverExternalCredentials();
164
- const summaryLines = formatDiscoverySummary(result);
167
+ const preview = await runExternalCredentialAutoImport({
168
+ authStorage: {
169
+ importCredentialIfAbsent: async () => ({
170
+ inserted: false,
171
+ reason: "skipped-existing",
172
+ provider: "",
173
+ entries: [],
174
+ }),
175
+ },
176
+ trigger: "bare-login",
177
+ });
178
+ const result = preview.discovery ?? { importable: [], skipped: [], environment: [] };
179
+ const candidates = filterAutoImportOAuthCredentials(result.importable);
180
+ const summaryLines = formatDiscoverySummary({ ...result, importable: candidates });
165
181
 
166
- if (result.importable.length === 0) {
182
+ if (candidates.length === 0) {
167
183
  this.ctx.chatContainer.addChild(new Spacer(1));
168
184
  for (const line of summaryLines) {
169
185
  this.ctx.chatContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
@@ -172,7 +188,7 @@ export class SelectorController {
172
188
  new Text(
173
189
  theme.fg(
174
190
  "warning",
175
- "No importable Claude/Codex credentials found. Use /login or add a custom provider.",
191
+ "No importable Claude/Codex OAuth credentials found. Use /login or add a custom provider.",
176
192
  ),
177
193
  1,
178
194
  0,
@@ -183,7 +199,7 @@ export class SelectorController {
183
199
  }
184
200
 
185
201
  const confirmed = await this.ctx.showHookConfirm(
186
- `Import ${result.importable.length} credential(s)?`,
202
+ `Import ${candidates.length} credential(s)?`,
187
203
  summaryLines.join("\n"),
188
204
  );
189
205
  if (!confirmed) {
@@ -191,9 +207,10 @@ export class SelectorController {
191
207
  return;
192
208
  }
193
209
 
194
- const summary = await importCredentials(result.importable, (provider, credential) =>
195
- this.ctx.session.modelRegistry.authStorage.upsertCredential(provider, credential),
196
- );
210
+ const summary = await runExternalCredentialAutoImport({
211
+ authStorage: this.ctx.session.modelRegistry.authStorage,
212
+ trigger: "bare-login",
213
+ });
197
214
  await this.ctx.session.modelRegistry.refresh();
198
215
 
199
216
  this.ctx.chatContainer.addChild(new Spacer(1));
@@ -206,13 +223,15 @@ export class SelectorController {
206
223
  ),
207
224
  );
208
225
  }
209
- for (const failure of summary.failed) {
226
+ for (const skip of summary.skipped) {
210
227
  this.ctx.chatContainer.addChild(
211
- new Text(
212
- theme.fg("error", `${theme.status.error} Failed ${failure.credential.provider}: ${failure.error}`),
213
- 1,
214
- 0,
215
- ),
228
+ new Text(theme.fg("dim", `${theme.status.info} Skipped ${skip.credential.provider}: ${skip.reason}`), 1, 0),
229
+ );
230
+ }
231
+ for (const failure of summary.failures) {
232
+ const provider = failure.credential?.provider ?? failure.origin ?? "credential discovery";
233
+ this.ctx.chatContainer.addChild(
234
+ new Text(theme.fg("error", `${theme.status.error} Failed ${provider}: ${failure.failureClass}`), 1, 0),
216
235
  );
217
236
  }
218
237
  if (summary.imported.length > 0) {
@@ -1280,7 +1299,11 @@ export class SelectorController {
1280
1299
  }
1281
1300
  }
1282
1301
 
1283
- async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
1302
+ async showOAuthSelector(
1303
+ mode: "login" | "logout",
1304
+ providerId?: string,
1305
+ options?: OAuthSelectorOptions,
1306
+ ): Promise<void> {
1284
1307
  if (providerId) {
1285
1308
  const oauthProvider = getOAuthProviders().find(provider => provider.id === providerId);
1286
1309
  if (!oauthProvider && !this.ctx.session.modelRegistry.getModelProfiles().has(providerId)) {
@@ -1307,6 +1330,45 @@ export class SelectorController {
1307
1330
  }
1308
1331
  }
1309
1332
 
1333
+ let externalCredentialCandidates: ReturnType<typeof filterAutoImportOAuthCredentials> = [];
1334
+ if (
1335
+ mode === "login" &&
1336
+ providerId === undefined &&
1337
+ options?.allowExternalCredentialDiscovery === true &&
1338
+ options.trigger === "bare-login"
1339
+ ) {
1340
+ const preview = await runExternalCredentialAutoImport({
1341
+ authStorage: {
1342
+ importCredentialIfAbsent: async () => ({
1343
+ inserted: false,
1344
+ reason: "skipped-existing",
1345
+ provider: "",
1346
+ entries: [],
1347
+ }),
1348
+ },
1349
+ trigger: "bare-login",
1350
+ discover: options.externalCredentialDiscover,
1351
+ });
1352
+ const result = preview.discovery ?? { importable: [], skipped: [], environment: [] };
1353
+ const candidates = filterAutoImportOAuthCredentials(result.importable);
1354
+ if (candidates.length > 0) {
1355
+ const confirmed = await this.ctx.showHookConfirm(
1356
+ `Import ${candidates.length} external credential(s)?`,
1357
+ `${formatDiscoverySummary({ ...result, importable: candidates }).join("\n")}\n\n${CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING}`,
1358
+ );
1359
+ if (confirmed) {
1360
+ const summary = await runExternalCredentialAutoImport({
1361
+ authStorage: this.ctx.session.modelRegistry.authStorage,
1362
+ trigger: "bare-login",
1363
+ discover: options.externalCredentialDiscover,
1364
+ });
1365
+ externalCredentialCandidates = summary.imported;
1366
+ if (externalCredentialCandidates.length > 0) {
1367
+ await this.ctx.session.modelRegistry.refresh("offline");
1368
+ }
1369
+ }
1370
+ }
1371
+ }
1310
1372
  this.showSelector(done => {
1311
1373
  let selector: OAuthSelectorComponent;
1312
1374
  selector = new OAuthSelectorComponent(
@@ -1337,6 +1399,7 @@ export class SelectorController {
1337
1399
  requestRender: () => {
1338
1400
  this.ctx.ui.requestRender();
1339
1401
  },
1402
+ externalCredentialCandidates,
1340
1403
  },
1341
1404
  );
1342
1405
  return { component: selector, focus: selector };
@@ -29,7 +29,7 @@ import {
29
29
  import { APP_NAME, adjustHsv, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@sayknow-cli/utils";
30
30
  import chalk from "chalk";
31
31
  import { AsyncJobManager } from "../async";
32
- import { KeybindingsManager } from "../config/keybindings";
32
+ import { type AppKeybinding, KeybindingsManager } from "../config/keybindings";
33
33
  import { isSettingsInitialized, type Settings, settings } from "../config/settings";
34
34
  import { DEFAULT_SKC_DEFINITION_NAMES } from "../defaults/skc-defaults";
35
35
  import type {
@@ -118,6 +118,24 @@ import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInpu
118
118
  import { UiHelpers } from "./utils/ui-helpers";
119
119
 
120
120
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
121
+ const DEFAULT_COMPOSER_PLACEHOLDER = "Type your message...";
122
+ const FRIENDLY_KEY_PARTS: Record<string, string> = {
123
+ alt: "Alt",
124
+ cmd: "Command",
125
+ command: "Command",
126
+ ctrl: "Control",
127
+ enter: "Enter",
128
+ meta: process.platform === "darwin" ? "Command" : "Meta",
129
+ option: "Option",
130
+ shift: "Shift",
131
+ };
132
+
133
+ function formatShortcutForPlaceholder(key: string): string {
134
+ return key
135
+ .split("+")
136
+ .map(part => FRIENDLY_KEY_PARTS[part.toLowerCase()] ?? part)
137
+ .join("+");
138
+ }
121
139
 
122
140
  const HINT_SHIMMER_PALETTE: ShimmerPalette = {
123
141
  low: "dim",
@@ -142,7 +160,7 @@ function configureDefaultComposerChrome(editor: CustomEditor): void {
142
160
  editor.setClosedBorderBox(true);
143
161
  editor.setPromptGutter(undefined);
144
162
  editor.setInputPrefix(getDefaultInputPrefix());
145
- editor.setPlaceholder("Type your message...");
163
+ editor.setPlaceholder(DEFAULT_COMPOSER_PLACEHOLDER);
146
164
  editor.setPaddingX(1);
147
165
  editor.setTopBorder(undefined);
148
166
  }
@@ -904,6 +922,31 @@ export class InteractiveMode implements InteractiveModeContext {
904
922
  this.editor.setMaxHeight(this.#computeEditorMaxHeight());
905
923
  }
906
924
 
925
+ #isPromptDeliveryBusy(): boolean {
926
+ return this.session.isStreaming || this.session.isCompacting;
927
+ }
928
+
929
+ #getFirstKeyForAction(action: AppKeybinding): string | undefined {
930
+ return this.keybindings.getKeys(action)[0];
931
+ }
932
+
933
+ #getMessageQueueShortcut(): string | undefined {
934
+ const preferredAction: AppKeybinding =
935
+ process.platform === "darwin" ? "app.message.followUp" : "app.message.queue";
936
+ const fallbackAction: AppKeybinding =
937
+ process.platform === "darwin" ? "app.message.queue" : "app.message.followUp";
938
+ return this.#getFirstKeyForAction(preferredAction) ?? this.#getFirstKeyForAction(fallbackAction);
939
+ }
940
+
941
+ #getComposerPlaceholder(): string {
942
+ if (!this.#isPromptDeliveryBusy()) return DEFAULT_COMPOSER_PLACEHOLDER;
943
+ const enterAction = this.settings.get("busyPromptMode") === "steer" ? "Steering" : "Message Queueing";
944
+ const parts = [`Enter: ${enterAction}`];
945
+ const queueKey = this.#getMessageQueueShortcut();
946
+ if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}: Message Queueing`);
947
+ return `${DEFAULT_COMPOSER_PLACEHOLDER} ${parts.join(" · ")}`;
948
+ }
949
+
907
950
  updateEditorChrome(): void {
908
951
  if (this.isBashMode) {
909
952
  this.editor.borderColor = this.isBashNoContext
@@ -927,6 +970,7 @@ export class InteractiveMode implements InteractiveModeContext {
927
970
  if (!this.isBashMode) {
928
971
  this.editor.setInputPrefix(getDefaultInputPrefix());
929
972
  }
973
+ this.editor.setPlaceholder(this.#getComposerPlaceholder());
930
974
  this.#setComposerTopBorder();
931
975
  this.ui.requestRender();
932
976
  }
@@ -2503,8 +2547,12 @@ export class InteractiveMode implements InteractiveModeContext {
2503
2547
  return this.#selectorController.handleSessionDeleteCommand();
2504
2548
  }
2505
2549
 
2506
- showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
2507
- return this.#selectorController.showOAuthSelector(mode, providerId);
2550
+ showOAuthSelector(
2551
+ mode: "login" | "logout",
2552
+ providerId?: string,
2553
+ options?: import("./types").OAuthSelectorOptions,
2554
+ ): Promise<void> {
2555
+ return this.#selectorController.showOAuthSelector(mode, providerId, options);
2508
2556
  }
2509
2557
 
2510
2558
  showHookConfirm(title: string, message: string): Promise<boolean> {
@@ -78,6 +78,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
78
78
  shutdown,
79
79
  getContextUsage: () => session.getContextUsage(),
80
80
  getSystemPrompt: () => session.systemPrompt,
81
+ getWorkflowGate: () => session.getWorkflowGateEmitter(),
81
82
  compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
82
83
  },
83
84
  // ExtensionCommandContextActions — commands invokable via prompt("/command")
@@ -49,6 +49,7 @@ const AGENT_SESSION_EVENT_TYPE_REGISTRY: Record<AgentWireEventType, true> = {
49
49
  todo_reminder: true,
50
50
  todo_auto_clear: true,
51
51
  irc_message: true,
52
+ subagent_steer_message: true,
52
53
  notice: true,
53
54
  thinking_level_changed: true,
54
55
  goal_updated: true,
@@ -50,6 +50,7 @@ export function agentSessionEventType(event: AgentSessionEvent): AgentWireEventT
50
50
  case "todo_reminder":
51
51
  case "todo_auto_clear":
52
52
  case "irc_message":
53
+ case "subagent_steer_message":
53
54
  case "notice":
54
55
  case "thinking_level_changed":
55
56
  case "goal_updated":
@@ -235,6 +235,22 @@ export function observeAgentSessionEvent(event: AgentSessionEvent): AgentWireOwn
235
235
  semantic: false,
236
236
  coalesceKey: null,
237
237
  });
238
+ case "subagent_steer_message": {
239
+ const details = recordObject(event.message.details);
240
+ return obs(event, {
241
+ kind: "rpc_subagent_steer",
242
+ signal: null,
243
+ evidence: {
244
+ from: str(details?.from) ?? null,
245
+ to: str(details?.to) ?? null,
246
+ state: str(details?.state) ?? null,
247
+ observationId: str(details?.observationId) ?? null,
248
+ },
249
+ severity: "info",
250
+ semantic: false,
251
+ coalesceKey: null,
252
+ });
253
+ }
238
254
  case "notice": {
239
255
  const level = event.level;
240
256
  return obs(event, {