@gajae-code/coding-agent 0.7.9 → 0.7.11

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 (123) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -0
  3. package/dist/types/config/keybindings.d.ts +11 -5
  4. package/dist/types/config/settings-schema.d.ts +10 -1
  5. package/dist/types/defaults/gjc-defaults.d.ts +7 -0
  6. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +10 -0
  8. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
  9. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  11. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -0
  12. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
  13. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  14. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  15. package/dist/types/modes/components/skill-message.d.ts +1 -0
  16. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  17. package/dist/types/modes/controllers/input-controller.d.ts +4 -0
  18. package/dist/types/modes/interactive-mode.d.ts +1 -0
  19. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
  20. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +20 -6
  21. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  22. package/dist/types/modes/types.d.ts +1 -0
  23. package/dist/types/notifications/html-format.d.ts +2 -0
  24. package/dist/types/notifications/recent-activity.d.ts +5 -1
  25. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  26. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  27. package/dist/types/notifications/threaded-render.d.ts +1 -0
  28. package/dist/types/sdk.d.ts +1 -1
  29. package/dist/types/session/agent-session.d.ts +8 -5
  30. package/dist/types/slash-commands/types.d.ts +2 -0
  31. package/dist/types/tools/ask.d.ts +2 -0
  32. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  33. package/dist/types/web/search/index.d.ts +4 -2
  34. package/dist/types/web/search/provider.d.ts +14 -0
  35. package/dist/types/web/search/providers/utils.d.ts +42 -13
  36. package/dist/types/web/search/render.d.ts +1 -0
  37. package/package.json +7 -7
  38. package/src/cli/update-cli.ts +24 -0
  39. package/src/cli/web-search-cli.ts +5 -7
  40. package/src/cli.ts +1 -1
  41. package/src/commands/launch.ts +1 -1
  42. package/src/commands/ralplan.ts +1 -0
  43. package/src/config/keybindings.ts +14 -5
  44. package/src/config/model-profiles.ts +16 -0
  45. package/src/config/settings-schema.ts +11 -1
  46. package/src/coordinator-mcp/server.ts +9 -3
  47. package/src/defaults/gjc/skills/deep-interview/SKILL.md +26 -2
  48. package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
  49. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
  50. package/src/defaults/gjc-defaults.ts +16 -0
  51. package/src/extensibility/extensions/runner.ts +2 -1
  52. package/src/extensibility/extensions/types.ts +11 -0
  53. package/src/gjc-runtime/launch-tmux.ts +209 -26
  54. package/src/gjc-runtime/ralplan-runtime.ts +22 -6
  55. package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
  56. package/src/gjc-runtime/state-runtime.ts +8 -5
  57. package/src/gjc-runtime/state-writer.ts +5 -5
  58. package/src/gjc-runtime/ultragoal-guard.ts +139 -19
  59. package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
  60. package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
  61. package/src/gjc-runtime/workflow-manifest.ts +0 -1
  62. package/src/hooks/skill-state.ts +33 -73
  63. package/src/internal-urls/docs-index.generated.ts +6 -4
  64. package/src/main.ts +2 -2
  65. package/src/migrate/executor.ts +23 -6
  66. package/src/modes/components/custom-editor.ts +8 -0
  67. package/src/modes/components/hook-selector.ts +21 -3
  68. package/src/modes/components/model-selector.ts +6 -2
  69. package/src/modes/components/settings-selector.ts +47 -5
  70. package/src/modes/components/skill-message.ts +18 -10
  71. package/src/modes/components/status-line/presets.ts +11 -0
  72. package/src/modes/controllers/command-controller.ts +23 -0
  73. package/src/modes/controllers/extension-ui-controller.ts +18 -2
  74. package/src/modes/controllers/input-controller.ts +41 -9
  75. package/src/modes/interactive-mode.ts +16 -7
  76. package/src/modes/prompt-action-autocomplete.ts +23 -1
  77. package/src/modes/shared/agent-wire/deep-interview-gate.ts +53 -9
  78. package/src/modes/slash-command-visibility.ts +9 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  81. package/src/modes/utils/ui-helpers.ts +76 -3
  82. package/src/notifications/html-format.ts +75 -1
  83. package/src/notifications/index.ts +12 -1
  84. package/src/notifications/recent-activity.ts +32 -8
  85. package/src/notifications/telegram-daemon-control.ts +9 -0
  86. package/src/notifications/telegram-daemon.ts +68 -25
  87. package/src/notifications/telegram-reference.ts +36 -14
  88. package/src/notifications/threaded-render.ts +14 -6
  89. package/src/prompts/agents/architect.md +7 -4
  90. package/src/prompts/agents/critic.md +15 -6
  91. package/src/prompts/agents/planner.md +8 -3
  92. package/src/prompts/system/system-prompt.md +5 -1
  93. package/src/prompts/tools/bash.md +1 -1
  94. package/src/sdk.ts +6 -11
  95. package/src/session/agent-session.ts +60 -17
  96. package/src/slash-commands/builtin-registry.ts +30 -10
  97. package/src/slash-commands/types.ts +2 -0
  98. package/src/tools/ask.ts +136 -16
  99. package/src/tools/bash.ts +24 -8
  100. package/src/utils/cmux-workspace.ts +233 -0
  101. package/src/utils/title-generator.ts +2 -0
  102. package/src/web/kagi.ts +1 -1
  103. package/src/web/parallel.ts +2 -2
  104. package/src/web/search/index.ts +137 -52
  105. package/src/web/search/provider.ts +80 -0
  106. package/src/web/search/providers/anthropic.ts +1 -1
  107. package/src/web/search/providers/brave.ts +1 -1
  108. package/src/web/search/providers/codex.ts +1 -1
  109. package/src/web/search/providers/duckduckgo.ts +1 -1
  110. package/src/web/search/providers/exa.ts +1 -1
  111. package/src/web/search/providers/gemini.ts +7 -3
  112. package/src/web/search/providers/jina.ts +1 -1
  113. package/src/web/search/providers/kimi.ts +6 -1
  114. package/src/web/search/providers/openai-compatible.ts +1 -1
  115. package/src/web/search/providers/parallel.ts +1 -1
  116. package/src/web/search/providers/perplexity.ts +2 -2
  117. package/src/web/search/providers/searxng.ts +1 -1
  118. package/src/web/search/providers/synthetic.ts +1 -1
  119. package/src/web/search/providers/tavily.ts +1 -1
  120. package/src/web/search/providers/utils.ts +67 -17
  121. package/src/web/search/providers/xai.ts +1 -1
  122. package/src/web/search/providers/zai.ts +1 -1
  123. package/src/web/search/render.ts +3 -0
