@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
@@ -15,6 +15,12 @@ export interface LspServerInfo {
15
15
  }
16
16
 
17
17
  export type WelcomeLogoMode = "unicode" | "square" | "ascii";
18
+ export interface WelcomeComponentOptions {
19
+ getViewportRows?: () => number | undefined;
20
+ getReservedBottomRows?: (termWidth: number) => number;
21
+ changelogMarkdown?: string;
22
+ collapseChangelog?: boolean;
23
+ }
18
24
 
19
25
  /**
20
26
  * Sayknow-CLI launch surface: a blue-gradient SAYKNOW wordmark, compact
@@ -32,6 +38,7 @@ export class WelcomeComponent implements Component {
32
38
  private recentSessions: RecentSession[] = [],
33
39
  private lspServers: LspServerInfo[] = [],
34
40
  private readonly logoMode: WelcomeLogoMode = "unicode",
41
+ private readonly options: WelcomeComponentOptions = {},
35
42
  ) {}
36
43
 
37
44
  invalidate(): void {}
@@ -52,6 +59,11 @@ export class WelcomeComponent implements Component {
52
59
  }
53
60
  requestRender();
54
61
  }, INTRO_TICK_MS);
62
+ this.#animTimer.unref?.();
63
+ }
64
+
65
+ dispose(): void {
66
+ this.#stopAnimation();
55
67
  }
56
68
 
57
69
  #stopAnimation(): void {
@@ -76,15 +88,20 @@ export class WelcomeComponent implements Component {
76
88
  }
77
89
 
78
90
  render(termWidth: number): string[] {
79
- // Box dimensions track the live viewport so wide terminals feel intentionally full-screen.
80
- const boxWidth = Math.max(0, termWidth - 2);
91
+ const boxWidth = Math.max(0, termWidth);
81
92
  if (boxWidth < 4) {
82
93
  return [];
83
94
  }
95
+
96
+ const targetRows = this.#targetRows(termWidth);
97
+ if (targetRows !== undefined && targetRows <= 0) {
98
+ return [];
99
+ }
100
+ const targetContentRows = targetRows === undefined ? undefined : Math.max(0, targetRows - 2);
84
101
  const dualContentWidth = boxWidth - 3; // 3 = │ + │ + │
85
- const preferredLeftCol = 36;
86
- const minLeftCol = 18; // wordmark plus Sayknow identity labels
87
- const minRightCol = 20;
102
+ const preferredLeftCol = 40;
103
+ const minLeftCol = 20; // wordmark plus Sayknow identity labels
104
+ const minRightCol = 24;
88
105
  const modelPill = this.#pill(theme.icon.model || "model", this.modelName, "statusLineModel");
89
106
  const providerPill = this.#pill(theme.icon.package || "provider", this.providerName, "statusLinePath");
90
107
  const logoLines = this.#logoLines();
@@ -130,24 +147,23 @@ export class WelcomeComponent implements Component {
130
147
  ...identityTail,
131
148
  ];
132
149
 
133
- // Right column separator
134
- const separatorWidth = Math.max(0, rightCol - 2); // padding on each side
135
- const separator = ` ${theme.fg("dim", theme.boxRound.horizontal.repeat(separatorWidth))}`;
150
+ const buildSeparator = (columnWidth: number): string =>
151
+ ` ${theme.fg("dim", theme.boxRound.horizontal.repeat(Math.max(0, columnWidth - 2)))}`;
152
+
153
+ const rightColumnWidth = showRightColumn ? rightCol : leftCol;
154
+ const separator = buildSeparator(rightColumnWidth);
155
+ const changelogBudget = Math.max(1, Math.min(6, Math.floor((targetContentRows ?? 18) * 0.3)));
136
156
 
137
- // Recent sessions content
138
157
  const sessionLines: string[] = [];
139
158
  if (this.recentSessions.length === 0) {
140
159
  sessionLines.push(` ${theme.fg("dim", t("welcome.noSessions"))}`);
141
160
  } else {
142
- // Reserve width for the bullet prefix (" • ") and the trailing " (timeAgo)"
143
- // so the relative time is never the part that gets truncated. The name
144
- // absorbs whatever space is left.
145
161
  const bulletPrefix = ` ${theme.md.bullet} `;
146
162
  const prefixWidth = visibleWidth(bulletPrefix);
147
163
  for (const session of this.recentSessions.slice(0, 3)) {
148
164
  const timeSuffixRaw = ` (${session.timeAgo})`;
149
165
  const timeWidth = visibleWidth(timeSuffixRaw);
150
- const nameBudget = Math.max(1, rightCol - prefixWidth - timeWidth);
166
+ const nameBudget = Math.max(1, rightColumnWidth - prefixWidth - timeWidth);
151
167
  const nameVis = visibleWidth(session.name);
152
168
  const name = nameVis > nameBudget ? truncateToWidth(session.name, nameBudget) : session.name;
153
169
  sessionLines.push(
@@ -156,12 +172,11 @@ export class WelcomeComponent implements Component {
156
172
  }
157
173
  }
158
174
 
159
- // LSP servers content
160
175
  const lspLines: string[] = [];
161
176
  if (this.lspServers.length === 0) {
162
177
  lspLines.push(` ${theme.fg("dim", t("welcome.noLsp"))}`);
163
178
  } else {
164
- for (const server of this.lspServers) {
179
+ for (const server of this.lspServers.slice(0, 4)) {
165
180
  const icon =
166
181
  server.status === "ready"
167
182
  ? theme.styledSymbol("status.success", "success")
@@ -179,8 +194,11 @@ export class WelcomeComponent implements Component {
179
194
  return ` ${theme.fg("accent", cmd)}${pad}${theme.fg("muted", desc)}`;
180
195
  };
181
196
 
182
- // Right column
183
197
  const rightLines = [
198
+ "",
199
+ ` ${theme.bold(theme.fg("accent", "What's New"))}`,
200
+ ...this.#whatsNewLines(rightColumnWidth, changelogBudget),
201
+ separator,
184
202
  ` ${theme.bold(theme.fg("accent", t("welcome.workflows")))}`,
185
203
  wf("/deep-interview", t("welcome.wf.deepInterview")),
186
204
  wf("/ralplan", t("welcome.wf.ralplan")),
@@ -210,7 +228,12 @@ export class WelcomeComponent implements Component {
210
228
  "",
211
229
  ];
212
230
 
213
- // Border characters (dim)
231
+ const contentRows =
232
+ targetContentRows ??
233
+ (showRightColumn ? Math.max(leftLines.length, rightLines.length) : leftLines.length + rightLines.length);
234
+ const outputRows = targetRows === undefined ? Math.max(3, contentRows + 2) : targetRows;
235
+ const bodyRows = Math.max(0, outputRows - 2);
236
+
214
237
  const hChar = theme.boxRound.horizontal;
215
238
  const h = theme.fg("dim", hChar);
216
239
  const v = theme.fg("dim", theme.boxRound.vertical);
@@ -220,8 +243,6 @@ export class WelcomeComponent implements Component {
220
243
  const br = theme.fg("dim", theme.boxRound.bottomRight);
221
244
 
222
245
  const lines: string[] = [];
223
-
224
- // Top border with embedded title
225
246
  const title = ` ${APP_NAME} · Sayknow-CLI v${this.version} `;
226
247
  const titlePrefixRaw = hChar.repeat(3);
227
248
  const titleStyled = theme.fg("dim", titlePrefixRaw) + theme.fg("muted", title);
@@ -233,22 +254,32 @@ export class WelcomeComponent implements Component {
233
254
  const afterTitle = titleSpace - titleVisLen;
234
255
  lines.push(tl + titleStyled + theme.fg("dim", hChar.repeat(afterTitle)) + tr);
235
256
  }
257
+ if (outputRows === 1) {
258
+ return lines;
259
+ }
236
260
 
237
- // Content rows
238
- const maxRows = showRightColumn ? Math.max(leftLines.length, rightLines.length) : leftLines.length;
239
- for (let i = 0; i < maxRows; i++) {
240
- const left = this.#fitToWidth(leftLines[i] ?? "", leftCol);
241
- if (showRightColumn) {
242
- const right = this.#fitToWidth(rightLines[i] ?? "", rightCol);
261
+ if (showRightColumn) {
262
+ const leftBlock = this.#fitBlock(leftLines, bodyRows, "center");
263
+ const rightBlock = this.#fitBlock(rightLines, bodyRows, "top");
264
+ for (let i = 0; i < bodyRows; i++) {
265
+ const left = this.#fitToWidth(leftBlock[i] ?? "", leftCol);
266
+ const right = this.#fitToWidth(rightBlock[i] ?? "", rightCol);
243
267
  lines.push(v + left + v + right + v);
244
- } else {
245
- lines.push(v + left + v);
246
268
  }
247
- }
248
- // Bottom border
249
- if (showRightColumn) {
250
269
  lines.push(bl + h.repeat(leftCol) + theme.fg("dim", theme.boxSharp.teeUp) + h.repeat(rightCol) + br);
251
270
  } else {
271
+ const compactLeftLines =
272
+ bodyRows < 14
273
+ ? [
274
+ this.#centerText(theme.bold(theme.fg("accent", "SKC Forge")), leftCol),
275
+ this.#centerText(this.#loadingLine(), leftCol),
276
+ this.#centerText(modelPill, leftCol),
277
+ ]
278
+ : leftLines;
279
+ const singleBlock = this.#fitBlock([...compactLeftLines, separator, ...rightLines], bodyRows, "top");
280
+ for (let i = 0; i < bodyRows; i++) {
281
+ lines.push(v + this.#fitToWidth(singleBlock[i] ?? "", leftCol) + v);
282
+ }
252
283
  lines.push(bl + h.repeat(leftCol) + br);
253
284
  }
254
285
 
@@ -274,6 +305,110 @@ export class WelcomeComponent implements Component {
274
305
  }
275
306
  return str + padding(width - visLen);
276
307
  }
308
+ #targetRows(termWidth: number): number | undefined {
309
+ const viewportRows = this.options.getViewportRows?.();
310
+ if (typeof viewportRows !== "number" || !Number.isFinite(viewportRows) || viewportRows <= 0) {
311
+ return undefined;
312
+ }
313
+ const reservedRows = Math.max(0, Math.floor(this.options.getReservedBottomRows?.(termWidth) ?? 0));
314
+ return Math.max(0, Math.floor(viewportRows) - reservedRows);
315
+ }
316
+
317
+ #loadingLine(): string {
318
+ const frames = theme.spinnerFrames;
319
+ const elapsed = this.#animStart == null ? 0 : performance.now() - this.#animStart;
320
+ const frame = frames.length > 0 ? (frames[Math.floor(elapsed / 100) % frames.length] ?? "*") : "*";
321
+ const label = this.#animStart == null ? "ready" : "warming workspace";
322
+ return `${theme.fg("warning", frame)} ${theme.fg("muted", label)}`;
323
+ }
324
+
325
+ #fitBlock(lines: string[], rows: number, align: "top" | "center"): string[] {
326
+ if (rows <= 0) return [];
327
+ const clipped =
328
+ lines.length > rows
329
+ ? rows === 1
330
+ ? [theme.fg("dim", " …")]
331
+ : [...lines.slice(0, rows - 1), theme.fg("dim", " …")]
332
+ : lines;
333
+ const missingRows = rows - clipped.length;
334
+ if (missingRows <= 0) return clipped;
335
+ const topPad = align === "center" ? Math.floor(missingRows / 2) : 0;
336
+ const bottomPad = missingRows - topPad;
337
+ return [...Array.from({ length: topPad }, () => ""), ...clipped, ...Array.from({ length: bottomPad }, () => "")];
338
+ }
339
+
340
+ #whatsNewLines(width: number, maxItems: number): string[] {
341
+ const changelog = this.options.changelogMarkdown?.trim();
342
+ if (!changelog) {
343
+ return [` ${theme.fg("dim", "Ready for your next prompt")}`];
344
+ }
345
+
346
+ const version = this.#latestChangelogVersion(changelog);
347
+ if (this.options.collapseChangelog) {
348
+ return [
349
+ ` ${theme.fg("muted", `Updated to v${version}`)}`,
350
+ ` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
351
+ ];
352
+ }
353
+
354
+ const itemLimit = Math.max(1, Math.floor(maxItems));
355
+ const items = this.#changelogItems(changelog);
356
+ if (items.length === 0) {
357
+ return [
358
+ ` ${theme.fg("muted", `Updated to v${version}`)}`,
359
+ ` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
360
+ ];
361
+ }
362
+
363
+ const prefix = ` ${theme.md.bullet} `;
364
+ const textWidth = Math.max(1, width - visibleWidth(prefix));
365
+ const visibleItems = items.slice(0, itemLimit).map(item => {
366
+ const text = visibleWidth(item) > textWidth ? truncateToWidth(item, textWidth) : item;
367
+ return `${theme.fg("dim", prefix)}${theme.fg("muted", text)}`;
368
+ });
369
+ if (items.length > visibleItems.length) {
370
+ visibleItems.push(` ${theme.fg("dim", `… ${theme.bold("/changelog")} for full notes`)}`);
371
+ }
372
+ return visibleItems;
373
+ }
374
+
375
+ #latestChangelogVersion(markdown: string): string {
376
+ const versionMatch = markdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
377
+ return versionMatch?.[1] ?? this.version;
378
+ }
379
+
380
+ #changelogItems(markdown: string): string[] {
381
+ const items: string[] = [];
382
+ let inFence = false;
383
+ for (const rawLine of markdown.split(/\r?\n/)) {
384
+ const line = rawLine.trim();
385
+ if (line.startsWith("```")) {
386
+ inFence = !inFence;
387
+ continue;
388
+ }
389
+ if (inFence || !line || /^#{1,6}\s+/.test(line) || /^-{3,}$/.test(line)) {
390
+ continue;
391
+ }
392
+ const withoutBullet = line
393
+ .replace(/^[-*]\s+/, "")
394
+ .replace(/^\d+\.\s+/, "")
395
+ .replace(/^>\s*/, "");
396
+ const cleaned = this.#stripMarkdown(withoutBullet);
397
+ if (cleaned) items.push(cleaned);
398
+ }
399
+ return items;
400
+ }
401
+
402
+ #stripMarkdown(text: string): string {
403
+ return text
404
+ .replace(/`([^`]+)`/g, "$1")
405
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
406
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
407
+ .replace(/__([^_]+)__/g, "$1")
408
+ .replace(/\*([^*]+)\*/g, "$1")
409
+ .replace(/[_~]/g, "")
410
+ .trim();
411
+ }
277
412
 
278
413
  #pill(icon: string, text: string, color: ThemeColor): string {
279
414
  return `${theme.fg("borderMuted", "[")} ${theme.fg(color, icon)} ${theme.fg("muted", text)} ${theme.fg(
@@ -556,6 +556,29 @@ export class CommandController {
556
556
  showMarkdownPanel(this.ctx, "Keyboard Shortcuts", hotkeys);
557
557
  }
558
558
 
559
+ handleHelpCommand(): void {
560
+ const sessionNewKey = this.ctx.keybindings.getDisplayString("app.session.new") || "/new";
561
+ const markdown = [
562
+ "**Beginner actions**",
563
+ "",
564
+ "| Action | How |",
565
+ "|---|---|",
566
+ `| Start a fresh session | \`${sessionNewKey}\` or \`/new\` |`,
567
+ "| Resume another session | `/resume` |",
568
+ "| Show session details | `/session info` |",
569
+ "| Delete current session | `/session delete` |",
570
+ "| Select a model | `/model` or `Ctrl+L` |",
571
+ "| Show all shortcuts | `?` on an empty prompt or `/hotkeys` |",
572
+ "",
573
+ "**Finding commands**",
574
+ "",
575
+ "- Type `/` to browse slash commands, then use arrows and Enter/Tab.",
576
+ "- Type `#` to browse prompt actions like starting a session, pasting an image, or moving the cursor.",
577
+ "- Type `!` for shell commands and `$` for Python snippets.",
578
+ ].join("\n");
579
+ showMarkdownPanel(this.ctx, "SKC Help", markdown);
580
+ }
581
+
559
582
  handleToolsCommand(): void {
560
583
  const tools = buildToolsMarkdown({ tools: this.ctx.session.agent.state.tools });
561
584
  showMarkdownPanel(this.ctx, "Available Tools", tools);
@@ -1253,17 +1276,18 @@ export class CommandController {
1253
1276
  async handleContributionPrepCommand(customInstructions?: string): Promise<void> {
1254
1277
  this.ctx.editor.setText("");
1255
1278
  try {
1256
- const result = await this.ctx.session.prepareContributionPrep({ customInstructions, spawnWorker: true });
1279
+ const result = await this.ctx.session.prepareContributionPrep({ customInstructions, spawnWorker: false });
1257
1280
  this.ctx.showStatus(
1258
1281
  [
1259
1282
  "Contribution prep artifacts written.",
1260
1283
  `Manifest: ${result.manifestPath}`,
1261
1284
  `Worker prompt: ${result.workerPromptPath}`,
1285
+ "Open the worker prompt from a separate terminal to keep this TUI session stable.",
1262
1286
  ].join("\n"),
1263
1287
  );
1264
1288
  this.ctx.chatContainer.addChild(
1265
1289
  new Text(
1266
- `${theme.fg("accent", `${theme.status.success} Contribution prep ready`)}\nManifest: ${result.manifestPath}`,
1290
+ `${theme.fg("accent", `${theme.status.success} Contribution prep ready`)}\nWorker prompt: ${result.workerPromptPath}`,
1267
1291
  1,
1268
1292
  1,
1269
1293
  ),
@@ -3,6 +3,7 @@ import { calculatePromptTokens } from "@sayknow-cli/agent-core/compaction/compac
3
3
  import type { AssistantMessage, ImageContent } from "@sayknow-cli/ai";
4
4
  import { parseRateLimitReason } from "@sayknow-cli/ai";
5
5
  import { type Component, Loader, TERMINAL, Text } from "@sayknow-cli/tui";
6
+ import { logger } from "@sayknow-cli/utils";
6
7
  import { settings } from "../../config/settings";
7
8
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
8
9
  import {
@@ -15,16 +16,56 @@ import { ToolExecutionComponent } from "../../modes/components/tool-execution";
15
16
  import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
16
17
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
17
18
  import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
19
+ import { summaryFromMessage } from "../../notifications/helpers";
18
20
  import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
19
21
  import type { AgentSessionEvent } from "../../session/agent-session";
20
22
  import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
21
23
  import type { ResolveToolDetails } from "../../tools/resolve";
22
24
  import { interruptHint } from "../shared";
23
25
  import { buildAbortDisplayMessage } from "../utils/abort-message";
26
+ import { ringTerminalBell } from "../utils/terminal-bell";
24
27
 
25
28
  type AgentSessionEventKind = AgentSessionEvent["type"];
26
29
 
27
30
  const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
31
+ const COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS = 10_000;
32
+
33
+ interface CompletionNotifyPayload {
34
+ type: "agent-turn-complete";
35
+ title: string;
36
+ body: string;
37
+ cwd: string;
38
+ sessionId?: string;
39
+ sessionName?: string;
40
+ lastAssistantMessage?: string;
41
+ stopReason?: string;
42
+ }
43
+
44
+ function completionNotifyShellCommand(command: string): string[] {
45
+ if (process.platform === "win32") {
46
+ return ["cmd.exe", "/d", "/s", "/c", command];
47
+ }
48
+ return ["/bin/sh", "-c", command];
49
+ }
50
+
51
+ function cleanNotificationEnvValue(value: string | undefined, max = 4000): string {
52
+ if (!value) return "";
53
+ return value.replaceAll("\0", "").slice(0, max);
54
+ }
55
+
56
+ function buildCompletionNotifyEnv(payload: CompletionNotifyPayload): Record<string, string> {
57
+ return {
58
+ SKC_NOTIFICATION_TYPE: payload.type,
59
+ SKC_NOTIFICATION_TITLE: cleanNotificationEnvValue(payload.title, 500),
60
+ SKC_NOTIFICATION_BODY: cleanNotificationEnvValue(payload.body),
61
+ SKC_NOTIFICATION_CWD: cleanNotificationEnvValue(payload.cwd, 2000),
62
+ SKC_NOTIFICATION_SESSION_ID: cleanNotificationEnvValue(payload.sessionId, 500),
63
+ SKC_NOTIFICATION_SESSION_NAME: cleanNotificationEnvValue(payload.sessionName, 500),
64
+ SKC_NOTIFICATION_LAST_ASSISTANT_MESSAGE: cleanNotificationEnvValue(payload.lastAssistantMessage),
65
+ SKC_NOTIFICATION_STOP_REASON: cleanNotificationEnvValue(payload.stopReason, 100),
66
+ SKC_NOTIFICATION_JSON: cleanNotificationEnvValue(JSON.stringify(payload), 8000),
67
+ };
68
+ }
28
69
 
29
70
  function friendlyRetryReason(errorMessage: string | undefined): string {
30
71
  if (!errorMessage) return "";
@@ -97,6 +138,7 @@ export class EventController {
97
138
  this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
98
139
  this.ctx.retryEscapeHandler = undefined;
99
140
  }
141
+ this.ctx.retryEscapePrimed = false;
100
142
  if (this.ctx.retryLoader) {
101
143
  this.ctx.retryLoader.stop();
102
144
  this.ctx.retryLoader = undefined;
@@ -200,6 +242,7 @@ export class EventController {
200
242
  this.ctx.retryLoader = undefined;
201
243
  this.ctx.statusContainer.clear();
202
244
  }
245
+ this.ctx.retryEscapePrimed = false;
203
246
  this.#cancelIdleCompaction();
204
247
  this.ctx.updateEditorBorderColor();
205
248
  this.ctx.ensureLoadingAnimation();
@@ -727,6 +770,7 @@ export class EventController {
727
770
  if (!this.ctx.retryEscapeHandler) {
728
771
  this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
729
772
  }
773
+ this.ctx.retryEscapePrimed = false;
730
774
  let escPressed = false;
731
775
  this.ctx.editor.onEscape = () => {
732
776
  if (!escPressed) {
@@ -777,6 +821,7 @@ export class EventController {
777
821
  this.ctx.retryLoader = undefined;
778
822
  this.ctx.statusContainer.clear();
779
823
  }
824
+ this.ctx.retryEscapePrimed = false;
780
825
  if (!event.success) {
781
826
  this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
782
827
  }
@@ -857,8 +902,46 @@ export class EventController {
857
902
  return lastAssistant?.usage ? calculatePromptTokens(lastAssistant.usage) : 0;
858
903
  }
859
904
 
905
+ #runCompletionNotifyCommand(payload: CompletionNotifyPayload): void {
906
+ const command = settings.getGlobal("completion.notifyCommand")?.trim();
907
+ if (!command) return;
908
+
909
+ try {
910
+ const proc = Bun.spawn(completionNotifyShellCommand(command), {
911
+ cwd: payload.cwd,
912
+ env: {
913
+ ...process.env,
914
+ ...buildCompletionNotifyEnv(payload),
915
+ },
916
+ stdin: "ignore",
917
+ stdout: "ignore",
918
+ stderr: "ignore",
919
+ });
920
+ proc.unref();
921
+ const timer = setTimeout(() => {
922
+ try {
923
+ proc.kill();
924
+ } catch {}
925
+ }, COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS);
926
+ timer.unref?.();
927
+ void proc.exited
928
+ .then(exitCode => {
929
+ clearTimeout(timer);
930
+ if (exitCode !== 0) {
931
+ logger.warn("completion notify command exited non-zero", { exitCode });
932
+ }
933
+ })
934
+ .catch(error => {
935
+ clearTimeout(timer);
936
+ logger.warn("completion notify command failed", { error: String(error) });
937
+ });
938
+ } catch (error) {
939
+ logger.warn("completion notify command failed to start", { error: String(error) });
940
+ }
941
+ }
942
+
860
943
  sendCompletionNotification(): void {
861
- if (this.ctx.isBackgrounded === false) return;
944
+ const isBackgrounded = this.ctx.isBackgrounded !== false;
862
945
  const notify = settings.get("completion.notify");
863
946
  if (notify === "off") return;
864
947
 
@@ -869,9 +952,24 @@ export class EventController {
869
952
  const last = this.ctx.session.getLastAssistantMessage?.();
870
953
  if (last?.stopReason === "aborted" || last?.stopReason === "error") return;
871
954
 
872
- const title = this.ctx.sessionManager.getSessionName();
873
- const message = title ? `${title}: Complete` : "Complete";
874
- TERMINAL.sendNotification(message);
955
+ const sessionName = this.ctx.sessionManager.getSessionName();
956
+ const title = sessionName ? `${sessionName}: Complete` : "Complete";
957
+ const summary = summaryFromMessage(last, 1000);
958
+ const body = summary ?? "Complete";
959
+ const sessionManager = this.ctx.sessionManager as { getCwd?: () => string; getSessionId?: () => string };
960
+ const payload: CompletionNotifyPayload = {
961
+ type: "agent-turn-complete",
962
+ title,
963
+ body,
964
+ cwd: sessionManager.getCwd?.() ?? settings.getCwd(),
965
+ sessionId: sessionManager.getSessionId?.(),
966
+ sessionName: sessionName || undefined,
967
+ lastAssistantMessage: summary,
968
+ stopReason: last?.stopReason,
969
+ };
970
+ ringTerminalBell("complete");
971
+ if (isBackgrounded) TERMINAL.sendNotification(title);
972
+ this.#runCompletionNotifyCommand(payload);
875
973
  }
876
974
 
877
975
  async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
@@ -24,6 +24,7 @@ import { HookSelectorComponent } from "../../modes/components/hook-selector";
24
24
  import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
25
25
  import type { InteractiveModeContext } from "../../modes/types";
26
26
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
27
+ import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
27
28
 
28
29
  const MAX_WIDGET_LINES = 10;
29
30
  const HOOK_SELECTOR_CHROME_ROWS = 7;
@@ -617,12 +618,15 @@ export class ExtensionUiController {
617
618
  const listChromeRows = dialogOptions?.outline === true ? HOOK_SELECTOR_OUTLINE_ROWS : 0;
618
619
  // Reserve rows for the inline custom-input editor so opening it doesn't
619
620
  // push the scrollable title past the viewport into terminal scrollback.
620
- const inlineInputRows = dialogOptions?.customInput ? HOOK_SELECTOR_INLINE_INPUT_ROWS : 0;
621
+ const inlineInputRows =
622
+ dialogOptions?.customInput || dialogOptions?.clarificationInput ? HOOK_SELECTOR_INLINE_INPUT_ROWS : 0;
621
623
  const availableTitleRows =
622
624
  this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - inlineInputRows - HOOK_SELECTOR_CHROME_ROWS;
623
625
  const scrollTitleRows =
624
626
  requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
625
627
 
628
+ ringTerminalBell(classifyHookSelectorBellEvent(title));
629
+
626
630
  this.ctx.hookSelector = new HookSelectorComponent(
627
631
  title,
628
632
  options,
@@ -657,7 +661,10 @@ export class ExtensionUiController {
657
661
  tui: this.ctx.ui,
658
662
  // Share the main prompt editor's autocomplete provider so the
659
663
  // inline "Other (type your own)" editor supports `@` file links.
660
- autocompleteProvider: dialogOptions?.customInput ? this.ctx.editor.getAutocompleteProvider() : undefined,
664
+ autocompleteProvider:
665
+ dialogOptions?.customInput || dialogOptions?.clarificationInput
666
+ ? this.ctx.editor.getAutocompleteProvider()
667
+ : undefined,
661
668
  outline: dialogOptions?.outline,
662
669
  wrapFocused: dialogOptions?.wrapFocused,
663
670
  scrollTitleRows,
@@ -673,6 +680,18 @@ export class ExtensionUiController {
673
680
  },
674
681
  }
675
682
  : undefined,
683
+ clarificationInput: dialogOptions?.clarificationInput
684
+ ? {
685
+ optionLabel: dialogOptions.clarificationInput.optionLabel,
686
+ allowEmpty: dialogOptions.clarificationInput.allowEmpty,
687
+ onSubmit: text => {
688
+ const optionLabel = dialogOptions.clarificationInput?.optionLabel;
689
+ this.hideHookSelector();
690
+ dialogOptions.clarificationInput?.onSubmit(text);
691
+ finish(optionLabel);
692
+ },
693
+ }
694
+ : undefined,
676
695
  },
677
696
  );
678
697
  // Detach (not dispose) the reusable editor before mounting the transient hook UI, so the