@gajae-code/coding-agent 0.15.5 → 0.15.6
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 +26 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +2 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +96 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +15 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -7
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +40 -5
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/config/autorouting-tier-map.ts +32 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +51 -9
- package/src/config/model-registry.ts +1021 -122
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/cli/session-cli.ts +12 -1
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +371 -37
- package/src/sdk/host/session-runtime.ts +364 -52
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +228 -96
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +678 -103
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +5 -4
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +772 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
package/src/lsp/config.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import * as piUtils from "@gajae-code/utils";
|
|
5
|
+
import { isRecord, logger, pathIsWithin } from "@gajae-code/utils";
|
|
5
6
|
import { YAML } from "bun";
|
|
6
7
|
import { getConfigDirPaths } from "../config";
|
|
7
8
|
import { type ClaudePluginRoot, getPreloadedPluginRoots } from "../discovery/helpers";
|
|
@@ -332,13 +333,13 @@ export function resolveCommand(command: string, cwd: string): string | null {
|
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
// Fall back to $PATH
|
|
335
|
-
return
|
|
336
|
+
return piUtils.$which(command);
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
/** Resolve an LSP executable without consulting project-controlled bin directories. */
|
|
339
340
|
function resolveTrustedLspCommand(command: string, cwd: string): string | null {
|
|
340
341
|
if (!path.isAbsolute(command) && (command.includes("/") || command.includes("\\"))) return null;
|
|
341
|
-
const discovered = path.isAbsolute(command) ? command :
|
|
342
|
+
const discovered = path.isAbsolute(command) ? command : piUtils.$which(command);
|
|
342
343
|
if (!discovered) return null;
|
|
343
344
|
if (isProjectControlledPath(discovered, cwd)) return null;
|
|
344
345
|
const canonical = canonicalExistingPath(discovered);
|
package/src/lsp/path-trust.ts
CHANGED
|
@@ -63,19 +63,25 @@ export function isProjectControlledPath(candidate: string, cwd: string): boolean
|
|
|
63
63
|
const home = os.homedir();
|
|
64
64
|
const stopPaths = new Set([path.resolve(home), canonicalPath(home)].map(normalizePathForComparison));
|
|
65
65
|
const lexicalTrustRoot = findProjectTrustRoot(cwd, stopPaths);
|
|
66
|
-
|
|
66
|
+
const candidateIsHomeScoped = pathIsLexicallyWithin(home, candidate) || pathIsWithin(home, candidate);
|
|
67
|
+
if (
|
|
68
|
+
lexicalTrustRoot !== undefined &&
|
|
69
|
+
pathIsLexicallyWithin(lexicalTrustRoot, candidate) &&
|
|
70
|
+
(!candidateIsHomeScoped || pathIsLexicallyWithin(home, lexicalTrustRoot))
|
|
71
|
+
)
|
|
72
|
+
return true;
|
|
67
73
|
|
|
68
74
|
const canonicalTrustRoots = new Set<string>();
|
|
69
75
|
if (lexicalTrustRoot !== undefined) canonicalTrustRoots.add(canonicalPath(lexicalTrustRoot));
|
|
70
76
|
const canonicalTrustRoot = findProjectTrustRoot(canonicalPath(cwd), stopPaths);
|
|
71
|
-
if (canonicalTrustRoot !== undefined) canonicalTrustRoots.add(canonicalTrustRoot);
|
|
77
|
+
if (canonicalTrustRoot !== undefined) canonicalTrustRoots.add(canonicalPath(canonicalTrustRoot));
|
|
72
78
|
for (const trustRoot of canonicalTrustRoots) {
|
|
73
79
|
if (
|
|
74
|
-
pathIsLexicallyWithin(trustRoot, canonicalParentPath(candidate)) ||
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
(pathIsLexicallyWithin(trustRoot, canonicalParentPath(candidate)) ||
|
|
81
|
+
pathIsWithin(trustRoot, canonicalPath(candidate))) &&
|
|
82
|
+
(!candidateIsHomeScoped || pathIsLexicallyWithin(home, trustRoot) || pathIsWithin(home, trustRoot))
|
|
83
|
+
)
|
|
77
84
|
return true;
|
|
78
|
-
}
|
|
79
85
|
}
|
|
80
86
|
return false;
|
|
81
87
|
}
|
package/src/main.ts
CHANGED
|
@@ -349,7 +349,9 @@ export async function submitInteractiveInput(
|
|
|
349
349
|
| "checkShutdownRequested"
|
|
350
350
|
| "waitForAgentEnd"
|
|
351
351
|
>,
|
|
352
|
-
session: Pick<AgentSession, "prompt" | "promptCustomMessage"
|
|
352
|
+
session: Pick<AgentSession, "prompt" | "promptCustomMessage"> & {
|
|
353
|
+
continuePersistedHistory?: AgentSession["continuePersistedHistory"];
|
|
354
|
+
},
|
|
353
355
|
input: SubmittedUserInput,
|
|
354
356
|
): Promise<void> {
|
|
355
357
|
if (input.cancelled) {
|
|
@@ -357,8 +359,13 @@ export async function submitInteractiveInput(
|
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
try {
|
|
362
|
+
if (input.started) {
|
|
363
|
+
if (!session.continuePersistedHistory) throw new Error("Session continuation is unavailable.");
|
|
364
|
+
await session.continuePersistedHistory();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
360
367
|
// Continue shortcuts submit an already-started empty prompt with no optimistic user message.
|
|
361
|
-
if (!
|
|
368
|
+
if (!mode.markPendingSubmissionStarted(input)) {
|
|
362
369
|
return;
|
|
363
370
|
}
|
|
364
371
|
const agentEnd = mode.waitForAgentEnd();
|
|
@@ -54,6 +54,7 @@ import type { AbortScope } from "../../sdk/host/control/operations";
|
|
|
54
54
|
import { SYNTHETIC_PROVIDER_ID } from "../../sdk/model-profile-namespace";
|
|
55
55
|
import type { SdkPromptTerminalOutcome } from "../../sdk/prompt-status";
|
|
56
56
|
import { PromptActivity, type PromptWatchdogClock, systemPromptWatchdogClock } from "../../sdk/prompt-watchdog";
|
|
57
|
+
import { validateRequiredPromptText } from "../../sdk/protocol/adapter-validation";
|
|
57
58
|
import { type SessionAttachment, SessionRouter, type SessionRouterFrame } from "../../sdk/router";
|
|
58
59
|
import { SessionListTraversalError, sessionListPageFromResponse, traverseSessionList } from "../../sdk/session-list";
|
|
59
60
|
import { resolveAcpAbortScope } from "./abort-scope";
|
|
@@ -994,7 +995,6 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
|
|
|
994
995
|
text.push(block.text);
|
|
995
996
|
break;
|
|
996
997
|
case "image":
|
|
997
|
-
if (block.uri) text.push(`[Image URI: ${block.uri}]`);
|
|
998
998
|
images.push({ data: block.data, mimeType: block.mimeType });
|
|
999
999
|
break;
|
|
1000
1000
|
case "resource_link":
|
|
@@ -1027,7 +1027,6 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
|
|
|
1027
1027
|
"unsupported_content",
|
|
1028
1028
|
`Unsupported embedded resource MIME type: ${mimeType}`,
|
|
1029
1029
|
);
|
|
1030
|
-
text.push(`[Resource: ${resource.uri}]\nMIME: ${mimeType}`);
|
|
1031
1030
|
images.push({ data: resource.blob, mimeType });
|
|
1032
1031
|
break;
|
|
1033
1032
|
}
|
|
@@ -1038,7 +1037,7 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
|
|
|
1038
1037
|
}
|
|
1039
1038
|
}
|
|
1040
1039
|
if (text.length === 0 && images.length === 0)
|
|
1041
|
-
throw new AcpSdkAdapterError("invalid_input", "
|
|
1040
|
+
throw new AcpSdkAdapterError("invalid_input", "Prompt must not be empty.");
|
|
1042
1041
|
return { text: text.join("\n"), images };
|
|
1043
1042
|
}
|
|
1044
1043
|
|
|
@@ -1576,10 +1575,17 @@ export class AcpAgent implements Agent {
|
|
|
1576
1575
|
if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${params.sessionId}`);
|
|
1577
1576
|
if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
|
|
1578
1577
|
if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
|
|
1579
|
-
// A new turn starts uncancelled; a stale flag must never settle it as `cancelled`.
|
|
1580
|
-
record.cancelRequested = false;
|
|
1581
1578
|
const payload = acpPromptPayload(params.prompt);
|
|
1582
1579
|
const skillInvocation = acpSkillInvocation(params.prompt);
|
|
1580
|
+
if (!skillInvocation) {
|
|
1581
|
+
const promptError = validateRequiredPromptText("turn.prompt", {
|
|
1582
|
+
text: payload.text,
|
|
1583
|
+
images: payload.images,
|
|
1584
|
+
});
|
|
1585
|
+
if (promptError) throw new AcpSdkAdapterError(promptError.code, promptError.message);
|
|
1586
|
+
}
|
|
1587
|
+
// A new turn starts uncancelled; a stale flag must never settle it as `cancelled`.
|
|
1588
|
+
record.cancelRequested = false;
|
|
1583
1589
|
if (isAcpUnavailableSlashCommand(payload.text)) {
|
|
1584
1590
|
await this.#publishSessionUpdate(
|
|
1585
1591
|
params.sessionId,
|
|
@@ -6,20 +6,41 @@
|
|
|
6
6
|
* SelectItem lists into nested SelectLists (list -> detail -> confirm).
|
|
7
7
|
*/
|
|
8
8
|
import type { SelectItem } from "@gajae-code/tui";
|
|
9
|
+
import { replaceTabs, shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
9
10
|
import type { JobsSnapshot } from "../jobs-observer";
|
|
11
|
+
import { sanitizeStatusText } from "../shared";
|
|
10
12
|
|
|
11
|
-
export type JobRefKind = "monitor" | "cron";
|
|
13
|
+
export type JobRefKind = "monitor" | "cron" | "folded";
|
|
12
14
|
|
|
13
15
|
export interface JobRef {
|
|
14
16
|
kind: JobRefKind;
|
|
15
17
|
id: string;
|
|
18
|
+
generation?: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
function encodeJobRefPart(value: string): string {
|
|
22
|
+
return encodeURIComponent(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decodeJobRefPart(value: string): string | undefined {
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(value);
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function displayText(text: string, max: number = TRUNCATE_LENGTHS.CONTENT): string {
|
|
34
|
+
const sanitized = shortenPath(replaceTabs(sanitizeStatusText(text)));
|
|
35
|
+
return truncateToWidth(sanitized, max);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function preview(text: string, max: number = TRUNCATE_LENGTHS.TITLE): string {
|
|
39
|
+
return displayText(text, max);
|
|
40
|
+
}
|
|
19
41
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
42
|
+
function snapshotJobKey(id: string, generation: string | undefined): string {
|
|
43
|
+
return `${id}\u0000${generation ?? ""}`;
|
|
23
44
|
}
|
|
24
45
|
|
|
25
46
|
/** Compact relative time, e.g. "in 5m", "2m ago", "now". */
|
|
@@ -42,30 +63,71 @@ export function parseJobRef(value: string): JobRef | null {
|
|
|
42
63
|
if ((kind === "monitor" || kind === "cron") && id.length > 0) {
|
|
43
64
|
return { kind, id };
|
|
44
65
|
}
|
|
66
|
+
if (kind === "folded") {
|
|
67
|
+
const generationSep = id.indexOf(":");
|
|
68
|
+
if (generationSep > 0 && generationSep < id.length - 1) {
|
|
69
|
+
const decodedId = decodeJobRefPart(id.slice(0, generationSep));
|
|
70
|
+
const generation = decodeJobRefPart(id.slice(generationSep + 1));
|
|
71
|
+
if (decodedId && generation) return { kind, id: decodedId, generation };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
45
74
|
return null;
|
|
46
75
|
}
|
|
47
76
|
|
|
48
77
|
/**
|
|
49
78
|
* Build the grouped jobs list: monitors first (newest-first), then crons
|
|
50
|
-
* (newest-first). The snapshot arrays are already
|
|
79
|
+
* (newest-first), then read-only folded jobs. The snapshot arrays are already
|
|
80
|
+
* sorted newest-first where a source has a timestamp.
|
|
51
81
|
*/
|
|
52
82
|
export function buildJobsListItems(snapshot: JobsSnapshot): SelectItem[] {
|
|
53
83
|
const items: SelectItem[] = [];
|
|
84
|
+
const foldedJobs = snapshot.foldedJobs ?? [];
|
|
85
|
+
const foldedKeys = new Set(foldedJobs.map(job => snapshotJobKey(job.id, job.generation)));
|
|
54
86
|
for (const monitor of snapshot.monitors) {
|
|
87
|
+
// A monitor can also be backgrounded; render it only in the folded section
|
|
88
|
+
// so a public job state has exactly one row.
|
|
89
|
+
if (monitor.backgrounded || foldedKeys.has(snapshotJobKey(monitor.id, monitor.generation))) continue;
|
|
55
90
|
items.push({
|
|
56
91
|
value: `monitor:${monitor.id}`,
|
|
57
|
-
label: `monitor · ${preview(monitor.label,
|
|
58
|
-
description: monitor.status,
|
|
59
|
-
hint: monitor.status === "failed" ? "failed" : undefined,
|
|
92
|
+
label: `monitor · ${preview(monitor.label, TRUNCATE_LENGTHS.SHORT)}`,
|
|
93
|
+
description: displayText(monitor.status, TRUNCATE_LENGTHS.SHORT),
|
|
94
|
+
hint: monitor.status === "failed" || monitor.deliveryState === "failed-visible" ? "failed" : undefined,
|
|
60
95
|
});
|
|
61
96
|
}
|
|
62
97
|
for (const cron of snapshot.crons) {
|
|
63
98
|
items.push({
|
|
64
99
|
value: `cron:${cron.id}`,
|
|
65
|
-
label: `cron · ${cron.humanSchedule}`,
|
|
100
|
+
label: `cron · ${preview(cron.humanSchedule, TRUNCATE_LENGTHS.SHORT)}`,
|
|
66
101
|
description: preview(cron.prompt),
|
|
67
102
|
});
|
|
68
103
|
}
|
|
104
|
+
for (const job of foldedJobs) {
|
|
105
|
+
const state = displayText(job.deliveryState, TRUNCATE_LENGTHS.SHORT);
|
|
106
|
+
const kind = preview(job.kind, TRUNCATE_LENGTHS.SHORT);
|
|
107
|
+
const deadLetter = (snapshot.deadLettered ?? []).find(
|
|
108
|
+
entry => snapshotJobKey(entry.jobId, entry.generation) === snapshotJobKey(job.id, job.generation),
|
|
109
|
+
);
|
|
110
|
+
const errorText = job.errorText ?? deadLetter?.lastError;
|
|
111
|
+
const description =
|
|
112
|
+
deadLetter || errorText
|
|
113
|
+
? displayText(
|
|
114
|
+
[
|
|
115
|
+
state,
|
|
116
|
+
deadLetter ? `attempt ${deadLetter.attempt}` : undefined,
|
|
117
|
+
errorText ? `error: ${errorText}` : undefined,
|
|
118
|
+
]
|
|
119
|
+
.filter((part): part is string => part !== undefined)
|
|
120
|
+
.join(" · "),
|
|
121
|
+
TRUNCATE_LENGTHS.CONTENT,
|
|
122
|
+
)
|
|
123
|
+
: state;
|
|
124
|
+
items.push({
|
|
125
|
+
value: `folded:${encodeJobRefPart(job.id)}:${encodeJobRefPart(job.generation)}`,
|
|
126
|
+
label: `${kind} · ${preview(job.label, TRUNCATE_LENGTHS.SHORT)}`,
|
|
127
|
+
description,
|
|
128
|
+
hint: job.status === "failed" || job.deliveryState === "failed-visible" ? "failed" : "read-only",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
69
131
|
return items;
|
|
70
132
|
}
|
|
71
133
|
|
|
@@ -80,21 +142,41 @@ export function buildJobDetailItems(snapshot: JobsSnapshot, ref: JobRef, output
|
|
|
80
142
|
if (!monitor) return [{ value: "back", label: "Back (job no longer present)" }];
|
|
81
143
|
const lastOutput = output.trim().split("\n").filter(Boolean).slice(-1)[0] ?? "(no output captured)";
|
|
82
144
|
return [
|
|
83
|
-
{ value: "noop", label: "Status", description: monitor.status },
|
|
145
|
+
{ value: "noop", label: "Status", description: displayText(monitor.status, TRUNCATE_LENGTHS.SHORT) },
|
|
84
146
|
{ value: "noop", label: "Label", description: preview(monitor.label) },
|
|
85
147
|
{ value: "noop", label: "Started", description: formatRelative(monitor.startTime) },
|
|
86
|
-
{ value: "noop", label: "Output", description: preview(lastOutput,
|
|
148
|
+
{ value: "noop", label: "Output", description: preview(lastOutput, TRUNCATE_LENGTHS.CONTENT) },
|
|
87
149
|
{ value: "action:cancel", label: "Cancel this monitor", hint: "stops the running job" },
|
|
88
150
|
{ value: "back", label: "Back" },
|
|
89
151
|
];
|
|
90
152
|
}
|
|
153
|
+
if (ref.kind === "folded") {
|
|
154
|
+
const job = snapshot.foldedJobs?.find(
|
|
155
|
+
entry => entry.id === ref.id && (ref.generation === undefined || entry.generation === ref.generation),
|
|
156
|
+
);
|
|
157
|
+
if (!job) return [{ value: "back", label: "Back (job no longer present)" }];
|
|
158
|
+
return [
|
|
159
|
+
{ value: "noop", label: "Status", description: displayText(job.status, TRUNCATE_LENGTHS.SHORT) },
|
|
160
|
+
{ value: "noop", label: "Kind", description: preview(job.kind, TRUNCATE_LENGTHS.SHORT) },
|
|
161
|
+
{ value: "noop", label: "Label", description: preview(job.label) },
|
|
162
|
+
{ value: "noop", label: "Generation", description: preview(job.generation, TRUNCATE_LENGTHS.CONTENT) },
|
|
163
|
+
...(job.errorText
|
|
164
|
+
? [{ value: "noop", label: "Error", description: preview(job.errorText, TRUNCATE_LENGTHS.CONTENT) }]
|
|
165
|
+
: []),
|
|
166
|
+
{ value: "back", label: "Back" },
|
|
167
|
+
];
|
|
168
|
+
}
|
|
91
169
|
const cron = snapshot.crons.find(c => c.id === ref.id);
|
|
92
170
|
if (!cron) return [{ value: "back", label: "Back (job no longer present)" }];
|
|
93
171
|
return [
|
|
94
|
-
{
|
|
172
|
+
{
|
|
173
|
+
value: "noop",
|
|
174
|
+
label: "Schedule",
|
|
175
|
+
description: `${preview(cron.humanSchedule, TRUNCATE_LENGTHS.SHORT)} (${preview(cron.cronExpression, TRUNCATE_LENGTHS.SHORT)})`,
|
|
176
|
+
},
|
|
95
177
|
{ value: "noop", label: "Recurring", description: cron.recurring ? "yes" : "no" },
|
|
96
178
|
{ value: "noop", label: "Next fire", description: formatRelative(cron.nextFireAt) },
|
|
97
|
-
{ value: "noop", label: "Prompt", description: preview(cron.prompt,
|
|
179
|
+
{ value: "noop", label: "Prompt", description: preview(cron.prompt, TRUNCATE_LENGTHS.CONTENT) },
|
|
98
180
|
{ value: "action:delete", label: "Delete this cron", hint: "removes the schedule" },
|
|
99
181
|
{ value: "back", label: "Back" },
|
|
100
182
|
];
|
|
@@ -102,8 +184,9 @@ export function buildJobDetailItems(snapshot: JobsSnapshot, ref: JobRef, output
|
|
|
102
184
|
|
|
103
185
|
/** Yes/No confirm items for a destructive action. */
|
|
104
186
|
export function buildConfirmItems(actionLabel: string): SelectItem[] {
|
|
187
|
+
const safeActionLabel = displayText(actionLabel, TRUNCATE_LENGTHS.TITLE);
|
|
105
188
|
return [
|
|
106
|
-
{ value: "no", label:
|
|
107
|
-
{ value: "yes", label: `Yes, ${
|
|
189
|
+
{ value: "no", label: "No, keep it" },
|
|
190
|
+
{ value: "yes", label: `Yes, ${safeActionLabel}` },
|
|
108
191
|
];
|
|
109
192
|
}
|
|
@@ -22,9 +22,18 @@ import {
|
|
|
22
22
|
normalizeTierMap,
|
|
23
23
|
validateAutoroutingSetup,
|
|
24
24
|
} from "../../config/autorouting-contract";
|
|
25
|
+
import {
|
|
26
|
+
getProxyRoutableProviders,
|
|
27
|
+
inspectProxyProviderId,
|
|
28
|
+
requiresQualifiedModelProfileRoleResolution,
|
|
29
|
+
resolveProxyMode,
|
|
30
|
+
rewriteSelectorForProxy,
|
|
31
|
+
tryResolveProxyProviderId,
|
|
32
|
+
} from "../../config/model-profile-activation";
|
|
25
33
|
|
|
26
34
|
import { isModelProfileProviderAvailable } from "../../config/model-profile-contract";
|
|
27
35
|
import {
|
|
36
|
+
deriveModelProfileMappedProviders,
|
|
28
37
|
getModelProfilePresentation,
|
|
29
38
|
groupModelProfilesForPresetLanding,
|
|
30
39
|
type ModelProfileDefinition,
|
|
@@ -399,6 +408,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
399
408
|
#closeAfterAssignment = false;
|
|
400
409
|
#unsubscribeCatalogChanged: () => void = () => {};
|
|
401
410
|
#unsubscribeProviderOrderChanged: () => void = () => {};
|
|
411
|
+
#unsubscribeAuthGeneration: () => void = () => {};
|
|
402
412
|
#disposed = false;
|
|
403
413
|
/** Standalone smart-routing entry: cancel closes the selector instead of falling back to the preset landing. */
|
|
404
414
|
#smartRoutingOnly = false;
|
|
@@ -413,6 +423,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
413
423
|
#providerAuthById = new Map<string, boolean>();
|
|
414
424
|
#bareProfileAuthByName = new Map<string, boolean>();
|
|
415
425
|
#providerAuthPending: boolean = false;
|
|
426
|
+
#providerAuthRefreshGeneration = 0;
|
|
416
427
|
#presetLoginHint?: string;
|
|
417
428
|
#authSessionId?: string;
|
|
418
429
|
#imageRoleFilter: boolean = false;
|
|
@@ -524,6 +535,14 @@ export class ModelSelectorComponent extends Container {
|
|
|
524
535
|
if (typeof this.#modelRegistry.onCatalogChanged === "function") {
|
|
525
536
|
this.#unsubscribeCatalogChanged = this.#modelRegistry.onCatalogChanged(() => {
|
|
526
537
|
if (this.#disposed) return;
|
|
538
|
+
if (this.#viewMode === "presets") {
|
|
539
|
+
this.#refreshCatalogView();
|
|
540
|
+
this.#clampPresetCursor();
|
|
541
|
+
void this.#refreshProviderAuth();
|
|
542
|
+
this.#renderPresetLanding();
|
|
543
|
+
this.#tui.requestRender();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
527
546
|
if (this.#refreshCatalogView()) this.#tui.requestRender();
|
|
528
547
|
});
|
|
529
548
|
}
|
|
@@ -538,6 +557,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
538
557
|
panel.updateProviderOrderHint(this.#providerOrderHintFor(panel.getProviderOrder()));
|
|
539
558
|
this.#tui.requestRender();
|
|
540
559
|
});
|
|
560
|
+
this.#unsubscribeAuthGeneration =
|
|
561
|
+
this.#modelRegistry.authStorage?.onGenerationChanged?.(() => {
|
|
562
|
+
if (this.#disposed || this.#viewMode !== "presets") return;
|
|
563
|
+
void this.#refreshProviderAuth();
|
|
564
|
+
}) ?? (() => {});
|
|
541
565
|
|
|
542
566
|
// Load models and do initial render
|
|
543
567
|
this.#loadModels().then(() => {
|
|
@@ -572,8 +596,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
572
596
|
override dispose(): void {
|
|
573
597
|
if (this.#disposed) return;
|
|
574
598
|
this.#disposed = true;
|
|
599
|
+
this.#providerAuthRefreshGeneration++;
|
|
575
600
|
this.#unsubscribeCatalogChanged();
|
|
576
601
|
this.#unsubscribeProviderOrderChanged();
|
|
602
|
+
this.#unsubscribeAuthGeneration();
|
|
577
603
|
super.dispose();
|
|
578
604
|
}
|
|
579
605
|
|
|
@@ -1301,13 +1327,61 @@ export class ModelSelectorComponent extends Container {
|
|
|
1301
1327
|
return this.#providerAuthById.get(providerId);
|
|
1302
1328
|
}
|
|
1303
1329
|
|
|
1330
|
+
#getProfileAvailableModels(): Model[] {
|
|
1331
|
+
const getAvailableForProfileActivation = this.#modelRegistry.getAvailableForProfileActivation;
|
|
1332
|
+
return typeof getAvailableForProfileActivation === "function"
|
|
1333
|
+
? getAvailableForProfileActivation.call(this.#modelRegistry)
|
|
1334
|
+
: this.#modelRegistry.getAvailable();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
#createProfileResolutionRegistry(availableModels: readonly Model[]) {
|
|
1338
|
+
return {
|
|
1339
|
+
getAvailable: () => availableModels as Model[],
|
|
1340
|
+
getApiKey: this.#modelRegistry.getApiKey?.bind(this.#modelRegistry) ?? (async () => undefined),
|
|
1341
|
+
resolveCanonicalModel: this.#modelRegistry.resolveCanonicalModel?.bind(this.#modelRegistry),
|
|
1342
|
+
getCanonicalVariants: this.#modelRegistry.getCanonicalVariants?.bind(this.#modelRegistry),
|
|
1343
|
+
getCanonicalId: this.#modelRegistry.getCanonicalId?.bind(this.#modelRegistry),
|
|
1344
|
+
resolveModelByLookupAlias: this.#modelRegistry.resolveModelByLookupAlias?.bind(this.#modelRegistry),
|
|
1345
|
+
lookupAliasExists: this.#modelRegistry.lookupAliasExists?.bind(this.#modelRegistry),
|
|
1346
|
+
clearCanonicalVariant: this.#modelRegistry.clearCanonicalVariant?.bind(this.#modelRegistry),
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
#getProfileAuthenticatedProviders(profile: ModelProfileDefinition): Set<string> {
|
|
1351
|
+
if (profile.source !== "user" && inspectProxyProviderId(this.#settings).status === "invalid") return new Set();
|
|
1352
|
+
const authenticated = new Set(
|
|
1353
|
+
[...this.#providerAuthById]
|
|
1354
|
+
.filter(([, providerAuthenticated]) => providerAuthenticated)
|
|
1355
|
+
.map(([provider]) => provider),
|
|
1356
|
+
);
|
|
1357
|
+
const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(this.#settings);
|
|
1358
|
+
if (proxyProvider !== undefined && this.#isProviderAuthenticated(proxyProvider) === true) {
|
|
1359
|
+
for (const provider of getProxyRoutableProviders(profile)) authenticated.add(provider);
|
|
1360
|
+
}
|
|
1361
|
+
return authenticated;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
#rewriteProfileSelectorForProxy(profile: ModelProfileDefinition, selector: string): string {
|
|
1365
|
+
if (profile.source === "user") return selector;
|
|
1366
|
+
const proxyProvider = tryResolveProxyProviderId(this.#settings);
|
|
1367
|
+
if (proxyProvider === undefined || this.#isProviderAuthenticated(proxyProvider) !== true) return selector;
|
|
1368
|
+
return rewriteSelectorForProxy(
|
|
1369
|
+
selector,
|
|
1370
|
+
proxyProvider,
|
|
1371
|
+
resolveProxyMode(this.#settings),
|
|
1372
|
+
this.#getProfileAvailableModels(),
|
|
1373
|
+
new Set(
|
|
1374
|
+
[...this.#providerAuthById].filter(([, authenticated]) => authenticated).map(([provider]) => provider),
|
|
1375
|
+
),
|
|
1376
|
+
getProxyRoutableProviders(profile),
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1304
1380
|
#getMissingProviders(profileOrProfiles: ModelProfileDefinition | ModelProfileDefinition[]): string[] {
|
|
1305
1381
|
const profiles = Array.isArray(profileOrProfiles) ? profileOrProfiles : [profileOrProfiles];
|
|
1306
1382
|
const missing = new Set<string>();
|
|
1307
1383
|
for (const profile of profiles) {
|
|
1308
|
-
const authenticated =
|
|
1309
|
-
profileRequiredProviders(profile).filter(provider => this.#isProviderAuthenticated(provider) === true),
|
|
1310
|
-
);
|
|
1384
|
+
const authenticated = this.#getProfileAuthenticatedProviders(profile);
|
|
1311
1385
|
if (isModelProfileProviderAvailable(profile, authenticated)) continue;
|
|
1312
1386
|
const alternativeGroups = profile.alternativeProviderGroups ?? [];
|
|
1313
1387
|
const alternativeProviders = new Set(alternativeGroups.flat());
|
|
@@ -1325,18 +1399,52 @@ export class ModelSelectorComponent extends Container {
|
|
|
1325
1399
|
#isPresetAuthenticated(profileOrProfiles: ModelProfileDefinition | ModelProfileDefinition[]): boolean {
|
|
1326
1400
|
const profiles = Array.isArray(profileOrProfiles) ? profileOrProfiles : [profileOrProfiles];
|
|
1327
1401
|
return profiles.every(profile => {
|
|
1402
|
+
if (profile.source !== "user") {
|
|
1403
|
+
if (inspectProxyProviderId(this.#settings).status === "invalid") return false;
|
|
1404
|
+
const proxyProvider = tryResolveProxyProviderId(this.#settings);
|
|
1405
|
+
if (
|
|
1406
|
+
proxyProvider !== undefined &&
|
|
1407
|
+
!(this.#modelRegistry.getConfiguredProviderIds?.() ?? []).includes(proxyProvider)
|
|
1408
|
+
)
|
|
1409
|
+
return false;
|
|
1410
|
+
try {
|
|
1411
|
+
if (resolveProxyMode(this.#settings) === "always") {
|
|
1412
|
+
const configuredProviders = this.#modelRegistry.getConfiguredProviderIds?.() ?? [];
|
|
1413
|
+
if (
|
|
1414
|
+
proxyProvider === undefined ||
|
|
1415
|
+
this.#isProviderAuthenticated(proxyProvider) !== true ||
|
|
1416
|
+
!configuredProviders.includes(proxyProvider)
|
|
1417
|
+
)
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
} catch {
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1328
1424
|
if (this.#getMissingProviders(profile).length > 0) return false;
|
|
1329
1425
|
const bindings = resolveProfileBindings(profile);
|
|
1330
|
-
const
|
|
1331
|
-
...(bindings.defaultSelector ? [bindings.defaultSelector] : []),
|
|
1332
|
-
...Object.values(bindings.modelRoles),
|
|
1333
|
-
...Object.values(bindings.agentModelOverrides),
|
|
1426
|
+
const assignments = [
|
|
1427
|
+
...(bindings.defaultSelector !== undefined ? [{ value: bindings.defaultSelector, isDefault: true }] : []),
|
|
1428
|
+
...Object.values(bindings.modelRoles).map(value => ({ value, isDefault: false })),
|
|
1429
|
+
...Object.values(bindings.agentModelOverrides).map(value => ({ value, isDefault: false })),
|
|
1334
1430
|
];
|
|
1335
1431
|
const bareAssignmentsAvailable = this.#bareProfileAuthByName.get(profile.name) ?? true;
|
|
1336
|
-
|
|
1337
|
-
|
|
1432
|
+
const availableModels = this.#getProfileAvailableModels();
|
|
1433
|
+
return assignments.every(assignment => {
|
|
1434
|
+
let selectors: string[];
|
|
1435
|
+
try {
|
|
1436
|
+
selectors = normalizeModelSelectorValue(assignment.value).map(selector =>
|
|
1437
|
+
this.#rewriteProfileSelectorForProxy(profile, selector),
|
|
1438
|
+
);
|
|
1439
|
+
} catch {
|
|
1440
|
+
return false;
|
|
1441
|
+
}
|
|
1442
|
+
const hasBareSelector = selectors.some(selector => !selector.includes("/"));
|
|
1443
|
+
if (!assignment.isDefault && !requiresQualifiedModelProfileRoleResolution(profile) && !hasBareSelector)
|
|
1444
|
+
return true;
|
|
1445
|
+
return selectors.some(selector => {
|
|
1338
1446
|
if (!selector.includes("/")) return bareAssignmentsAvailable;
|
|
1339
|
-
const resolved = resolveModelRoleValue(selector,
|
|
1447
|
+
const resolved = resolveModelRoleValue(selector, availableModels, {
|
|
1340
1448
|
settings: this.#settings,
|
|
1341
1449
|
modelRegistry: this.#modelRegistry,
|
|
1342
1450
|
aliasIntent: "preset-equivalent",
|
|
@@ -1357,7 +1465,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1357
1465
|
return (
|
|
1358
1466
|
resolveModelRoleValue(
|
|
1359
1467
|
replacementSelector,
|
|
1360
|
-
|
|
1468
|
+
availableModels.filter(model => model.provider === provider),
|
|
1361
1469
|
{
|
|
1362
1470
|
settings: this.#settings,
|
|
1363
1471
|
modelRegistry: this.#modelRegistry,
|
|
@@ -1368,8 +1476,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
1368
1476
|
);
|
|
1369
1477
|
}) ?? false
|
|
1370
1478
|
);
|
|
1371
|
-
})
|
|
1372
|
-
);
|
|
1479
|
+
});
|
|
1480
|
+
});
|
|
1373
1481
|
});
|
|
1374
1482
|
}
|
|
1375
1483
|
|
|
@@ -1383,10 +1491,23 @@ export class ModelSelectorComponent extends Container {
|
|
|
1383
1491
|
}
|
|
1384
1492
|
|
|
1385
1493
|
async #refreshProviderAuth(): Promise<void> {
|
|
1494
|
+
const refreshGeneration = ++this.#providerAuthRefreshGeneration;
|
|
1495
|
+
this.#providerAuthById = new Map();
|
|
1496
|
+
this.#bareProfileAuthByName = new Map();
|
|
1497
|
+
const availableModels = this.#getProfileAvailableModels();
|
|
1498
|
+
const resolutionRegistry = this.#createProfileResolutionRegistry(availableModels);
|
|
1386
1499
|
const providers = new Set<string>();
|
|
1387
1500
|
for (const profiles of this.#getPresetGroups().values()) {
|
|
1388
1501
|
for (const profile of profiles) {
|
|
1389
1502
|
for (const provider of profileRequiredProviders(profile)) providers.add(provider);
|
|
1503
|
+
for (const group of profile.alternativeProviderGroups ?? []) {
|
|
1504
|
+
for (const provider of group) providers.add(provider);
|
|
1505
|
+
}
|
|
1506
|
+
for (const provider of deriveModelProfileMappedProviders(profile)) providers.add(provider);
|
|
1507
|
+
if (profile.source !== "user") {
|
|
1508
|
+
const proxyProvider = tryResolveProxyProviderId(this.#settings);
|
|
1509
|
+
if (proxyProvider !== undefined) providers.add(proxyProvider);
|
|
1510
|
+
}
|
|
1390
1511
|
const bindings = resolveProfileBindings(profile);
|
|
1391
1512
|
const values = [
|
|
1392
1513
|
...(bindings.defaultSelector ? [bindings.defaultSelector] : []),
|
|
@@ -1395,7 +1516,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1395
1516
|
];
|
|
1396
1517
|
for (const value of values) {
|
|
1397
1518
|
for (const selector of normalizeModelSelectorValue(value)) {
|
|
1398
|
-
const resolved = resolveModelRoleValue(selector,
|
|
1519
|
+
const resolved = resolveModelRoleValue(selector, availableModels, {
|
|
1399
1520
|
settings: this.#settings,
|
|
1400
1521
|
modelRegistry: this.#modelRegistry,
|
|
1401
1522
|
aliasIntent: "preset-equivalent",
|
|
@@ -1419,6 +1540,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
1419
1540
|
}
|
|
1420
1541
|
}),
|
|
1421
1542
|
);
|
|
1543
|
+
if (
|
|
1544
|
+
this.#disposed ||
|
|
1545
|
+
this.#viewMode !== "presets" ||
|
|
1546
|
+
refreshGeneration !== this.#providerAuthRefreshGeneration
|
|
1547
|
+
)
|
|
1548
|
+
return;
|
|
1422
1549
|
this.#providerAuthById = new Map(entries);
|
|
1423
1550
|
const profileAuthEntries = await Promise.all(
|
|
1424
1551
|
[...this.#getPresetGroups().values()].flat().map(async profile => {
|
|
@@ -1436,7 +1563,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1436
1563
|
try {
|
|
1437
1564
|
const resolution = await resolveModelChainWithAuth(
|
|
1438
1565
|
normalizeModelSelectorValue(value),
|
|
1439
|
-
|
|
1566
|
+
resolutionRegistry,
|
|
1440
1567
|
this.#settings,
|
|
1441
1568
|
this.#authSessionId,
|
|
1442
1569
|
{
|
|
@@ -1455,11 +1582,23 @@ export class ModelSelectorComponent extends Container {
|
|
|
1455
1582
|
return [profile.name, available.every(Boolean)] as const;
|
|
1456
1583
|
}),
|
|
1457
1584
|
);
|
|
1585
|
+
if (
|
|
1586
|
+
this.#disposed ||
|
|
1587
|
+
this.#viewMode !== "presets" ||
|
|
1588
|
+
refreshGeneration !== this.#providerAuthRefreshGeneration
|
|
1589
|
+
)
|
|
1590
|
+
return;
|
|
1458
1591
|
this.#bareProfileAuthByName = new Map(profileAuthEntries);
|
|
1459
1592
|
} finally {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1593
|
+
if (
|
|
1594
|
+
!this.#disposed &&
|
|
1595
|
+
this.#viewMode === "presets" &&
|
|
1596
|
+
refreshGeneration === this.#providerAuthRefreshGeneration
|
|
1597
|
+
) {
|
|
1598
|
+
this.#providerAuthPending = false;
|
|
1599
|
+
this.#renderPresetLanding();
|
|
1600
|
+
this.#tui.requestRender();
|
|
1601
|
+
}
|
|
1463
1602
|
}
|
|
1464
1603
|
}
|
|
1465
1604
|
|
|
@@ -1676,6 +1815,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1676
1815
|
this.#smartRoutingPanel = undefined;
|
|
1677
1816
|
this.#viewMode = "presets";
|
|
1678
1817
|
this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, this.#getPresetRows().length - 1));
|
|
1818
|
+
void this.#refreshProviderAuth();
|
|
1679
1819
|
this.#renderPresetLanding();
|
|
1680
1820
|
this.#tui.requestRender();
|
|
1681
1821
|
}
|
|
@@ -307,7 +307,9 @@ const jobsSegment: StatusLineSegment = {
|
|
|
307
307
|
id: "jobs",
|
|
308
308
|
render(ctx) {
|
|
309
309
|
const { jobs } = ctx;
|
|
310
|
-
const
|
|
310
|
+
const foldedJobCount = jobs.foldedJobs?.filter(job => job.backgrounded).length ?? 0;
|
|
311
|
+
const visible =
|
|
312
|
+
jobs.activeMonitorCount > 0 || jobs.activeCronCount > 0 || foldedJobCount > 0 || jobs.worstState === "failed";
|
|
311
313
|
if (!visible) {
|
|
312
314
|
return { content: "", visible: false };
|
|
313
315
|
}
|
|
@@ -318,6 +320,9 @@ const jobsSegment: StatusLineSegment = {
|
|
|
318
320
|
if (jobs.activeCronCount > 0) {
|
|
319
321
|
parts.push(withIcon(theme.icon.time, `${jobs.activeCronCount}`));
|
|
320
322
|
}
|
|
323
|
+
if (foldedJobCount > 0) {
|
|
324
|
+
parts.push(withIcon(theme.icon.pause || theme.icon.agents, `${foldedJobCount} folded`));
|
|
325
|
+
}
|
|
321
326
|
if (parts.length === 0) {
|
|
322
327
|
// Nothing active but a failure is unacknowledged — keep a drill-in marker.
|
|
323
328
|
parts.push(withIcon(theme.icon.warning, "jobs"));
|
|
@@ -312,7 +312,7 @@ export class ExtensionUiController {
|
|
|
312
312
|
session.retryNow();
|
|
313
313
|
return { retried: true, immediate: true };
|
|
314
314
|
case "bash.background":
|
|
315
|
-
if (!session.requestForegroundBashBackground())
|
|
315
|
+
if (!(await session.requestForegroundBashBackground()))
|
|
316
316
|
throw Object.assign(new Error("The active bash command cannot be moved to a managed background job."), {
|
|
317
317
|
code: "not_foldable",
|
|
318
318
|
});
|