@sayknow-cli/coding-agent 0.3.0 → 0.3.2

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 (233) hide show
  1. package/bin/skc.js +4 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/notify-cli.d.ts +2 -0
  4. package/dist/types/cli/plugin-cli.d.ts +2 -0
  5. package/dist/types/cli.d.ts +6 -0
  6. package/dist/types/commands/mcp.d.ts +70 -0
  7. package/dist/types/commands/plugin.d.ts +6 -0
  8. package/dist/types/commands/session.d.ts +6 -0
  9. package/dist/types/config/keybindings.d.ts +2 -2
  10. package/dist/types/config/model-profile-activation.d.ts +8 -1
  11. package/dist/types/config/settings-schema.d.ts +39 -2
  12. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  13. package/dist/types/defaults/skc-defaults.d.ts +2 -13
  14. package/dist/types/extensibility/shared-events.d.ts +1 -0
  15. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  16. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  17. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  18. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  19. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  20. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  21. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  22. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  23. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  24. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  25. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  26. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  27. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  28. package/dist/types/lsp/types.d.ts +2 -0
  29. package/dist/types/main.d.ts +2 -0
  30. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  31. package/dist/types/modes/components/model-selector.d.ts +8 -0
  32. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  33. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  34. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  35. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  36. package/dist/types/notifications/config.d.ts +9 -1
  37. package/dist/types/notifications/engine.d.ts +59 -0
  38. package/dist/types/notifications/html-format.d.ts +11 -0
  39. package/dist/types/notifications/index.d.ts +149 -1
  40. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  41. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  42. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  43. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  44. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  45. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  46. package/dist/types/notifications/recent-activity.d.ts +35 -0
  47. package/dist/types/notifications/telegram-daemon.d.ts +133 -16
  48. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  49. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  50. package/dist/types/notifications/threaded-render.d.ts +6 -1
  51. package/dist/types/notifications/topic-registry.d.ts +12 -9
  52. package/dist/types/runtime-mcp/types.d.ts +7 -0
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +16 -4
  55. package/dist/types/session/blob-store.d.ts +25 -0
  56. package/dist/types/session/session-manager.d.ts +57 -0
  57. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  58. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  59. package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
  60. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  61. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  62. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  63. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  64. package/dist/types/system-prompt.d.ts +2 -0
  65. package/dist/types/task/executor.d.ts +9 -1
  66. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  67. package/dist/types/tools/fetch.d.ts +23 -0
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/telegram-send.d.ts +32 -0
  70. package/dist/types/utils/changelog.d.ts +1 -0
  71. package/dist/types/web/insane/bridge.d.ts +103 -0
  72. package/dist/types/web/insane/url-guard.d.ts +25 -0
  73. package/dist/types/web/scrapers/types.d.ts +5 -0
  74. package/dist/types/web/scrapers/utils.d.ts +7 -1
  75. package/dist/types/web/search/provider.d.ts +18 -1
  76. package/dist/types/web/search/providers/insane.d.ts +53 -0
  77. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  78. package/dist/types/web/search/types.d.ts +12 -4
  79. package/package.json +14 -10
  80. package/scripts/g004-tmux-smoke.ts +100 -0
  81. package/scripts/g005-daemon-smoke.ts +180 -0
  82. package/scripts/g011-daemon-path-smoke.ts +153 -0
  83. package/scripts/verify-insane-vendor.ts +132 -0
  84. package/src/cli/args.ts +1 -1
  85. package/src/cli/fast-help.ts +30 -2
  86. package/src/cli/mcp-cli.ts +272 -0
  87. package/src/cli/notify-cli.ts +152 -5
  88. package/src/cli/plugin-cli.ts +66 -3
  89. package/src/cli.ts +30 -12
  90. package/src/commands/mcp.ts +117 -0
  91. package/src/commands/plugin.ts +4 -0
  92. package/src/commands/session.ts +18 -0
  93. package/src/commands/team.ts +1 -1
  94. package/src/config/keybindings.ts +2 -2
  95. package/src/config/model-profile-activation.ts +55 -7
  96. package/src/config/settings-schema.ts +30 -1
  97. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  98. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  99. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  100. package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
  101. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  103. package/src/defaults/skc-defaults.ts +2 -27
  104. package/src/export/html/index.ts +2 -2
  105. package/src/extensibility/extensions/runner.ts +1 -0
  106. package/src/extensibility/shared-events.ts +1 -0
  107. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  108. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  109. package/src/extensibility/skc-plugins/index.ts +9 -0
  110. package/src/extensibility/skc-plugins/injection.ts +109 -0
  111. package/src/extensibility/skc-plugins/installer.ts +434 -0
  112. package/src/extensibility/skc-plugins/loader.ts +3 -1
  113. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  114. package/src/extensibility/skc-plugins/observability.ts +84 -0
  115. package/src/extensibility/skc-plugins/paths.ts +1 -1
  116. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  117. package/src/extensibility/skc-plugins/registry.ts +180 -0
  118. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  119. package/src/extensibility/skc-plugins/schema.ts +250 -20
  120. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  121. package/src/extensibility/skc-plugins/types.ts +199 -3
  122. package/src/extensibility/skc-plugins/validation.ts +80 -0
  123. package/src/extensibility/skills.ts +15 -0
  124. package/src/hooks/skill-state.ts +57 -0
  125. package/src/internal-urls/docs-index.generated.ts +17 -13
  126. package/src/lsp/config.ts +16 -3
  127. package/src/lsp/defaults.json +7 -0
  128. package/src/lsp/types.ts +2 -0
  129. package/src/main.ts +14 -3
  130. package/src/modes/bridge/bridge-mode.ts +11 -0
  131. package/src/modes/components/assistant-message.ts +49 -1
  132. package/src/modes/components/custom-editor.ts +2 -0
  133. package/src/modes/components/footer.ts +2 -3
  134. package/src/modes/components/hook-editor.ts +1 -1
  135. package/src/modes/components/hook-selector.ts +67 -43
  136. package/src/modes/components/model-selector.ts +56 -11
  137. package/src/modes/components/status-line/git-utils.ts +25 -0
  138. package/src/modes/components/status-line.ts +10 -11
  139. package/src/modes/components/welcome.ts +2 -3
  140. package/src/modes/controllers/event-controller.ts +15 -0
  141. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  142. package/src/modes/controllers/selector-controller.ts +53 -11
  143. package/src/modes/interactive-mode.ts +50 -3
  144. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  145. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  146. package/src/modes/theme/defaults/index.ts +2 -0
  147. package/src/modes/utils/context-usage.ts +2 -2
  148. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  149. package/src/notifications/attachment-registry.ts +23 -0
  150. package/src/notifications/chat-adapters.ts +147 -0
  151. package/src/notifications/config.ts +23 -2
  152. package/src/notifications/engine.ts +100 -0
  153. package/src/notifications/html-format.ts +38 -0
  154. package/src/notifications/index.ts +417 -45
  155. package/src/notifications/lifecycle-commands.ts +238 -0
  156. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  157. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  158. package/src/notifications/managed-daemon.ts +163 -0
  159. package/src/notifications/operator-runtime.ts +171 -0
  160. package/src/notifications/rate-limit-pool.ts +19 -0
  161. package/src/notifications/recent-activity.ts +132 -0
  162. package/src/notifications/telegram-daemon.ts +984 -242
  163. package/src/notifications/telegram-reference.ts +25 -7
  164. package/src/notifications/threaded-inbound.ts +60 -4
  165. package/src/notifications/threaded-render.ts +20 -2
  166. package/src/notifications/topic-registry.ts +23 -9
  167. package/src/prompts/agents/executor.md +2 -2
  168. package/src/runtime-mcp/transports/stdio.ts +38 -4
  169. package/src/runtime-mcp/types.ts +7 -0
  170. package/src/sdk.ts +157 -10
  171. package/src/session/agent-session.ts +248 -125
  172. package/src/session/blob-store.ts +196 -8
  173. package/src/session/session-manager.ts +762 -12
  174. package/src/skc-runtime/launch-tmux.ts +85 -22
  175. package/src/skc-runtime/ledger-event-renderer.ts +1 -0
  176. package/src/skc-runtime/psmux-detect.ts +239 -0
  177. package/src/skc-runtime/ralplan-runtime.ts +2 -2
  178. package/src/skc-runtime/team-runtime.ts +56 -23
  179. package/src/skc-runtime/tmux-common.ts +88 -4
  180. package/src/skc-runtime/tmux-sessions.ts +111 -9
  181. package/src/skc-runtime/ultragoal-guard.ts +25 -8
  182. package/src/skc-runtime/ultragoal-runtime.ts +75 -15
  183. package/src/skc-runtime/workflow-manifest.generated.json +29 -0
  184. package/src/skc-runtime/workflow-manifest.ts +7 -2
  185. package/src/slash-commands/builtin-registry.ts +23 -3
  186. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  187. package/src/slash-commands/helpers/parse.ts +2 -1
  188. package/src/system-prompt.ts +9 -0
  189. package/src/task/executor.ts +31 -7
  190. package/src/task/index.ts +2 -0
  191. package/src/tools/ask.ts +5 -1
  192. package/src/tools/bash.ts +9 -0
  193. package/src/tools/composer-bash-policy.ts +96 -0
  194. package/src/tools/fetch.ts +94 -1
  195. package/src/tools/index.ts +6 -1
  196. package/src/tools/telegram-send.ts +137 -0
  197. package/src/utils/changelog.ts +8 -0
  198. package/src/web/insane/bridge.ts +350 -0
  199. package/src/web/insane/url-guard.ts +159 -0
  200. package/src/web/scrapers/types.ts +143 -45
  201. package/src/web/scrapers/utils.ts +70 -19
  202. package/src/web/search/provider.ts +77 -18
  203. package/src/web/search/providers/anthropic.ts +70 -3
  204. package/src/web/search/providers/codex.ts +1 -119
  205. package/src/web/search/providers/gemini.ts +99 -0
  206. package/src/web/search/providers/insane.ts +551 -0
  207. package/src/web/search/providers/openai-compatible.ts +66 -32
  208. package/src/web/search/providers/text-citations.ts +111 -0
  209. package/src/web/search/types.ts +13 -2
  210. package/vendor/insane-search/LICENSE +21 -0
  211. package/vendor/insane-search/MANIFEST.json +24 -0
  212. package/vendor/insane-search/engine/__init__.py +23 -0
  213. package/vendor/insane-search/engine/__main__.py +128 -0
  214. package/vendor/insane-search/engine/bias_check.py +183 -0
  215. package/vendor/insane-search/engine/executor.py +254 -0
  216. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  217. package/vendor/insane-search/engine/learning.py +175 -0
  218. package/vendor/insane-search/engine/phase0.py +214 -0
  219. package/vendor/insane-search/engine/safety.py +91 -0
  220. package/vendor/insane-search/engine/templates/package.json +11 -0
  221. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  222. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  223. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  224. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  225. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  226. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  227. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  228. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  229. package/vendor/insane-search/engine/transport.py +211 -0
  230. package/vendor/insane-search/engine/url_transforms.py +98 -0
  231. package/vendor/insane-search/engine/validators.py +331 -0
  232. package/vendor/insane-search/engine/waf_detector.py +214 -0
  233. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -201,6 +201,7 @@ export class EventController {
201
201
  this.ctx.statusContainer.clear();
202
202
  }
