@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
@@ -28,7 +28,9 @@ import { getTabBarTheme } from "../shared";
28
28
  import { DynamicBorder } from "./dynamic-border";
29
29
  import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
30
30
  import { getSettingsForTab, type SettingDef } from "./settings-defs";
31
+ import type { StatusLineSegmentOptions } from "./status-line";
31
32
  import { getPreset } from "./status-line/presets";
33
+ import { ALL_SEGMENT_IDS } from "./status-line/segments";
32
34
 
33
35
  // Settings UI strings are translated by deterministic keys derived from the
34
36
  // setting path; English (the schema value) is the fallback. Keys match those
@@ -49,7 +51,6 @@ function tSettingOptions<T extends { value: string; label?: string; description?
49
51
  description: o.description ? td(`set:${path}:o:${o.value}:d`, o.description) : o.description,
50
52
  }));
51
53
  }
52
-
53
54
  /**
54
55
  * A submenu component for selecting from a list of options.
55
56
  */
@@ -79,8 +80,6 @@ class TextInputSubmenu extends Container {
79
80
  this.#input = new Input();
80
81
  if (currentValue) {
81
82
  this.#input.setValue(currentValue);
82
- // Move cursor to end of pre-filled value (ctrl+e = cursorLineEnd).
83
- this.#input.handleInput("\x05");
84
83
  }
85
84
  this.#input.onSubmit = value => {
86
85
  this.onSubmit(value); // empty string clears the setting
@@ -182,6 +181,471 @@ class SelectSubmenu extends Container {
182
181
  this.#selectList.handleInput(data);
183
182
  }
184
183
  }
