@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.
- package/CHANGELOG.md +47 -0
- package/README.md +2 -0
- package/dist/types/config/keybindings.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/defaults/gjc-defaults.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +10 -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/gjc-runtime/ultragoal-guard.d.ts +4 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- 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/shared/agent-wire/deep-interview-gate.d.ts +20 -6
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.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/tools/ask.d.ts +2 -0
- package/dist/types/utils/cmux-workspace.d.ts +52 -0
- package/dist/types/web/search/index.d.ts +4 -2
- package/dist/types/web/search/provider.d.ts +14 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/dist/types/web/search/render.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +24 -0
- package/src/cli/web-search-cli.ts +5 -7
- 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/config/settings-schema.ts +11 -1
- package/src/coordinator-mcp/server.ts +9 -3
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +26 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
- package/src/defaults/gjc-defaults.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/gjc-runtime/launch-tmux.ts +209 -26
- 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/gjc-runtime/ultragoal-guard.ts +139 -19
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
- package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
- package/src/gjc-runtime/workflow-manifest.ts +0 -1
- package/src/hooks/skill-state.ts +33 -73
- package/src/internal-urls/docs-index.generated.ts +6 -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/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +6 -2
- package/src/modes/components/settings-selector.ts +47 -5
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/controllers/command-controller.ts +23 -0
- package/src/modes/controllers/extension-ui-controller.ts +18 -2
- 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/shared/agent-wire/deep-interview-gate.ts +53 -9
- 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/html-format.ts +75 -1
- package/src/notifications/index.ts +12 -1
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +68 -25
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- 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/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/sdk.ts +6 -11
- 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/ask.ts +136 -16
- package/src/tools/bash.ts +24 -8
- package/src/utils/cmux-workspace.ts +233 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +137 -52
- package/src/web/search/provider.ts +80 -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/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
|
|
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();
|
|
@@ -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
|
-
|
|
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.#
|
|
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
|
|
@@ -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
|
|
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
|
}
|
|
@@ -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 =
|
|
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:
|
|
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.
|
|
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
|
}
|