@gajae-code/coding-agent 0.9.1 → 0.9.2
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 +24 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/model-profile-activation.d.ts +7 -7
- package/dist/types/config/model-registry.d.ts +7 -3
- package/dist/types/config/model-resolver.d.ts +2 -2
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +9 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -2
- package/dist/types/modes/components/thinking-selector.d.ts +6 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +29 -0
- package/dist/types/notifications/index.d.ts +12 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/threaded-render.d.ts +2 -0
- package/dist/types/notifications/topic-registry.d.ts +4 -5
- package/dist/types/runtime-credential-selector.d.ts +7 -0
- package/dist/types/sdk.d.ts +7 -1
- package/dist/types/session/agent-session.d.ts +6 -3
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/package.json +7 -7
- package/src/cli/args.ts +8 -0
- package/src/cli/notify-cli.ts +27 -1
- package/src/cli.ts +5 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/notify.ts +2 -2
- package/src/config/model-profile-activation.ts +23 -7
- package/src/config/model-registry.ts +21 -4
- package/src/config/model-resolver.ts +5 -16
- package/src/config/settings-schema.ts +3 -2
- package/src/config/settings.ts +24 -1
- package/src/extensibility/runtime-skill-discovery.ts +229 -0
- package/src/gjc-runtime/launch-tmux.ts +72 -12
- package/src/gjc-runtime/psmux-detect.ts +17 -2
- package/src/gjc-runtime/team-runtime.ts +35 -3
- package/src/main.ts +26 -3
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/thinking-selector.ts +79 -18
- package/src/modes/controllers/input-controller.ts +50 -9
- package/src/modes/controllers/selector-controller.ts +7 -5
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +48 -15
- package/src/notifications/config-commands.ts +70 -0
- package/src/notifications/index.ts +156 -9
- package/src/notifications/rich-render.ts +1 -0
- package/src/notifications/telegram-daemon-cli.ts +13 -5
- package/src/notifications/telegram-daemon.ts +87 -4
- package/src/notifications/threaded-render.ts +14 -0
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/runtime-credential-selector.ts +35 -0
- package/src/sdk.ts +68 -5
- package/src/session/agent-session.ts +42 -18
- package/src/system-prompt.ts +4 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/skill-discovery.ts +73 -0
- package/src/tools/skill.ts +15 -2
|
@@ -158,11 +158,43 @@ export interface GjcTmuxProfileContext {
|
|
|
158
158
|
version?: string | null;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
function tmuxExitMarkerPath(sessionStateFile: string): string {
|
|
162
|
+
return path.join(path.dirname(sessionStateFile), "tmux-exit.json");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildPosixTmuxExitMarkerPrefix(markerPath: string): string {
|
|
166
|
+
const markerDir = path.dirname(markerPath);
|
|
167
|
+
return [
|
|
168
|
+
`__gjc_tmux_exit_marker=${shellQuote(markerPath)}`,
|
|
169
|
+
"__gjc_tmux_write_exit_marker() { __gjc_tmux_status=$?",
|
|
170
|
+
"__gjc_tmux_ended_at=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)",
|
|
171
|
+
`mkdir -p ${shellQuote(markerDir)} 2>/dev/null || true`,
|
|
172
|
+
'printf \'{"schema_version":1,"source":"tmux_inner_shell","ended_at":"%s","exit_code":%s}\\n\' "$__gjc_tmux_ended_at" "$__gjc_tmux_status" > "$__gjc_tmux_exit_marker" 2>/dev/null || true',
|
|
173
|
+
"}",
|
|
174
|
+
"trap __gjc_tmux_write_exit_marker EXIT",
|
|
175
|
+
].join("; ");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildPowerShellTmuxExitMarkerFinally(markerPath: string): string {
|
|
179
|
+
const markerDir = path.win32.dirname(markerPath);
|
|
180
|
+
return [
|
|
181
|
+
"} finally {",
|
|
182
|
+
"\ttry {",
|
|
183
|
+
`\t\tNew-Item -ItemType Directory -Force -Path ${powershellQuote(markerDir)} -ErrorAction Stop | Out-Null`,
|
|
184
|
+
"\t\t$__gjcTmuxExitMarker = @{ schema_version = 1; source = 'tmux_inner_shell'; ended_at = (Get-Date).ToUniversalTime().ToString('o'); exit_code = $__gjcTmuxExitCode } | ConvertTo-Json -Compress",
|
|
185
|
+
`\t\tSet-Content -LiteralPath ${powershellQuote(markerPath)} -Value $__gjcTmuxExitMarker -Encoding UTF8 -ErrorAction Stop`,
|
|
186
|
+
"\t} catch {",
|
|
187
|
+
"\t}",
|
|
188
|
+
"}",
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
interface CommandResolutionContext {
|
|
162
193
|
cwd: string;
|
|
163
194
|
argv: string[];
|
|
164
195
|
execPath: string;
|
|
165
196
|
extraEnv?: Record<string, string>;
|
|
197
|
+
tmuxExitMarkerPath?: string;
|
|
166
198
|
platform?: NodeJS.Platform;
|
|
167
199
|
}
|
|
168
200
|
|
|
@@ -286,7 +318,17 @@ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, r
|
|
|
286
318
|
const innerArgs = stripRootTmuxFlag(rawArgs).map(powershellQuote).join(" ");
|
|
287
319
|
const invocation = `& ${resolvedCommand} ${innerArgs}`;
|
|
288
320
|
const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
|
|
289
|
-
const script =
|
|
321
|
+
const script = context.tmuxExitMarkerPath
|
|
322
|
+
? [
|
|
323
|
+
...envLines,
|
|
324
|
+
"$__gjcTmuxExitCode = 1",
|
|
325
|
+
"try {",
|
|
326
|
+
`\t${invocation}`,
|
|
327
|
+
"\tif ($null -ne $LASTEXITCODE) { $__gjcTmuxExitCode = $LASTEXITCODE } else { $__gjcTmuxExitCode = 1 }",
|
|
328
|
+
buildPowerShellTmuxExitMarkerFinally(context.tmuxExitMarkerPath),
|
|
329
|
+
"exit $__gjcTmuxExitCode",
|
|
330
|
+
].join("\n")
|
|
331
|
+
: [...envLines, invocation, exitLine].join("\n");
|
|
290
332
|
// Encode the script as UTF-16LE base64 for pwsh -EncodedCommand. Do NOT
|
|
291
333
|
// prepend a UTF-16LE BOM (0xFF 0xFE): the BOM survives the decode and is
|
|
292
334
|
// inserted as a literal U+FEFF character in front of the first script
|
|
@@ -366,7 +408,9 @@ function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[])
|
|
|
366
408
|
if (isWindowsPlatform(context.platform)) return buildWindowsPowerShellInnerCommand(context, rawArgs);
|
|
367
409
|
const command = resolveCurrentGjcCommand(context);
|
|
368
410
|
const quoted = [...command, ...stripRootTmuxFlag(rawArgs)].map(shellQuote).join(" ");
|
|
369
|
-
|
|
411
|
+
const invocation = `env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments(context.extraEnv)} ${quoted}`;
|
|
412
|
+
if (!context.tmuxExitMarkerPath) return `exec ${invocation}`;
|
|
413
|
+
return `${buildPosixTmuxExitMarkerPrefix(context.tmuxExitMarkerPath)}; ${invocation}; exit $?`;
|
|
370
414
|
}
|
|
371
415
|
|
|
372
416
|
function visibleWidth(value: string): number {
|
|
@@ -404,6 +448,9 @@ function truncateVisibleTail(value: string, maxWidth: number): string {
|
|
|
404
448
|
const GJC_TMUX_WINDOW_BRANCH_SEPARATOR = "-";
|
|
405
449
|
const GJC_TMUX_WINDOW_TITLE_PREFIX = "GJC-";
|
|
406
450
|
const GJC_TMUX_TERMINAL_TITLE_PREFIX = "GJC: ";
|
|
451
|
+
const GJC_TMUX_ROOT_TERMINAL_TITLE_OPTION = "@gjc-root-terminal-title";
|
|
452
|
+
const GJC_TMUX_ROOT_TERMINAL_TITLE_SESSION_OPTION = "@gjc-root-terminal-title-session";
|
|
453
|
+
const GJC_TMUX_DYNAMIC_SESSION_TITLE = "GJC: #{session_name}";
|
|
407
454
|
|
|
408
455
|
function sanitizeTmuxWindowTitleSegment(value: string): string {
|
|
409
456
|
return value.replace(/:+/g, "-");
|
|
@@ -445,17 +492,31 @@ function sanitizeGjcTmuxRootTerminalTitle(title: string): string {
|
|
|
445
492
|
return title.replace(TERMINAL_TITLE_CONTROL_CHARS, "").trim() || "GJC";
|
|
446
493
|
}
|
|
447
494
|
|
|
448
|
-
function
|
|
449
|
-
|
|
495
|
+
function buildGjcTmuxRootTerminalTitleFormat(sessionName: string): string {
|
|
496
|
+
if (!sessionName.startsWith(GJC_TMUX_SESSION_PREFIX)) return GJC_TMUX_DYNAMIC_SESSION_TITLE;
|
|
497
|
+
return `#{?#{==:#{${GJC_TMUX_ROOT_TERMINAL_TITLE_SESSION_OPTION}},#{session_name}},#{${GJC_TMUX_ROOT_TERMINAL_TITLE_OPTION}},${GJC_TMUX_DYNAMIC_SESSION_TITLE}}`;
|
|
450
498
|
}
|
|
451
499
|
|
|
452
|
-
function buildGjcTmuxRootTerminalTitleCommands(
|
|
453
|
-
|
|
500
|
+
function buildGjcTmuxRootTerminalTitleCommands(
|
|
501
|
+
target: string,
|
|
502
|
+
sessionName: string,
|
|
503
|
+
title: string,
|
|
504
|
+
): GjcTmuxProfileCommand[] {
|
|
505
|
+
const sanitized = sanitizeGjcTmuxRootTerminalTitle(title);
|
|
506
|
+
const format = buildGjcTmuxRootTerminalTitleFormat(sessionName);
|
|
454
507
|
return [
|
|
508
|
+
{
|
|
509
|
+
description: "remember tmux client terminal title fallback",
|
|
510
|
+
args: ["set-option", "-t", target, GJC_TMUX_ROOT_TERMINAL_TITLE_OPTION, sanitized],
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
description: "remember tmux client terminal title session",
|
|
514
|
+
args: ["set-option", "-t", target, GJC_TMUX_ROOT_TERMINAL_TITLE_SESSION_OPTION, sessionName],
|
|
515
|
+
},
|
|
455
516
|
{ description: "enable tmux client terminal title", args: ["set-option", "-t", target, "set-titles", "on"] },
|
|
456
517
|
{
|
|
457
|
-
description: "set tmux client terminal title",
|
|
458
|
-
args: ["set-option", "-t", target, "set-titles-string",
|
|
518
|
+
description: "set dynamic tmux client terminal title",
|
|
519
|
+
args: ["set-option", "-t", target, "set-titles-string", format],
|
|
459
520
|
},
|
|
460
521
|
];
|
|
461
522
|
}
|
|
@@ -468,10 +529,8 @@ function applyGjcTmuxRootTerminalTitleProfile(context: {
|
|
|
468
529
|
options: TmuxSpawnOptions;
|
|
469
530
|
}): void {
|
|
470
531
|
if (!context.title) return;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
context.title,
|
|
474
|
-
)) {
|
|
532
|
+
const optionTarget = buildGjcTmuxExactOptionTarget(context.target, { env: context.options.env });
|
|
533
|
+
for (const command of buildGjcTmuxRootTerminalTitleCommands(optionTarget, context.target, context.title)) {
|
|
475
534
|
context.spawnSync(context.tmuxCommand, command.args, context.options);
|
|
476
535
|
}
|
|
477
536
|
}
|
|
@@ -695,6 +754,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
695
754
|
// session, which would split/send workers into the wrong session.
|
|
696
755
|
[GJC_TMUX_ACTIVE_SESSION_ENV]: sessionName,
|
|
697
756
|
},
|
|
757
|
+
tmuxExitMarkerPath: tmuxExitMarkerPath(sessionStateFile),
|
|
698
758
|
platform,
|
|
699
759
|
},
|
|
700
760
|
context.rawArgs,
|
|
@@ -115,6 +115,20 @@ function outputMentionsPsmux(output: string): boolean {
|
|
|
115
115
|
return PSMUX_VERSION_MARKERS.some(marker => output.includes(marker));
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function normalizedCommandBaseName(command: string): string {
|
|
119
|
+
const normalized = command
|
|
120
|
+
.trim()
|
|
121
|
+
.replace(/^['"]|['"]$/g, "")
|
|
122
|
+
.replace(/\\/g, "/");
|
|
123
|
+
const basename = normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
|
|
124
|
+
return basename.endsWith(".exe") ? basename.slice(0, -4) : basename;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isNamedPsmuxCommand(command: string): boolean {
|
|
128
|
+
const basename = normalizedCommandBaseName(command);
|
|
129
|
+
return basename === "psmux" || basename === "pmux";
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
function resolveBinaryPath(candidate: string): string | null {
|
|
119
133
|
return activeBinaryResolver(candidate);
|
|
120
134
|
}
|
|
@@ -194,13 +208,14 @@ export function resolveGjcTmuxBinary(options: ResolveGjcTmuxBinaryOptions = {}):
|
|
|
194
208
|
const runner = options.runner ?? readSpawnRunner();
|
|
195
209
|
const explicit = env.GJC_TMUX_COMMAND?.trim() || env.GJC_TEAM_TMUX_COMMAND?.trim();
|
|
196
210
|
if (explicit) {
|
|
197
|
-
const isPsmux =
|
|
211
|
+
const isPsmux =
|
|
212
|
+
platform === "win32" && isNamedPsmuxCommand(explicit) ? true : detectPsmux(explicit, { env, runner });
|
|
198
213
|
return { command: explicit, isPsmux, viaExplicitOverride: true };
|
|
199
214
|
}
|
|
200
215
|
if (platform === "win32") {
|
|
201
216
|
for (const candidate of PSMUX_BINARY_NAMES) {
|
|
202
217
|
if (resolveBinaryPath(candidate)) {
|
|
203
|
-
const isPsmux = detectPsmux(candidate, { env, runner });
|
|
218
|
+
const isPsmux = isNamedPsmuxCommand(candidate) ? true : detectPsmux(candidate, { env, runner });
|
|
204
219
|
return { command: candidate, isPsmux, viaExplicitOverride: false };
|
|
205
220
|
}
|
|
206
221
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { getWorktreesDir } from "@gajae-code/utils/dirs";
|
|
4
5
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
6
|
import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
|
|
6
7
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
GJC_TMUX_ACTIVE_SESSION_ENV,
|
|
26
27
|
GJC_TMUX_PROFILE_OPTION,
|
|
27
28
|
GJC_TMUX_PROFILE_VALUE,
|
|
29
|
+
resolveGjcTmuxBinary,
|
|
28
30
|
resolveGjcTmuxCommand,
|
|
29
31
|
} from "./tmux-common";
|
|
30
32
|
|
|
@@ -288,6 +290,7 @@ export interface GjcTeamStartOptions {
|
|
|
288
290
|
cwd?: string;
|
|
289
291
|
env?: NodeJS.ProcessEnv;
|
|
290
292
|
dryRun?: boolean;
|
|
293
|
+
platform?: NodeJS.Platform;
|
|
291
294
|
mailboxDeliveryTransport?: GjcTeamMailboxDeliveryTransport;
|
|
292
295
|
}
|
|
293
296
|
|
|
@@ -1844,18 +1847,41 @@ function findWorktreePath(repoRoot: string, worktreePath: string): string | null
|
|
|
1844
1847
|
if (line.startsWith("worktree ") && path.resolve(line.slice("worktree ".length)) === resolved) return resolved;
|
|
1845
1848
|
return null;
|
|
1846
1849
|
}
|
|
1850
|
+
export function resolveWorkerWorktreePath(input: {
|
|
1851
|
+
repoRoot: string;
|
|
1852
|
+
stateDir: string;
|
|
1853
|
+
teamName: string;
|
|
1854
|
+
workerId: string;
|
|
1855
|
+
platform: NodeJS.Platform;
|
|
1856
|
+
isPsmux: boolean;
|
|
1857
|
+
}): string {
|
|
1858
|
+
if (input.platform === "win32" && input.isPsmux) {
|
|
1859
|
+
const slug = stableHash([input.repoRoot, input.stateDir, input.teamName].join("\0")).slice(0, 12);
|
|
1860
|
+
return path.join(getWorktreesDir(), `team-${slug}-${input.workerId}`);
|
|
1861
|
+
}
|
|
1862
|
+
return path.join(input.stateDir, "worktrees", input.workerId);
|
|
1863
|
+
}
|
|
1847
1864
|
async function ensureWorkerWorktree(
|
|
1848
1865
|
cwd: string,
|
|
1849
1866
|
dir: string,
|
|
1850
1867
|
teamName: string,
|
|
1851
1868
|
worker: GjcTeamWorker,
|
|
1852
1869
|
mode: GjcTeamWorktreeMode,
|
|
1870
|
+
platform: NodeJS.Platform,
|
|
1871
|
+
isPsmux: boolean,
|
|
1853
1872
|
): Promise<GjcTeamWorker> {
|
|
1854
1873
|
if (!mode.enabled) return worker;
|
|
1855
1874
|
if (!isGitRepository(cwd)) throw new Error(`team_worktree_requires_git_repo:${cwd}`);
|
|
1856
1875
|
const repoRoot = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1857
1876
|
const baseRef = runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
1858
|
-
const worktreePath =
|
|
1877
|
+
const worktreePath = resolveWorkerWorktreePath({
|
|
1878
|
+
repoRoot,
|
|
1879
|
+
stateDir: dir,
|
|
1880
|
+
teamName,
|
|
1881
|
+
workerId: worker.id,
|
|
1882
|
+
platform,
|
|
1883
|
+
isPsmux,
|
|
1884
|
+
});
|
|
1859
1885
|
const existing = findWorktreePath(repoRoot, worktreePath);
|
|
1860
1886
|
let created = false;
|
|
1861
1887
|
const branchName = mode.detached
|
|
@@ -2994,7 +3020,9 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
2994
3020
|
const dir = teamDir(stateRoot, teamName);
|
|
2995
3021
|
const createdAt = now();
|
|
2996
3022
|
const worktreeMode = resolveDefaultWorktreeMode(options.worktreeMode);
|
|
2997
|
-
const
|
|
3023
|
+
const platform = options.platform ?? process.platform;
|
|
3024
|
+
const tmuxBinary = resolveGjcTmuxBinary({ env, platform });
|
|
3025
|
+
const tmuxCommand = tmuxBinary.command;
|
|
2998
3026
|
const tmuxContext = options.dryRun
|
|
2999
3027
|
? { sessionName: "dry-run", windowIndex: "0", leaderPaneId: "%dry-run-leader", target: "dry-run:0" }
|
|
3000
3028
|
: readCurrentTmuxLeaderContext(tmuxCommand, env);
|
|
@@ -3003,7 +3031,11 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
3003
3031
|
const workers: GjcTeamWorker[] = [];
|
|
3004
3032
|
try {
|
|
3005
3033
|
for (const worker of initialWorkers)
|
|
3006
|
-
workers.push(
|
|
3034
|
+
workers.push(
|
|
3035
|
+
options.dryRun
|
|
3036
|
+
? worker
|
|
3037
|
+
: await ensureWorkerWorktree(cwd, dir, teamName, worker, worktreeMode, platform, tmuxBinary.isPsmux),
|
|
3038
|
+
);
|
|
3007
3039
|
} catch (error) {
|
|
3008
3040
|
await rollbackCreatedWorktrees(workers);
|
|
3009
3041
|
throw error;
|
package/src/main.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type { InteractiveMode } from "./modes/interactive-mode";
|
|
|
38
38
|
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
|
|
39
39
|
import type { SubmittedUserInput } from "./modes/types";
|
|
40
40
|
import { applyCliRuntimeApiKeyOverride } from "./runtime-api-key";
|
|
41
|
+
import { parseCliCredentialSelector } from "./runtime-credential-selector";
|
|
41
42
|
import type { MCPManager } from "./runtime-mcp";
|
|
42
43
|
import {
|
|
43
44
|
type CreateAgentSessionOptions,
|
|
@@ -217,10 +218,14 @@ export async function applyStartupModelProfiles(args: {
|
|
|
217
218
|
startupModel?: CreateAgentSessionOptions["model"];
|
|
218
219
|
startupThinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
219
220
|
}): Promise<void> {
|
|
220
|
-
const applyProfile = async (
|
|
221
|
+
const applyProfile = async (
|
|
222
|
+
profileName: string,
|
|
223
|
+
persistDefault: boolean,
|
|
224
|
+
options: { thinkingLevelOverride?: CreateAgentSessionOptions["thinkingLevel"] } = {},
|
|
225
|
+
): Promise<void> => {
|
|
221
226
|
await activateModelProfile(
|
|
222
227
|
{ session: args.session, modelRegistry: args.modelRegistry, settings: args.settings, profileName },
|
|
223
|
-
{ persistDefault },
|
|
228
|
+
{ persistDefault, thinkingLevelOverride: options.thinkingLevelOverride },
|
|
224
229
|
);
|
|
225
230
|
};
|
|
226
231
|
|
|
@@ -234,7 +239,11 @@ export async function applyStartupModelProfiles(args: {
|
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
if (defaultProfile) {
|
|
237
|
-
await applyProfile(defaultProfile, false
|
|
242
|
+
await applyProfile(defaultProfile, false, {
|
|
243
|
+
thinkingLevelOverride: args.settings.has("defaultThinkingLevel")
|
|
244
|
+
? args.settings.get("defaultThinkingLevel")
|
|
245
|
+
: undefined,
|
|
246
|
+
});
|
|
238
247
|
}
|
|
239
248
|
if (args.parsedArgs.mpreset) {
|
|
240
249
|
await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true);
|
|
@@ -1002,6 +1011,20 @@ export async function runRootCommand(
|
|
|
1002
1011
|
deps.rlmPreset?.applyOptions(sessionOptions, settingsInstance);
|
|
1003
1012
|
|
|
1004
1013
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
1014
|
+
if (parsedArgs.apiKey && parsedArgs.credential) {
|
|
1015
|
+
process.stderr.write(`${chalk.red("--api-key and --credential cannot be used together")}\n`);
|
|
1016
|
+
process.exit(1);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (parsedArgs.credential) {
|
|
1020
|
+
try {
|
|
1021
|
+
sessionOptions.credentialSelector = parseCliCredentialSelector(parsedArgs.credential);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1024
|
+
process.stderr.write(`${chalk.red(message)}\n`);
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1005
1028
|
if (parsedArgs.apiKey) {
|
|
1006
1029
|
if (!sessionOptions.model && !sessionOptions.modelPattern) {
|
|
1007
1030
|
process.stderr.write(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ThinkingLevel, type ThinkingLevel as ThinkingLevelValue } from "@gajae-code/agent-core";
|
|
2
2
|
import type { Effort } from "@gajae-code/ai";
|
|
3
3
|
import {
|
|
4
4
|
Container,
|
|
@@ -654,7 +654,7 @@ export interface SettingsRuntimeContext {
|
|
|
654
654
|
/** Available thinking levels (from session) */
|
|
655
655
|
availableThinkingLevels: Effort[];
|
|
656
656
|
/** Current thinking level (from session) */
|
|
657
|
-
thinkingLevel:
|
|
657
|
+
thinkingLevel: ThinkingLevelValue | undefined;
|
|
658
658
|
/** Available themes */
|
|
659
659
|
availableThemes: string[];
|
|
660
660
|
/** Available model profile names (from the model registry) */
|
|
@@ -843,7 +843,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
843
843
|
|
|
844
844
|
// Special case: inject runtime options for thinking level
|
|
845
845
|
if (def.path === "defaultThinkingLevel") {
|
|
846
|
-
options = this.context.availableThinkingLevels.map(level => {
|
|
846
|
+
options = [ThinkingLevel.Off, ...this.context.availableThinkingLevels].map(level => {
|
|
847
847
|
const baseOpt = options.find(o => o.value === level);
|
|
848
848
|
return baseOpt || { value: level, label: level };
|
|
849
849
|
});
|
|
@@ -1,53 +1,114 @@
|
|
|
1
1
|
import { ThinkingLevel, type ThinkingLevel as ThinkingLevelValue } from "@gajae-code/agent-core";
|
|
2
|
-
import { Container, type SelectItem, SelectList } from "@gajae-code/tui";
|
|
3
|
-
import { getSelectListTheme } from "../../modes/theme/theme";
|
|
2
|
+
import { Container, type SelectItem, SelectList, Text } from "@gajae-code/tui";
|
|
3
|
+
import { getSelectListTheme, theme } from "../../modes/theme/theme";
|
|
4
4
|
import { getThinkingLevelMetadata, type ThinkingLevelValue as ThinkingMetadataValue } from "../../thinking-metadata";
|
|
5
5
|
import { DynamicBorder } from "./dynamic-border";
|
|
6
6
|
|
|
7
|
+
const SCOPE_ITEMS = [
|
|
8
|
+
{
|
|
9
|
+
value: "session",
|
|
10
|
+
label: "Apply for this session",
|
|
11
|
+
description: "Use this effort until the session changes it again",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
value: "default",
|
|
15
|
+
label: "Set as default",
|
|
16
|
+
description: "Save this effort for future sessions",
|
|
17
|
+
},
|
|
18
|
+
] as const satisfies readonly SelectItem[];
|
|
19
|
+
|
|
20
|
+
export interface ThinkingSelectorSelection {
|
|
21
|
+
level: ThinkingLevelValue;
|
|
22
|
+
persistDefault: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
/**
|
|
8
26
|
* Component that renders a thinking level selector with borders
|
|
9
27
|
*/
|
|
10
28
|
export class ThinkingSelectorComponent extends Container {
|
|
11
29
|
#selectList: SelectList;
|
|
30
|
+
#levelList: SelectList;
|
|
31
|
+
#selectedLevel: ThinkingLevelValue | undefined;
|
|
32
|
+
readonly #onSelect: (selection: ThinkingSelectorSelection) => void;
|
|
33
|
+
readonly #onCancel: () => void;
|
|
12
34
|
|
|
13
35
|
constructor(
|
|
14
36
|
currentLevel: ThinkingLevelValue | undefined,
|
|
15
37
|
availableLevels: ThinkingLevelValue[],
|
|
16
|
-
onSelect: (
|
|
38
|
+
onSelect: (selection: ThinkingSelectorSelection) => void,
|
|
17
39
|
onCancel: () => void,
|
|
18
40
|
) {
|
|
19
41
|
super();
|
|
20
42
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
43
|
+
this.#onSelect = onSelect;
|
|
44
|
+
this.#onCancel = onCancel;
|
|
24
45
|
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
const currentValue = currentLevel ?? ThinkingLevel.Off;
|
|
47
|
+
const thinkingLevels: SelectItem[] = availableLevels.map(level => {
|
|
48
|
+
const metadata = getThinkingLevelMetadata(level as ThinkingMetadataValue);
|
|
49
|
+
return level === currentValue
|
|
50
|
+
? { ...metadata, label: metadata.label + theme.fg("muted", " (current)") }
|
|
51
|
+
: metadata;
|
|
52
|
+
});
|
|
27
53
|
|
|
28
|
-
|
|
29
|
-
this.#selectList =
|
|
54
|
+
this.#levelList = new SelectList(thinkingLevels, thinkingLevels.length, getSelectListTheme());
|
|
55
|
+
this.#selectList = this.#levelList;
|
|
30
56
|
|
|
31
57
|
// Preselect current level
|
|
32
|
-
const currentIndex = thinkingLevels.findIndex(item => item.value ===
|
|
58
|
+
const currentIndex = thinkingLevels.findIndex(item => item.value === currentValue);
|
|
33
59
|
if (currentIndex !== -1) {
|
|
34
|
-
this.#
|
|
60
|
+
this.#levelList.setSelectedIndex(currentIndex);
|
|
35
61
|
}
|
|
36
62
|
|
|
37
|
-
this.#
|
|
38
|
-
|
|
63
|
+
this.#levelList.onSelect = item => {
|
|
64
|
+
this.#selectedLevel = item.value as ThinkingLevelValue;
|
|
65
|
+
this.#renderScopeList();
|
|
39
66
|
};
|
|
40
67
|
|
|
41
|
-
this.#
|
|
42
|
-
onCancel();
|
|
68
|
+
this.#levelList.onCancel = () => {
|
|
69
|
+
this.#onCancel();
|
|
43
70
|
};
|
|
44
71
|
|
|
45
|
-
this
|
|
72
|
+
this.#renderLevelList();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#renderLevelList(): void {
|
|
76
|
+
this.detachAll();
|
|
77
|
+
this.#selectedLevel = undefined;
|
|
78
|
+
this.#selectList = this.#levelList;
|
|
79
|
+
this.addChild(new DynamicBorder());
|
|
80
|
+
this.addChild(this.#levelList);
|
|
81
|
+
this.addChild(new DynamicBorder());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#renderScopeList(): void {
|
|
85
|
+
const level = this.#selectedLevel;
|
|
86
|
+
if (!level) return;
|
|
87
|
+
|
|
88
|
+
const scopeList = new SelectList(SCOPE_ITEMS, SCOPE_ITEMS.length, getSelectListTheme());
|
|
89
|
+
scopeList.onSelect = item => {
|
|
90
|
+
this.#onSelect({
|
|
91
|
+
level,
|
|
92
|
+
persistDefault: item.value === "default",
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
scopeList.onCancel = () => {
|
|
96
|
+
this.#renderLevelList();
|
|
97
|
+
};
|
|
46
98
|
|
|
47
|
-
|
|
99
|
+
const metadata = getThinkingLevelMetadata(level as ThinkingMetadataValue);
|
|
100
|
+
this.detachAll();
|
|
101
|
+
this.#selectList = scopeList;
|
|
102
|
+
this.addChild(new DynamicBorder());
|
|
103
|
+
this.addChild(new Text(theme.fg("muted", ` Reasoning effort: ${metadata.label}`), 0, 0));
|
|
104
|
+
this.addChild(scopeList);
|
|
48
105
|
this.addChild(new DynamicBorder());
|
|
49
106
|
}
|
|
50
107
|
|
|
108
|
+
handleInput(data: string): void {
|
|
109
|
+
this.#selectList.handleInput(data);
|
|
110
|
+
}
|
|
111
|
+
|
|
51
112
|
getSelectList(): SelectList {
|
|
52
113
|
return this.#selectList;
|
|
53
114
|
}
|
|
@@ -27,6 +27,8 @@ interface Expandable {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
|
|
30
|
+
const IMAGE_PLACEHOLDER_PATTERN = /\[image ([1-9]\d*)\]/g;
|
|
31
|
+
const IMAGE_PLACEHOLDER_PRESENT_PATTERN = /\[image [1-9]\d*\]/;
|
|
30
32
|
|
|
31
33
|
function isExpandable(obj: unknown): obj is Expandable {
|
|
32
34
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
@@ -383,6 +385,7 @@ export class InputController {
|
|
|
383
385
|
this.ctx.isBashMode = trimmed.startsWith("!");
|
|
384
386
|
this.ctx.isBashNoContext = trimmed.startsWith("!!");
|
|
385
387
|
this.ctx.isPythonMode = trimmed.startsWith("$") && !trimmed.startsWith("${");
|
|
388
|
+
this.#clearPendingImagesIfPlaceholdersRemoved(text);
|
|
386
389
|
if (
|
|
387
390
|
wasBashMode !== this.ctx.isBashMode ||
|
|
388
391
|
wasBashNoContext !== this.ctx.isBashNoContext ||
|
|
@@ -418,7 +421,7 @@ export class InputController {
|
|
|
418
421
|
}
|
|
419
422
|
|
|
420
423
|
const runner = this.ctx.session.extensionRunner;
|
|
421
|
-
let inputImages = this
|
|
424
|
+
let inputImages = this.#visiblePendingImagesForText(text);
|
|
422
425
|
|
|
423
426
|
if (runner?.hasHandlers("input")) {
|
|
424
427
|
const result = await runner.emitInput(text, inputImages, "interactive");
|
|
@@ -498,7 +501,7 @@ export class InputController {
|
|
|
498
501
|
|
|
499
502
|
// Queue input during compaction
|
|
500
503
|
if (this.ctx.session.isCompacting) {
|
|
501
|
-
if (
|
|
504
|
+
if ((inputImages?.length ?? 0) > 0) {
|
|
502
505
|
this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
|
|
503
506
|
return;
|
|
504
507
|
}
|
|
@@ -520,11 +523,13 @@ export class InputController {
|
|
|
520
523
|
// typed since queuing intact. Same protection as #783, applied to
|
|
521
524
|
// the streaming/queue path.
|
|
522
525
|
const streamingBehavior = this.#busyStreamingBehavior();
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
)
|
|
526
|
+
const promptOptions =
|
|
527
|
+
streamingBehavior === "followUp"
|
|
528
|
+
? { streamingBehavior, images, followUpQueuePolicy: "sequential" as const }
|
|
529
|
+
: { streamingBehavior, images };
|
|
530
|
+
await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, promptOptions), {
|
|
531
|
+
imageCount: images?.length ?? 0,
|
|
532
|
+
});
|
|
528
533
|
this.ctx.updatePendingMessagesDisplay();
|
|
529
534
|
this.ctx.ui.requestRender();
|
|
530
535
|
return;
|
|
@@ -869,7 +874,9 @@ export class InputController {
|
|
|
869
874
|
details,
|
|
870
875
|
attribution: "user",
|
|
871
876
|
},
|
|
872
|
-
|
|
877
|
+
streamingBehavior === "followUp"
|
|
878
|
+
? { streamingBehavior, followUpQueuePolicy: "sequential" }
|
|
879
|
+
: { streamingBehavior },
|
|
873
880
|
);
|
|
874
881
|
}
|
|
875
882
|
if (this.ctx.session.isStreaming) {
|
|
@@ -911,7 +918,10 @@ export class InputController {
|
|
|
911
918
|
this.ctx.editor.addToHistory(text);
|
|
912
919
|
this.ctx.editor.setText("");
|
|
913
920
|
await this.ctx.withLocalSubmission(text, () =>
|
|
914
|
-
this.ctx.session.prompt(text, {
|
|
921
|
+
this.ctx.session.prompt(text, {
|
|
922
|
+
streamingBehavior: "followUp",
|
|
923
|
+
followUpQueuePolicy: "sequential",
|
|
924
|
+
}),
|
|
915
925
|
);
|
|
916
926
|
this.ctx.updatePendingMessagesDisplay();
|
|
917
927
|
this.ctx.ui.requestRender();
|
|
@@ -1104,6 +1114,37 @@ export class InputController {
|
|
|
1104
1114
|
return `[image ${this.ctx.pendingImages.length}]`;
|
|
1105
1115
|
}
|
|
1106
1116
|
|
|
1117
|
+
#visiblePendingImagesForText(text: string): InteractiveModeContext["pendingImages"] | undefined {
|
|
1118
|
+
if (this.ctx.pendingImages.length === 0) {
|
|
1119
|
+
return undefined;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const images: InteractiveModeContext["pendingImages"] = [];
|
|
1123
|
+
const seenImageIndexes = new Set<number>();
|
|
1124
|
+
for (const match of text.matchAll(IMAGE_PLACEHOLDER_PATTERN)) {
|
|
1125
|
+
const placeholderNumberText = match[1];
|
|
1126
|
+
if (!placeholderNumberText) continue;
|
|
1127
|
+
const placeholderNumber = Number.parseInt(placeholderNumberText, 10);
|
|
1128
|
+
const imageIndex = placeholderNumber - 1;
|
|
1129
|
+
if (imageIndex < 0 || imageIndex >= this.ctx.pendingImages.length || seenImageIndexes.has(imageIndex)) {
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
const image = this.ctx.pendingImages[imageIndex];
|
|
1133
|
+
if (!image) continue;
|
|
1134
|
+
images.push(image);
|
|
1135
|
+
seenImageIndexes.add(imageIndex);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return images.length > 0 ? images : undefined;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
#clearPendingImagesIfPlaceholdersRemoved(text: string): void {
|
|
1142
|
+
if (this.ctx.pendingImages.length === 0 || IMAGE_PLACEHOLDER_PRESENT_PATTERN.test(text)) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
this.ctx.pendingImages = [];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1107
1148
|
async handleImagePaste(): Promise<boolean> {
|
|
1108
1149
|
try {
|
|
1109
1150
|
const image = await readImageFromClipboard();
|
|
@@ -443,12 +443,13 @@ export class SelectorController {
|
|
|
443
443
|
const selector = new ThinkingSelectorComponent(
|
|
444
444
|
this.ctx.session.thinkingLevel,
|
|
445
445
|
availableLevels,
|
|
446
|
-
|
|
446
|
+
selection => {
|
|
447
447
|
done();
|
|
448
448
|
|
|
449
|
+
const { level, persistDefault } = selection;
|
|
449
450
|
const configuredDefault = this.ctx.settings.get("defaultThinkingLevel");
|
|
450
451
|
const levelToApply = level === ThinkingLevel.Inherit ? configuredDefault : level;
|
|
451
|
-
this.ctx.session.setThinkingLevel(levelToApply,
|
|
452
|
+
this.ctx.session.setThinkingLevel(levelToApply, persistDefault);
|
|
452
453
|
const effectiveLevel = this.ctx.session.thinkingLevel ?? ThinkingLevel.Off;
|
|
453
454
|
const requestedLabel =
|
|
454
455
|
level === ThinkingLevel.Inherit ? `${level} (configured default: ${configuredDefault})` : level;
|
|
@@ -458,9 +459,11 @@ export class SelectorController {
|
|
|
458
459
|
this.ctx.statusLine.invalidate();
|
|
459
460
|
this.ctx.updateEditorBorderColor();
|
|
460
461
|
this.ctx.updateEditorTopBorder();
|
|
462
|
+
if (persistDefault) void this.ctx.notifyConfigChanged?.();
|
|
461
463
|
this.ctx.ui.requestRender();
|
|
464
|
+
const scopeLabel = persistDefault ? "Default reasoning effort" : "Reasoning effort";
|
|
462
465
|
this.ctx.showStatus(
|
|
463
|
-
|
|
466
|
+
`${scopeLabel} set to ${requestedLabel}. Effective effort: ${effectiveLevel}.${clampedSuffix}`,
|
|
464
467
|
);
|
|
465
468
|
},
|
|
466
469
|
() => {
|
|
@@ -468,10 +471,9 @@ export class SelectorController {
|
|
|
468
471
|
this.ctx.ui.requestRender();
|
|
469
472
|
},
|
|
470
473
|
);
|
|
471
|
-
return { component: selector, focus: selector
|
|
474
|
+
return { component: selector, focus: selector };
|
|
472
475
|
});
|
|
473
476
|
}
|
|
474
|
-
|
|
475
477
|
showSettingsSelector(): void {
|
|
476
478
|
getAvailableThemes().then(availableThemes => {
|
|
477
479
|
this.showSelector(done => {
|