package/src/main.ts CHANGED
@@ -671,7 +671,7 @@ async function buildSessionOptions(
671
671
  options.thinkingLevel = scopedModels[0].thinkingLevel;
672
672
  }
673
673
 
674
- // Scoped models for Ctrl+P cycling - fill in default thinking levels when not explicit
674
+ // Scoped models for Alt+N cycling - fill in default thinking levels when not explicit
675
675
  if (scopedModels.length > 0) {
676
676
  const defaultThinkingLevel = activeSettings.get("defaultThinkingLevel");
677
677
  options.scopedModels = scopedModels.map(scopedModel => ({
@@ -1067,7 +1067,7 @@ export async function runRootCommand(
1067
1067
  return `${scopedModel.model.id}${thinkingStr}`;
1068
1068
  })
1069
1069
  .join(", ");
1070
- process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`)}\n`);
1070
+ process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Alt+N to cycle)")}`)}\n`);
1071
1071
  }
1072
1072
 
1073
1073
  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) === ".gjc" ? 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,
@@ -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;
@@ -288,6 +291,11 @@ export class CustomEditor extends Editor {
288
291
  return;
289
292
  }
290
293
 
294
+ // Intercept configured command palette shortcut before model cycling (Ctrl+P default).
295
+ if (this.#matchesAction(data, "app.commandPalette.open") && this.onOpenCommandPalette) {
296
+ this.onOpenCommandPalette();
297
+ return;
298
+ }
291
299
  // Intercept configured backward model cycling (check before forward cycling)
