@gajae-code/coding-agent 0.7.10 → 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 (88) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/dist/types/config/keybindings.d.ts +11 -5
  4. package/dist/types/defaults/gjc-defaults.d.ts +7 -0
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  6. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
  7. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
  8. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  9. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  10. package/dist/types/modes/components/skill-message.d.ts +1 -0
  11. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  12. package/dist/types/modes/controllers/input-controller.d.ts +4 -0
  13. package/dist/types/modes/interactive-mode.d.ts +1 -0
  14. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
  15. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  16. package/dist/types/modes/types.d.ts +1 -0
  17. package/dist/types/sdk.d.ts +1 -1
  18. package/dist/types/session/agent-session.d.ts +8 -5
  19. package/dist/types/slash-commands/types.d.ts +2 -0
  20. package/dist/types/utils/cmux-workspace.d.ts +30 -1
  21. package/dist/types/web/search/index.d.ts +3 -1
  22. package/dist/types/web/search/provider.d.ts +9 -0
  23. package/dist/types/web/search/providers/utils.d.ts +42 -13
  24. package/package.json +7 -7
  25. package/src/cli/update-cli.ts +24 -0
  26. package/src/cli/web-search-cli.ts +2 -5
  27. package/src/cli.ts +1 -1
  28. package/src/commands/launch.ts +1 -1
  29. package/src/commands/ralplan.ts +1 -0
  30. package/src/config/keybindings.ts +14 -5
  31. package/src/config/model-profiles.ts +16 -0
  32. package/src/coordinator-mcp/server.ts +9 -3
  33. package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
  34. package/src/defaults/gjc-defaults.ts +16 -0
  35. package/src/extensibility/extensions/runner.ts +2 -1
  36. package/src/gjc-runtime/launch-tmux.ts +164 -42
  37. package/src/gjc-runtime/ralplan-runtime.ts +22 -6
  38. package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
  39. package/src/gjc-runtime/state-runtime.ts +8 -5
  40. package/src/gjc-runtime/state-writer.ts +5 -5
  41. package/src/internal-urls/docs-index.generated.ts +5 -4
  42. package/src/main.ts +2 -2
  43. package/src/migrate/executor.ts +23 -6
  44. package/src/modes/components/custom-editor.ts +8 -0
  45. package/src/modes/components/model-selector.ts +6 -2
  46. package/src/modes/components/settings-selector.ts +0 -2
  47. package/src/modes/components/skill-message.ts +18 -10
  48. package/src/modes/controllers/command-controller.ts +23 -0
  49. package/src/modes/controllers/input-controller.ts +41 -9
  50. package/src/modes/interactive-mode.ts +16 -7
  51. package/src/modes/prompt-action-autocomplete.ts +23 -1
  52. package/src/modes/slash-command-visibility.ts +9 -0
  53. package/src/modes/types.ts +1 -0
  54. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  55. package/src/modes/utils/ui-helpers.ts +76 -3
  56. package/src/notifications/telegram-daemon.ts +4 -4
  57. package/src/prompts/agents/architect.md +7 -4
  58. package/src/prompts/agents/critic.md +15 -6
  59. package/src/prompts/agents/planner.md +8 -3
  60. package/src/prompts/tools/bash.md +1 -1
  61. package/src/sdk.ts +3 -6
  62. package/src/session/agent-session.ts +60 -17
  63. package/src/slash-commands/builtin-registry.ts +30 -10
  64. package/src/slash-commands/types.ts +2 -0
  65. package/src/tools/bash.ts +24 -8
  66. package/src/utils/cmux-workspace.ts +134 -7
  67. package/src/utils/title-generator.ts +1 -1
  68. package/src/web/kagi.ts +1 -1
  69. package/src/web/parallel.ts +2 -2
  70. package/src/web/search/index.ts +118 -45
  71. package/src/web/search/provider.ts +65 -0
  72. package/src/web/search/providers/anthropic.ts +1 -1
  73. package/src/web/search/providers/brave.ts +1 -1
  74. package/src/web/search/providers/codex.ts +1 -1
  75. package/src/web/search/providers/duckduckgo.ts +1 -1
  76. package/src/web/search/providers/exa.ts +1 -1
  77. package/src/web/search/providers/gemini.ts +7 -3
  78. package/src/web/search/providers/jina.ts +1 -1
  79. package/src/web/search/providers/kimi.ts +6 -1
  80. package/src/web/search/providers/openai-compatible.ts +1 -1
  81. package/src/web/search/providers/parallel.ts +1 -1
  82. package/src/web/search/providers/perplexity.ts +2 -2
  83. package/src/web/search/providers/searxng.ts +1 -1
  84. package/src/web/search/providers/synthetic.ts +1 -1
  85. package/src/web/search/providers/tavily.ts +1 -1
  86. package/src/web/search/providers/utils.ts +67 -17
  87. package/src/web/search/providers/xai.ts +1 -1
  88. package/src/web/search/providers/zai.ts +1 -1
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();
@@ -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
@@ -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
  }