184
+ const STATUS_LINE_CUSTOM_EDITOR_ID = "statusLine.customEditor";
185
+ const STATUS_LINE_USAGE_MODE_ID = "statusLine.usageMode";
186
+ const PUBLIC_STATUS_SEGMENTS = ALL_SEGMENT_IDS.filter(id => id !== "pi");
187
+
188
+ type StatusLineDraft = Required<
189
+ Pick<StatusLinePreviewSettings, "preset" | "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
190
+ >;
191
+
192
+ const BOOL_VALUES = ["true", "false"];
193
+ const PATH_LENGTH_OPTIONS: SelectItem[] = [16, 24, 32, 40, 50, 60, 80].map(value => ({
194
+ value: String(value),
195
+ label: String(value),
196
+ }));
197
+ const TIME_FORMAT_OPTIONS: SelectItem[] = [
198
+ { value: "24h", label: "24h" },
199
+ { value: "12h", label: "12h" },
200
+ ];
201
+ const USAGE_MODE_VALUES = ["used", "remaining"] as const;
202
+ type UsageMode = (typeof USAGE_MODE_VALUES)[number];
203
+
204
+ function cloneSegmentOptions(options: StatusLineSegmentOptions | undefined): StatusLineSegmentOptions {
205
+ return mergeSegmentOptions(undefined, options);
206
+ }
207
+
208
+ function mergeSegmentOptions(
209
+ base: StatusLineSegmentOptions | undefined,
210
+ overrides: StatusLineSegmentOptions | undefined,
211
+ ): StatusLineSegmentOptions {
212
+ return {
213
+ model: base?.model || overrides?.model ? { ...(base?.model ?? {}), ...(overrides?.model ?? {}) } : undefined,
214
+ path: base?.path || overrides?.path ? { ...(base?.path ?? {}), ...(overrides?.path ?? {}) } : undefined,
215
+ git: base?.git || overrides?.git ? { ...(base?.git ?? {}), ...(overrides?.git ?? {}) } : undefined,
216
+ time: base?.time || overrides?.time ? { ...(base?.time ?? {}), ...(overrides?.time ?? {}) } : undefined,
217
+ };
218
+ }
219
+
220
+ function effectiveSegmentOptions(
221
+ preset: StatusLinePreset,
222
+ options: StatusLineSegmentOptions | undefined,
223
+ ): StatusLineSegmentOptions {
224
+ return mergeSegmentOptions(getPreset(preset).segmentOptions, options);
225
+ }
226
+
227
+ function effectiveCustomSegments(
228
+ preset: StatusLinePreset,
229
+ leftSegments: StatusLineSegmentId[],
230
+ rightSegments: StatusLineSegmentId[],
231
+ ): { leftSegments: StatusLineSegmentId[]; rightSegments: StatusLineSegmentId[] } {
232
+ if (preset === "custom") {
233
+ return { leftSegments: [...leftSegments], rightSegments: [...rightSegments] };
234
+ }
235
+ const presetDef = getPreset(preset);
236
+ return {
237
+ leftSegments: [...presetDef.leftSegments],
238
+ rightSegments: [...presetDef.rightSegments],
239
+ };
240
+ }
241
+
242
+ function getSavedUsageMode(): UsageMode {
243
+ const segmentOptions = settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions;
244
+ return segmentOptions.usage?.mode === "remaining" ? "remaining" : "used";
245
+ }
246
+
247
+ function setSavedUsageMode(mode: string): StatusLineSegmentOptions {
248
+ const normalizedMode: UsageMode = mode === "remaining" ? "remaining" : "used";
249
+ const segmentOptions = settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions;
250
+ const nextOptions: StatusLineSegmentOptions = {
251
+ ...segmentOptions,
252
+ usage: {
253
+ ...(segmentOptions.usage ?? {}),
254
+ mode: normalizedMode,
255
+ },
256
+ };
257
+ settings.set("statusLine.segmentOptions", nextOptions as Record<string, unknown>);
258
+ return nextOptions;
259
+ }
260
+
261
+ function statusSegmentLabel(id: StatusLineSegmentId): string {
262
+ return id.replace(/_/g, " ");
263
+ }
264
+
265
+ function segmentPlacement(
266
+ id: StatusLineSegmentId,
267
+ leftSegments: StatusLineSegmentId[],
268
+ rightSegments: StatusLineSegmentId[],
269
+ ): "hidden" | "left" | "right" {
270
+ if (leftSegments.includes(id)) return "left";
271
+ if (rightSegments.includes(id)) return "right";
272
+ return "hidden";
273
+ }
274
+
275
+ class StatusLineCustomEditor extends Container {
276
+ #list!: SettingsList;
277
+ #draft: StatusLineDraft;
278
+ #currentWidthPreviewText!: Text;
279
+ #narrowWidthPreviewText!: Text;
280
+ #previewHighlightSegment: StatusLineSegmentId | undefined;
281
+
282
+ constructor(
283
+ private readonly callbacks: SettingsCallbacks,
284
+ private readonly done: (value?: string) => void,
285
+ ) {
286
+ super();
287
+ const preset = settings.get("statusLine.preset");
288
+ const seeded = effectiveCustomSegments(
289
+ preset,
290
+ settings.get("statusLine.leftSegments"),
291
+ settings.get("statusLine.rightSegments"),
292
+ );
293
+ this.#draft = {
294
+ preset: "custom",
295
+ leftSegments: seeded.leftSegments,
296
+ rightSegments: seeded.rightSegments,
297
+ separator: settings.get("statusLine.separator"),
298
+ segmentOptions: effectiveSegmentOptions(
299
+ preset,
300
+ settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions,
301
+ ),
302
+ };
303
+ this.#preview();
304
+ this.#build();
305
+ }
306
+
307
+ #build(): void {
308
+ this.clear();
309
+ this.addChild(new Text(theme.bold(theme.fg("accent", "Status Line Custom Editor")), 0, 0));
310
+ this.addChild(new Spacer(1));
311
+ this.addChild(new Text(theme.fg("muted", "Current width preview:"), 0, 0));
312
+ this.#currentWidthPreviewText = new Text(this.#statusLinePreview(), 0, 0);
313
+ this.addChild(this.#currentWidthPreviewText);
314
+ this.addChild(new Text(theme.fg("muted", "Narrow width preview:"), 0, 0));
315
+ this.#narrowWidthPreviewText = new Text(this.#statusLinePreview(40), 0, 0);
316
+ this.addChild(this.#narrowWidthPreviewText);
317
+ this.addChild(new Spacer(1));
318
+ this.#list = new SettingsList(
319
+ this.#items(),
320
+ 14,
321
+ getSettingsListTheme(),
322
+ (id, value) => this.#handleChange(id, value),
323
+ () => this.#cancel(),
324
+ item => this.#setSelectedItem(item),
325
+ );
326
+ this.addChild(this.#list);
327
+ }
328
+
329
+ #refresh(): void {
330
+ this.#refreshPreview();
331
+ this.#list.setItems(this.#items());
332
+ }
333
+
334
+ #refreshPreview(): void {
335
+ this.#currentWidthPreviewText.setText(this.#statusLinePreview());
336
+ this.#narrowWidthPreviewText.setText(this.#statusLinePreview(40));
337
+ }
338
+
339
+ #statusLinePreview(width?: number): string {
340
+ return this.callbacks.getStatusLinePreview?.(width) ?? theme.fg("dim", "(preview not available)");
341
+ }
342
+ #setSelectedItem(item: SettingItem | undefined): void {
343
+ this.#previewHighlightSegment = this.#highlightSegmentForItem(item);
344
+ this.#preview();
345
+ this.#refreshPreview();
346
+ }
347
+
348
+ #highlightSegmentForItem(item: SettingItem | undefined): StatusLineSegmentId | undefined {
349
+ if (!item) return undefined;
350
+ if (item.id.startsWith("segment.")) {
351
+ return item.id.slice("segment.".length) as StatusLineSegmentId;
352
+ }
353
+ if (item.id.startsWith("moveup.")) {
354
+ return item.id.slice("moveup.".length) as StatusLineSegmentId;
355
+ }
356
+ if (item.id.startsWith("movedown.")) {
357
+ return item.id.slice("movedown.".length) as StatusLineSegmentId;
358
+ }
359
+ if (item.id.startsWith("option.")) {
360
+ const [segment] = item.id.slice("option.".length).split(".");
361
+ return segment as StatusLineSegmentId;
362
+ }
363
+ return undefined;
364
+ }
365
+
366
+ #items(): SettingItem[] {
367
+ const items: SettingItem[] = [
368
+ {
369
+ id: "action.save",
370
+ label: "Save custom status line",
371
+ description: "Persist the previewed custom status line to user config.",
372
+ currentValue: "approve",
373
+ values: ["approve"],
374
+ },
375
+ {
376
+ id: "action.cancel",
377
+ label: "Cancel and restore",
378
+ description: "Close without persisting draft settings and restore the previous preview.",
379
+ currentValue: "restore",
380
+ values: ["restore"],
381
+ },
382
+ {
383
+ id: "separator",
384
+ label: "Separator",
385
+ description: "Separator style between status line segments.",
386
+ currentValue: this.#draft.separator,
387
+ submenu: (currentValue, done) =>
388
+ new SelectSubmenu(
389
+ "Status Line Separator",
390
+ "Style of separators between segments.",
391
+ [
392
+ { value: "powerline", label: "Powerline" },
393
+ { value: "powerline-thin", label: "Thin chevron" },
394
+ { value: "slash", label: "Slash" },
395
+ { value: "pipe", label: "Pipe" },
396
+ { value: "block", label: "Block" },
397
+ { value: "none", label: "None" },
398
+ { value: "ascii", label: "ASCII" },
399
+ ],
400
+ currentValue,
401
+ done,
402
+ () => done(),
403
+ ),
404
+ },
405
+ ];
406
+
407
+ for (const id of PUBLIC_STATUS_SEGMENTS) {
408
+ items.push({
409
+ id: `segment.${id}`,
410
+ label: `Segment: ${statusSegmentLabel(id)}`,
411
+ description: "Cycle placement: hidden → left → right. Use move actions below to reorder visible segments.",
412
+ currentValue: segmentPlacement(id, this.#draft.leftSegments, this.#draft.rightSegments),
413
+ values: ["hidden", "left", "right"],
414
+ });
415
+ if (segmentPlacement(id, this.#draft.leftSegments, this.#draft.rightSegments) !== "hidden") {
416
+ items.push(
417
+ {
418
+ id: `moveup.${id}`,
419
+ label: `Move left: ${statusSegmentLabel(id)}`,
420
+ currentValue: "←",
421
+ values: ["←"],
422
+ },
423
+ {
424
+ id: `movedown.${id}`,
425
+ label: `Move right: ${statusSegmentLabel(id)}`,
426
+ currentValue: "→",
427
+ values: ["→"],
428
+ },
429
+ );
430
+ }
431
+ }
432
+
433
+ items.push(
434
+ {
435
+ id: "option.model.showThinkingLevel",
436
+ label: "Model: show thinking level",
437
+ currentValue: String(this.#draft.segmentOptions.model?.showThinkingLevel !== false),
438
+ values: BOOL_VALUES,
439
+ },
440
+ {
441
+ id: "option.path.abbreviate",
442
+ label: "Path: abbreviate",
443
+ currentValue: String(this.#draft.segmentOptions.path?.abbreviate !== false),
444
+ values: BOOL_VALUES,
445
+ },
446
+ {
447
+ id: "option.path.maxLength",
448
+ label: "Path: max length",
449
+ currentValue: String(this.#draft.segmentOptions.path?.maxLength ?? 32),
450
+ submenu: (currentValue, done) =>
451
+ new SelectSubmenu(
452
+ "Path max length",
453
+ "Maximum displayed path length.",
454
+ PATH_LENGTH_OPTIONS,
455
+ currentValue,
456
+ done,
457
+ () => done(),
458
+ ),
459
+ },
460
+ {
461
+ id: "option.path.stripWorkPrefix",
462
+ label: "Path: strip work prefix",
463
+ currentValue: String(this.#draft.segmentOptions.path?.stripWorkPrefix === true),
464
+ values: BOOL_VALUES,
465
+ },
466
+ {
467
+ id: "option.git.showBranch",
468
+ label: "Git: show branch",
469
+ currentValue: String(this.#draft.segmentOptions.git?.showBranch !== false),
470
+ values: BOOL_VALUES,
471
+ },
472
+ {
473
+ id: "option.git.showStaged",
474
+ label: "Git: show staged",
475
+ currentValue: String(this.#draft.segmentOptions.git?.showStaged !== false),
476
+ values: BOOL_VALUES,
477
+ },
478
+ {
479
+ id: "option.git.showUnstaged",
480
+ label: "Git: show unstaged",
481
+ currentValue: String(this.#draft.segmentOptions.git?.showUnstaged !== false),
482
+ values: BOOL_VALUES,
483
+ },
484
+ {
485
+ id: "option.git.showUntracked",
486
+ label: "Git: show untracked",
487
+ currentValue: String(this.#draft.segmentOptions.git?.showUntracked !== false),
488
+ values: BOOL_VALUES,
489
+ },
490
+ {
491
+ id: "option.time.format",
492
+ label: "Time: format",
493
+ currentValue: this.#draft.segmentOptions.time?.format ?? "24h",
494
+ submenu: (currentValue, done) =>
495
+ new SelectSubmenu(
496
+ "Time format",
497
+ "Clock format for the time segment.",
498
+ TIME_FORMAT_OPTIONS,
499
+ currentValue,
500
+ done,
501
+ () => done(),
502
+ ),
503
+ },
504
+ {
505
+ id: "option.time.showSeconds",
506
+ label: "Time: show seconds",
507
+ currentValue: String(this.#draft.segmentOptions.time?.showSeconds === true),
508
+ values: BOOL_VALUES,
509
+ },
510
+ );
511
+
512
+ return items;
513
+ }
514
+
515
+ #handleChange(id: string, value: string): void {
516
+ if (id === "action.save") {
517
+ this.#save();
518
+ return;
519
+ }
520
+ if (id === "action.cancel") {
521
+ this.#cancel();
522
+ return;
523
+ }
524
+ if (id === "separator") {
525
+ this.#draft.separator = value as StatusLineSeparatorStyle;
526
+ } else if (id.startsWith("segment.")) {
527
+ this.#setSegmentPlacement(
528
+ id.slice("segment.".length) as StatusLineSegmentId,
529
+ value as "hidden" | "left" | "right",
530
+ );
531
+ } else if (id.startsWith("moveup.")) {
532
+ this.#moveSegment(id.slice("moveup.".length) as StatusLineSegmentId, -1);
533
+ } else if (id.startsWith("movedown.")) {
534
+ this.#moveSegment(id.slice("movedown.".length) as StatusLineSegmentId, 1);
535
+ } else if (id.startsWith("option.")) {
536
+ this.#setOption(id.slice("option.".length), value);
537
+ }
538
+ this.#preview();
539
+ this.#refresh();
540
+ }
541
+
542
+ #setSegmentPlacement(id: StatusLineSegmentId, placement: "hidden" | "left" | "right"): void {
543
+ this.#draft.leftSegments = this.#draft.leftSegments.filter(segment => segment !== id);
544
+ this.#draft.rightSegments = this.#draft.rightSegments.filter(segment => segment !== id);
545
+ if (placement === "left") this.#draft.leftSegments.push(id);
546
+ if (placement === "right") this.#draft.rightSegments.push(id);
547
+ }
548
+
549
+ #moveSegment(id: StatusLineSegmentId, delta: -1 | 1): void {
550
+ const group = this.#draft.leftSegments.includes(id) ? this.#draft.leftSegments : this.#draft.rightSegments;
551
+ const index = group.indexOf(id);
552
+ if (index < 0) return;
553
+ const nextIndex = Math.max(0, Math.min(group.length - 1, index + delta));
554
+ if (nextIndex === index) return;
555
+ const [segment] = group.splice(index, 1);
556
+ if (segment) group.splice(nextIndex, 0, segment);
557
+ }
558
+
559
+ #setOption(path: string, value: string): void {
560
+ const bool = value === "true";
561
+ switch (path) {
562
+ case "model.showThinkingLevel":
563
+ this.#draft.segmentOptions.model = { ...(this.#draft.segmentOptions.model ?? {}), showThinkingLevel: bool };
564
+ break;
565
+ case "path.abbreviate":
566
+ this.#draft.segmentOptions.path = { ...(this.#draft.segmentOptions.path ?? {}), abbreviate: bool };
567
+ break;
568
+ case "path.maxLength":
569
+ this.#draft.segmentOptions.path = { ...(this.#draft.segmentOptions.path ?? {}), maxLength: Number(value) };
570
+ break;
571
+ case "path.stripWorkPrefix":
572
+ this.#draft.segmentOptions.path = { ...(this.#draft.segmentOptions.path ?? {}), stripWorkPrefix: bool };
573
+ break;
574
+ case "git.showBranch":
575
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showBranch: bool };
576
+ break;
577
+ case "git.showStaged":
578
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showStaged: bool };
579
+ break;
580
+ case "git.showUnstaged":
581
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showUnstaged: bool };
582
+ break;
583
+ case "git.showUntracked":
584
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showUntracked: bool };
585
+ break;
586
+ case "time.format":
587
+ this.#draft.segmentOptions.time = {
588
+ ...(this.#draft.segmentOptions.time ?? {}),
589
+ format: value as "12h" | "24h",
590
+ };
591
+ break;
592
+ case "time.showSeconds":
593
+ this.#draft.segmentOptions.time = { ...(this.#draft.segmentOptions.time ?? {}), showSeconds: bool };
594
+ break;
595
+ }
596
+ }
597
+
598
+ #preview(): void {
599
+ this.callbacks.onStatusLinePreview?.({
600
+ preset: "custom",
601
+ leftSegments: [...this.#draft.leftSegments],
602
+ rightSegments: [...this.#draft.rightSegments],
603
+ separator: this.#draft.separator,
604
+ segmentOptions: cloneSegmentOptions(this.#draft.segmentOptions),
605
+ previewHighlightSegment: this.#previewHighlightSegment,
606
+ });
607
+ }
608
+
609
+ #restorePreview(): void {
610
+ this.callbacks.onStatusLinePreview?.({
611
+ preset: settings.get("statusLine.preset"),
612
+ leftSegments: settings.get("statusLine.leftSegments"),
613
+ rightSegments: settings.get("statusLine.rightSegments"),
614
+ separator: settings.get("statusLine.separator"),
615
+ segmentOptions: cloneSegmentOptions(settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions),
616
+ sessionAccent: settings.get("statusLine.sessionAccent"),
617
+ previewHighlightSegment: undefined,
618
+ });
619
+ }
620
+
621
+ #save(): void {
622
+ settings.set("statusLine.preset", "custom");
623
+ settings.set("statusLine.leftSegments", [...this.#draft.leftSegments]);
624
+ settings.set("statusLine.rightSegments", [...this.#draft.rightSegments]);
625
+ settings.set("statusLine.separator", this.#draft.separator);
626
+ settings.set(
627
+ "statusLine.segmentOptions",
628
+ cloneSegmentOptions(this.#draft.segmentOptions) as Record<string, unknown>,
629
+ );
630
+ this.callbacks.onChange("statusLine.preset", "custom");
631
+ this.callbacks.onChange("statusLine.leftSegments", [...this.#draft.leftSegments]);
632
+ this.callbacks.onChange("statusLine.rightSegments", [...this.#draft.rightSegments]);
633
+ this.callbacks.onChange("statusLine.separator", this.#draft.separator);
634
+ this.callbacks.onChange("statusLine.segmentOptions", cloneSegmentOptions(this.#draft.segmentOptions));
635
+ this.#previewHighlightSegment = undefined;
636
+ this.#preview();
637
+ this.done("saved");
638
+ }
639
+
640
+ #cancel(): void {
641
+ this.#restorePreview();
642
+ this.done();
643
+ }
644
+
645
+ handleInput(data: string): void {
646
+ this.#list.handleInput(data);
647
+ }
648
+ }
185
649
 
186
650
  function getSettingsTabs(): Tab[] {
187
651
  return [
@@ -215,7 +679,10 @@ export interface StatusLinePreviewSettings {
215
679
  leftSegments?: StatusLineSegmentId[];
216
680
  rightSegments?: StatusLineSegmentId[];
217
681
  separator?: StatusLineSeparatorStyle;
682
+ segmentOptions?: StatusLineSegmentOptions;
683
+ previewHighlightSegment?: StatusLineSegmentId;
218
684
  sessionAccent?: boolean;
685
+ maxRows?: number;
219
686
  }
220
687
 
221
688
  export interface SettingsCallbacks {
@@ -228,7 +695,7 @@ export interface SettingsCallbacks {
228
695
  /** Called for status line preview while configuring */
229
696
  onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
230
697
  /** Get current rendered status line for inline preview */
231
- getStatusLinePreview?: () => string;
698
+ getStatusLinePreview?: (width?: number) => string;
232
699
  /** Called when plugins change */
233
700
  onPluginsChanged?: () => void;
234
701
  /** Called when settings panel is closed */
@@ -395,7 +862,9 @@ export class SettingsSelectorComponent extends Container {
395
862
  } else if (def.path === "theme.dark" || def.path === "theme.light") {
396
863
  options = this.context.availableThemes.map(t => ({ value: t, label: t }));
397
864
  }
398
-
865
+ if (def.path === "statusLine.preset") {
866
+ options = options.filter(option => option.value !== "custom");
867
+ }
399
868
  // Preview handlers
400
869
  let onPreview: ((value: string) => void | Promise<void>) | undefined;
401
870
  let onPreviewCancel: (() => void) | undefined;
@@ -410,25 +879,37 @@ export class SettingsSelectorComponent extends Container {
410
879
  };
411
880
  } else if (def.path === "statusLine.preset") {
412
881
  onPreview = value => {
413
- const presetDef = getPreset(
414
- value as "default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "custom",
415
- );
882
+ const presetDef = getPreset(value as StatusLinePreset);
416
883
  this.callbacks.onStatusLinePreview?.({
417
884
  preset: value as StatusLinePreset,
418
885
  leftSegments: presetDef.leftSegments,
419
886
  rightSegments: presetDef.rightSegments,
420
887
  separator: presetDef.separator,
888
+ previewHighlightSegment: undefined,
421
889
  });
422
890
  this.#updateStatusPreview();
423
891
  };
424
892
  onPreviewCancel = () => {
425
893
  const currentPreset = settings.get("statusLine.preset");
426
894
  const presetDef = getPreset(currentPreset);
895
+ const savedCustomSettings =
896
+ currentPreset === "custom"
897
+ ? {
898
+ leftSegments: settings.get("statusLine.leftSegments"),
899
+ rightSegments: settings.get("statusLine.rightSegments"),
900
+ separator: settings.get("statusLine.separator"),
901
+ segmentOptions: cloneSegmentOptions(
902
+ settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions,
903
+ ),
904
+ }
905
+ : {};
427
906
  this.callbacks.onStatusLinePreview?.({
428
907
  preset: currentPreset,
429
908
  leftSegments: presetDef.leftSegments,
430
909
  rightSegments: presetDef.rightSegments,
431
910
  separator: presetDef.separator,
911
+ ...savedCustomSettings,
912
+ previewHighlightSegment: undefined,
432
913
  });
433
914
  this.#updateStatusPreview();
434
915
  };
@@ -439,7 +920,19 @@ export class SettingsSelectorComponent extends Container {
439
920
  };
440
921
  onPreviewCancel = () => {
441
922
  const separator = settings.get("statusLine.separator");
442
- this.callbacks.onStatusLinePreview?.({ separator });
923
+ this.callbacks.onStatusLinePreview?.({ separator, previewHighlightSegment: undefined });
924
+ this.#updateStatusPreview();
925
+ };
926
+ } else if (def.path === "statusLine.maxRows") {
927
+ onPreview = value => {
928
+ this.callbacks.onStatusLinePreview?.({ maxRows: Number(value) });
929
+ this.#updateStatusPreview();
930
+ };
931
+ onPreviewCancel = () => {
932
+ this.callbacks.onStatusLinePreview?.({
933
+ maxRows: settings.get("statusLine.maxRows"),
934
+ previewHighlightSegment: undefined,
935
+ });
443
936
  this.#updateStatusPreview();
444
937
  };
445
938
  }
@@ -532,10 +1025,20 @@ export class SettingsSelectorComponent extends Container {
532
1025
  }
533
1026
 
534
1027
  this.#currentList = new SettingsList(
535
- this.#buildItemsForDefs(defs),
1028
+ this.#buildItemsForTab(defs, tabId),
536
1029
  10,
537
1030
  getSettingsListTheme(),
538
1031
  (id, newValue) => {
1032
+ if (id === STATUS_LINE_USAGE_MODE_ID) {
1033
+ const segmentOptions = setSavedUsageMode(newValue);
1034
+ this.callbacks.onChange("statusLine.segmentOptions", segmentOptions);
1035
+ if (tabId === "appearance") {
1036
+ this.#triggerStatusLinePreview();
1037
+ }
1038
+ this.#refreshCurrentTabItems(defs);
1039
+ return;
1040
+ }
1041
+
539
1042
  const def = defs.find(d => d.path === id);
540
1043
  if (!def) return;
541
1044
 
@@ -576,10 +1079,45 @@ export class SettingsSelectorComponent extends Container {
576
1079
  return items;
577
1080
  }
578
1081
 
1082
+ #buildItemsForTab(defs: SettingDef[], tabId: SettingTab): SettingItem[] {
1083
+ const items = this.#buildItemsForDefs(defs);
1084
+ if (tabId === "appearance") {
1085
+ const customEditorItem: SettingItem = {
1086
+ id: STATUS_LINE_CUSTOM_EDITOR_ID,
1087
+ label: "Status Line Custom Editor",
1088
+ description:
1089
+ "Edit custom status line segments, placement, separator, and typed segment options with live previews.",
1090
+ currentValue: "open",
1091
+ submenu: (_currentValue, done) => new StatusLineCustomEditor(this.callbacks, done),
1092
+ };
1093
+ const presetIndex = items.findIndex(item => item.id === "statusLine.preset");
1094
+ if (presetIndex >= 0) {
1095
+ items.splice(presetIndex + 1, 0, customEditorItem);
1096
+ } else {
1097
+ items.push(customEditorItem);
1098
+ }
1099
+ {
1100
+ const usageModeItem: SettingItem = {
1101
+ id: STATUS_LINE_USAGE_MODE_ID,
1102
+ label: "Status Line Usage Mode",
1103
+ description: "Show provider quota in the status line as used or remaining.",
1104
+ currentValue: getSavedUsageMode(),
1105
+ values: [...USAGE_MODE_VALUES],
1106
+ };
1107
+ if (presetIndex >= 0) {
1108
+ items.splice(presetIndex + 2, 0, usageModeItem);
1109
+ } else {
1110
+ items.push(usageModeItem);
1111
+ }
1112
+ }
1113
+ }
1114
+ return items;
1115
+ }
1116
+
579
1117
  /** Re-evaluate condition gates against the current settings and refresh the active list. */
580
1118
  #refreshCurrentTabItems(defs: SettingDef[]): void {
581
1119
  if (this.#currentTabId === "plugins" || !this.#currentList) return;
582
- this.#currentList.setItems(this.#buildItemsForDefs(defs));
1120
+ this.#currentList.setItems(this.#buildItemsForTab(defs, this.#currentTabId));
583
1121
  }
584
1122
 
585
1123
  /**
@@ -601,7 +1139,9 @@ export class SettingsSelectorComponent extends Container {
601
1139
  leftSegments: settings.get("statusLine.leftSegments"),
602
1140
  rightSegments: settings.get("statusLine.rightSegments"),
603
1141
  separator: settings.get("statusLine.separator"),
1142
+ segmentOptions: cloneSegmentOptions(settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions),
604
1143
  sessionAccent: settings.get("statusLine.sessionAccent"),
1144
+ previewHighlightSegment: undefined,
605
1145
  };
606
1146
  this.callbacks.onStatusLinePreview?.(statusLineSettings);
607
1147
  this.#updateStatusPreview();