@sayknow-cli/coding-agent 0.3.5 → 0.3.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 (238) hide show
  1. package/README.md +76 -0
  2. package/dist/types/capability/mcp.d.ts +2 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/telegram-cli.d.ts +13 -0
  6. package/dist/types/cli/update-cli.d.ts +24 -1
  7. package/dist/types/commands/local-provider.d.ts +34 -0
  8. package/dist/types/commands/telegram.d.ts +22 -0
  9. package/dist/types/config/keybindings.d.ts +12 -6
  10. package/dist/types/config/model-registry.d.ts +5 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +284 -4
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/config/telegram-autostart.d.ts +14 -0
  15. package/dist/types/config/telegram-env-bridge.d.ts +18 -0
  16. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  17. package/dist/types/defaults/skc-defaults.d.ts +7 -0
  18. package/dist/types/discovery/cursor.d.ts +4 -1
  19. package/dist/types/discovery/index.d.ts +0 -1
  20. package/dist/types/discovery/windsurf.d.ts +3 -1
  21. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  22. package/dist/types/i18n/messages/en.d.ts +1 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  24. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  25. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  26. package/dist/types/modes/components/model-selector.d.ts +2 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +5 -1
  28. package/dist/types/modes/components/skill-message.d.ts +1 -0
  29. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  30. package/dist/types/modes/components/status-line.d.ts +15 -3
  31. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  32. package/dist/types/modes/components/welcome.d.ts +9 -1
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/input-controller.d.ts +6 -1
  35. package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
  38. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  40. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  41. package/dist/types/modes/theme/theme.d.ts +1 -1
  42. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  43. package/dist/types/modes/types.d.ts +5 -1
  44. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  45. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  46. package/dist/types/notifications/html-format.d.ts +2 -0
  47. package/dist/types/notifications/recent-activity.d.ts +5 -1
  48. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  49. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  50. package/dist/types/notifications/telegram-daemon.d.ts +4 -10
  51. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  52. package/dist/types/notifications/threaded-render.d.ts +1 -0
  53. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  54. package/dist/types/runtime-mcp/config.d.ts +2 -0
  55. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  56. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  57. package/dist/types/runtime-mcp/types.d.ts +6 -0
  58. package/dist/types/sdk.d.ts +1 -1
  59. package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
  60. package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
  61. package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
  62. package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
  63. package/dist/types/skc-runtime/state-writer.d.ts +1 -0
  64. package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
  65. package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
  66. package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
  67. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
  68. package/dist/types/slash-commands/types.d.ts +2 -0
  69. package/dist/types/system-prompt.d.ts +7 -0
  70. package/dist/types/tools/skill.d.ts +1 -1
  71. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  72. package/dist/types/web/search/index.d.ts +4 -2
  73. package/dist/types/web/search/provider.d.ts +14 -0
  74. package/dist/types/web/search/providers/utils.d.ts +42 -13
  75. package/dist/types/web/search/render.d.ts +1 -0
  76. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  77. package/package.json +8 -8
  78. package/scripts/build-binary.ts +3 -0
  79. package/scripts/verify-insane-vendor.ts +0 -5
  80. package/src/capability/mcp.ts +2 -0
  81. package/src/cli/local-provider-smoke.ts +632 -0
  82. package/src/cli/notify-cli.ts +4 -2
  83. package/src/cli/telegram-cli.ts +156 -0
  84. package/src/cli/update-cli.ts +179 -13
  85. package/src/cli/web-search-cli.ts +5 -7
  86. package/src/cli.ts +20 -1
  87. package/src/commands/launch.ts +47 -2
  88. package/src/commands/local-provider.ts +62 -0
  89. package/src/commands/notify.ts +1 -1
  90. package/src/commands/ralplan.ts +1 -0
  91. package/src/commands/telegram.ts +47 -0
  92. package/src/config/keybindings.ts +15 -6
  93. package/src/config/mcp-schema.json +4 -0
  94. package/src/config/model-registry.ts +55 -1
  95. package/src/config/models-config-schema.ts +9 -0
  96. package/src/config/settings-schema.ts +256 -4
  97. package/src/config/settings.ts +11 -0
  98. package/src/config/telegram-autostart.ts +107 -0
  99. package/src/config/telegram-env-bridge.ts +92 -0
  100. package/src/coordinator-mcp/server.ts +276 -34
  101. package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
  103. package/src/defaults/skc-defaults.ts +16 -0
  104. package/src/discovery/builtin.ts +12 -0
  105. package/src/discovery/cursor.ts +5 -84
  106. package/src/discovery/gemini.ts +3 -89
  107. package/src/discovery/index.ts +4 -1
  108. package/src/discovery/mcp-json.ts +11 -0
  109. package/src/discovery/opencode.ts +5 -122
  110. package/src/discovery/windsurf.ts +4 -82
  111. package/src/edit/read-file.ts +9 -1
  112. package/src/extensibility/extensions/runner.ts +2 -1
  113. package/src/extensibility/extensions/types.ts +11 -0
  114. package/src/extensibility/skills.ts +30 -2
  115. package/src/hooks/skill-state.ts +33 -73
  116. package/src/i18n/messages/de.ts +1 -0
  117. package/src/i18n/messages/en.ts +1 -0
  118. package/src/i18n/messages/es.ts +1 -0
  119. package/src/i18n/messages/fr.ts +1 -0
  120. package/src/i18n/messages/ja.settings.ts +28 -0
  121. package/src/i18n/messages/ja.ts +1 -0
  122. package/src/i18n/messages/ko.settings.ts +28 -0
  123. package/src/i18n/messages/ko.ts +1 -0
  124. package/src/i18n/messages/zh.settings.ts +28 -0
  125. package/src/i18n/messages/zh.ts +1 -0
  126. package/src/internal-urls/docs-index.generated.ts +12 -7
  127. package/src/main.ts +5 -2
  128. package/src/migrate/executor.ts +23 -6
  129. package/src/modes/acp/acp-agent.ts +7 -6
  130. package/src/modes/components/agent-dashboard.ts +3 -35
  131. package/src/modes/components/custom-editor.ts +26 -0
  132. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  133. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  134. package/src/modes/components/hook-selector.ts +21 -3
  135. package/src/modes/components/model-selector.ts +246 -11
  136. package/src/modes/components/session-observer-overlay.ts +3 -3
  137. package/src/modes/components/session-selector.ts +19 -9
  138. package/src/modes/components/settings-selector.ts +551 -11
  139. package/src/modes/components/skill-message.ts +18 -10
  140. package/src/modes/components/status-line/presets.ts +11 -0
  141. package/src/modes/components/status-line/segments.ts +16 -19
  142. package/src/modes/components/status-line/types.ts +6 -2
  143. package/src/modes/components/status-line.ts +281 -117
  144. package/src/modes/components/tree-selector.ts +28 -8
  145. package/src/modes/components/two-column-body.ts +36 -0
  146. package/src/modes/components/welcome.ts +165 -30
  147. package/src/modes/controllers/command-controller.ts +26 -2
  148. package/src/modes/controllers/event-controller.ts +102 -4
  149. package/src/modes/controllers/extension-ui-controller.ts +21 -2
  150. package/src/modes/controllers/input-controller.ts +214 -47
  151. package/src/modes/controllers/selector-controller.ts +35 -33
  152. package/src/modes/interactive-mode.ts +36 -30
  153. package/src/modes/prompt-action-autocomplete.ts +48 -8
  154. package/src/modes/rpc/rpc-types.ts +1 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
  156. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  157. package/src/modes/slash-command-visibility.ts +9 -0
  158. package/src/modes/theme/theme.ts +5 -1
  159. package/src/modes/tmux-scroll.ts +92 -0
  160. package/src/modes/types.ts +8 -1
  161. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  162. package/src/modes/utils/terminal-bell.ts +48 -0
  163. package/src/modes/utils/ui-helpers.ts +76 -3
  164. package/src/notifications/daemon-paths.ts +22 -0
  165. package/src/notifications/html-format.ts +75 -1
  166. package/src/notifications/index.ts +32 -2
  167. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  168. package/src/notifications/recent-activity.ts +32 -8
  169. package/src/notifications/telegram-daemon-cli.ts +125 -13
  170. package/src/notifications/telegram-daemon-control.ts +9 -0
  171. package/src/notifications/telegram-daemon.ts +88 -46
  172. package/src/notifications/telegram-reference.ts +36 -14
  173. package/src/notifications/threaded-render.ts +14 -6
  174. package/src/prompts/agents/architect.md +7 -4
  175. package/src/prompts/agents/critic.md +15 -6
  176. package/src/prompts/agents/planner.md +8 -3
  177. package/src/prompts/system/system-prompt.md +20 -2
  178. package/src/prompts/tools/bash.md +1 -1
  179. package/src/runtime-mcp/config-writer.ts +28 -0
  180. package/src/runtime-mcp/config.ts +7 -0
  181. package/src/runtime-mcp/loader.ts +3 -0
  182. package/src/runtime-mcp/manager.ts +3 -0
  183. package/src/runtime-mcp/types.ts +6 -0
  184. package/src/sdk.ts +10 -11
  185. package/src/session/agent-session.ts +103 -13
  186. package/src/session/session-manager.ts +58 -18
  187. package/src/skc-runtime/launch-tmux.ts +34 -8
  188. package/src/skc-runtime/launch-worktree.ts +16 -2
  189. package/src/skc-runtime/ralplan-runtime.ts +22 -6
  190. package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
  191. package/src/skc-runtime/session-state-sidecar.ts +167 -24
  192. package/src/skc-runtime/state-runtime.ts +8 -5
  193. package/src/skc-runtime/state-writer.ts +5 -5
  194. package/src/skc-runtime/team-runtime.ts +21 -6
  195. package/src/skc-runtime/tmux-common.ts +16 -0
  196. package/src/skc-runtime/tmux-sessions.ts +13 -9
  197. package/src/skc-runtime/ultragoal-guard.ts +150 -24
  198. package/src/skc-runtime/ultragoal-runtime.ts +16 -124
  199. package/src/skc-runtime/workflow-manifest.generated.json +0 -8
  200. package/src/skc-runtime/workflow-manifest.ts +0 -1
  201. package/src/slash-commands/builtin-registry.ts +136 -23
  202. package/src/slash-commands/types.ts +2 -0
  203. package/src/system-prompt.ts +8 -0
  204. package/src/task/executor.ts +5 -0
  205. package/src/tools/ast-grep.ts +10 -7
  206. package/src/tools/bash.ts +39 -14
  207. package/src/tools/index.ts +8 -1
  208. package/src/tools/skill.ts +1 -1
  209. package/src/utils/cmux-workspace.ts +233 -0
  210. package/src/utils/title-generator.ts +2 -0
  211. package/src/web/kagi.ts +1 -1
  212. package/src/web/parallel.ts +2 -2
  213. package/src/web/search/index.ts +137 -52
  214. package/src/web/search/provider.ts +80 -0
  215. package/src/web/search/providers/anthropic.ts +1 -1
  216. package/src/web/search/providers/brave.ts +1 -1
  217. package/src/web/search/providers/codex.ts +1 -1
  218. package/src/web/search/providers/duckduckgo.ts +1 -1
  219. package/src/web/search/providers/exa.ts +1 -1
  220. package/src/web/search/providers/gemini.ts +7 -3
  221. package/src/web/search/providers/jina.ts +1 -1
  222. package/src/web/search/providers/kimi.ts +6 -1
  223. package/src/web/search/providers/openai-compatible.ts +1 -1
  224. package/src/web/search/providers/parallel.ts +1 -1
  225. package/src/web/search/providers/perplexity.ts +2 -2
  226. package/src/web/search/providers/searxng.ts +1 -1
  227. package/src/web/search/providers/synthetic.ts +1 -1
  228. package/src/web/search/providers/tavily.ts +1 -1
  229. package/src/web/search/providers/utils.ts +67 -17
  230. package/src/web/search/providers/xai.ts +1 -1
  231. package/src/web/search/providers/zai.ts +1 -1
  232. package/src/web/search/render.ts +3 -0
  233. package/src/workflow/workflow-intent-diff.ts +199 -0
  234. package/vendor/insane-search/MANIFEST.json +1 -3
  235. package/vendor/insane-search/engine/tests/test_u1.py +0 -28
  236. package/vendor/insane-search/engine/transport.py +2 -3
  237. package/vendor/insane-search/engine/validators.py +0 -14
  238. package/src/defaults/skc/rules/ponytail.md +0 -68