@@ -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);
@@ -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
  }
@@ -29,6 +29,8 @@ interface PromptActionAutocompleteOptions {
29
29
  copyCurrentLine: () => void;
30
30
  copyPrompt: () => void;
31
31
  pasteImage: () => void;
32
+ newSession: () => void;
33
+ showHelp: () => void;
32
34
  scrollTmuxToPreviousUserInput: () => void;
33
35
  undo: (prefix: string) => void;
34
36
  moveCursorToMessageEnd: () => void;
@@ -101,6 +103,12 @@ function mergeAutocompleteSuggestions(
101
103
 
102
104
  return { items, prefix: primary.prefix };
103
105
  }
106
+ const ADVANCED_SLASH_COMMAND_PRIORITIES = new Map<string, number>([["grok-build-usage", -100]]);
107
+
108
+ function getSlashCommandPriority(command: SlashCommand | undefined, item: AutocompleteItem): number {
109
+ if (command?.priority !== undefined) return command.priority;
110
+ return ADVANCED_SLASH_COMMAND_PRIORITIES.get(item.value) ?? 0;
111
+ }
104
112
 
105
113
  function sortSlashCommandSuggestions(
106
114
  suggestions: { items: AutocompleteItem[]; prefix: string } | null,
@@ -123,7 +131,7 @@ function sortSlashCommandSuggestions(
123
131
  index,
124
132
  commandIndex,
125
133
  matchRank: getSlashCommandMatchRank(query, lowerName),
126
- priority: command?.priority ?? 0,
134
+ priority: getSlashCommandPriority(command, item),
127
135
  score: Math.max(nameScore, descScore),
128
136
  };
129
137
  })
