@gajae-code/coding-agent 0.7.8 → 0.7.10
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 +40 -0
- package/README.md +8 -0
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-profile-activation.d.ts +19 -2
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +19 -3
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/modes/components/hook-input.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line.d.ts +3 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/notifications/daemon-paths.d.ts +9 -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-cli.d.ts +8 -2
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +1 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +26 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/cmux-workspace.d.ts +23 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +5 -0
- package/dist/types/web/search/render.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/notify-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +3 -2
- package/src/cli.ts +1 -0
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +92 -11
- package/src/config/model-profiles.ts +15 -2
- package/src/config/model-registry.ts +81 -5
- package/src/config/settings-schema.ts +14 -3
- package/src/coordinator-mcp/server.ts +4 -2
- package/src/deep-interview/render-middleware.ts +1 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +40 -8
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +366 -48
- package/src/gjc-runtime/team-runtime.ts +69 -4
- package/src/gjc-runtime/ultragoal-guard.ts +150 -24
- 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 +8 -7
- package/src/modes/components/custom-model-preset-wizard.ts +2 -4
- package/src/modes/components/hook-editor.ts +7 -4
- package/src/modes/components/hook-input.ts +4 -0
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +84 -13
- package/src/modes/components/settings-selector.ts +83 -24
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +10 -2
- package/src/modes/components/status-line.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +130 -7
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +64 -13
- package/src/modes/types.ts +6 -1
- package/src/notifications/daemon-paths.ts +22 -0
- 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-cli.ts +13 -4
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +69 -45
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/sdk.ts +13 -6
- package/src/session/agent-session.ts +10 -1
- package/src/tools/ask.ts +147 -17
- package/src/tools/eval.ts +2 -2
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/cmux-workspace.ts +106 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/search/index.ts +22 -10
- package/src/web/search/provider.ts +15 -0
- package/src/web/search/render.ts +3 -0
|
@@ -36,6 +36,18 @@ export declare const askSchema: z.ZodObject<{
|
|
|
36
36
|
dimension: z.ZodString;
|
|
37
37
|
ambiguity: z.ZodNumber;
|
|
38
38
|
}, z.core.$strip>>;
|
|
39
|
+
workflowGate: z.ZodOptional<z.ZodObject<{
|
|
40
|
+
stage: z.ZodEnum<{
|
|
41
|
+
"deep-interview": "deep-interview";
|
|
42
|
+
ralplan: "ralplan";
|
|
43
|
+
ultragoal: "ultragoal";
|
|
44
|
+
}>;
|
|
45
|
+
kind: z.ZodEnum<{
|
|
46
|
+
approval: "approval";
|
|
47
|
+
execution: "execution";
|
|
48
|
+
question: "question";
|
|
49
|
+
}>;
|
|
50
|
+
}, z.core.$strip>>;
|
|
39
51
|
}, z.core.$strip>>;
|
|
40
52
|
}, z.core.$strip>;
|
|
41
53
|
export type AskToolInput = z.infer<typeof askSchema>;
|
|
@@ -47,6 +59,7 @@ export interface QuestionResult {
|
|
|
47
59
|
multi: boolean;
|
|
48
60
|
selectedOptions: string[];
|
|
49
61
|
customInput?: string;
|
|
62
|
+
clarificationQuestion?: string;
|
|
50
63
|
}
|
|
51
64
|
export interface AskToolDetails {
|
|
52
65
|
question?: string;
|
|
@@ -54,6 +67,7 @@ export interface AskToolDetails {
|
|
|
54
67
|
multi?: boolean;
|
|
55
68
|
selectedOptions?: string[];
|
|
56
69
|
customInput?: string;
|
|
70
|
+
clarificationQuestion?: string;
|
|
57
71
|
/** Multi-part question mode */
|
|
58
72
|
results?: QuestionResult[];
|
|
59
73
|
}
|
|
@@ -87,6 +101,18 @@ export declare class AskTool implements AgentTool<typeof askSchema, AskToolDetai
|
|
|
87
101
|
dimension: z.ZodString;
|
|
88
102
|
ambiguity: z.ZodNumber;
|
|
89
103
|
}, z.core.$strip>>;
|
|
104
|
+
workflowGate: z.ZodOptional<z.ZodObject<{
|
|
105
|
+
stage: z.ZodEnum<{
|
|
106
|
+
"deep-interview": "deep-interview";
|
|
107
|
+
ralplan: "ralplan";
|
|
108
|
+
ultragoal: "ultragoal";
|
|
109
|
+
}>;
|
|
110
|
+
kind: z.ZodEnum<{
|
|
111
|
+
approval: "approval";
|
|
112
|
+
execution: "execution";
|
|
113
|
+
question: "question";
|
|
114
|
+
}>;
|
|
115
|
+
}, z.core.$strip>>;
|
|
90
116
|
}, z.core.$strip>>;
|
|
91
117
|
}, z.core.$strip>;
|
|
92
118
|
readonly strict = true;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { AgentTool } from "@gajae-code/agent-core";
|
|
2
2
|
import { type UltragoalAskBlockDiagnostic } from "../gjc-runtime/ultragoal-guard";
|
|
3
|
+
export interface UltragoalAskGuardContext {
|
|
4
|
+
activeSkillState?: {
|
|
5
|
+
skill?: string;
|
|
6
|
+
session_id?: string;
|
|
7
|
+
} | null;
|
|
8
|
+
sessionId?: string | null;
|
|
9
|
+
}
|
|
3
10
|
export declare function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiagnostic): string;
|
|
4
|
-
export declare function assertUltragoalAskAllowed(cwd: string): Promise<void>;
|
|
5
|
-
export declare function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: () => string): T;
|
|
11
|
+
export declare function assertUltragoalAskAllowed(cwd: string, context?: UltragoalAskGuardContext): Promise<void>;
|
|
12
|
+
export declare function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: () => string, getContext?: () => UltragoalAskGuardContext): T;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface CmuxWorkspaceRenameCommand {
|
|
2
|
+
command: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
}
|
|
5
|
+
export interface CmuxWorkspaceRenameProcess {
|
|
6
|
+
exited: Promise<number>;
|
|
7
|
+
kill(): void;
|
|
8
|
+
unref(): void;
|
|
9
|
+
}
|
|
10
|
+
export interface CmuxWorkspaceTitleSyncOptions {
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
isTty?: boolean;
|
|
13
|
+
which?: (command: string) => string | null;
|
|
14
|
+
spawn?: (command: string[], options: {
|
|
15
|
+
env: NodeJS.ProcessEnv;
|
|
16
|
+
stdin: "ignore";
|
|
17
|
+
stdout: "ignore";
|
|
18
|
+
stderr: "ignore";
|
|
19
|
+
}) => CmuxWorkspaceRenameProcess;
|
|
20
|
+
}
|
|
21
|
+
export declare function sanitizeCmuxWorkspaceTitle(title: string | undefined): string | undefined;
|
|
22
|
+
export declare function buildCmuxWorkspaceRenameCommand(sessionName: string | undefined, env?: NodeJS.ProcessEnv): CmuxWorkspaceRenameCommand | null;
|
|
23
|
+
export declare function syncCmuxWorkspaceTitle(sessionName: string | undefined, options?: CmuxWorkspaceTitleSyncOptions): void;
|
|
@@ -110,7 +110,7 @@ export declare class WebSearchTool implements AgentTool<typeof webSearchSchema,
|
|
|
110
110
|
/** Web search tool as CustomTool (for TUI rendering support) */
|
|
111
111
|
export declare const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRenderDetails>;
|
|
112
112
|
export declare function getSearchTools(): CustomTool<any, any>[];
|
|
113
|
-
export { getSearchProvider, setPreferredSearchProvider, setSearchFallbackProviders } from "./provider";
|
|
113
|
+
export { getConfiguredSearchProviderPreference, getSearchProvider, setPreferredSearchProvider, setSearchFallbackProviders, } from "./provider";
|
|
114
114
|
export { setSearchHardTimeoutMs } from "./providers/utils";
|
|
115
115
|
export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
|
|
116
116
|
export { isConfigurableSearchProviderId, isSearchProviderPreference } from "./types";
|
|
@@ -8,6 +8,11 @@ export declare function getSearchProvider(id: SearchProviderId): Promise<SearchP
|
|
|
8
8
|
export declare const SEARCH_PROVIDER_ORDER: SearchProviderId[];
|
|
9
9
|
export declare function setPreferredSearchProvider(provider: SearchProviderId | "auto"): void;
|
|
10
10
|
export declare function setSearchFallbackProviders(ids: readonly string[]): void;
|
|
11
|
+
export interface SearchProviderPreferenceSource {
|
|
12
|
+
get(path: "providers.webSearch" | "web_search.provider"): SearchProviderId | "auto";
|
|
13
|
+
has(path: "providers.webSearch" | "web_search.provider"): boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function getConfiguredSearchProviderPreference(settings: SearchProviderPreferenceSource): SearchProviderId | "auto";
|
|
11
16
|
export interface ResolveProviderChainOptions {
|
|
12
17
|
authStorage: AuthStorage;
|
|
13
18
|
sessionId?: string;
|
|
@@ -10,6 +10,7 @@ import type { SearchResponse } from "./types";
|
|
|
10
10
|
export interface SearchRenderDetails {
|
|
11
11
|
response: SearchResponse;
|
|
12
12
|
error?: string;
|
|
13
|
+
warning?: string;
|
|
13
14
|
}
|
|
14
15
|
/** Render web search result with tree-based layout */
|
|
15
16
|
export declare function renderSearchResult(result: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.10",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
53
53
|
"@babel/parser": "^7.29.3",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@gajae-code/stats": "0.7.
|
|
56
|
-
"@gajae-code/agent-core": "0.7.
|
|
57
|
-
"@gajae-code/ai": "0.7.
|
|
58
|
-
"@gajae-code/natives": "0.7.
|
|
59
|
-
"@gajae-code/tui": "0.7.
|
|
60
|
-
"@gajae-code/utils": "0.7.
|
|
55
|
+
"@gajae-code/stats": "0.7.10",
|
|
56
|
+
"@gajae-code/agent-core": "0.7.10",
|
|
57
|
+
"@gajae-code/ai": "0.7.10",
|
|
58
|
+
"@gajae-code/natives": "0.7.10",
|
|
59
|
+
"@gajae-code/tui": "0.7.10",
|
|
60
|
+
"@gajae-code/utils": "0.7.10",
|
|
61
61
|
"@puppeteer/browsers": "^2.13.0",
|
|
62
62
|
"@types/turndown": "5.0.6",
|
|
63
63
|
"@xterm/headless": "^6.0.0",
|
package/src/cli/notify-cli.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Handles `gjc notify` setup/status and the hidden daemon entrypoint.
|
|
5
5
|
*/
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
|
-
import { APP_NAME } from "@gajae-code/utils";
|
|
7
|
+
import { APP_NAME } from "@gajae-code/utils/dirs";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import type { Settings } from "../config/settings";
|
|
10
10
|
import { getNotificationConfig, maskToken } from "../notifications/config";
|
|
@@ -9,6 +9,7 @@ import chalk from "chalk";
|
|
|
9
9
|
import { Settings } from "../config/settings";
|
|
10
10
|
import { initTheme, theme } from "../modes/theme/theme";
|
|
11
11
|
import {
|
|
12
|
+
getConfiguredSearchProviderPreference,
|
|
12
13
|
isConfigurableSearchProviderId,
|
|
13
14
|
isSearchProviderPreference,
|
|
14
15
|
runSearchQuery,
|
|
@@ -170,8 +171,8 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
|
|
|
170
171
|
|
|
171
172
|
await initTheme();
|
|
172
173
|
const settings = await Settings.init();
|
|
173
|
-
const configuredProvider = settings
|
|
174
|
-
if (
|
|
174
|
+
const configuredProvider = getConfiguredSearchProviderPreference(settings);
|
|
175
|
+
if (isSearchProviderPreference(configuredProvider)) {
|
|
175
176
|
setPreferredSearchProvider(configuredProvider);
|
|
176
177
|
}
|
|
177
178
|
const configuredFallback = settings.get("web_search.fallback");
|
package/src/cli.ts
CHANGED
|
@@ -26,6 +26,7 @@ export const commands: CommandEntry[] = [
|
|
|
26
26
|
{ name: "codex-native-hook", load: () => import("./commands/codex-native-hook").then(m => m.default) },
|
|
27
27
|
{ name: "state", load: () => import("./commands/state").then(m => m.default) },
|
|
28
28
|
{ name: "setup", load: () => import("./commands/setup").then(m => m.default) },
|
|
29
|
+
{ name: "acp", load: () => import("./commands/acp").then(m => m.default) },
|
|
29
30
|
{ name: "skills", load: () => import("./commands/skills").then(m => m.default) },
|
|
30
31
|
{ name: "session", load: () => import("./commands/session").then(m => m.default) },
|
|
31
32
|
{ name: "harness", load: () => import("./commands/harness").then(m => m.default) },
|
|
@@ -118,7 +118,7 @@ export const KEYBINDINGS = {
|
|
|
118
118
|
},
|
|
119
119
|
"app.message.followUp": {
|
|
120
120
|
defaultKeys: [],
|
|
121
|
-
description: "Send follow-up message (no default; Ctrl+Enter
|
|
121
|
+
description: "Send follow-up message (no default; Ctrl+Enter submits)",
|
|
122
122
|
},
|
|
123
123
|
"app.message.queue": {
|
|
124
124
|
defaultKeys: "alt+enter",
|
|
@@ -5,6 +5,7 @@ import { formatClampedModelSelector } from "../thinking";
|
|
|
5
5
|
import {
|
|
6
6
|
aggregateModelProfileRequiredProviders,
|
|
7
7
|
formatAvailableProfileNames,
|
|
8
|
+
formatModelProfileDisplayLabel,
|
|
8
9
|
resolveProfileBindings,
|
|
9
10
|
} from "./model-profiles";
|
|
10
11
|
import {
|
|
@@ -46,7 +47,7 @@ export interface PrepareModelProfileActivationOptions {
|
|
|
46
47
|
export interface PreparedModelProfileActivation {
|
|
47
48
|
profileName: string;
|
|
48
49
|
session: ModelProfileActivationSession & { setModelTemporary: AgentSession["setModelTemporary"] };
|
|
49
|
-
settings: Pick<Settings, "get" | "override" | "set" | "flush">;
|
|
50
|
+
settings: Pick<Settings, "clearOverride" | "get" | "getGlobal" | "override" | "set" | "flush">;
|
|
50
51
|
previousModel: Model<Api> | undefined;
|
|
51
52
|
previousThinkingLevel: ThinkingLevel | undefined;
|
|
52
53
|
previousAgentModelOverrides: Record<string, string>;
|
|
@@ -108,8 +109,8 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
|
|
|
108
109
|
return true;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
export function formatModelProfileCredentialError(
|
|
112
|
-
return `Model profile "${
|
|
112
|
+
export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
|
|
113
|
+
return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
function resolveModelProfileName(profileName: string, profiles: ReadonlyMap<string, unknown>): string {
|
|
@@ -183,6 +184,7 @@ export async function prepareModelProfileActivation(
|
|
|
183
184
|
const available = formatAvailableProfileNames(profiles);
|
|
184
185
|
throw new Error(`Unknown model profile "${options.profileName}". Available profiles: ${available}`);
|
|
185
186
|
}
|
|
187
|
+
const profileLabel = formatModelProfileDisplayLabel(profile);
|
|
186
188
|
|
|
187
189
|
const allProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
|
|
188
190
|
const alternativeGroups = profile.alternativeProviderGroups ?? [];
|
|
@@ -202,19 +204,19 @@ export async function prepareModelProfileActivation(
|
|
|
202
204
|
// Check strict (non-alternative) providers — all must be authenticated.
|
|
203
205
|
const strictMissing = missingProviders.filter(p => !alternativeSet.has(p));
|
|
204
206
|
if (strictMissing.length > 0) {
|
|
205
|
-
throw new Error(formatModelProfileCredentialError(
|
|
207
|
+
throw new Error(formatModelProfileCredentialError(profileLabel, strictMissing));
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
// Check alternative groups — at least one provider per group must be authenticated.
|
|
209
211
|
for (const group of alternativeGroups) {
|
|
210
212
|
const groupAuthenticated = group.some(p => authenticatedProviders.includes(p));
|
|
211
213
|
if (!groupAuthenticated) {
|
|
212
|
-
throw new Error(formatModelProfileCredentialError(
|
|
214
|
+
throw new Error(formatModelProfileCredentialError(profileLabel, [...group]));
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
if (authenticatedProviders.length === 0) {
|
|
217
|
-
throw new Error(formatModelProfileCredentialError(
|
|
219
|
+
throw new Error(formatModelProfileCredentialError(profileLabel, missingProviders));
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
const availableModels = options.modelRegistry.getAll();
|
|
@@ -229,9 +231,7 @@ export async function prepareModelProfileActivation(
|
|
|
229
231
|
})
|
|
230
232
|
: undefined;
|
|
231
233
|
if (bindings.defaultSelector && !resolvedDefault?.model) {
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Model profile "${options.profileName}" default selector did not resolve: ${bindings.defaultSelector}`,
|
|
234
|
-
);
|
|
234
|
+
throw new Error(`Model profile "${profileLabel}" default selector did not resolve: ${bindings.defaultSelector}`);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
const modelRoles: Record<string, string> = {};
|
|
@@ -241,7 +241,7 @@ export async function prepareModelProfileActivation(
|
|
|
241
241
|
modelRegistry: options.modelRegistry,
|
|
242
242
|
});
|
|
243
243
|
if (!resolved.model) {
|
|
244
|
-
throw new Error(`Model profile "${
|
|
244
|
+
throw new Error(`Model profile "${profileLabel}" ${role} selector did not resolve: ${selector}`);
|
|
245
245
|
}
|
|
246
246
|
modelRoles[role] = formatClampedModelSelector(selector, resolved.model);
|
|
247
247
|
}
|
|
@@ -256,7 +256,7 @@ export async function prepareModelProfileActivation(
|
|
|
256
256
|
modelRegistry: options.modelRegistry,
|
|
257
257
|
});
|
|
258
258
|
if (!resolved.model) {
|
|
259
|
-
throw new Error(`Model profile "${
|
|
259
|
+
throw new Error(`Model profile "${profileLabel}" ${role} selector did not resolve: ${selector}`);
|
|
260
260
|
}
|
|
261
261
|
agentModelOverrides[role] = formatClampedModelSelector(selector, resolved.model);
|
|
262
262
|
}
|
|
@@ -357,6 +357,87 @@ export async function applyPreparedModelProfileActivation(
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
export interface MaterializeModelProfileForDeletionResult {
|
|
361
|
+
modelRoles: Record<string, string>;
|
|
362
|
+
agentModelOverrides: Record<string, string>;
|
|
363
|
+
previousModelRoles: Record<string, string>;
|
|
364
|
+
previousAgentModelOverrides: Record<string, string>;
|
|
365
|
+
previousDefaultProfile: string | undefined;
|
|
366
|
+
previousPersistedDefaultProfile: string | undefined;
|
|
367
|
+
previousActiveModelProfile: string | undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function materializeModelProfileForDeletion(
|
|
371
|
+
options: PrepareModelProfileActivationOptions & {
|
|
372
|
+
settings: Pick<Settings, "clearOverride" | "flush" | "get" | "getGlobal" | "override" | "set">;
|
|
373
|
+
},
|
|
374
|
+
): Promise<MaterializeModelProfileForDeletionResult> {
|
|
375
|
+
const prepared = await prepareModelProfileActivation(options);
|
|
376
|
+
const previousDefaultProfile = prepared.settings.get("modelProfile.default");
|
|
377
|
+
const previousPersistedDefaultProfile = prepared.settings.getGlobal("modelProfile.default");
|
|
378
|
+
const nextModelRoles = {
|
|
379
|
+
...prepared.previousModelRoles,
|
|
380
|
+
...(prepared.defaultModel
|
|
381
|
+
? {
|
|
382
|
+
default: formatModelSelectorValue(
|
|
383
|
+
`${prepared.defaultModel.provider}/${prepared.defaultModel.id}`,
|
|
384
|
+
prepared.defaultThinkingLevel,
|
|
385
|
+
),
|
|
386
|
+
}
|
|
387
|
+
: {}),
|
|
388
|
+
...prepared.modelRoles,
|
|
389
|
+
};
|
|
390
|
+
const nextAgentModelOverrides = {
|
|
391
|
+
...prepared.previousAgentModelOverrides,
|
|
392
|
+
...prepared.agentModelOverrides,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
prepared.settings.set("modelRoles", nextModelRoles);
|
|
397
|
+
prepared.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
|
|
398
|
+
prepared.settings.set("modelProfile.default", undefined);
|
|
399
|
+
prepared.settings.clearOverride("modelProfile.default");
|
|
400
|
+
prepared.settings.override("modelRoles", nextModelRoles);
|
|
401
|
+
prepared.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
|
|
402
|
+
prepared.session.setActiveModelProfile?.(undefined);
|
|
403
|
+
await prepared.settings.flush();
|
|
404
|
+
} catch (error) {
|
|
405
|
+
prepared.settings.set("modelRoles", prepared.previousModelRoles);
|
|
406
|
+
prepared.settings.set("task.agentModelOverrides", prepared.previousAgentModelOverrides);
|
|
407
|
+
prepared.settings.set("modelProfile.default", previousPersistedDefaultProfile);
|
|
408
|
+
prepared.settings.override("modelRoles", prepared.previousModelRoles);
|
|
409
|
+
prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverrides);
|
|
410
|
+
prepared.settings.override("modelProfile.default", previousDefaultProfile);
|
|
411
|
+
prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
modelRoles: nextModelRoles,
|
|
417
|
+
agentModelOverrides: nextAgentModelOverrides,
|
|
418
|
+
previousModelRoles: prepared.previousModelRoles,
|
|
419
|
+
previousAgentModelOverrides: prepared.previousAgentModelOverrides,
|
|
420
|
+
previousDefaultProfile,
|
|
421
|
+
previousPersistedDefaultProfile,
|
|
422
|
+
previousActiveModelProfile: prepared.previousActiveModelProfile,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function restoreMaterializedModelProfileForDeletion(options: {
|
|
427
|
+
settings: Pick<Settings, "flush" | "override" | "set">;
|
|
428
|
+
session: Pick<ModelProfileActivationSession, "setActiveModelProfile">;
|
|
429
|
+
snapshot: MaterializeModelProfileForDeletionResult;
|
|
430
|
+
}): Promise<void> {
|
|
431
|
+
options.settings.set("modelRoles", options.snapshot.previousModelRoles);
|
|
432
|
+
options.settings.set("task.agentModelOverrides", options.snapshot.previousAgentModelOverrides);
|
|
433
|
+
options.settings.set("modelProfile.default", options.snapshot.previousPersistedDefaultProfile);
|
|
434
|
+
options.settings.override("modelRoles", options.snapshot.previousModelRoles);
|
|
435
|
+
options.settings.override("task.agentModelOverrides", options.snapshot.previousAgentModelOverrides);
|
|
436
|
+
options.settings.override("modelProfile.default", options.snapshot.previousDefaultProfile);
|
|
437
|
+
options.session.setActiveModelProfile?.(options.snapshot.previousActiveModelProfile);
|
|
438
|
+
await options.settings.flush();
|
|
439
|
+
}
|
|
440
|
+
|
|
360
441
|
export async function activateModelProfile(
|
|
361
442
|
options: PrepareModelProfileActivationOptions,
|
|
362
443
|
applyOptions: { persistDefault?: boolean } = {},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeText } from "@gajae-code/utils";
|
|
1
2
|
import type { GjcModelAssignmentTargetId } from "./model-registry";
|
|
2
3
|
import type { ModelsConfig } from "./models-config-schema";
|
|
3
4
|
|
|
@@ -98,7 +99,7 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
98
99
|
}),
|
|
99
100
|
profile("claude-opus", ["anthropic"], {
|
|
100
101
|
default: "anthropic/claude-opus-4-8:xhigh",
|
|
101
|
-
executor: "anthropic/claude-sonnet-
|
|
102
|
+
executor: "anthropic/claude-sonnet-5",
|
|
102
103
|
planner: "anthropic/claude-opus-4-8:low",
|
|
103
104
|
critic: "anthropic/claude-opus-4-8:high",
|
|
104
105
|
architect: "anthropic/claude-opus-4-8:xhigh",
|
|
@@ -267,6 +268,10 @@ export interface ModelProfilePresentation {
|
|
|
267
268
|
providerGroup: string;
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
function sanitizeModelProfileLabel(value: string): string {
|
|
272
|
+
return sanitizeText(value).replace(/\s+/g, " ").trim();
|
|
273
|
+
}
|
|
274
|
+
|
|
270
275
|
const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
271
276
|
"codex-eco": { displayName: "Codex Eco", providerGroup: "CODEX" },
|
|
272
277
|
"codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
|
|
@@ -332,7 +337,15 @@ export function getModelProfilePresentation(
|
|
|
332
337
|
const displayName = typeof profile === "string" ? undefined : profile.displayName;
|
|
333
338
|
const presentation = PROFILE_PRESENTATION[name];
|
|
334
339
|
if (presentation) return presentation;
|
|
335
|
-
return { displayName:
|
|
340
|
+
return { displayName: formatModelProfileDisplayLabel({ name, displayName }), providerGroup: "CUSTOM" };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function formatModelProfileDisplayLabel(profile: Pick<ModelProfileDefinition, "name" | "displayName">): string {
|
|
344
|
+
return (
|
|
345
|
+
sanitizeModelProfileLabel(profile.displayName ?? profile.name) ||
|
|
346
|
+
sanitizeModelProfileLabel(profile.name) ||
|
|
347
|
+
"Unnamed profile"
|
|
348
|
+
);
|
|
336
349
|
}
|
|
337
350
|
|
|
338
351
|
export function groupModelProfilesForPresetLanding(
|
|
@@ -83,7 +83,8 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
|
|
86
|
-
|
|
86
|
+
export const MODEL_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
87
|
+
export const MODEL_PROFILE_NAME_PATTERN_DESCRIPTION = "lowercase letters, numbers, dots, underscores, or hyphens";
|
|
87
88
|
export type GjcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
|
|
88
89
|
|
|
89
90
|
export interface GjcModelAssignmentTargetInfo extends ModelRoleInfo {
|
|
@@ -1590,10 +1591,7 @@ export class ModelRegistry {
|
|
|
1590
1591
|
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
1591
1592
|
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
1592
1593
|
}
|
|
1593
|
-
await
|
|
1594
|
-
await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(checkedConfig.data, null, 2));
|
|
1595
|
-
this.#modelsConfigFile.invalidate();
|
|
1596
|
-
this.#reloadStaticModels();
|
|
1594
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
1597
1595
|
const modelMapping = { ...definition.model_mapping };
|
|
1598
1596
|
const profile: ModelProfileDefinition = {
|
|
1599
1597
|
name: normalizedName,
|
|
@@ -1604,6 +1602,84 @@ export class ModelRegistry {
|
|
|
1604
1602
|
};
|
|
1605
1603
|
return profile;
|
|
1606
1604
|
}
|
|
1605
|
+
|
|
1606
|
+
async renameCustomModelProfile(name: string, displayName: string): Promise<ModelProfileDefinition> {
|
|
1607
|
+
const normalizedName = name.trim();
|
|
1608
|
+
const nextDisplayName = displayName.trim();
|
|
1609
|
+
if (!normalizedName) throw new Error("Profile name is required.");
|
|
1610
|
+
if (!nextDisplayName) throw new Error("Profile display name is required.");
|
|
1611
|
+
const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "rename");
|
|
1612
|
+
const nextProfiles = {
|
|
1613
|
+
...(current.profiles ?? {}),
|
|
1614
|
+
[normalizedName]: {
|
|
1615
|
+
...profile,
|
|
1616
|
+
display_name: nextDisplayName,
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
1619
|
+
const checkedConfig = ModelsConfigSchema.safeParse({ ...current, profiles: nextProfiles });
|
|
1620
|
+
if (!checkedConfig.success) {
|
|
1621
|
+
const first = checkedConfig.error.issues[0];
|
|
1622
|
+
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
1623
|
+
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
1624
|
+
}
|
|
1625
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
1626
|
+
const modelMapping = { ...profile.model_mapping };
|
|
1627
|
+
return {
|
|
1628
|
+
name: normalizedName,
|
|
1629
|
+
displayName: nextDisplayName,
|
|
1630
|
+
requiredProviders: aggregateModelProfileRequiredProviders(profile.required_providers, { modelMapping }),
|
|
1631
|
+
modelMapping,
|
|
1632
|
+
source: "user",
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
async deleteCustomModelProfile(name: string): Promise<ModelProfileConfig> {
|
|
1637
|
+
const normalizedName = name.trim();
|
|
1638
|
+
if (!normalizedName) throw new Error("Profile name is required.");
|
|
1639
|
+
const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "delete");
|
|
1640
|
+
const nextProfiles = { ...(current.profiles ?? {}) };
|
|
1641
|
+
delete nextProfiles[normalizedName];
|
|
1642
|
+
const checkedConfig = ModelsConfigSchema.safeParse({
|
|
1643
|
+
...current,
|
|
1644
|
+
profiles: Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
|
|
1645
|
+
});
|
|
1646
|
+
if (!checkedConfig.success) {
|
|
1647
|
+
const first = checkedConfig.error.issues[0];
|
|
1648
|
+
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
1649
|
+
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
1650
|
+
}
|
|
1651
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
1652
|
+
return profile;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
#loadCustomProfileForMutation(
|
|
1656
|
+
normalizedName: string,
|
|
1657
|
+
action: "rename" | "delete",
|
|
1658
|
+
): { current: ModelsConfig; profile: ModelProfileConfig } {
|
|
1659
|
+
const loaded = this.#modelsConfigFile.tryLoad();
|
|
1660
|
+
if (loaded.status === "error") {
|
|
1661
|
+
throw new Error(
|
|
1662
|
+
`Cannot ${action} custom model profile because ${this.#modelsConfigFile.path()} is invalid. Fix the existing config before modifying presets.`,
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
const current = loaded.value ?? this.#modelsConfigFile.createDefault();
|
|
1666
|
+
const profile = current.profiles?.[normalizedName];
|
|
1667
|
+
if (!profile) {
|
|
1668
|
+
const existing = this.#modelProfiles.get(normalizedName);
|
|
1669
|
+
if (existing && existing.source !== "user") {
|
|
1670
|
+
throw new Error(`Cannot ${action} bundled model profile: ${normalizedName}.`);
|
|
1671
|
+
}
|
|
1672
|
+
throw new Error(`Custom model profile does not exist: ${normalizedName}.`);
|
|
1673
|
+
}
|
|
1674
|
+
return { current, profile };
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
async #writeCheckedModelsConfig(config: ModelsConfig): Promise<void> {
|
|
1678
|
+
await fs.mkdir(path.dirname(this.#modelsConfigFile.path()), { recursive: true });
|
|
1679
|
+
await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(config, null, 2));
|
|
1680
|
+
this.#modelsConfigFile.invalidate();
|
|
1681
|
+
this.#reloadStaticModels();
|
|
1682
|
+
}
|
|
1607
1683
|
applyConfiguredModelBindings(targetSettings: Settings): void {
|
|
1608
1684
|
this.#modelBindingsTargetSettings = targetSettings;
|
|
1609
1685
|
this.#applyConfiguredModelBindingsToTarget();
|
|
@@ -280,7 +280,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
280
280
|
ui: {
|
|
281
281
|
tab: "interaction",
|
|
282
282
|
label: "Terminal Bell",
|
|
283
|
-
description:
|
|
283
|
+
description:
|
|
284
|
+
"Emit a BEL character for local terminal notifications. Windows Terminal may keep BEL silent depending on profile/system sound settings; use completion.notifyCommand for a PowerShell Console.Beep workaround.",
|
|
284
285
|
},
|
|
285
286
|
},
|
|
286
287
|
"notifications.bellOnComplete": {
|
|
@@ -454,7 +455,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
454
455
|
// Status line
|
|
455
456
|
"statusLine.preset": {
|
|
456
457
|
type: "enum",
|
|
457
|
-
values: ["default", "minimal", "compact", "full", "nerd", "ascii", "custom"] as const,
|
|
458
|
+
values: ["default", "default-usage", "minimal", "compact", "full", "nerd", "ascii", "custom"] as const,
|
|
458
459
|
default: "default",
|
|
459
460
|
ui: {
|
|
460
461
|
tab: "appearance",
|
|
@@ -462,6 +463,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
462
463
|
description: "Pre-built status line configurations",
|
|
463
464
|
options: [
|
|
464
465
|
{ value: "default", label: "Default", description: "Model, path, git, context, tokens, cost" },
|
|
466
|
+
{
|
|
467
|
+
value: "default-usage",
|
|
468
|
+
label: "Default + Usage",
|
|
469
|
+
description: "Default layout with provider usage quota",
|
|
470
|
+
},
|
|
465
471
|
{ value: "minimal", label: "Minimal", description: "Path and git only" },
|
|
466
472
|
{ value: "compact", label: "Compact", description: "Model, git, cost, context" },
|
|
467
473
|
{ value: "full", label: "Full", description: "All segments including time" },
|
|
@@ -1179,7 +1185,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1179
1185
|
tab: "interaction",
|
|
1180
1186
|
label: "Completion Notification Command",
|
|
1181
1187
|
description:
|
|
1182
|
-
"Optional user-level shell command to run when an agent turn completes; receives GJC_NOTIFICATION_* environment variables.",
|
|
1188
|
+
"Optional user-level shell command to run when an agent turn completes; receives GJC_NOTIFICATION_* environment variables. On Windows, this can call PowerShell [Console]::Beep when terminal BEL is silent.",
|
|
1183
1189
|
},
|
|
1184
1190
|
},
|
|
1185
1191
|
|
|
@@ -2204,6 +2210,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
2204
2210
|
ui: { tab: "tools", label: "Web Search", description: "Enable the web_search tool for web searching" },
|
|
2205
2211
|
},
|
|
2206
2212
|
|
|
2213
|
+
"web_search.provider": {
|
|
2214
|
+
type: "enum",
|
|
2215
|
+
values: ["auto", ...CONFIGURABLE_SEARCH_PROVIDER_IDS] as const,
|
|
2216
|
+
default: "auto",
|
|
2217
|
+
},
|
|
2207
2218
|
"web_search.fallback": {
|
|
2208
2219
|
type: "array",
|
|
2209
2220
|
default: EMPTY_STRING_ARRAY,
|
|
@@ -1273,8 +1273,10 @@ async function sendTmuxPromptKeys(
|
|
|
1273
1273
|
prompt: string,
|
|
1274
1274
|
runner: CommandRunner = runCommand,
|
|
1275
1275
|
): Promise<boolean> {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1276
|
+
const typed = await runner(["tmux", "send-keys", "-t", target, "-l", prompt]);
|
|
1277
|
+
if (typed.exitCode !== 0) return false;
|
|
1278
|
+
const submitted = await runner(["tmux", "send-keys", "-t", target, "C-m"]);
|
|
1279
|
+
return submitted.exitCode === 0;
|
|
1278
1280
|
}
|
|
1279
1281
|
|
|
1280
1282
|
function boundedLineCount(value: unknown): number {
|
|
@@ -49,6 +49,7 @@ interface ThresholdModel {
|
|
|
49
49
|
type DeepInterviewModel = RoundQuestionModel | TopologyQuestionModel | ProgressModel | ThresholdModel;
|
|
50
50
|
|
|
51
51
|
function normalizeText(text: string): string {
|
|
52
|
+
if (typeof text !== "string") return "";
|
|
52
53
|
return text.trim().replaceAll("\r\n", "\n");
|
|
53
54
|
}
|
|
54
55
|
|