package/src/main.ts CHANGED
@@ -30,6 +30,7 @@ import { activateModelProfile } from "./config/model-profile-activation";
30
30
  import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
31
31
  import { resolveCliModel, resolveModelRoleValue, resolveModelScope, type ScopedModel } from "./config/model-resolver";
32
32
  import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
33
+ import { maybeAutostartTelegramRemote } from "./config/telegram-autostart";
33
34
  import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory } from "./defaults/skc-grok-cli";
34
35
  import { initializeWithSettings } from "./discovery";
35
36
  import { exportFromFile } from "./export/html";
@@ -671,7 +672,7 @@ async function buildSessionOptions(
671
672
  options.thinkingLevel = scopedModels[0].thinkingLevel;
672
673
  }
673
674
 
674
- // Scoped models for Ctrl+P cycling - fill in default thinking levels when not explicit
675
+ // Scoped models for Alt+N cycling - fill in default thinking levels when not explicit
675
676
  if (scopedModels.length > 0) {
676
677
  const defaultThinkingLevel = activeSettings.get("defaultThinkingLevel");
677
678
  options.scopedModels = scopedModels.map(scopedModel => ({
@@ -845,6 +846,8 @@ export async function runRootCommand(
845
846
 
846
847
  // Initialize discovery system with settings for provider persistence
847
848
  logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
849
+ // Auto-start Telegram Remote gateway if enabled in settings
850
+ if (!parsedArgs.print && !autoPrint) await maybeAutostartTelegramRemote();
848
851
 
849
852
  // Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
850
853
  const smolModel = parsedArgs.smol ?? $env.PI_SMOL_MODEL;
@@ -1067,7 +1070,7 @@ export async function runRootCommand(
1067
1070
  return `${scopedModel.model.id}${thinkingStr}`;
1068
1071
  })
1069
1072
  .join(", ");
1070
- process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`)}\n`);
1073
+ process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Alt+N to cycle)")}`)}\n`);
1071
1074
  }
1072
1075
 
1073
1076
  if ($env.PI_TIMING) {
@@ -12,11 +12,28 @@ import * as path from "node:path";
12
12
  import { upsertMCPServer } from "../runtime-mcp/config-writer";
13
13
  import type { MigrateAction } from "./types";
14
14
 
15
- async function ensureRealDirectoryPathNoFollow(directory: string): Promise<void> {
16
- const resolved = path.resolve(directory);
17
- const parsed = path.parse(resolved);
18
- let current = parsed.root;
19
- for (const part of resolved.slice(parsed.root.length).split(path.sep).filter(Boolean)) {
15
+ function trustedSkillDestinationBase(destination: string): string {
16
+ const skillDir = path.dirname(path.resolve(destination));
17
+ const skillsRoot = path.dirname(skillDir);
18
+ const scopeRoot = path.dirname(skillsRoot);
19
+ return path.basename(scopeRoot) === ".skc" ? path.dirname(scopeRoot) : scopeRoot;
20
+ }
21
+
22
+ async function ensureRealDirectoryPathNoFollow(directory: string, trustedBase: string): Promise<void> {
23
+ const resolvedBase = path.resolve(trustedBase);
24
+ const resolvedDirectory = path.resolve(directory);
25
+ const relative = path.relative(resolvedBase, resolvedDirectory);
26
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
27
+ throw new Error(`skill destination escapes trusted base: ${resolvedDirectory}`);
28
+ }
29
+
30
+ const baseStat = await fs.stat(resolvedBase);
31
+ if (!baseStat.isDirectory()) {
32
+ throw new Error(`skill destination base is not a directory: ${resolvedBase}`);
33
+ }
34
+
35
+ let current = resolvedBase;
36
+ for (const part of relative.split(path.sep).filter(part => part && part !== ".")) {
20
37
  current = path.join(current, part);
21
38
  try {
22
39
  const stat = await fs.lstat(current);
@@ -36,7 +53,7 @@ async function ensureRealDirectoryPathNoFollow(directory: string): Promise<void>
36
53
 
37
54
  async function writeSkillFileNoFollow(destination: string, content: string): Promise<void> {
38
55
  const skillDir = path.dirname(destination);
39
- await ensureRealDirectoryPathNoFollow(skillDir);
56
+ await ensureRealDirectoryPathNoFollow(skillDir, trustedSkillDestinationBase(destination));
40
57
  const handle = await fs.open(
41
58
  destination,
42
59
  fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_TRUNC | fsSync.constants.O_NOFOLLOW,
@@ -710,9 +710,6 @@ export class AcpAgent implements Agent {
710
710
  text: string,
711
711
  options: { directAliasMayCollide?: boolean } = {},
712
712
  ): Promise<boolean> {
713
- if (!text.startsWith("/")) {
714
- return false;
715
- }
716
713
  if (!record.session.skillsSettings?.enableSkillCommands) {
717
714
  return false;
718
715
  }
@@ -758,9 +755,13 @@ export class AcpAgent implements Agent {
758
755
  }
759
756
  return true;
760
757
  }
761
- const spaceIndex = text.indexOf(" ");
762
- const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
763
- const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
758
+ const slashText = text.trimStart();
759
+ if (!slashText.startsWith("/")) {
760
+ return false;
761
+ }
762
+ const spaceIndex = slashText.indexOf(" ");
763
+ const commandName = spaceIndex === -1 ? slashText.slice(1) : slashText.slice(1, spaceIndex);
764
+ const args = spaceIndex === -1 ? "" : slashText.slice(spaceIndex + 1).trim();
764
765
  if (
765
766
  !isNamespacedSkillSlashCommandName(commandName) &&
766
767
  options.directAliasMayCollide !== true &&
@@ -23,12 +23,11 @@ import {
23
23
  extractPrintableText,
24
24
  Input,
25
25
  matchesKey,
26
- padding,
27
26
  replaceTabs,
27
+ resolveTerminalColumns,
28
28
  Spacer,
29
29
  Text,
30
30
  truncateToWidth,
31
- visibleWidth,
32
31
  wrapTextWithAnsi,
33
32
  } from "@sayknow-cli/tui";
34
33
  import { isEnoent, prompt } from "@sayknow-cli/utils";
@@ -51,6 +50,7 @@ import { shortenPath } from "../../tools/render-utils";
51
50
  import { theme } from "../theme/theme";
52
51
  import { matchesAppInterrupt } from "../utils/keybinding-matchers";
53
52
  import { DynamicBorder } from "./dynamic-border";
53
+ import { TwoColumnBody } from "./two-column-body";
54
54
 
55
55
  type SourceTabId = "all" | AgentSource;
56
56
  type AgentScope = "project" | "user";
@@ -295,38 +295,6 @@ class AgentInspectorPane implements Component {
295
295
  invalidate(): void {}
296
296
  }
297
297
 
298
- class TwoColumnBody implements Component {
299
- constructor(
300
- private readonly leftPane: AgentListPane,
301
- private readonly rightPane: AgentInspectorPane,
302
- private readonly maxHeight: number,
303
- ) {}
304
-
305
- render(width: number): string[] {
306
- const leftWidth = Math.floor(width * 0.5);
307
- const rightWidth = width - leftWidth - 3;
308
- const leftLines = this.leftPane.render(leftWidth);
309
- const rightLines = this.rightPane.render(rightWidth);
310
- const lineCount = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
311
- const out: string[] = [];
312
- const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
313
-
314
- for (let i = 0; i < lineCount; i++) {
315
- const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
316
- const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
317
- const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
318
- out.push(leftPadded + separator + right);
319
- }
320
-
321
- return out;
322
- }
323
-
324
- invalidate(): void {
325
- this.leftPane.invalidate?.();
326
- this.rightPane.invalidate?.();
327
- }
328
- }
329
-
330
298
  export class AgentDashboard extends Container {
331
299
  #settingsManager: Settings | null = null;
332
300
  #allAgents: DashboardAgent[] = [];
@@ -870,7 +838,7 @@ export class AgentDashboard extends Container {
870
838
  }
871
839
 
872
840
  #uiWidth(): number {
873
- return Math.max(40, process.stdout.columns ?? 100);
841
+ return Math.max(40, resolveTerminalColumns());
874
842
  }
875
843
 
876
844
  /** Rebuild layout and request a TUI render pass (for use after async state changes). */
@@ -9,6 +9,7 @@ type ConfigurableEditorAction = Extract<
9
9
  | "app.exit"
10
10
  | "app.suspend"
11
11
  | "app.thinking.cycle"
12
+ | "app.commandPalette.open"
12
13
  | "app.model.cycleForward"
13
14
  | "app.model.cycleBackward"
14
15
  | "app.model.select"
@@ -33,6 +34,7 @@ const CONFIGURABLE_EDITOR_ACTIONS = [
33
34
  "app.exit",
34
35
  "app.suspend",
35
36
  "app.thinking.cycle",
37
+ "app.commandPalette.open",
36
38
  "app.model.cycleForward",
37
39
  "app.model.cycleBackward",
38
40
  "app.model.select",
@@ -76,6 +78,7 @@ export class CustomEditor extends Editor {
76
78
  onClear?: () => void;
77
79
  onExit?: () => void;
78
80
  onCycleThinkingLevel?: () => void;
81
+ onOpenCommandPalette?: () => void;
79
82
  onCycleModelForward?: () => void;
80
83
  onCycleModelBackward?: () => void;
81
84
  onSelectModel?: () => void;
@@ -100,6 +103,10 @@ export class CustomEditor extends Editor {
100
103
  onQueue?: () => void;
101
104
  /** Called when Caps Lock is pressed. */
102
105
  onCapsLock?: () => void;
106
+ /** Called when PageUp/PageDown should scroll the transcript viewport instead of prompt history. */
107
+ onViewportPageScroll?: (direction: -1 | 1) => void;
108
+ /** Called before regular composer input should return the transcript viewport to the live bottom. */
109
+ onViewportFollowLive?: () => void;
103
110
 
104
111
  /** Custom key handlers from extensions and non-built-in app actions. */
105
112
  #customKeyHandlers = new Map<KeyId, () => boolean | undefined>();
@@ -240,6 +247,20 @@ export class CustomEditor extends Editor {
240
247
  return;
241
248
  }
242
249
  }
250
+ if (!this.isShowingAutocomplete()) {
251
+ if (matchesKey(data, "pageUp") && this.onViewportPageScroll) {
252
+ this.onViewportPageScroll(-1);
253
+ return;
254
+ }
255
+ if (matchesKey(data, "pageDown") && this.onViewportPageScroll) {
256
+ this.onViewportPageScroll(1);
257
+ return;
258
+ }
259
+ }
260
+
261
+ if (!matchesKey(data, "pageUp") && !matchesKey(data, "pageDown")) {
262
+ this.onViewportFollowLive?.();
263
+ }
243
264
  // Intercept configured image paste (async - fires and handles result)
244
265
  if (this.#matchesAction(data, "app.clipboard.pasteImage") && this.onPasteImage) {
245
266
  void this.onPasteImage();
@@ -288,6 +309,11 @@ export class CustomEditor extends Editor {
288
309
  return;
289
310
  }
290
311
 
312
+ // Intercept configured command palette shortcut before model cycling (Ctrl+P default).
313
+ if (this.#matchesAction(data, "app.commandPalette.open") && this.onOpenCommandPalette) {
314
+ this.onOpenCommandPalette();
315
+ return;
316
+ }
291
317
  // Intercept configured backward model cycling (check before forward cycling)
292
318
  if (this.#matchesAction(data, "app.model.cycleBackward") && this.onCycleModelBackward) {
293
319
  this.onCycleModelBackward();
@@ -6,15 +6,6 @@ import { DynamicBorder } from "./dynamic-border";
6
6
 
7
7
  const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
8
8
 
9
- type WizardStep = "name" | "display-name" | "provider" | "model" | "confirm";
10
-
11
- interface WizardState {
12
- name: string;
13
- displayName: string;
14
- provider: string;
15
- model: string;
16
- }
17
-
18
9
  export interface CustomModelPresetWizardSubmit {
19
10
  name: string;
20
11
  profile: ModelProfileConfig;
@@ -23,25 +14,21 @@ export interface CustomModelPresetWizardSubmit {
23
14
  export class CustomModelPresetWizardComponent extends Container {
24
15
  #contentContainer: Container;
25
16
  #input: Input | null = null;
26
- #step: WizardStep = "name";
27
- #selectedIndex = 0;
28
17
  #lastError: string | null = null;
29
- #state: WizardState = {
30
- name: "",
31
- displayName: "",
32
- provider: "",
33
- model: "",
34
- };
18
+ #name = "";
19
+ #snapshot: ModelProfileConfig;
35
20
  #onSubmit: (input: CustomModelPresetWizardSubmit) => void;
36
21
  #onCancel: () => void;
37
22
  #onRender: () => void;
38
23
 
39
24
  constructor(
25
+ snapshot: ModelProfileConfig,
40
26
  onSubmit: (input: CustomModelPresetWizardSubmit) => void,
41
27
  onCancel: () => void,
42
28
  onRender: () => void = () => {},
43
29
  ) {
44
30
  super();
31
+ this.#snapshot = snapshot;
45
32
  this.#onSubmit = onSubmit;
46
33
  this.#onCancel = onCancel;
47
34
  this.#onRender = onRender;
@@ -51,7 +38,7 @@ export class CustomModelPresetWizardComponent extends Container {
51
38
  this.addChild(new TruncatedText(theme.bold("Create custom model preset")));
52
39
  this.addChild(
53
40
  new TruncatedText(
54
- theme.fg("muted", " Save provider/model as a selectable profile. Secrets are not requested."),
41
+ theme.fg("muted", " Save the current default and explicit role models as a selectable profile."),
55
42
  0,
56
43
  0,
57
44
  ),
@@ -72,222 +59,76 @@ export class CustomModelPresetWizardComponent extends Container {
72
59
 
73
60
  handleInput(keyData: string): void {
74
61
  if (matchesAppInterrupt(keyData)) {
75
- if (this.#step === "name") {
76
- this.#onCancel();
77
- return;
78
- }
79
- this.#goBack();
62
+ this.#onCancel();
80
63
  return;
81
64
  }
82
65
 
83
66
  if (this.#input) {
84
67
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
85
- this.#saveInputAndProceed();
68
+ this.#saveInputAndSubmit();
86
69
  return;
87
70
  }
88
71
  this.#input.handleInput(keyData);
89
- return;
90
- }
91
-
92
- if (matchesKey(keyData, "up")) {
93
- this.#moveSelection(-1);
94
- return;
95
- }
96
- if (matchesKey(keyData, "down")) {
97
- this.#moveSelection(1);
98
- return;
99
- }
100
- if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
101
- this.#selectCurrentOption();
102
72
  }
103
73
  }
104
74
 
105
75
  #renderStep(): void {
106
76
  this.#contentContainer.clear();
107
77
  this.#input = null;
108
- switch (this.#step) {
109
- case "name":
110
- this.#renderInputStep(
111
- "Step 1: Preset id",
112
- "Enter a unique preset id:",
113
- this.#state.name,
114
- "e.g. my-fast-coder",
115
- );
116
- break;
117
- case "display-name":
118
- this.#renderInputStep(
119
- "Step 2: Display name",
120
- "Enter a display name:",
121
- this.#state.displayName,
122
- "e.g. My Fast Coder",
123
- );
124
- break;
125
- case "provider":
126
- this.#renderInputStep(
127
- "Step 3: Provider",
128
- "Enter the provider id:",
129
- this.#state.provider,
130
- "e.g. openai-codex, anthropic, my-oai",
131
- );
132
- break;
133
- case "model":
134
- this.#renderInputStep(
135
- "Step 4: Model",
136
- "Enter the model id or provider/model selector:",
137
- this.#state.model,
138
- "e.g. gpt-5.5:medium or my-oai/gpt-example:low",
139
- );
140
- break;
141
- case "confirm":
142
- this.#renderConfirmStep();
143
- break;
144
- }
145
- }
146
-
147
- #renderInputStep(title: string, prompt: string, value: string, hint: string): void {
148
- this.#contentContainer.addChild(new Text(theme.fg("accent", title)));
78
+ this.#contentContainer.addChild(new Text(theme.fg("accent", "Preset id")));
149
79
  this.#contentContainer.addChild(new Spacer(1));
150
80
  if (this.#lastError) {
151
81
  this.#contentContainer.addChild(new Text(theme.fg("error", this.#lastError), 0, 0));
152
82
  this.#contentContainer.addChild(new Spacer(1));
153
83
  }
154
- this.#contentContainer.addChild(new Text(prompt, 0, 0));
84
+ this.#contentContainer.addChild(new Text("Enter a unique preset id:", 0, 0));
155
85
  this.#contentContainer.addChild(new Spacer(1));
156
86
  this.#input = new Input();
157
- this.#input.setValue(value);
87
+ this.#input.setValue(this.#name);
158
88
  this.#contentContainer.addChild(this.#input);
159
89
  this.#contentContainer.addChild(new Spacer(1));
160
- this.#addHelp(hint);
161
- this.#addHelp("[Enter to continue, Esc to go back]");
90
+ this.#addSnapshotPreview();
91
+ this.#addHelp("e.g. my-fast-coder");
92
+ this.#addHelp("[Enter to create, Esc to cancel]");
162
93
  }
163
94
 
164
- #renderConfirmStep(): void {
165
- this.#contentContainer.addChild(new Text(theme.fg("accent", "Confirm custom preset")));
166
- this.#contentContainer.addChild(new Spacer(1));
167
- if (this.#lastError) {
168
- this.#contentContainer.addChild(new Text(theme.fg("error", this.#lastError), 0, 0));
169
- this.#contentContainer.addChild(new Spacer(1));
95
+ #addSnapshotPreview(): void {
96
+ this.#contentContainer.addChild(new Text(theme.fg("muted", "Snapshot:"), 0, 0));
97
+ for (const [role, selector] of Object.entries(this.#snapshot.model_mapping)) {
98
+ this.#contentContainer.addChild(new Text(` ${role}: ${selector}`, 0, 0));
170
99
  }
171
- this.#contentContainer.addChild(new Text(`Preset id: ${this.#state.name}`, 0, 0));
172
- this.#contentContainer.addChild(new Text(`Display name: ${this.#state.displayName}`, 0, 0));
173
- this.#contentContainer.addChild(new Text(`Provider: ${this.#state.provider}`, 0, 0));
174
- this.#contentContainer.addChild(new Text(`Default model: ${this.#selector()}`, 0, 0));
100
+ this.#contentContainer.addChild(new Text(` providers: ${this.#snapshot.required_providers.join(", ")}`, 0, 0));
175
101
  this.#contentContainer.addChild(new Spacer(1));
176
- this.#addOption(0, "Create preset");
177
- this.#addOption(1, "Go back");
178
- this.#addHelp("[↑↓ to navigate, Enter to select, Esc to go back]");
179
- }
180
-
181
- #addOption(index: number, label: string): void {
182
- const selected = index === this.#selectedIndex;
183
- const prefix = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
184
- this.#contentContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
185
102
  }
186
103
 
187
104
  #addHelp(text: string): void {
188
105
  this.#contentContainer.addChild(new Text(theme.fg("muted", text), 0, 0));
189
106
  }
190
107
 
191
- #saveInputAndProceed(): void {
108
+ #saveInputAndSubmit(): void {
192
109
  const value = this.#input?.getValue().trim() ?? "";
193
110
  if (!value) {
194
- this.#lastError = this.#emptyFieldMessage();
111
+ this.#lastError = "Preset id is required.";
195
112
  this.#renderStep();
196
113
  this.#onRender();
197
114
  return;
198
115
  }
199
- const validationError = this.#validateCurrentInput(value);
200
- if (validationError) {
201
- this.#lastError = validationError;
116
+ if (!PROFILE_NAME_PATTERN.test(value)) {
117
+ this.#lastError = "Preset id must use lowercase letters, numbers, dots, underscores, or hyphens.";
202
118
  this.#renderStep();
203
119
  this.#onRender();
204
120
  return;
205
121
  }
122
+ this.#name = value;
206
123
  this.#lastError = null;
207
- if (this.#step === "name") {
208
- this.#state.name = value;
209
- this.#step = "display-name";
210
- } else if (this.#step === "display-name") {
211
- this.#state.displayName = value;
212
- this.#step = "provider";
213
- } else if (this.#step === "provider") {
214
- this.#state.provider = value;
215
- this.#step = "model";
216
- } else if (this.#step === "model") {
217
- this.#state.model = value;
218
- this.#step = "confirm";
219
- this.#selectedIndex = 0;
220
- }
221
- this.#renderStep();
222
- this.#onRender();
223
- }
224
-
225
- #emptyFieldMessage(): string {
226
- switch (this.#step) {
227
- case "name":
228
- return "Preset id is required.";
229
- case "display-name":
230
- return "Display name is required.";
231
- case "provider":
232
- return "Provider is required.";
233
- case "model":
234
- return "Model is required.";
235
- case "confirm":
236
- return "Value is required.";
237
- }
238
- }
239
-
240
- #validateCurrentInput(value: string): string | undefined {
241
- if (this.#step === "name" && !PROFILE_NAME_PATTERN.test(value)) {
242
- return "Preset id must use lowercase letters, numbers, dots, underscores, or hyphens.";
243
- }
244
- if (this.#step === "provider" && !PROFILE_NAME_PATTERN.test(value)) {
245
- return "Provider id must use lowercase letters, numbers, dots, underscores, or hyphens.";
246
- }
247
- if (this.#step === "model" && value.includes(",")) {
248
- return "Model must be one selector, not a comma-separated list.";
249
- }
250
- return undefined;
251
- }
252
-
253
- #selectCurrentOption(): void {
254
- if (this.#step !== "confirm") return;
255
- if (this.#selectedIndex === 0) {
256
- this.#onSubmit(this.#buildInput());
257
- return;
258
- }
259
- this.#goBack();
260
- }
261
-
262
- #buildInput(): CustomModelPresetWizardSubmit {
263
- return {
264
- name: this.#state.name,
124
+ this.#onSubmit({
125
+ name: value,
265
126
  profile: {
266
- required_providers: [this.#state.provider],
267
- display_name: this.#state.displayName,
268
- model_mapping: { default: this.#selector() },
127
+ ...this.#snapshot,
128
+ display_name: value,
129
+ model_mapping: { ...this.#snapshot.model_mapping },
130
+ required_providers: [...this.#snapshot.required_providers],
269
131
  },
270
- };
271
- }
272
-
273
- #selector(): string {
274
- return this.#state.model.includes("/") ? this.#state.model : `${this.#state.provider}/${this.#state.model}`;
275
- }
276
-
277
- #moveSelection(delta: number): void {
278
- this.#selectedIndex = (this.#selectedIndex + delta + 2) % 2;
279
- this.#renderStep();
280
- this.#onRender();
281
- }
282
-
283
- #goBack(): void {
284
- if (this.#step === "display-name") this.#step = "name";
285
- else if (this.#step === "provider") this.#step = "display-name";
286
- else if (this.#step === "model") this.#step = "provider";
287
- else if (this.#step === "confirm") this.#step = "model";
288
- this.#selectedIndex = 0;
289
- this.#lastError = null;
290
- this.#renderStep();
291
- this.#onRender();
132
+ });
292
133
  }
293
134
  }
@@ -11,20 +11,12 @@
11
11
  * - Space: Toggle selected item (or master switch)
12
12
  * - Esc: Close dashboard (clears search first if active)
13
13
  */
14
- import {
15
- type Component,
16
- Container,
17
- matchesKey,
18
- padding,
19
- Spacer,
20
- Text,
21
- truncateToWidth,
22
- visibleWidth,
23
- } from "@sayknow-cli/tui";
14
+ import { Container, matchesKey, Spacer, Text } from "@sayknow-cli/tui";
24
15
  import { Settings } from "../../../config/settings";
25
16
  import { DynamicBorder } from "../../../modes/components/dynamic-border";
26
17
  import { theme } from "../../../modes/theme/theme";
27
18
  import { matchesAppInterrupt } from "../../../modes/utils/keybinding-matchers";
19
+ import { TwoColumnBody } from "../two-column-body";
28
20
  import { ExtensionList } from "./extension-list";
29
21
  import { InspectorPanel } from "./inspector-panel";
30
22
  import {
@@ -310,41 +302,3 @@ export class ExtensionDashboard extends Container {
310
302
  }
311
303
  }
312
304
  }
313
-
314
- /**
315
- * Two-column body component for side-by-side rendering.
316
- */
317
- class TwoColumnBody implements Component {
318
- constructor(
319
- private readonly leftPane: ExtensionList,
320
- private readonly rightPane: InspectorPanel,
321
- private readonly maxHeight: number,
322
- ) {}
323
-
324
- render(width: number): string[] {
325
- const leftWidth = Math.floor(width * 0.5);
326
- const rightWidth = Math.max(0, width - leftWidth - 3);
327
-
328
- const leftLines = this.leftPane.render(leftWidth);
329
- const rightLines = this.rightPane.render(rightWidth);
330
-
331
- // Limit to maxHeight lines
332
- const numLines = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
333
- const combined: string[] = [];
334
- const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
335
-
336
- for (let i = 0; i < numLines; i++) {
337
- const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
338
- const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
339
- const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
340
- combined.push(leftPadded + separator + right);
341
- }
342
-
343
- return combined;
344
- }
345
-
346
- invalidate(): void {
347
- this.leftPane.invalidate?.();
348
- this.rightPane.invalidate?.();
349
- }
350
- }
@@ -59,6 +59,11 @@ export interface HookSelectorOptions {
59
59
  optionLabel: string;
60
60
  onSubmit: (text: string) => void;
61
61
  };
62
+ clarificationInput?: {
63
+ optionLabel: string;
64
+ onSubmit: (text: string) => void;
65
+ allowEmpty?: boolean;
66
+ };
62
67
  /**
63
68
  * Autocomplete provider for the inline custom-input editor. When present,
64
69
  * the "Other (type your own)" editor gains the same `@` file-link and `/`
@@ -339,6 +344,8 @@ export class HookSelectorComponent extends Container {
339
344
  #outline: boolean;
340
345
  #scrollTitleRows: number | undefined;
341
346
  #customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
347
+ #clarificationInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
348
+ #activeInput: { onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
342
349
  #inputArea: Container;
343
350
  #inlineEditor: Editor | undefined;
344
351
  #helpTextComponent: Text;
@@ -366,6 +373,7 @@ export class HookSelectorComponent extends Container {
366
373
  this.#wrapFocused = opts?.wrapFocused === true;
367
374
  this.#outline = opts?.outline === true;
368
375
  this.#customInput = opts?.customInput;
376
+ this.#clarificationInput = opts?.clarificationInput;
369
377
  this.#tui = opts?.tui;
370
378
  this.#autocompleteProvider = opts?.autocompleteProvider;
371
379
 
@@ -501,7 +509,11 @@ export class HookSelectorComponent extends Container {
501
509
  const selected = this.#options[this.#selectedIndex];
502
510
  if (!selected) return;
503
511
  if (this.#customInput && selected === this.#customInput.optionLabel) {
504
- this.#enterInputMode();
512
+ this.#enterInputMode(this.#customInput);
513
+ return;
514
+ }
515
+ if (this.#clarificationInput && selected === this.#clarificationInput.optionLabel) {
516
+ this.#enterInputMode(this.#clarificationInput);
505
517
  return;
506
518
  }
507
519
  this.#onSelectCallback(selected);
@@ -536,14 +548,19 @@ export class HookSelectorComponent extends Container {
536
548
  return;
537
549
  }
538
550
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
539
- this.#customInput?.onSubmit(editor.getExpandedText());
551
+ const text = editor.getExpandedText();
552
+ if (this.#activeInput?.allowEmpty === false && text.trim() === "") {
553
+ return;
554
+ }
555
+ this.#activeInput?.onSubmit(text);
540
556
  return;
541
557
  }
542
558
  editor.handleInput(keyData);
543
559
  }
544
560
 
545
- #enterInputMode(): void {
561
+ #enterInputMode(input: { onSubmit: (text: string) => void; allowEmpty?: boolean }): void {
546
562
  if (this.#inlineEditor) return;
563
+ this.#activeInput = input;
547
564
  // Stop the auto-select countdown for good: the user is actively typing,
548
565
  // matching the old behavior where the separate editor had no timeout.
549
566
  if (this.#countdown) {
@@ -578,6 +595,7 @@ export class HookSelectorComponent extends Container {
578
595
  #exitInputMode(): void {
579
596
  if (!this.#inlineEditor) return;
580
597
  this.#inlineEditor = undefined;
598
+ this.#activeInput = undefined;
581
599
  this.#inputArea.clear();
582
600
  this.#helpTextComponent.setText(theme.fg("dim", this.#baseHelpText));
583
601
  this.invalidate();