@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.
- package/CHANGELOG.md +25 -0
- package/README.md +1 -1
- package/dist/types/config/keybindings.d.ts +11 -5
- package/dist/types/defaults/gjc-defaults.d.ts +7 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
- package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +4 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +8 -5
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/utils/cmux-workspace.d.ts +30 -1
- package/dist/types/web/search/index.d.ts +3 -1
- package/dist/types/web/search/provider.d.ts +9 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/package.json +7 -7
- package/src/cli/update-cli.ts +24 -0
- package/src/cli/web-search-cli.ts +2 -5
- package/src/cli.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/ralplan.ts +1 -0
- package/src/config/keybindings.ts +14 -5
- package/src/config/model-profiles.ts +16 -0
- package/src/coordinator-mcp/server.ts +9 -3
- package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
- package/src/defaults/gjc-defaults.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/gjc-runtime/launch-tmux.ts +164 -42
- package/src/gjc-runtime/ralplan-runtime.ts +22 -6
- package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +8 -5
- package/src/gjc-runtime/state-writer.ts +5 -5
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/main.ts +2 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/model-selector.ts +6 -2
- package/src/modes/components/settings-selector.ts +0 -2
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/controllers/command-controller.ts +23 -0
- package/src/modes/controllers/input-controller.ts +41 -9
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/prompt-action-autocomplete.ts +23 -1
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/telegram-daemon.ts +4 -4
- package/src/prompts/agents/architect.md +7 -4
- package/src/prompts/agents/critic.md +15 -6
- package/src/prompts/agents/planner.md +8 -3
- package/src/prompts/tools/bash.md +1 -1
- package/src/sdk.ts +3 -6
- package/src/session/agent-session.ts +60 -17
- package/src/slash-commands/builtin-registry.ts +30 -10
- package/src/slash-commands/types.ts +2 -0
- package/src/tools/bash.ts +24 -8
- package/src/utils/cmux-workspace.ts +134 -7
- package/src/utils/title-generator.ts +1 -1
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +118 -45
- package/src/web/search/provider.ts +65 -0
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/brave.ts +1 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/gemini.ts +7 -3
- package/src/web/search/providers/jina.ts +1 -1
- package/src/web/search/providers/kimi.ts +6 -1
- package/src/web/search/providers/openai-compatible.ts +1 -1
- package/src/web/search/providers/parallel.ts +1 -1
- package/src/web/search/providers/perplexity.ts +2 -2
- package/src/web/search/providers/searxng.ts +1 -1
- package/src/web/search/providers/synthetic.ts +1 -1
- package/src/web/search/providers/tavily.ts +1 -1
- package/src/web/search/providers/utils.ts +67 -17
- package/src/web/search/providers/xai.ts +1 -1
- 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
|
|
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("(
|
|
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) {
|
package/src/migrate/executor.ts
CHANGED
|
@@ -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
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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.#
|
|
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.#
|
|
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
|
|
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(
|
|
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 : " ",
|
|
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,
|
|
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.
|
|
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
|
|
607
|
-
//
|
|
608
|
-
// skill
|
|
609
|
-
//
|
|
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
|
|
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
|
-
)
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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(
|
|
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
|
|
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
|
+
}
|
package/src/modes/types.ts
CHANGED
|
@@ -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
|
-
"|
|
|
55
|
-
"|
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
1123
|
+
await replyHtml(body);
|
|
1124
1124
|
return true;
|
|
1125
1125
|
}
|
|
1126
1126
|
|