@@ -358,6 +366,20 @@ export function createPromptActionAutocompleteProvider(
358
366
  ): PromptActionAutocompleteProvider {
359
367
  const editorKeybindings = getKeybindings();
360
368
  const actions: PromptActionDefinition[] = [
369
+ {
370
+ id: "new-session",
371
+ label: "Start new session",
372
+ description: formatKeyHints(options.keybindings.getKeys("app.session.new")) || "/new",
373
+ keywords: ["new", "session", "fresh", "clear", "start", "conversation"],
374
+ execute: options.newSession,
375
+ },
376
+ {
377
+ id: "help",
378
+ label: "Open command help",
379
+ description: "/help",
380
+ keywords: ["help", "commands", "command", "palette", "shortcuts", "beginner"],
381
+ execute: options.showHelp,
382
+ },
361
383
  {
362
384
  id: "copy-line",
363
385
  label: "Copy current line",
@@ -0,0 +1,9 @@
1
+ const BEGINNER_HIDDEN_EXTENSION_COMMANDS = new Set(["grok-build-usage"]);
2
+
3
+ export function shouldShowExtensionCommand(name: string, activeProvider: string | undefined): boolean {
4
+ if (!BEGINNER_HIDDEN_EXTENSION_COMMANDS.has(name)) return true;
5
+ if (name === "grok-build-usage") {
6
+ return activeProvider === "grok-build" || Boolean(process.env.GROK_CLI_OAUTH_TOKEN);
7
+ }
8
+ return true;
9
+ }
@@ -209,6 +209,7 @@ export interface InteractiveModeContext {
209
209
  handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
210
210
  handleChangelogCommand(showFull?: boolean): Promise<void>;
211
211
  handleHotkeysCommand(): void;
212
+ handleHelpCommand(): void;
212
213
  handleToolsCommand(): void;
213
214
  handleContextCommand(): void;
214
215
  handleDumpCommand(): void;
@@ -38,6 +38,8 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
38
38
  `| \`${appKey(bindings, "app.clear")}\` | Clear editor (first) / exit (second) |`,
39
39
  `| \`${appKey(bindings, "app.exit")}\` | Exit (when editor is empty) |`,
40
40
  `| \`${appKey(bindings, "app.suspend")}\` | Suspend to background |`,
41
+ `| \`${appKey(bindings, "app.commandPalette.open")}\` | Open command palette |`,
42
+ `| \`${appKey(bindings, "app.session.new")}\` | Start a fresh session |`,
41
43
  `| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
42
44
  `| \`${appKey(bindings, "app.model.cycleForward")}\` | Cycle configured model roles |`,
43
45
  `| \`${appKey(bindings, "app.model.cycleBackward")}\` | Cycle configured model roles temporarily |`,
@@ -51,8 +53,9 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
51
53
  `| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
52
54
  `| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image from clipboard |`,
53
55
  `| \`${appKey(bindings, "app.stt.toggle")}\` | Toggle speech-to-text recording |`,
54
- "| `#` | Open prompt actions |",
55
- "| `/` | Slash commands |",
56
+ "| `?` | Show help / shortcuts |",
57
+ "| `#` | Prompt actions (command-palette style actions) |",
58
+ "| `/` | Slash commands (try `/help` or `/new`) |",
56
59
  "| `!` | Run bash command |",
57
60
  "| `!!` | Run bash command (excluded from context) |",
58
61
  "| `$` | Run Python in shared kernel |",
@@ -2,6 +2,8 @@ import type { AgentMessage } from "@gajae-code/agent-core";
2
2
  import type { AssistantMessage, ImageContent, Message } from "@gajae-code/ai";
3
3
  import { type Component, Spacer, Text, TruncatedText } from "@gajae-code/tui";
4
4
  import { settings } from "../../config/settings";
5
+ import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
6
+ import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
5
7
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
6
8
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
7
9
  import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
@@ -610,9 +612,80 @@ export class UiHelpers {
610
612
  this.ctx.updatePendingMessagesDisplay();
611
613
  this.ctx.showStatus("Queued message for after compaction");
612
614
  }
615
+ #hasSkillInvocations(text: string): boolean {
616
+ return parseSkillInvocations(text, this.ctx.skillCommands ?? new Map()).length > 0;
617
+ }
618
+
619
+ #isCompactionCommandMessage(text: string): boolean {
620
+ return this.#hasSkillInvocations(text) || this.isKnownSlashCommand(text);
621
+ }
622
+
623
+ async #deliverQueuedSkillMessage(message: CompactionQueuedMessage): Promise<boolean> {
624
+ const invocations = parseSkillInvocations(message.text, this.ctx.skillCommands ?? new Map());
625
+ if (invocations.length === 0) {
626
+ return false;
627
+ }
628
+
629
+ for (let index = 0; index < invocations.length; index += 1) {
630
+ const invocation = invocations[index];
631
+ if (!invocation) continue;
632
+
633
+ const activationResult = await resolveSubskillActivationForSkillInvocation({
634
+ cwd: this.ctx.sessionManager.getCwd(),
635
+ sessionId: this.ctx.session.sessionId,
636
+ skillName: invocation.skill.name,
637
+ args: invocation.args,
638
+ });
639
+ const built = await buildSkillPromptMessage(invocation.skill, activationResult.cleanedArgs, {
640
+ subskillActivation: activationResult.activation,
641
+ subskillActivationSet: activationResult.activeSubskillsToPersist,
642
+ cwd: this.ctx.sessionManager.getCwd(),
643
+ sessionId: this.ctx.session.sessionId,
644
+ });
645
+ const details: SkillPromptDetails = built.details;
646
+ const displayText = `/${invocation.commandName}${activationResult.cleanedArgs ? ` ${activationResult.cleanedArgs}` : ""}`;
647
+
648
+ if (this.ctx.session.isStreaming) {
649
+ const tag = this.ctx.session.enqueueCustomMessageDisplay(displayText, message.mode);
650
+ details.__pendingDisplayTag = tag;
651
+ }
652
+
653
+ const isLast = index === invocations.length - 1;
654
+ if (!this.ctx.session.isStreaming && !isLast) {
655
+ await this.ctx.session.sendCustomMessage({
656
+ customType: SKILL_PROMPT_MESSAGE_TYPE,
657
+ content: built.message,
658
+ display: true,
659
+ details,
660
+ attribution: "user",
661
+ });
662
+ continue;
663
+ }
613
664
 
665
+ await this.ctx.session.promptCustomMessage(
666
+ {
667
+ customType: SKILL_PROMPT_MESSAGE_TYPE,
668
+ content: built.message,
669
+ display: true,
670
+ details,
671
+ attribution: "user",
672
+ },
673
+ { streamingBehavior: message.mode },
674
+ );
675
+ }
676
+
677
+ if (this.ctx.session.isStreaming) {
678
+ this.ctx.updatePendingMessagesDisplay();
679
+ this.ctx.ui.requestRender();
680
+ }
681
+
682
+ return true;
683
+ }
614
684
  async #deliverQueuedMessage(message: CompactionQueuedMessage): Promise<void> {
615
- if (this.ctx.isKnownSlashCommand(message.text)) {
685
+ if (await this.#deliverQueuedSkillMessage(message)) {
686
+ return;
687
+ }
688
+ if (this.isKnownSlashCommand(message.text)) {
616
689
  await this.ctx.session.prompt(message.text);
617
690
  return;
618
691
  }
@@ -671,14 +744,14 @@ export class UiHelpers {
671
744
 
672
745
  let firstPromptIndex = -1;
673
746
  for (let i = 0; i < queuedMessages.length; i++) {
674
- if (!this.ctx.isKnownSlashCommand(queuedMessages[i].text)) {
747
+ if (!this.#isCompactionCommandMessage(queuedMessages[i].text)) {
675
748
  firstPromptIndex = i;
676
749
  break;
677
750
  }
678
751
  }
679
752
  if (firstPromptIndex === -1) {
680
753
  for (const message of queuedMessages) {
681
- await this.ctx.session.prompt(message.text);
754
+ await this.#deliverQueuedMessage(message);
682
755
  }
683
756
  return;
684
757
  }
@@ -13,7 +13,7 @@ import { parseInThreadConfigCommand } from "./config-commands";
13
13
  import { daemonPaths } from "./daemon-paths";
14
14
  import {
15
15
  buildCompactChoiceGrid,
16
- pre,
16
+ code,
17
17
  splitTelegramHtml,
18
18
  TELEGRAM_MESSAGE_LIMIT,
19
19
  TELEGRAM_PARSE_MODE,
@@ -1117,10 +1117,10 @@ export class TelegramNotificationDaemon {
1117
1117
  limit: 10,
1118
1118
  includeInternal: false,
1119
1119
  });
1120
- const lines = recent.length
1121
- ? recent.map(e => `• ${e.sessionId}${e.path ? ` (${e.path})` : ""}`).join("\n")
1120
+ const body = recent.length
1121
+ ? recent.map(e => `• ${code(e.sessionId)}${e.path ? ` (${code(e.path)})` : ""}`).join("\n")
1122
1122
  : "No recent sessions.";
1123
- await replyHtml(pre(lines));
1123
+ await replyHtml(body);
1124
1124
  return true;
1125
1125
  }
1126
1126