203
203
  this.#cancelIdleCompaction();
204
+ this.ctx.updateEditorBorderColor();
204
205
  this.ctx.ensureLoadingAnimation();
205
206
  this.ctx.ui.requestRender();
206
207
  }
@@ -633,6 +634,7 @@ export class EventController {
633
634
  this.#readToolCallArgs.clear();
634
635
  this.#readToolCallAssistantComponents.clear();
635
636
  this.#lastAssistantComponent = undefined;
637
+ this.ctx.updateEditorBorderColor();
636
638
  this.ctx.ui.requestRender();
637
639
  this.#scheduleIdleCompaction();
638
640
  this.sendCompletionNotification();
@@ -642,6 +644,7 @@ export class EventController {
642
644
  event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
643
645
  ): Promise<void> {
644
646
  this.#cancelIdleCompaction();
647
+ this.ctx.updateEditorBorderColor();
645
648
  this.ctx.autoCompactionEscapeHandler = this.ctx.editor.onEscape;
646
649
  this.ctx.editor.onEscape = () => {
647
650
  this.ctx.session.abortCompaction();
@@ -672,13 +675,20 @@ export class EventController {
672
675
  this.ctx.autoCompactionLoader = undefined;
673
676
  this.ctx.statusContainer.clear();
674
677
  }
678
+ this.ctx.updateEditorBorderColor();
675
679
  const isHandoffAction = event.action === "handoff";
680
+ const continuationDisabled = event.continuationSkipReason === "auto_continue_disabled_non_resumable_tail";
676
681
  if (event.aborted) {
677
682
  this.ctx.showStatus(isHandoffAction ? "Auto-handoff cancelled" : "Auto context-full maintenance cancelled");
678
683
  } else if (event.result) {
679
684
  this.ctx.rebuildChatFromMessages();
680
685
  this.ctx.statusLine.invalidate();
681
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
+ }
682
692
  } else if (event.errorMessage) {
683
693
  this.ctx.showWarning(event.errorMessage);
684
694
  } else if (isHandoffAction) {
@@ -691,6 +701,11 @@ export class EventController {
691
701
  } else if (event.skipped) {
692
702
  // Benign skip: no model selected, no candidate models available, or nothing
693
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
+ }
694
709
  } else {
695
710
  this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
696
711
  }
@@ -26,8 +26,6 @@ import type { InteractiveModeContext } from "../../modes/types";
26
26
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
27
27
 
28
28
  const MAX_WIDGET_LINES = 10;
29
- const HOOK_SELECTOR_MOUSE_REPORTING_ENABLE = "\x1b[?1006h\x1b[?1000h";
30
- const HOOK_SELECTOR_MOUSE_REPORTING_DISABLE = "\x1b[?1000l\x1b[?1006l";
31
29
  const HOOK_SELECTOR_CHROME_ROWS = 7;
32
30
  const HOOK_SELECTOR_OUTLINE_ROWS = 2;
33
31
  const HOOK_SELECTOR_INLINE_INPUT_ROWS = 2;
@@ -36,7 +34,6 @@ export class ExtensionUiController {
36
34
  #extensionTerminalInputUnsubscribers = new Set<() => void>();
37
35
  #hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
38
36
  #hookWidgetsBelow = new Map<string, ExtensionUiComponent>();
39
- #hookSelectorMouseReportingEnabled = false;
40
37
  #activeHookCustomComponent?: Component & { dispose?(): void };
41
38
  #activeHookCustomOverlay?: OverlayHandle;
42
39
 
@@ -625,9 +622,6 @@ export class ExtensionUiController {
625
622
  this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - inlineInputRows - HOOK_SELECTOR_CHROME_ROWS;
626
623
  const scrollTitleRows =
627
624
  requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
628
- if (scrollTitleRows !== undefined) {
629
- this.#enableHookSelectorMouseReporting();
630
- }
631
625
 
632
626
  this.ctx.hookSelector = new HookSelectorComponent(
633
627
  title,
@@ -692,31 +686,10 @@ export class ExtensionUiController {
692
686
  return promise;
693
687
  }
694
688
 
695
- #enableHookSelectorMouseReporting(): void {
696
- if (this.#hookSelectorMouseReportingEnabled) return;
697
- this.#hookSelectorMouseReportingEnabled = true;
698
- this.#writeTerminalControl(HOOK_SELECTOR_MOUSE_REPORTING_ENABLE);
699
- }
700
-
701
- #disableHookSelectorMouseReporting(): void {
702
- if (!this.#hookSelectorMouseReportingEnabled) return;
703
- this.#hookSelectorMouseReportingEnabled = false;
704
- this.#writeTerminalControl(HOOK_SELECTOR_MOUSE_REPORTING_DISABLE);
705
- }
706
-
707
- #writeTerminalControl(sequence: string): void {
708
- try {
709
- this.ctx.ui.terminal.write(sequence);
710
- } catch {
711
- // Terminal teardown can race selector cleanup; normal shutdown restores modes.
712
- }
713
- }
714
-
715
689
  /**
716
690
  * Hide the hook selector.
717
691
  */
718
692
  hideHookSelector(): void {
719
- this.#disableHookSelectorMouseReporting();
720
693
  this.ctx.hookSelector?.dispose();
721
694
  this.ctx.editorContainer.clear();
722
695
  this.ctx.editorContainer.addChild(this.ctx.editor);
@@ -4,8 +4,10 @@ import type { OAuthProvider } from "@sayknow-cli/ai/utils/oauth/types";
4
4
  import type { Component, OverlayHandle } from "@sayknow-cli/tui";
5
5
  import { Input, Loader, Spacer, Text } from "@sayknow-cli/tui";
6
6
  import { getAgentDbPath, getProjectDir } from "@sayknow-cli/utils";
7
- import { activateModelProfile } from "../../config/model-profile-activation";
7
+ import { activateModelProfile, materializeActiveModelProfileAssignment } from "../../config/model-profile-activation";
8
8
  import { recommendModelProfileForProvider } from "../../config/model-profiles";
9
+ import { SKC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
10
+ import { formatModelSelectorValue } from "../../config/model-resolver";
9
11
  import { settings } from "../../config/settings";
10
12
  import { DebugSelectorComponent } from "../../debug";
11
13
  import { disableProvider, enableProvider } from "../../discovery";
@@ -667,7 +669,8 @@ export class SelectorController {
667
669
 
668
670
  showModelSelector(options?: { temporaryOnly?: boolean }): void {
669
671
  this.showSelector(done => {
670
- const selector = new ModelSelectorComponent(
672
+ let modelSelector: ModelSelectorComponent;
673
+ modelSelector = new ModelSelectorComponent(
671
674
  this.ctx.ui,
672
675
  this.ctx.session.model,
673
676
  this.ctx.settings,
@@ -717,38 +720,73 @@ export class SelectorController {
717
720
  this.ctx.ui.requestRender();
718
721
  return;
719
722
  }
720
- const { model, role, thinkingLevel, selector } = selection;
723
+ const { model, role, thinkingLevel, selector: selectedSelector } = selection;
721
724
  if (role === null) {
722
725
  // Temporary: update agent state but don't persist to settings
723
726
  await this.ctx.session.setModelTemporary(model, thinkingLevel);
724
727
  this.ctx.statusLine.invalidate();
725
728
  this.ctx.updateEditorBorderColor();
726
- this.ctx.showStatus(`Temporary model: ${selector ?? model.id}`);
729
+ this.ctx.showStatus(`Temporary model: ${selectedSelector ?? model.id}`);
727
730
  done();
728
731
  this.ctx.ui.requestRender();
729
732
  } else if (role === "default") {
730
733
  // Default: update agent state and persist as the active default model.
731
734
  await this.ctx.session.setModel(model, role, {
732
- selector,
735
+ selector: selectedSelector,
733
736
  thinkingLevel,
734
737
  });
738
+ const value = formatModelSelectorValue(
739
+ selectedSelector ?? `${model.provider}/${model.id}`,
740
+ thinkingLevel,
741
+ );
742
+ materializeActiveModelProfileAssignment({
743
+ session: this.ctx.session,
744
+ settings: this.ctx.settings,
745
+ role,
746
+ selector: value,
747
+ });
735
748
  if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
736
749
  this.ctx.session.setThinkingLevel(thinkingLevel);
737
750
  }
751
+ modelSelector.refreshRoleAssignments({
752
+ currentModel: this.ctx.session.model,
753
+ currentThinkingLevel: this.ctx.session.thinkingLevel,
754
+ activeModelProfile:
755
+ this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
756
+ });
738
757
  this.ctx.statusLine.invalidate();
739
758
  this.ctx.updateEditorBorderColor();
740
- this.ctx.showStatus(`Default model: ${selector ?? model.id}`);
759
+ this.ctx.showStatus(`Default model: ${selectedSelector ?? model.id}`);
741
760
  done();
742
761
  this.ctx.ui.requestRender();
743
762
  } else {
744
- // Role-agent assignments configure Task dispatch and must not switch the active chat model.
745
763
  const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
746
764
  if (!apiKey) {
747
765
  throw new Error(`No API key for ${model.provider}/${model.id}`);
748
766
  }
749
- const overrides = this.ctx.settings.get("task.agentModelOverrides");
750
- const value = selector ?? `${model.provider}/${model.id}`;
751
- this.ctx.settings.set("task.agentModelOverrides", { ...overrides, [role]: value });
767
+ const value =
768
+ selectedSelector ?? formatModelSelectorValue(`${model.provider}/${model.id}`, thinkingLevel);
769
+ const materializedProfile = materializeActiveModelProfileAssignment({
770
+ session: this.ctx.session,
771
+ settings: this.ctx.settings,
772
+ role,
773
+ selector: value,
774
+ });
775
+ if (!materializedProfile) {
776
+ const target = SKC_MODEL_ASSIGNMENT_TARGETS[role];
777
+ if (target.settingsPath === "modelRoles") {
778
+ this.ctx.settings.setModelRole(role, value);
779
+ } else {
780
+ const overrides = this.ctx.settings.get("task.agentModelOverrides");
781
+ this.ctx.settings.set("task.agentModelOverrides", { ...overrides, [role]: value });
782
+ }
783
+ }
784
+ modelSelector.refreshRoleAssignments({
785
+ currentModel: this.ctx.session.model,
786
+ currentThinkingLevel: this.ctx.session.thinkingLevel,
787
+ activeModelProfile:
788
+ this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
789
+ });
752
790
  this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
753
791
  this.ctx.showStatus(`${role} agent model: ${value}`);
754
792
  done();
@@ -765,11 +803,15 @@ export class SelectorController {
765
803
  {
766
804
  ...options,
767
805
  sessionId: this.ctx.session.sessionId,
806
+ currentThinkingLevel: this.ctx.session.thinkingLevel,
807
+ activeModelProfile:
808
+ this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
768
809
  isFastForProvider: provider => this.ctx.session.isFastForProvider(provider),
769
810
  isFastForSubagentProvider: provider => this.ctx.session.isFastForSubagentProvider(provider),
811
+ isCurrentModelFastModeActive: () => this.ctx.session.isFastModeActive(),
770
812
  },
771
813
  );
772
- return { component: selector, focus: selector };
814
+ return { component: modelSelector, focus: modelSelector };
773
815
  });
774
816
  }
775
817
 
@@ -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",
@@ -138,11 +156,11 @@ function getShellInputPrefix(isNoContext: boolean): string {
138
156
 
139
157
  function configureDefaultComposerChrome(editor: CustomEditor): void {
140
158
  editor.setBorderVisible(true);
141
- editor.setBorderStyle("sharp");
159
+ editor.setBorderStyle("round");
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
  }
@@ -414,6 +432,8 @@ export class InteractiveMode implements InteractiveModeContext {
414
432
  this.#resizeHandler = () => {
415
433
  this.#syncEditorMaxHeight();
416
434
  this.updateEditorChrome();
435
+ this.editor.invalidate();
436
+ this.ui.requestRender(true, "resize");
417
437
  };
418
438
  process.stdout.on("resize", this.#resizeHandler);
419
439
  try {
@@ -551,6 +571,7 @@ export class InteractiveMode implements InteractiveModeContext {
551
571
  this.ui.addChild(this.hookWidgetContainerAbove);
552
572
  this.ui.addChild(this.editorContainer);
553
573
  this.ui.addChild(this.hookWidgetContainerBelow);
574
+ this.ui.setBottomPinnedComponent(this.statusLine);
554
575
  this.ui.setFocus(this.editor);
555
576
 
556
577
  this.#inputController.setupKeyHandlers();
@@ -904,6 +925,31 @@ export class InteractiveMode implements InteractiveModeContext {
904
925
  this.editor.setMaxHeight(this.#computeEditorMaxHeight());
905
926
  }
906
927
 
928
+ #isPromptDeliveryBusy(): boolean {
929
+ return this.session.isStreaming || this.session.isCompacting;
930
+ }
931
+
932
+ #getFirstKeyForAction(action: AppKeybinding): string | undefined {
933
+ return this.keybindings.getKeys(action)[0];
934
+ }
935
+
936
+ #getMessageQueueShortcut(): string | undefined {
937
+ const preferredAction: AppKeybinding =
938
+ process.platform === "darwin" ? "app.message.followUp" : "app.message.queue";
939
+ const fallbackAction: AppKeybinding =
940
+ process.platform === "darwin" ? "app.message.queue" : "app.message.followUp";
941
+ return this.#getFirstKeyForAction(preferredAction) ?? this.#getFirstKeyForAction(fallbackAction);
942
+ }
943
+
944
+ #getComposerPlaceholder(): string {
945
+ if (!this.#isPromptDeliveryBusy()) return DEFAULT_COMPOSER_PLACEHOLDER;
946
+ const enterAction = this.settings.get("busyPromptMode") === "steer" ? "Steering" : "Message Queueing";
947
+ const parts = [`Enter: ${enterAction}`];
948
+ const queueKey = this.#getMessageQueueShortcut();
949
+ if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}: Message Queueing`);
950
+ return `${DEFAULT_COMPOSER_PLACEHOLDER} ${parts.join(" · ")}`;
951
+ }
952
+
907
953
  updateEditorChrome(): void {
908
954
  if (this.isBashMode) {
909
955
  this.editor.borderColor = this.isBashNoContext
@@ -927,6 +973,7 @@ export class InteractiveMode implements InteractiveModeContext {
927
973
  if (!this.isBashMode) {
928
974
  this.editor.setInputPrefix(getDefaultInputPrefix());
929
975
  }
976
+ this.editor.setPlaceholder(this.#getComposerPlaceholder());
930
977
  this.#setComposerTopBorder();
931
978
  this.ui.requestRender();
932
979
  }
@@ -73,7 +73,7 @@ const RPC_COMMAND_SCOPE_REGISTRY: Record<RpcCommandType, BridgeCommandScope> = {
73
73
  get_login_providers: "admin",
74
74
  login: "admin",
75
75
  negotiate_unattended: "control",
76
- workflow_gate_response: "prompt",
76
+ workflow_gate_response: "control",
77
77
  };
78
78
 
79
79
  export const RPC_COMMAND_TYPES: readonly RpcCommandType[] = Object.keys(RPC_COMMAND_SCOPE_REGISTRY) as RpcCommandType[];
@@ -0,0 +1,99 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/jaybeyond/Sayknow_CLI/main/packages/coding-agent/theme-schema.json",
3
+ "name": "gruvbox-dark",
4
+ "vars": {
5
+ "bgHard": "#1d2021",
6
+ "surface": "#32302f",
7
+ "surfaceBright": "#504945",
8
+ "borderNeutral": "#504945",
9
+ "borderSubtle": "#3c3836",
10
+ "fg": "#ebdbb2",
11
+ "muted": "#a89984",
12
+ "dim": "#7c6f64",
13
+ "gray": "#928374",
14
+ "red": "#fb4934",
15
+ "green": "#b8bb26",
16
+ "yellow": "#fabd2f",
17
+ "blue": "#83a598",
18
+ "purple": "#d3869b",
19
+ "aqua": "#8ec07c",
20
+ "orange": "#fe8019",
21
+ "diffRemovalRed": "#cc241d"
22
+ },
23
+ "colors": {
24
+ "accent": "orange",
25
+ "border": "borderNeutral",
26
+ "borderAccent": "orange",
27
+ "borderMuted": "borderSubtle",
28
+ "success": "green",
29
+ "error": "red",
30
+ "warning": "yellow",
31
+ "muted": "muted",
32
+ "dim": "dim",
33
+ "text": "fg",
34
+ "thinkingText": "muted",
35
+ "selectedBg": "surfaceBright",
36
+ "userMessageBg": "surface",
37
+ "userMessageText": "fg",
38
+ "customMessageBg": "surface",
39
+ "customMessageText": "fg",
40
+ "customMessageLabel": "orange",
41
+ "toolPendingBg": "surface",
42
+ "toolSuccessBg": "#283626",
43
+ "toolErrorBg": "#3c2323",
44
+ "toolTitle": "fg",
45
+ "toolOutput": "muted",
46
+ "mdHeading": "yellow",
47
+ "mdLink": "blue",
48
+ "mdLinkUrl": "muted",
49
+ "mdCode": "aqua",
50
+ "mdCodeBlock": "fg",
51
+ "mdCodeBlockBorder": "borderNeutral",
52
+ "mdQuote": "muted",
53
+ "mdQuoteBorder": "borderNeutral",
54
+ "mdHr": "dim",
55
+ "mdListBullet": "orange",
56
+ "toolDiffAdded": "green",
57
+ "toolDiffRemoved": "diffRemovalRed",
58
+ "toolDiffContext": "muted",
59
+ "syntaxComment": "gray",
60
+ "syntaxKeyword": "red",
61
+ "syntaxFunction": "green",
62
+ "syntaxVariable": "blue",
63
+ "syntaxString": "green",
64
+ "syntaxNumber": "purple",
65
+ "syntaxType": "yellow",
66
+ "syntaxOperator": "aqua",
67
+ "syntaxPunctuation": "muted",
68
+ "thinkingOff": "dim",
69
+ "thinkingMinimal": "muted",
70
+ "thinkingLow": "aqua",
71
+ "thinkingMedium": "yellow",
72
+ "thinkingHigh": "orange",
73
+ "thinkingXhigh": "red",
74
+ "bashMode": "green",
75
+ "pythonMode": "yellow",
76
+ "statusLineBg": "bgHard",
77
+ "statusLineSep": "dim",
78
+ "statusLineModel": "orange",
79
+ "statusLinePath": "blue",
80
+ "statusLineGitClean": "green",
81
+ "statusLineGitDirty": "yellow",
82
+ "statusLineContext": "aqua",
83
+ "statusLineSpend": "yellow",
84
+ "statusLineStaged": "green",
85
+ "statusLineDirty": "yellow",
86
+ "statusLineUntracked": "diffRemovalRed",
87
+ "statusLineOutput": "fg",
88
+ "statusLineCost": "orange",
89
+ "statusLineSubagents": "purple"
90
+ },
91
+ "export": {
92
+ "pageBg": "#1d2021",
93
+ "cardBg": "#282828",
94
+ "infoBg": "#32302f"
95
+ },
96
+ "symbols": {
97
+ "preset": "unicode"
98
+ }
99
+ }
@@ -1,6 +1,7 @@
1
1
  import blue_octopus from "./blue-octopus.json" with { type: "json" };
2
2
  import claude_code from "./claude-code.json" with { type: "json" };
3
3
  import codex from "./codex.json" with { type: "json" };
4
+ import gruvbox_dark from "./gruvbox-dark.json" with { type: "json" };
4
5
  import opencode from "./opencode.json" with { type: "json" };
5
6
  import red_octopus from "./red-octopus.json" with { type: "json" };
6
7
 
@@ -8,6 +9,7 @@ export const defaultThemes = {
8
9
  "blue-octopus": blue_octopus,
9
10
  "claude-code": claude_code,
10
11
  codex,
12
+ "gruvbox-dark": gruvbox_dark,
11
13
  opencode,
12
14
  "red-octopus": red_octopus,
13
15
  };
@@ -204,14 +204,14 @@ export function computeContextBreakdown(
204
204
  if (contextWindow > 0) {
205
205
  const compactionSettings = session.settings.getGroup("compaction") as CompactionSettings;
206
206
  if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
207
- const threshold = resolveThresholdTokens(contextWindow, compactionSettings, model?.maxTokens ?? 0);
207
+ const threshold = resolveThresholdTokens(contextWindow, compactionSettings);
208
208
  autoCompactBufferTokens = Math.max(0, contextWindow - threshold);
209
209
  } else {
210
210
  autoCompactBufferTokens = 0;
211
211
  }
212
212
  // Even when fully disabled, fall back to a sensible reserve floor for display.
213
213
  if (autoCompactBufferTokens === 0 && compactionSettings.enabled) {
214
- autoCompactBufferTokens = effectiveReserveTokens(contextWindow, compactionSettings, model?.maxTokens ?? 0);
214
+ autoCompactBufferTokens = effectiveReserveTokens(contextWindow, compactionSettings);
215
215
  }
216
216
  }
217
217
  autoCompactBufferTokens = Math.min(autoCompactBufferTokens, Math.max(0, contextWindow - usedTokens));
@@ -23,7 +23,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
23
23
  "|-----|--------|",
24
24
  "| `Enter` | Send / queue while busy |",
25
25
  `| \`${appKey(bindings, "app.message.queue")}\` | Queue message for next turn |`,
26
- "| `Shift+Enter` | New line |",
26
+ "| `Shift+Enter` / `Ctrl+J` | New line |",
27
27
  "| `Ctrl+W` / `Option+Backspace` | Delete word backwards |",
28
28
  "| `Ctrl+U` | Delete to start of line |",
29
29
  "| `Ctrl+K` | Delete to end of line |",
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Process-wide registry mapping a session id to a sink that delivers a local
3
+ * file to the session's connected Telegram chat as a document. Registered by
4
+ * the notifications extension; consumed by the telegram_send tool.
5
+ */
6
+
7
+ /** Delivers a local file to the session's Telegram chat. */
8
+ export type TelegramFileSink = (file: { path: string; caption?: string }) => Promise<{ ok: boolean; error?: string }>;
9
+
10
+ const sinks = new Map<string, TelegramFileSink>();
11
+
12
+ /** Register `sink` for `sessionId`. Returns a disposer that clears it. */
13
+ export function registerTelegramFileSink(sessionId: string, sink: TelegramFileSink): () => void {
14
+ sinks.set(sessionId, sink);
15
+ return () => {
16
+ if (sinks.get(sessionId) === sink) sinks.delete(sessionId);
17
+ };
18
+ }
19
+
20
+ /** The Telegram file sink for `sessionId`, if one is registered. */
21
+ export function getTelegramFileSink(sessionId: string): TelegramFileSink | undefined {
22
+ return sinks.get(sessionId);
23
+ }
@@ -0,0 +1,147 @@
1
+ import type {
2
+ NotificationAdapterPayload,
3
+ NotificationEvent,
4
+ NotificationPresentationAdapter,
5
+ NotificationReplyRoute,
6
+ } from "./engine";
7
+ import { truncate } from "./helpers";
8
+
9
+ type AdapterKind = "discord" | "slack";
10
+
11
+ interface ChatAdapterOptions {
12
+ kind: AdapterKind;
13
+ channelId?: string;
14
+ }
15
+
16
+ interface InboundShape {
17
+ sessionId?: unknown;
18
+ actionId?: unknown;
19
+ answer?: unknown;
20
+ text?: unknown;
21
+ value?: unknown;
22
+ }
23
+
24
+ function text(value: unknown): string | undefined {
25
+ return typeof value === "string" && value.length > 0 ? value : undefined;
26
+ }
27
+
28
+ function publicLine(label: string, value: unknown): string | undefined {
29
+ const v = text(value);
30
+ return v ? `${label}: ${truncate(v, 280)}` : undefined;
31
+ }
32
+
33
+ function actionText(event: Extract<NotificationEvent, { type: "action_needed" }>, format: "discord" | "slack"): string {
34
+ if (event.kind === "idle") {
35
+ const summary = text(event.summary);
36
+ return summary ? `Agent idle\n${truncate(summary, 1200)}` : "Agent idle";
37
+ }
38
+ const question = truncate(text(event.question) ?? "Question", 1200);
39
+ const options = Array.isArray(event.options) ? event.options.map(option => String(option)) : [];
40
+ const lines = [`Question: ${question}`];
41
+ if (options.length > 0) {
42
+ lines.push(
43
+ ...options.map((option, index) => {
44
+ const label = truncate(option, 180);
45
+ return format === "slack" ? `${index + 1}. ${label}` : `**${index + 1}.** ${label}`;
46
+ }),
47
+ );
48
+ } else {
49
+ lines.push("Reply with text.");
50
+ }
51
+ return lines.join("\n");
52
+ }
53
+
54
+ function frameText(event: Extract<NotificationEvent, { type: "frame" }>): string | undefined {
55
+ const frame = event.frame;
56
+ const kind = text(frame.type);
57
+ if (!kind) return undefined;
58
+ const lines = [`SKC ${kind.replace(/_/g, " ")}`];
59
+ for (const line of [
60
+ publicLine("title", frame.title),
61
+ publicLine("repo", frame.repo),
62
+ publicLine("branch", frame.branch),
63
+ publicLine("task", frame.task),
64
+ publicLine("goal", frame.goal),
65
+ publicLine("model", frame.model),
66
+ ]) {
67
+ if (line) lines.push(line);
68
+ }
69
+ const body = text(frame.text) ?? text(frame.lastMessage) ?? text(frame.caption);
70
+ if (body) lines.push(truncate(body, 1800));
71
+ return lines.join("\n");
72
+ }
73
+
74
+ function routeFromInbound(input: unknown): NotificationReplyRoute | undefined {
75
+ const raw = input as InboundShape;
76
+ if (!raw || typeof raw !== "object") return undefined;
77
+ const sessionId = text(raw.sessionId);
78
+ const actionId = text(raw.actionId);
79
+ if (!sessionId || !actionId) return undefined;
80
+ const answer = raw.answer ?? raw.value ?? raw.text;
81
+ if (typeof answer !== "string" && typeof answer !== "number" && typeof answer !== "object") return undefined;
82
+ return { sessionId, actionId, answer: answer as NotificationReplyRoute["answer"] };
83
+ }
84
+
85
+ class DiscordNotificationAdapter implements NotificationPresentationAdapter {
86
+ readonly kind = "discord" as const;
87
+ constructor(private readonly opts: ChatAdapterOptions) {}
88
+
89
+ render(event: NotificationEvent): NotificationAdapterPayload[] {
90
+ if (event.type === "action_resolved") return [];
91
+ const content = event.type === "action_needed" ? actionText(event, "discord") : frameText(event);
92
+ if (!content) return [];
93
+ const payload: Record<string, unknown> = {
94
+ content,
95
+ allowed_mentions: { parse: [] },
96
+ };
97
+ if (this.opts.channelId) payload.channel_id = this.opts.channelId;
98
+ return [
99
+ {
100
+ adapter: this.kind,
101
+ channelKey: this.opts.channelId,
102
+ body: payload,
103
+ route: event.type === "action_needed" ? { sessionId: event.sessionId, actionId: event.id } : undefined,
104
+ },
105
+ ];
106
+ }
107
+
108
+ mapInbound(input: unknown): NotificationReplyRoute | undefined {
109
+ return routeFromInbound(input);
110
+ }
111
+ }
112
+
113
+ class SlackNotificationAdapter implements NotificationPresentationAdapter {
114
+ readonly kind = "slack" as const;
115
+ constructor(private readonly opts: ChatAdapterOptions) {}
116
+
117
+ render(event: NotificationEvent): NotificationAdapterPayload[] {
118
+ if (event.type === "action_resolved") return [];
119
+ const textValue = event.type === "action_needed" ? actionText(event, "slack") : frameText(event);
120
+ if (!textValue) return [];
121
+ const payload: Record<string, unknown> = {
122
+ text: textValue,
123
+ mrkdwn: true,
124
+ };
125
+ if (this.opts.channelId) payload.channel = this.opts.channelId;
126
+ return [
127
+ {
128
+ adapter: this.kind,
129
+ channelKey: this.opts.channelId,
130
+ body: payload,
131
+ route: event.type === "action_needed" ? { sessionId: event.sessionId, actionId: event.id } : undefined,
132
+ },
133
+ ];
134
+ }
135
+
136
+ mapInbound(input: unknown): NotificationReplyRoute | undefined {
137
+ return routeFromInbound(input);
138
+ }
139
+ }
140
+
141
+ export function createDiscordAdapter(opts: Omit<ChatAdapterOptions, "kind"> = {}): NotificationPresentationAdapter {
142
+ return new DiscordNotificationAdapter({ ...opts, kind: "discord" });
143
+ }
144
+
145
+ export function createSlackAdapter(opts: Omit<ChatAdapterOptions, "kind"> = {}): NotificationPresentationAdapter {
146
+ return new SlackNotificationAdapter({ ...opts, kind: "slack" });
147
+ }