292
300
  if (this.#matchesAction(data, "app.model.cycleBackward") && this.onCycleModelBackward) {
293
301
  this.onCycleModelBackward();
@@ -58,6 +58,11 @@ export interface HookSelectorOptions {
58
58
  optionLabel: string;
59
59
  onSubmit: (text: string) => void;
60
60
  };
61
+ clarificationInput?: {
62
+ optionLabel: string;
63
+ onSubmit: (text: string) => void;
64
+ allowEmpty?: boolean;
65
+ };
61
66
  /**
62
67
  * Autocomplete provider for the inline custom-input editor. When present,
63
68
  * the "Other (type your own)" editor gains the same `@` file-link and `/`
@@ -338,6 +343,8 @@ export class HookSelectorComponent extends Container {
338
343
  #outline: boolean;
339
344
  #scrollTitleRows: number | undefined;
340
345
  #customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
346
+ #clarificationInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
347
+ #activeInput: { onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
341
348
  #inputArea: Container;
342
349
  #inlineEditor: Editor | undefined;
343
350
  #helpTextComponent: Text;
@@ -365,6 +372,7 @@ export class HookSelectorComponent extends Container {
365
372
  this.#wrapFocused = opts?.wrapFocused === true;
366
373
  this.#outline = opts?.outline === true;
367
374
  this.#customInput = opts?.customInput;
375
+ this.#clarificationInput = opts?.clarificationInput;
368
376
  this.#tui = opts?.tui;
369
377
  this.#autocompleteProvider = opts?.autocompleteProvider;
370
378
 
@@ -500,7 +508,11 @@ export class HookSelectorComponent extends Container {
500
508
  const selected = this.#options[this.#selectedIndex];
501
509
  if (!selected) return;
502
510
  if (this.#customInput && selected === this.#customInput.optionLabel) {
503
- this.#enterInputMode();
511
+ this.#enterInputMode(this.#customInput);
512
+ return;
513
+ }
514
+ if (this.#clarificationInput && selected === this.#clarificationInput.optionLabel) {
515
+ this.#enterInputMode(this.#clarificationInput);
504
516
  return;
505
517
  }
506
518
  this.#onSelectCallback(selected);
@@ -535,14 +547,19 @@ export class HookSelectorComponent extends Container {
535
547
  return;
536
548
  }
537
549
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
538
- this.#customInput?.onSubmit(editor.getExpandedText());
550
+ const text = editor.getExpandedText();
551
+ if (this.#activeInput?.allowEmpty === false && text.trim() === "") {
552
+ return;
553
+ }
554
+ this.#activeInput?.onSubmit(text);
539
555
  return;
540
556
  }
541
557
  editor.handleInput(keyData);
542
558
  }
543
559
 
544
- #enterInputMode(): void {
560
+ #enterInputMode(input: { onSubmit: (text: string) => void; allowEmpty?: boolean }): void {
545
561
  if (this.#inlineEditor) return;
562
+ this.#activeInput = input;
546
563
  // Stop the auto-select countdown for good: the user is actively typing,
547
564
  // matching the old behavior where the separate editor had no timeout.
548
565
  if (this.#countdown) {
@@ -577,6 +594,7 @@ export class HookSelectorComponent extends Container {
577
594
  #exitInputMode(): void {
578
595
  if (!this.#inlineEditor) return;
579
596
  this.#inlineEditor = undefined;
597
+ this.#activeInput = undefined;
580
598
  this.#inputArea.clear();
581
599
  this.#helpTextComponent.setText(theme.fg("dim", this.#baseHelpText));
582
600
  this.invalidate();
@@ -405,7 +405,7 @@ export class ModelSelectorComponent extends Container {
405
405
  // Create search input
406
406
  this.#searchInput = new Input();
407
407
  if (initialSearchInput) {
408
- this.#searchInput.setValue(initialSearchInput);
408
+ this.#setSearchInputValue(initialSearchInput);
409
409
  }
410
410
  this.#searchInput.onSubmit = () => {
411
411
  const selectedItem = this.#getSelectedItem();
@@ -1012,6 +1012,10 @@ export class ModelSelectorComponent extends Container {
1012
1012
  if (!this.#relocatePresetCursor(targetIdentity)) this.#clampPresetCursor();
1013
1013
  }
1014
1014
 
1015
+ #setSearchInputValue(value: string): void {
1016
+ this.#searchInput.setValue(value);
1017
+ }
1018
+
1015
1019
  #switchToModelMode(seed?: string): void {
1016
1020
  this.#viewMode = "models";
1017
1021
  this.#expandedPresetProviderId = undefined;
@@ -1021,7 +1025,7 @@ export class ModelSelectorComponent extends Container {
1021
1025
  this.#presetLoginHint = undefined;
1022
1026
  this.#activeTabIndex = 0;
1023
1027
  this.#selectedIndex = 0;
1024
- this.#searchInput.setValue(seed ?? this.#searchInput.getValue());
1028
+ this.#setSearchInputValue(seed ?? this.#searchInput.getValue());
1025
1029
  this.#updateTabBar();
1026
1030
  this.#filterModels(this.#searchInput.getValue());
1027
1031
  }
@@ -60,8 +60,6 @@ class TextInputSubmenu extends Container {
60
60
  this.#input = new Input();
61
61
  if (currentValue) {
62
62
  this.#input.setValue(currentValue);
63
- // Move cursor to end of pre-filled value (ctrl+e = cursorLineEnd).
64
- this.#input.handleInput("\x05");
65
63
  }
66
64
  this.#input.onSubmit = value => {
67
65
  this.onSubmit(value); // empty string clears the setting
@@ -164,6 +162,7 @@ class SelectSubmenu extends Container {
164
162
  }
165
163
  }
166
164
  const STATUS_LINE_CUSTOM_EDITOR_ID = "statusLine.customEditor";
165
+ const STATUS_LINE_USAGE_MODE_ID = "statusLine.usageMode";
167
166
  const PUBLIC_STATUS_SEGMENTS = ALL_SEGMENT_IDS.filter(id => id !== "pi");
168
167
 
169
168
  type StatusLineDraft = Required<
@@ -183,6 +182,8 @@ const USAGE_MODE_OPTIONS: SelectItem[] = [
183
182
  { value: "used", label: "Used" },
184
183
  { value: "remaining", label: "Remaining" },
185
184
  ];
185
+ const USAGE_MODE_VALUES = ["used", "remaining"] as const;
186
+ type UsageMode = (typeof USAGE_MODE_VALUES)[number];
186
187
 
187
188
  function cloneSegmentOptions(options: StatusLineSegmentOptions | undefined): StatusLineSegmentOptions {
188
189
  return mergeSegmentOptions(undefined, options);
@@ -223,6 +224,25 @@ function effectiveCustomSegments(
223
224
  };
224
225
  }
225
226
 
227
+ function getSavedUsageMode(): UsageMode {
228
+ const segmentOptions = settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions;
229
+ return segmentOptions.usage?.mode === "remaining" ? "remaining" : "used";
230
+ }
231
+
232
+ function setSavedUsageMode(mode: string): StatusLineSegmentOptions {
233
+ const normalizedMode: UsageMode = mode === "remaining" ? "remaining" : "used";
234
+ const segmentOptions = settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions;
235
+ const nextOptions: StatusLineSegmentOptions = {
236
+ ...segmentOptions,
237
+ usage: {
238
+ ...(segmentOptions.usage ?? {}),
239
+ mode: normalizedMode,
240
+ },
241
+ };
242
+ settings.set("statusLine.segmentOptions", nextOptions as Record<string, unknown>);
243
+ return nextOptions;
244
+ }
245
+
226
246
  function statusSegmentLabel(id: StatusLineSegmentId): string {
227
247
  return id.replace(/_/g, " ");
228
248
  }
@@ -844,9 +864,7 @@ export class SettingsSelectorComponent extends Container {
844
864
  };
845
865
  } else if (def.path === "statusLine.preset") {
846
866
  onPreview = value => {
847
- const presetDef = getPreset(
848
- value as "default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "custom",
849
- );
867
+ const presetDef = getPreset(value as StatusLinePreset);
850
868
  this.callbacks.onStatusLinePreview?.({
851
869
  preset: value as StatusLinePreset,
852
870
  leftSegments: presetDef.leftSegments,
@@ -984,6 +1002,16 @@ export class SettingsSelectorComponent extends Container {
984
1002
  10,
985
1003
  getSettingsListTheme(),
986
1004
  (id, newValue) => {
1005
+ if (id === STATUS_LINE_USAGE_MODE_ID) {
1006
+ const segmentOptions = setSavedUsageMode(newValue);
1007
+ this.callbacks.onChange("statusLine.segmentOptions", segmentOptions);
1008
+ if (tabId === "appearance") {
1009
+ this.#triggerStatusLinePreview();
1010
+ }
1011
+ this.#refreshCurrentTabItems(defs);
1012
+ return;
1013
+ }
1014
+
987
1015
  const def = defs.find(d => d.path === id);
988
1016
  if (!def) return;
989
1017
 
@@ -1048,6 +1076,20 @@ export class SettingsSelectorComponent extends Container {
1048
1076
  } else {
1049
1077
  items.push(customEditorItem);
1050
1078
  }
1079
+ {
1080
+ const usageModeItem: SettingItem = {
1081
+ id: STATUS_LINE_USAGE_MODE_ID,
1082
+ label: "Status Line Usage Mode",
1083
+ description: "Show provider quota in the status line as used or remaining.",
1084
+ currentValue: getSavedUsageMode(),
1085
+ values: [...USAGE_MODE_VALUES],
1086
+ };
1087
+ if (presetIndex >= 0) {
1088
+ items.splice(presetIndex + 2, 0, usageModeItem);
1089
+ } else {
1090
+ items.push(usageModeItem);
1091
+ }
1092
+ }
1051
1093
  }
1052
1094
  return items;
1053
1095
  }
@@ -13,18 +13,20 @@ import {
13
13
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
14
14
  import type { CustomMessage, SkillPromptDetails } from "../../session/messages";
15
15
 
16
- const COLLAPSED_ARGS_PREVIEW_WIDTH = 96;
16
+ const DEFAULT_COLLAPSED_ARGS_PREVIEW_WIDTH = 96;
17
17
  const COLLAPSED_ARGS_PREVIEW_LINES = 6;
18
+ const SKILL_BOX_HORIZONTAL_PADDING = 1;
18
19
  export class SkillMessageComponent extends Container {
19
20
  #box: Box;
20
21
  #contentComponent?: Component;
21
22
  #expanded = false;
23
+ #argsPreviewWidth = DEFAULT_COLLAPSED_ARGS_PREVIEW_WIDTH;
22
24
 
23
25
  constructor(private readonly message: CustomMessage<SkillPromptDetails>) {
24
26
  super();
25
27
  this.addChild(new Spacer(1));
26
28
 
27
- this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
29
+ this.#box = new Box(SKILL_BOX_HORIZONTAL_PADDING, 1, t => theme.bg("customMessageBg", t));
28
30
  this.#rebuild();
29
31
  }
30
32
 
@@ -40,6 +42,15 @@ export class SkillMessageComponent extends Container {
40
42
  this.#rebuild();
41
43
  }
42
44
 
45
+ override render(width: number): string[] {
46
+ const contentWidth = Math.max(1, width - SKILL_BOX_HORIZONTAL_PADDING * 2);
47
+ if (this.#argsPreviewWidth !== contentWidth) {
48
+ this.#argsPreviewWidth = contentWidth;
49
+ this.#rebuild();
50
+ }
51
+ return super.render(width);
52
+ }
53
+
43
54
  #rebuild(): void {
44
55
  if (this.#contentComponent) {
45
56
  this.removeChild(this.#contentComponent);
@@ -57,7 +68,7 @@ export class SkillMessageComponent extends Container {
57
68
  // Collapsed view keeps args readable without exposing the full prompt body.
58
69
  // Each visual line is bounded, but multi-line arguments remain multi-line so
59
70
  // the invocation context is not mistaken for a one-line truncated payload.
60
- const argsPreview = args ? this.#formatArgsPreview(args) : [];
71
+ const argsPreview = args ? this.#formatArgsPreview(args, this.#argsPreviewWidth) : [];
61
72
  const header = `${theme.fg("customMessageLabel", theme.bold("[skill]"))} ${theme.fg("customMessageText", name)}`;
62
73
  this.#box.addChild(new Text(header, 0, 0));
63
74
  if (argsPreview.length > 0) {
@@ -111,19 +122,19 @@ export class SkillMessageComponent extends Container {
111
122
  this.#box.addChild(this.#contentComponent);
112
123
  }
113
124
 
114
- #formatArgsPreview(args: string): string[] {
125
+ #formatArgsPreview(args: string, width: number): string[] {
115
126
  const preview: string[] = [];
116
127
  let omitted = false;
117
128
  const sourceLines = replaceTabs(args).split(/\r?\n/);
118
129
  for (let index = 0; index < sourceLines.length; index += 1) {
119
130
  const sourceLine = sourceLines[index]?.trimEnd() ?? "";
120
- const wrapped = wrapTextWithAnsi(sourceLine.length > 0 ? sourceLine : " ", COLLAPSED_ARGS_PREVIEW_WIDTH);
131
+ const wrapped = wrapTextWithAnsi(sourceLine.length > 0 ? sourceLine : " ", width);
121
132
  for (const line of wrapped.length > 0 ? wrapped : [""]) {
122
133
  if (preview.length >= COLLAPSED_ARGS_PREVIEW_LINES) {
123
134
  omitted = true;
124
135
  break;
125
136
  }
126
- preview.push(truncateToWidth(line, COLLAPSED_ARGS_PREVIEW_WIDTH));
137
+ preview.push(truncateToWidth(line, width));
127
138
  }
128
139
  if (omitted) break;
129
140
  }
@@ -132,10 +143,7 @@ export class SkillMessageComponent extends Container {
132
143
  omitted ||= replaceTabs(args).trimEnd().length > visibleSource.trimEnd().length;
133
144
  }
134
145
  if (omitted && preview.length > 0) {
135
- preview[preview.length - 1] = truncateToWidth(
136
- `${preview[preview.length - 1]} …`,
137
- COLLAPSED_ARGS_PREVIEW_WIDTH,
138
- );
146
+ preview[preview.length - 1] = truncateToWidth(`${preview[preview.length - 1]} …`, width);
139
147
  }
140
148
  return preview;
141
149
  }
@@ -12,6 +12,17 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
12
12
  },
13
13
  },
14
14
 
15
+ "default-usage": {
16
+ leftSegments: ["model", "mode", "git", "pr", "path"],
17
+ rightSegments: ["session_name", "jobs", "token_rate", "usage", "context_pct", "cost"],
18
+ separator: "slash",
19
+ segmentOptions: {
20
+ model: { showThinkingLevel: true },
21
+ path: { abbreviate: true, maxLength: 32, stripWorkPrefix: true },
22
+ git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
23
+ },
24
+ },
25
+
15
26
  minimal: {
16
27
  leftSegments: ["path", "git"],
17
28
  rightSegments: ["session_name", "jobs", "mode", "context_pct"],
@@ -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, "GJC 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);
@@ -617,7 +617,8 @@ export class ExtensionUiController {
617
617
  const listChromeRows = dialogOptions?.outline === true ? HOOK_SELECTOR_OUTLINE_ROWS : 0;
618
618
  // Reserve rows for the inline custom-input editor so opening it doesn't
619
619
  // push the scrollable title past the viewport into terminal scrollback.
620
- const inlineInputRows = dialogOptions?.customInput ? HOOK_SELECTOR_INLINE_INPUT_ROWS : 0;
620
+ const inlineInputRows =
621
+ dialogOptions?.customInput || dialogOptions?.clarificationInput ? HOOK_SELECTOR_INLINE_INPUT_ROWS : 0;
621
622
  const availableTitleRows =
622
623
  this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - inlineInputRows - HOOK_SELECTOR_CHROME_ROWS;
623
624
  const scrollTitleRows =
@@ -659,7 +660,10 @@ export class ExtensionUiController {
659
660
  tui: this.ctx.ui,
660
661
  // Share the main prompt editor's autocomplete provider so the
661
662
  // inline "Other (type your own)" editor supports `@` file links.
662
- autocompleteProvider: dialogOptions?.customInput ? this.ctx.editor.getAutocompleteProvider() : undefined,
663
+ autocompleteProvider:
664
+ dialogOptions?.customInput || dialogOptions?.clarificationInput
665
+ ? this.ctx.editor.getAutocompleteProvider()
666
+ : undefined,
663
667
  outline: dialogOptions?.outline,
664
668
  wrapFocused: dialogOptions?.wrapFocused,
665
669
  scrollTitleRows,
@@ -675,6 +679,18 @@ export class ExtensionUiController {
675
679
  },
676
680
  }
677
681
  : undefined,
682
+ clarificationInput: dialogOptions?.clarificationInput
683
+ ? {
684
+ optionLabel: dialogOptions.clarificationInput.optionLabel,
685
+ allowEmpty: dialogOptions.clarificationInput.allowEmpty,
686
+ onSubmit: text => {
687
+ const optionLabel = dialogOptions.clarificationInput?.optionLabel;
688
+ this.hideHookSelector();
689
+ dialogOptions.clarificationInput?.onSubmit(text);
690
+ finish(optionLabel);
691
+ },
692
+ }
693
+ : undefined,
678
694
  },
679
695
  );
680
696
  // Detach (not dispose) the reusable editor before mounting the transient hook UI, so the
@@ -156,6 +156,8 @@ export class InputController {
156
156
  this.ctx.editor.onSuspend = () => this.handleCtrlZ();
157
157
  this.ctx.editor.setActionKeys("app.thinking.cycle", this.ctx.keybindings.getKeys("app.thinking.cycle"));
158
158
  this.ctx.editor.onCycleThinkingLevel = () => this.cycleThinkingLevel();
159
+ this.ctx.editor.setActionKeys("app.commandPalette.open", this.ctx.keybindings.getKeys("app.commandPalette.open"));
160
+ this.ctx.editor.onOpenCommandPalette = () => this.openCommandPalette();
159
161
  this.ctx.editor.setActionKeys("app.model.cycleForward", this.ctx.keybindings.getKeys("app.model.cycleForward"));
160
162
  this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
161
163
  this.ctx.editor.setActionKeys("app.model.cycleBackward", this.ctx.keybindings.getKeys("app.model.cycleBackward"));
@@ -201,7 +203,7 @@ export class InputController {
201
203
  this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
202
204
  this.ctx.editor.onQueue = () => void this.handleQueueSubmit();
203
205
  this.ctx.editor.onTabDeclined = () => {
204
- if (this.ctx.session.isStreaming) void this.handleQueueSubmit();
206
+ if (this.ctx.session.isStreaming || this.ctx.session.isCompacting) void this.handleQueueSubmit();
205
207
  };
206
208
 
207
209
  this.ctx.editor.clearCustomKeyHandlers();
@@ -492,7 +494,7 @@ export class InputController {
492
494
  }
493
495
 
494
496
  handleDequeue(): void {
495
- const restored = this.restoreQueuedMessagesToEditor();
497
+ const restored = this.restoreLatestQueuedMessageToEditor();
496
498
  if (restored === 0) {
497
499
  this.ctx.showStatus("No queued messages to restore");
498
500
  } else {
@@ -603,13 +605,15 @@ export class InputController {
603
605
  const text = this.ctx.editor.getText().trim();
604
606
  if (!text) return;
605
607
 
606
- // Compaction first: while compacting, free text gets queued via
607
- // `queueCompactionMessage`, and `/skill:*` rides the same queue so a
608
- // skill typed during compaction is not lost or short-circuited through
609
- // `promptCustomMessage`. The skill text is queued verbatim; whether
610
- // the queued entry is later re-parsed into a skill invocation is a
611
- // separate concern owned by the compaction-resume path.
608
+ // Compaction first: while compacting, queue free text and `/skill:*`
609
+ // commands in the compaction-local queue. `flushCompactionQueue`
610
+ // replays skill entries through the custom-message skill path after
611
+ // compaction finishes so they are not degraded into plain prompts.
612
612
  if (this.ctx.session.isCompacting) {
613
+ if (this.ctx.pendingImages.length > 0) {
614
+ this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
615
+ return;
616
+ }
613
617
  this.ctx.queueCompactionMessage(text, "followUp");
614
618
  return;
615
619
  }
@@ -643,10 +647,28 @@ export class InputController {
643
647
  return this.handleFollowUp();
644
648
  }
645
649
 
650
+ restoreLatestQueuedMessageToEditor(options?: { currentText?: string }): number {
651
+ const compactionQueued = this.ctx.compactionQueuedMessages.pop();
652
+ const queuedText = compactionQueued?.text ?? this.ctx.session.popLastQueuedMessage();
653
+ if (!queuedText) {
654
+ this.ctx.updatePendingMessagesDisplay();
655
+ return 0;
656
+ }
657
+
658
+ this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
659
+ const currentText = options?.currentText ?? this.ctx.editor.getText();
660
+ const combinedText = [queuedText, currentText].filter(t => t.trim()).join("\n\n");
661
+ this.ctx.editor.setText(combinedText);
662
+ this.ctx.updatePendingMessagesDisplay();
663
+ return 1;
664
+ }
665
+
646
666
  restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
647
667
  this.ctx.locallySubmittedUserSignatures.clear();
648
668
  const { steering, followUp } = this.ctx.session.clearQueue();
649
- const allQueued = [...steering, ...followUp];
669
+ const compactionQueued = (this.ctx.compactionQueuedMessages ?? []).map(entry => entry.text);
670
+ this.ctx.compactionQueuedMessages = [];
671
+ const allQueued = [...steering, ...followUp, ...compactionQueued];
650
672
  if (allQueued.length === 0) {
651
673
  this.ctx.updatePendingMessagesDisplay();
652
674
  if (options?.abort) {
@@ -859,6 +881,8 @@ export class InputController {
859
881
  copyCurrentLine: () => this.handleCopyCurrentLine(),
860
882
  copyPrompt: () => this.handleCopyPrompt(),
861
883
  pasteImage: () => void this.handleImagePaste(),
884
+ newSession: () => void this.ctx.handleClearCommand(),
885
+ showHelp: () => this.ctx.handleHelpCommand(),
862
886
  scrollTmuxToPreviousUserInput: () => this.scrollTmuxToPreviousUserInput(),
863
887
  undo: prefix => this.ctx.editor.undoPastTransientText(prefix),
864
888
  moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(),
@@ -903,6 +927,14 @@ export class InputController {
903
927
  }
904
928
  }
905
929
 
930
+ openCommandPalette(): void {
931
+ if (this.ctx.editor.getText().trim().length > 0) {
932
+ this.ctx.showStatus("Command palette opens from an empty prompt. Type / for inline commands.");
933
+ return;
934
+ }
935
+ this.ctx.editor.handleInput("/");
936
+ }
937
+
906
938
  cycleThinkingLevel(): void {
907
939
  const newLevel = this.ctx.session.cycleThinkingLevel();
908
940
  if (newLevel === undefined) {
@@ -71,7 +71,6 @@ import { normalizeLocalScheme } from "../tools/path-utils";
71
71
  import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
72
72
  import { formatPhaseDisplayName } from "../tools/todo-write";
73
73
  import { ToolError } from "../tools/tool-errors";
74
-
75
74
  import type { EventBus } from "../utils/event-bus";
76
75
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
77
76
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
@@ -103,6 +102,7 @@ import { JobsObserver } from "./jobs-observer";
103
102
  import { OAuthManualInputManager } from "./oauth-manual-input";
104
103
  import { SessionObserverRegistry } from "./session-observer-registry";
105
104
  import { interruptHint } from "./shared";
105
+ import { shouldShowExtensionCommand } from "./slash-command-visibility";
106
106
  import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
107
107
  import type { Theme } from "./theme/theme";
108
108
  import {
@@ -452,13 +452,16 @@ export class InteractiveMode implements InteractiveModeContext {
452
452
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
453
453
 
454
454
  const builtinCommandNames = new Set(BUILTIN_SLASH_COMMANDS.map(c => c.name));
455
+ const activeProvider = this.session.model?.provider;
455
456
  const hookCommands: SlashCommand[] = (
456
457
  this.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? []
457
- ).map(cmd => ({
458
- name: cmd.name,
459
- description: cmd.description ?? "(hook command)",
460
- getArgumentCompletions: cmd.getArgumentCompletions,
461
- }));
458
+ )
459
+ .filter(cmd => shouldShowExtensionCommand(cmd.name, activeProvider))
460
+ .map(cmd => ({
461
+ name: cmd.name,
462
+ description: cmd.description ?? "(hook command)",
463
+ getArgumentCompletions: cmd.getArgumentCompletions,
464
+ }));
462
465
 
463
466
  // Convert custom commands (TypeScript) to SlashCommand format
464
467
  const customCommands: SlashCommand[] = this.session.customCommands.map(loaded => ({
@@ -2035,7 +2038,9 @@ export class InteractiveMode implements InteractiveModeContext {
2035
2038
  const sessionId = this.sessionManager.getSessionId();
2036
2039
  const sessionFile = this.sessionManager.getSessionFile();
2037
2040
  if (sessionId && sessionFile) {
2038
- process.stderr.write(`\n${chalk.dim(`Resume this session with ${APP_NAME} --resume ${sessionId}`)}\n`);
2041
+ process.stderr.write(
2042
+ `\n${chalk.dim("Resume this session with:")}\n${chalk.dim(`${APP_NAME} --resume ${sessionId}`)}\n`,
2043
+ );
2039
2044
  }
2040
2045
 
2041
2046
  await postmortem.quit(0);
@@ -2311,6 +2316,10 @@ export class InteractiveMode implements InteractiveModeContext {
2311
2316
  this.#commandController.handleHotkeysCommand();
2312
2317
  }
2313
2318
 
2319
+ handleHelpCommand(): void {
2320
+ this.#commandController.handleHelpCommand();
2321
+ }
2322
+
2314
2323
  handleToolsCommand(): void {
2315
2324
  this.#commandController.handleToolsCommand();
2316
2325
  }