@gajae-code/coding-agent 0.7.10 → 0.8.0
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 +58 -0
- package/README.md +2 -2
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +25 -2
- package/dist/types/defaults/gjc-defaults.d.ts +7 -0
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- 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/session-state-sidecar.d.ts +5 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/components/status-line.d.ts +8 -0
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +5 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -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/system-prompt.d.ts +7 -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/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +10 -10
- package/src/capability/mcp.ts +2 -0
- 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/mcp-schema.json +5 -1
- package/src/config/model-profiles.ts +16 -0
- package/src/config/settings-schema.ts +18 -2
- package/src/coordinator-mcp/server.ts +15 -3
- package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
- package/src/defaults/gjc-defaults.ts +16 -0
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- 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/session-state-sidecar.ts +167 -24
- package/src/gjc-runtime/state-runtime.ts +8 -5
- package/src/gjc-runtime/state-writer.ts +5 -5
- package/src/gjc-runtime/team-runtime.ts +56 -51
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/main.ts +2 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/components/agent-dashboard.ts +1 -34
- package/src/modes/components/custom-editor.ts +26 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/model-selector.ts +64 -4
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +13 -2
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/components/status-line.ts +215 -86
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +171 -35
- package/src/modes/controllers/command-controller.ts +23 -0
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/input-controller.ts +192 -39
- package/src/modes/controllers/selector-controller.ts +25 -28
- package/src/modes/interactive-mode.ts +36 -30
- package/src/modes/prompt-action-autocomplete.ts +23 -1
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/index.ts +20 -1
- package/src/notifications/lifecycle-control-runtime.ts +12 -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/system/system-prompt.md +15 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +7 -6
- package/src/session/agent-session.ts +81 -18
- package/src/session/session-manager.ts +58 -18
- package/src/slash-commands/builtin-registry.ts +30 -10
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +39 -14
- package/src/tools/index.ts +8 -1
- 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/workflow/workflow-intent-diff.ts +199 -0
- package/dist/types/discovery/vscode.d.ts +0 -1
- package/src/discovery/vscode.ts +0 -105
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,
|
|
@@ -23,13 +23,11 @@ import {
|
|
|
23
23
|
extractPrintableText,
|
|
24
24
|
Input,
|
|
25
25
|
matchesKey,
|
|
26
|
-
padding,
|
|
27
26
|
replaceTabs,
|
|
28
27
|
resolveTerminalColumns,
|
|
29
28
|
Spacer,
|
|
30
29
|
Text,
|
|
31
30
|
truncateToWidth,
|
|
32
|
-
visibleWidth,
|
|
33
31
|
wrapTextWithAnsi,
|
|
34
32
|
} from "@gajae-code/tui";
|
|
35
33
|
import { isEnoent, prompt } from "@gajae-code/utils";
|
|
@@ -52,6 +50,7 @@ import { shortenPath } from "../../tools/render-utils";
|
|
|
52
50
|
import { theme } from "../theme/theme";
|
|
53
51
|
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
|
54
52
|
import { DynamicBorder } from "./dynamic-border";
|
|
53
|
+
import { TwoColumnBody } from "./two-column-body";
|
|
55
54
|
|
|
56
55
|
type SourceTabId = "all" | AgentSource;
|
|
57
56
|
type AgentScope = "project" | "user";
|
|
@@ -296,38 +295,6 @@ class AgentInspectorPane implements Component {
|
|
|
296
295
|
invalidate(): void {}
|
|
297
296
|
}
|
|
298
297
|
|
|
299
|
-
class TwoColumnBody implements Component {
|
|
300
|
-
constructor(
|
|
301
|
-
private readonly leftPane: AgentListPane,
|
|
302
|
-
private readonly rightPane: AgentInspectorPane,
|
|
303
|
-
private readonly maxHeight: number,
|
|
304
|
-
) {}
|
|
305
|
-
|
|
306
|
-
render(width: number): string[] {
|
|
307
|
-
const leftWidth = Math.floor(width * 0.5);
|
|
308
|
-
const rightWidth = width - leftWidth - 3;
|
|
309
|
-
const leftLines = this.leftPane.render(leftWidth);
|
|
310
|
-
const rightLines = this.rightPane.render(rightWidth);
|
|
311
|
-
const lineCount = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
|
312
|
-
const out: string[] = [];
|
|
313
|
-
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
314
|
-
|
|
315
|
-
for (let i = 0; i < lineCount; i++) {
|
|
316
|
-
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
317
|
-
const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
|
|
318
|
-
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
|
319
|
-
out.push(leftPadded + separator + right);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return out;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
invalidate(): void {
|
|
326
|
-
this.leftPane.invalidate?.();
|
|
327
|
-
this.rightPane.invalidate?.();
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
298
|
export class AgentDashboard extends Container {
|
|
332
299
|
#settingsManager: Settings | null = null;
|
|
333
300
|
#allAgents: DashboardAgent[] = [];
|
|
@@ -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;
|
|
@@ -100,6 +103,10 @@ export class CustomEditor extends Editor {
|
|
|
100
103
|
onQueue?: () => void;
|
|
101
104
|
/** Called when Caps Lock is pressed. */
|
|
102
105
|
onCapsLock?: () => void;
|
|
106
|
+
/** Called when PageUp/PageDown should scroll the transcript viewport instead of prompt history. */
|
|
107
|
+
onViewportPageScroll?: (direction: -1 | 1) => void;
|
|
108
|
+
/** Called before regular composer input should return the transcript viewport to the live bottom. */
|
|
109
|
+
onViewportFollowLive?: () => void;
|
|
103
110
|
|
|
104
111
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
105
112
|
#customKeyHandlers = new Map<KeyId, () => boolean | undefined>();
|
|
@@ -240,6 +247,20 @@ export class CustomEditor extends Editor {
|
|
|
240
247
|
return;
|
|
241
248
|
}
|
|
242
249
|
}
|
|
250
|
+
if (!this.isShowingAutocomplete()) {
|
|
251
|
+
if (matchesKey(data, "pageUp") && this.onViewportPageScroll) {
|
|
252
|
+
this.onViewportPageScroll(-1);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (matchesKey(data, "pageDown") && this.onViewportPageScroll) {
|
|
256
|
+
this.onViewportPageScroll(1);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!matchesKey(data, "pageUp") && !matchesKey(data, "pageDown")) {
|
|
262
|
+
this.onViewportFollowLive?.();
|
|
263
|
+
}
|
|
243
264
|
// Intercept configured image paste (async - fires and handles result)
|
|
244
265
|
if (this.#matchesAction(data, "app.clipboard.pasteImage") && this.onPasteImage) {
|
|
245
266
|
void this.onPasteImage();
|
|
@@ -288,6 +309,11 @@ export class CustomEditor extends Editor {
|
|
|
288
309
|
return;
|
|
289
310
|
}
|
|
290
311
|
|
|
312
|
+
// Intercept configured command palette shortcut before model cycling (Ctrl+P default).
|
|
313
|
+
if (this.#matchesAction(data, "app.commandPalette.open") && this.onOpenCommandPalette) {
|
|
314
|
+
this.onOpenCommandPalette();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
291
317
|
// Intercept configured backward model cycling (check before forward cycling)
|
|
292
318
|
if (this.#matchesAction(data, "app.model.cycleBackward") && this.onCycleModelBackward) {
|
|
293
319
|
this.onCycleModelBackward();
|
|
@@ -11,20 +11,12 @@
|
|
|
11
11
|
* - Space: Toggle selected item (or master switch)
|
|
12
12
|
* - Esc: Close dashboard (clears search first if active)
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
15
|
-
type Component,
|
|
16
|
-
Container,
|
|
17
|
-
matchesKey,
|
|
18
|
-
padding,
|
|
19
|
-
Spacer,
|
|
20
|
-
Text,
|
|
21
|
-
truncateToWidth,
|
|
22
|
-
visibleWidth,
|
|
23
|
-
} from "@gajae-code/tui";
|
|
14
|
+
import { Container, matchesKey, Spacer, Text } from "@gajae-code/tui";
|
|
24
15
|
import { Settings } from "../../../config/settings";
|
|
25
16
|
import { DynamicBorder } from "../../../modes/components/dynamic-border";
|
|
26
17
|
import { theme } from "../../../modes/theme/theme";
|
|
27
18
|
import { matchesAppInterrupt } from "../../../modes/utils/keybinding-matchers";
|
|
19
|
+
import { TwoColumnBody } from "../two-column-body";
|
|
28
20
|
import { ExtensionList } from "./extension-list";
|
|
29
21
|
import { InspectorPanel } from "./inspector-panel";
|
|
30
22
|
import {
|
|
@@ -310,41 +302,3 @@ export class ExtensionDashboard extends Container {
|
|
|
310
302
|
}
|
|
311
303
|
}
|
|
312
304
|
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Two-column body component for side-by-side rendering.
|
|
316
|
-
*/
|
|
317
|
-
class TwoColumnBody implements Component {
|
|
318
|
-
constructor(
|
|
319
|
-
private readonly leftPane: ExtensionList,
|
|
320
|
-
private readonly rightPane: InspectorPanel,
|
|
321
|
-
private readonly maxHeight: number,
|
|
322
|
-
) {}
|
|
323
|
-
|
|
324
|
-
render(width: number): string[] {
|
|
325
|
-
const leftWidth = Math.floor(width * 0.5);
|
|
326
|
-
const rightWidth = Math.max(0, width - leftWidth - 3);
|
|
327
|
-
|
|
328
|
-
const leftLines = this.leftPane.render(leftWidth);
|
|
329
|
-
const rightLines = this.rightPane.render(rightWidth);
|
|
330
|
-
|
|
331
|
-
// Limit to maxHeight lines
|
|
332
|
-
const numLines = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
|
333
|
-
const combined: string[] = [];
|
|
334
|
-
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
335
|
-
|
|
336
|
-
for (let i = 0; i < numLines; i++) {
|
|
337
|
-
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
338
|
-
const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
|
|
339
|
-
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
|
340
|
-
combined.push(leftPadded + separator + right);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return combined;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
invalidate(): void {
|
|
347
|
-
this.leftPane.invalidate?.();
|
|
348
|
-
this.rightPane.invalidate?.();
|
|
349
|
-
}
|
|
350
|
-
}
|
|
@@ -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,16 +1025,56 @@ 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
|
}
|
|
1028
1032
|
|
|
1033
|
+
/**
|
|
1034
|
+
* "What is this session running right now" summary for the preset landing
|
|
1035
|
+
* header: active preset (when one is applied), the effective current model
|
|
1036
|
+
* and thinking level, and one line per assigned role (default, executor,
|
|
1037
|
+
* planner, critic, architect).
|
|
1038
|
+
*/
|
|
1039
|
+
#formatCurrentSessionLines(): string[] {
|
|
1040
|
+
const lines: string[] = [];
|
|
1041
|
+
const parts: string[] = [];
|
|
1042
|
+
if (this.#activeModelProfile) {
|
|
1043
|
+
const profile = this.#getProfileByName(this.#activeModelProfile);
|
|
1044
|
+
const displayName = profile ? getModelProfilePresentation(profile).displayName : this.#activeModelProfile;
|
|
1045
|
+
parts.push(`preset ${theme.fg("accent", displayName)}`);
|
|
1046
|
+
}
|
|
1047
|
+
if (this.#currentModel) {
|
|
1048
|
+
parts.push(this.#formatAssignedModelLabel(this.#currentModel, this.#currentThinkingLevel));
|
|
1049
|
+
}
|
|
1050
|
+
if (parts.length > 0) lines.push(theme.fg("muted", `Current: ${parts.join(" · ")}`));
|
|
1051
|
+
for (const role of PROFILE_ROLE_PREVIEW_ORDER) {
|
|
1052
|
+
const assigned = this.#roles[role];
|
|
1053
|
+
if (!assigned) continue;
|
|
1054
|
+
const label = GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase();
|
|
1055
|
+
lines.push(
|
|
1056
|
+
theme.fg("dim", ` ${label}: ${this.#formatAssignedModelLabel(assigned.model, assigned.thinkingLevel)}`),
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
return lines;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
#formatAssignedModelLabel(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
1063
|
+
let label = `${model.provider}/${model.id}`;
|
|
1064
|
+
if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
|
|
1065
|
+
label += ` (${getThinkingLevelMetadata(thinkingLevel).label})`;
|
|
1066
|
+
}
|
|
1067
|
+
return label;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1029
1070
|
#renderPresetLanding(): void {
|
|
1030
1071
|
this.#headerContainer.clear();
|
|
1031
1072
|
this.#tabBar = null;
|
|
1032
1073
|
this.#listContainer.clear();
|
|
1033
1074
|
this.#headerContainer.addChild(new Text(theme.fg("accent", "Model presets"), 0, 0));
|
|
1075
|
+
for (const line of this.#formatCurrentSessionLines()) {
|
|
1076
|
+
this.#headerContainer.addChild(new Text(line, 0, 0));
|
|
1077
|
+
}
|
|
1034
1078
|
const rows = this.#getPresetRows();
|
|
1035
1079
|
for (let i = 0; i < rows.length; i++) {
|
|
1036
1080
|
const row = rows[i];
|
|
@@ -1061,17 +1105,23 @@ export class ModelSelectorComponent extends Container {
|
|
|
1061
1105
|
if (row.kind === "group") {
|
|
1062
1106
|
const authenticated = this.#isPresetGroupUsable(row.profiles);
|
|
1063
1107
|
const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
|
|
1108
|
+
const containsActive =
|
|
1109
|
+
this.#activeModelProfile !== undefined &&
|
|
1110
|
+
row.profiles.some(profile => profile.name === this.#activeModelProfile);
|
|
1064
1111
|
const label = `${mark} ${row.groupId}`;
|
|
1065
1112
|
const renderedLabel = selected ? theme.fg("accent", label) : authenticated ? label : theme.fg("dim", label);
|
|
1066
|
-
|
|
1113
|
+
const activeSuffix = containsActive ? theme.fg("muted", " (current)") : "";
|
|
1114
|
+
this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}${activeSuffix}`, 0, 0));
|
|
1067
1115
|
continue;
|
|
1068
1116
|
}
|
|
1069
1117
|
const presentation = getModelProfilePresentation(row.profile);
|
|
1070
1118
|
const authenticated = this.#isPresetAuthenticated(row.profile);
|
|
1071
1119
|
const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
|
|
1120
|
+
const isActive = row.profile.name === this.#activeModelProfile;
|
|
1072
1121
|
const label = ` ${mark} ${presentation.displayName}`;
|
|
1073
1122
|
const renderedLabel = selected ? theme.fg("accent", label) : authenticated ? label : theme.fg("dim", label);
|
|
1074
|
-
|
|
1123
|
+
const activeSuffix = isActive ? theme.fg("muted", " (current)") : "";
|
|
1124
|
+
this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}${activeSuffix}`, 0, 0));
|
|
1075
1125
|
}
|
|
1076
1126
|
if (this.#presetLoginHint) {
|
|
1077
1127
|
this.#listContainer.addChild(new Spacer(1));
|
|
@@ -1534,7 +1584,17 @@ export class ModelSelectorComponent extends Container {
|
|
|
1534
1584
|
const missing = this.#getMissingProviders(row.profiles);
|
|
1535
1585
|
this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
|
|
1536
1586
|
this.#renderPresetLanding();
|
|
1587
|
+
return;
|
|
1537
1588
|
}
|
|
1589
|
+
// Enter toggles a usable group's expansion so drilling into presets
|
|
1590
|
+
// works without reaching for the arrow keys (right/left still work).
|
|
1591
|
+
if (this.#expandedPresetProviderId === row.groupId) {
|
|
1592
|
+
this.#collapseSelectedPresetProvider();
|
|
1593
|
+
} else {
|
|
1594
|
+
this.#expandSelectedPresetProvider();
|
|
1595
|
+
}
|
|
1596
|
+
this.#presetLoginHint = undefined;
|
|
1597
|
+
this.#renderPresetLanding();
|
|
1538
1598
|
return;
|
|
1539
1599
|
}
|
|
1540
1600
|
const missing = this.#getMissingProviders(row.profile);
|
|
@@ -50,6 +50,14 @@ class SessionList implements Component {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
#clampSelectedIndex(): void {
|
|
54
|
+
if (this.#filteredSessions.length === 0) {
|
|
55
|
+
this.#selectedIndex = 0;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.#selectedIndex = Math.max(0, Math.min(this.#selectedIndex, this.#filteredSessions.length - 1));
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
#filterSessions(query: string): void {
|
|
54
62
|
this.#filteredSessions = fuzzyFilter(this.allSessions, query, session => {
|
|
55
63
|
const parts = [
|
|
@@ -62,7 +70,7 @@ class SessionList implements Component {
|
|
|
62
70
|
];
|
|
63
71
|
return parts.filter(Boolean).join(" ");
|
|
64
72
|
});
|
|
65
|
-
this.#
|
|
73
|
+
this.#clampSelectedIndex();
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
removeSession(sessionPath: string): void {
|
|
@@ -71,10 +79,6 @@ class SessionList implements Component {
|
|
|
71
79
|
this.allSessions.splice(index, 1);
|
|
72
80
|
// Re-filter to update filteredSessions
|
|
73
81
|
this.#filterSessions(this.#searchInput.getValue());
|
|
74
|
-
// Adjust selectedIndex if we deleted the last item or beyond
|
|
75
|
-
if (this.#selectedIndex >= this.#filteredSessions.length) {
|
|
76
|
-
this.#selectedIndex = Math.max(0, this.#filteredSessions.length - 1);
|
|
77
|
-
}
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
invalidate(): void {
|
|
@@ -182,6 +186,12 @@ class SessionList implements Component {
|
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
handleInput(keyData: string): void {
|
|
189
|
+
const moveSelection = (delta: number): void => {
|
|
190
|
+
if (this.#filteredSessions.length === 0) return;
|
|
191
|
+
this.#selectedIndex += delta;
|
|
192
|
+
this.#clampSelectedIndex();
|
|
193
|
+
};
|
|
194
|
+
|
|
185
195
|
// Delete key - request delete confirmation from parent
|
|
186
196
|
if (matchesKey(keyData, "delete")) {
|
|
187
197
|
const selected = this.#filteredSessions[this.#selectedIndex];
|
|
@@ -193,22 +203,22 @@ class SessionList implements Component {
|
|
|
193
203
|
|
|
194
204
|
// Up arrow
|
|
195
205
|
if (matchesKey(keyData, "up")) {
|
|
196
|
-
|
|
206
|
+
moveSelection(-1);
|
|
197
207
|
return;
|
|
198
208
|
}
|
|
199
209
|
// Down arrow
|
|
200
210
|
if (matchesKey(keyData, "down")) {
|
|
201
|
-
|
|
211
|
+
moveSelection(1);
|
|
202
212
|
return;
|
|
203
213
|
}
|
|
204
214
|
// Page up - jump up by maxVisible items
|
|
205
215
|
if (matchesKey(keyData, "pageUp")) {
|
|
206
|
-
|
|
216
|
+
moveSelection(-this.#maxVisible);
|
|
207
217
|
return;
|
|
208
218
|
}
|
|
209
219
|
// Page down - jump down by maxVisible items
|
|
210
220
|
if (matchesKey(keyData, "pageDown")) {
|
|
211
|
-
|
|
221
|
+
moveSelection(this.#maxVisible);
|
|
212
222
|
return;
|
|
213
223
|
}
|
|
214
224
|
// Enter
|
|
@@ -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
|
|
@@ -672,6 +670,7 @@ export interface StatusLinePreviewSettings {
|
|
|
672
670
|
segmentOptions?: StatusLineSegmentOptions;
|
|
673
671
|
previewHighlightSegment?: StatusLineSegmentId;
|
|
674
672
|
sessionAccent?: boolean;
|
|
673
|
+
maxRows?: number;
|
|
675
674
|
}
|
|
676
675
|
|
|
677
676
|
export interface SettingsCallbacks {
|
|
@@ -910,6 +909,18 @@ export class SettingsSelectorComponent extends Container {
|
|
|
910
909
|
this.callbacks.onStatusLinePreview?.({ separator, previewHighlightSegment: undefined });
|
|
911
910
|
this.#updateStatusPreview();
|
|
912
911
|
};
|
|
912
|
+
} else if (def.path === "statusLine.maxRows") {
|
|
913
|
+
onPreview = value => {
|
|
914
|
+
this.callbacks.onStatusLinePreview?.({ maxRows: Number(value) });
|
|
915
|
+
this.#updateStatusPreview();
|
|
916
|
+
};
|
|
917
|
+
onPreviewCancel = () => {
|
|
918
|
+
this.callbacks.onStatusLinePreview?.({
|
|
919
|
+
maxRows: settings.get("statusLine.maxRows"),
|
|
920
|
+
previewHighlightSegment: undefined,
|
|
921
|
+
});
|
|
922
|
+
this.#updateStatusPreview();
|
|
923
|
+
};
|
|
913
924
|
}
|
|
914
925
|
|
|
915
926
|
// Provide status line preview for theme selection
|
|
@@ -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
|
}
|