fix: harden team launch bootstrap provisioning
This commit is contained in:
parent
85959b6954
commit
bf3011624d
53 changed files with 1720 additions and 235 deletions
|
|
@ -22,6 +22,13 @@ Default local run target:
|
||||||
- Do not start the browser/web dev mode for normal development or smoke checks. The browser path is limited and lacks the full desktop runtime, IPC, terminal, provider auth, and team lifecycle behavior.
|
- Do not start the browser/web dev mode for normal development or smoke checks. The browser path is limited and lacks the full desktop runtime, IPC, terminal, provider auth, and team lifecycle behavior.
|
||||||
- When documenting or recommending startup commands, point contributors to the desktop app unless a task explicitly asks for browser-mode internals.
|
- When documenting or recommending startup commands, point contributors to the desktop app unless a task explicitly asks for browser-mode internals.
|
||||||
|
|
||||||
|
Live team smoke runtime:
|
||||||
|
|
||||||
|
- Use the orchestrator source launcher by default for live/dev smoke loops: `/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source`
|
||||||
|
- The source launcher runs `src/entrypoints/cli.tsx` through Bun, so it reflects local orchestrator source edits immediately and cannot accidentally test stale `dist` output.
|
||||||
|
- Use the built wrapper only for release or production-like smoke checks. Build first in `/Users/belief/dev/projects/claude/agent_teams_orchestrator` with `bun run build`, then set `CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH=/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli`.
|
||||||
|
- Do not use `cli-dev` or `bun run build:dev` as proof for the production wrapper. `cli` reads `dist/local-cli/cli.js`; `cli-dev` reads `dist/local-cli-dev/cli.js`.
|
||||||
|
|
||||||
Fast local lint:
|
Fast local lint:
|
||||||
|
|
||||||
- Use `pnpm lint:fast:files -- <changed files>` for quick preflight on files you touched.
|
- Use `pnpm lint:fast:files -- <changed files>` for quick preflight on files you touched.
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -92,11 +92,11 @@ If you want the FRESHEST version, clone the repo and run it from the `dev` branc
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Table of contents](#table-of-contents)
|
- [Table of contents](#table-of-contents)
|
||||||
- [What is this](#what-is-this)
|
- [What is this](#what-is-this)
|
||||||
- [Developer architecture docs](#developer-architecture-docs)
|
|
||||||
- [Comparison](#comparison)
|
- [Comparison](#comparison)
|
||||||
- [Quick start](#quick-start)
|
- [Quick start](#quick-start)
|
||||||
- [FAQ](#faq)
|
- [FAQ](#faq)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
|
- [Developer architecture docs](#developer-architecture-docs)
|
||||||
- [Tech stack](#tech-stack)
|
- [Tech stack](#tech-stack)
|
||||||
- [Build for distribution](#build-for-distribution)
|
- [Build for distribution](#build-for-distribution)
|
||||||
- [Scripts](#scripts)
|
- [Scripts](#scripts)
|
||||||
|
|
@ -159,15 +159,6 @@ An orchestration layer for AI agent teams across Claude, Codex, and OpenCode.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Developer architecture docs
|
|
||||||
|
|
||||||
For feature architecture and implementation guidance:
|
|
||||||
|
|
||||||
- Canonical standard - [docs/FEATURE_ARCHITECTURE_STANDARD.md](docs/FEATURE_ARCHITECTURE_STANDARD.md)
|
|
||||||
- Repo working instructions - [CLAUDE.md](CLAUDE.md)
|
|
||||||
- Feature root guidance - [src/features/README.md](src/features/README.md)
|
|
||||||
- Reference implementation - `src/features/recent-projects`
|
|
||||||
|
|
||||||
## Comparison
|
## Comparison
|
||||||
|
|
||||||
| Feature | Agent Teams | Gastown | Paperclip | Cursor | Claude Code CLI |
|
| Feature | Agent Teams | Gastown | Paperclip | Cursor | Claude Code CLI |
|
||||||
|
|
@ -263,6 +254,15 @@ Yes. Run multiple teams in one project or across different projects, even simult
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
### Developer architecture docs
|
||||||
|
|
||||||
|
For feature architecture and implementation guidance:
|
||||||
|
|
||||||
|
- Canonical standard - [docs/FEATURE_ARCHITECTURE_STANDARD.md](docs/FEATURE_ARCHITECTURE_STANDARD.md)
|
||||||
|
- Repo working instructions - [CLAUDE.md](CLAUDE.md)
|
||||||
|
- Feature root guidance - [src/features/README.md](src/features/README.md)
|
||||||
|
- Reference implementation - `src/features/recent-projects`
|
||||||
|
|
||||||
## Tech stack
|
## Tech stack
|
||||||
|
|
||||||
Electron 40, React 19, TypeScript 5, Tailwind CSS 3, Zustand 4. Data from `~/.claude/` (session logs, todos, tasks). The desktop app works with local runtime/session state, while some runtime modes may also use provider or startup capability services when required.
|
Electron 40, React 19, TypeScript 5, Tailwind CSS 3, Zustand 4. Data from `~/.claude/` (session logs, todos, tasks). The desktop app works with local runtime/session state, while some runtime modes may also use provider or startup capability services when required.
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,32 @@ Use this mode to inspect interactive CLI behavior, terminal prompts, and pane ou
|
||||||
as equivalent to the process backend for recovery semantics; persisted pane IDs can help discovery,
|
as equivalent to the process backend for recovery semantics; persisted pane IDs can help discovery,
|
||||||
but app restart does not make old panes a fully app-owned runtime again.
|
but app restart does not make old panes a fully app-owned runtime again.
|
||||||
|
|
||||||
|
## Live Smoke Runtime Launcher
|
||||||
|
|
||||||
|
Live/dev smoke checks should run the orchestrator from source unless the test explicitly says it is
|
||||||
|
validating packaged output. This keeps app smoke tests aligned with the source tree and avoids a stale
|
||||||
|
`dist` bundle hiding runtime changes.
|
||||||
|
|
||||||
|
Default live/dev smoke launcher:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH=/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source
|
||||||
|
```
|
||||||
|
|
||||||
|
The source launcher executes `src/entrypoints/cli.tsx` through Bun. It is the right default for local
|
||||||
|
debug loops, live model/provider checks, and cross-repo runtime fixes.
|
||||||
|
|
||||||
|
Release or production-like smoke checks must validate the built wrapper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/belief/dev/projects/claude/agent_teams_orchestrator
|
||||||
|
bun run build
|
||||||
|
export CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH=/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
`cli` reads `dist/local-cli/cli.js`. `cli-dev` reads `dist/local-cli-dev/cli.js`, so a passing
|
||||||
|
`cli-dev` smoke is not proof that the production wrapper is fresh.
|
||||||
|
|
||||||
## Member State Meanings
|
## Member State Meanings
|
||||||
|
|
||||||
Common `launch-state.json` cases:
|
Common `launch-state.json` cases:
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
|
|
@ -256,6 +256,10 @@
|
||||||
background: var(--cyber-hero-bg);
|
background: var(--cyber-hero-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#hero.cyber-hero {
|
||||||
|
padding-top: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
.cyber-hero__background,
|
.cyber-hero__background,
|
||||||
.cyber-hero__monterey,
|
.cyber-hero__monterey,
|
||||||
.cyber-hero__wash,
|
.cyber-hero__wash,
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ const docsHref = computed(() => {
|
||||||
.app-footer__robot-stage {
|
.app-footer__robot-stage {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: clamp(24px, 7vw, 112px);
|
right: clamp(24px, 7vw, 112px);
|
||||||
bottom: calc(100% - 18px);
|
bottom: calc(100% - 11px);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
width: clamp(178px, 16vw, 236px);
|
width: clamp(178px, 16vw, 236px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.app-layout__main {
|
.app-layout__main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-top: 64px;
|
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 78% 18%, rgba(0, 234, 255, 0.08), transparent 32%),
|
radial-gradient(circle at 78% 18%, rgba(0, 234, 255, 0.08), transparent 32%),
|
||||||
radial-gradient(circle at 18% 72%, rgba(47, 125, 255, 0.07), transparent 38%),
|
radial-gradient(circle at 18% 72%, rgba(47, 125, 255, 0.07), transparent 38%),
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ export interface AnthropicRuntimeReconciliation {
|
||||||
fastModeResetReason: string | null;
|
fastModeResetReason: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AnthropicEffortSupportResolution =
|
||||||
|
| { kind: 'supported'; source: 'catalog' | 'runtime-capability' | 'static-fallback' }
|
||||||
|
| { kind: 'unsupported-by-catalog'; supportedEfforts: EffortLevel[] }
|
||||||
|
| { kind: 'unsupported-by-runtime-capability'; supportedEfforts: EffortLevel[] }
|
||||||
|
| { kind: 'unverified-catalog-missing' };
|
||||||
|
|
||||||
function getAnthropicCatalog(
|
function getAnthropicCatalog(
|
||||||
source: AnthropicRuntimeProfileSource
|
source: AnthropicRuntimeProfileSource
|
||||||
): CliProviderModelCatalog | null {
|
): CliProviderModelCatalog | null {
|
||||||
|
|
@ -77,17 +83,89 @@ function hasCatalogTruth(selection: AnthropicRuntimeSelection): boolean {
|
||||||
return selection.catalogSource !== 'unavailable' && selection.catalogStatus !== 'unavailable';
|
return selection.catalogSource !== 'unavailable' && selection.catalogStatus !== 'unavailable';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripOneMillionSuffix(model: string): string {
|
||||||
|
return model
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/(?:\[1m\])+$/i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKnownAnthropicReasoningModel(model: string | null | undefined): boolean {
|
||||||
|
const normalized = stripOneMillionSuffix(model ?? '');
|
||||||
|
return (
|
||||||
|
normalized === 'opus' ||
|
||||||
|
normalized === 'sonnet' ||
|
||||||
|
normalized === 'claude-opus-4-7' ||
|
||||||
|
normalized.startsWith('claude-opus-4-7-') ||
|
||||||
|
normalized === 'claude-opus-4-6' ||
|
||||||
|
normalized.startsWith('claude-opus-4-6-') ||
|
||||||
|
normalized === 'claude-sonnet-4-6' ||
|
||||||
|
normalized.startsWith('claude-sonnet-4-6-')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRuntimeReasoningEfforts(
|
||||||
|
capabilities: CliProviderRuntimeCapabilities | null | undefined
|
||||||
|
): EffortLevel[] {
|
||||||
|
return normalizeEffortLevels(capabilities?.reasoningEffort?.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAnthropicEffortSupport(params: {
|
||||||
|
selection: AnthropicRuntimeSelection;
|
||||||
|
effort: EffortLevel;
|
||||||
|
runtimeCapabilities?: CliProviderRuntimeCapabilities | null;
|
||||||
|
}): AnthropicEffortSupportResolution {
|
||||||
|
if (params.selection.catalogModel) {
|
||||||
|
return params.selection.supportedEfforts.includes(params.effort)
|
||||||
|
? { kind: 'supported', source: 'catalog' }
|
||||||
|
: { kind: 'unsupported-by-catalog', supportedEfforts: params.selection.supportedEfforts };
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeReasoning = params.runtimeCapabilities?.reasoningEffort;
|
||||||
|
const runtimeEfforts = normalizeRuntimeReasoningEfforts(params.runtimeCapabilities);
|
||||||
|
if (runtimeReasoning) {
|
||||||
|
if (
|
||||||
|
runtimeReasoning.supported !== true ||
|
||||||
|
runtimeReasoning.configPassthrough !== true ||
|
||||||
|
!runtimeEfforts.includes(params.effort)
|
||||||
|
) {
|
||||||
|
return { kind: 'unsupported-by-runtime-capability', supportedEfforts: runtimeEfforts };
|
||||||
|
}
|
||||||
|
if (isKnownAnthropicReasoningModel(params.selection.resolvedLaunchModel)) {
|
||||||
|
return { kind: 'supported', source: 'runtime-capability' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!runtimeReasoning &&
|
||||||
|
isKnownAnthropicReasoningModel(params.selection.resolvedLaunchModel) &&
|
||||||
|
(params.effort === 'low' ||
|
||||||
|
params.effort === 'medium' ||
|
||||||
|
params.effort === 'high' ||
|
||||||
|
params.effort === 'max')
|
||||||
|
) {
|
||||||
|
return { kind: 'supported', source: 'static-fallback' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kind: 'unverified-catalog-missing' };
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveAnthropicRuntimeSelection(params: {
|
export function resolveAnthropicRuntimeSelection(params: {
|
||||||
source: AnthropicRuntimeProfileSource;
|
source: AnthropicRuntimeProfileSource;
|
||||||
selectedModel?: string | null;
|
selectedModel?: string | null;
|
||||||
limitContext: boolean;
|
limitContext: boolean;
|
||||||
|
availableLaunchModels?: Iterable<string>;
|
||||||
}): AnthropicRuntimeSelection {
|
}): AnthropicRuntimeSelection {
|
||||||
const catalog = getAnthropicCatalog(params.source);
|
const catalog = getAnthropicCatalog(params.source);
|
||||||
|
const availableLaunchModels =
|
||||||
|
catalog !== null
|
||||||
|
? catalog.models.map((model) => model.launchModel)
|
||||||
|
: params.availableLaunchModels;
|
||||||
const resolvedLaunchModel =
|
const resolvedLaunchModel =
|
||||||
resolveAnthropicLaunchModel({
|
resolveAnthropicLaunchModel({
|
||||||
selectedModel: params.selectedModel,
|
selectedModel: params.selectedModel,
|
||||||
limitContext: params.limitContext,
|
limitContext: params.limitContext,
|
||||||
availableLaunchModels: catalog?.models.map((model) => model.launchModel),
|
availableLaunchModels,
|
||||||
defaultLaunchModel: catalog?.defaultLaunchModel ?? null,
|
defaultLaunchModel: catalog?.defaultLaunchModel ?? null,
|
||||||
}) ?? null;
|
}) ?? null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export type {
|
export type {
|
||||||
|
AnthropicEffortSupportResolution,
|
||||||
AnthropicFastModeResolution,
|
AnthropicFastModeResolution,
|
||||||
AnthropicRuntimeProfileSource,
|
AnthropicRuntimeProfileSource,
|
||||||
AnthropicRuntimeReconciliation,
|
AnthropicRuntimeReconciliation,
|
||||||
|
|
@ -6,6 +7,7 @@ export type {
|
||||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||||
export {
|
export {
|
||||||
reconcileAnthropicRuntimeSelections,
|
reconcileAnthropicRuntimeSelections,
|
||||||
|
resolveAnthropicEffortSupport,
|
||||||
resolveAnthropicFastMode,
|
resolveAnthropicFastMode,
|
||||||
resolveAnthropicRuntimeSelection,
|
resolveAnthropicRuntimeSelection,
|
||||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export type {
|
export type {
|
||||||
|
AnthropicEffortSupportResolution,
|
||||||
AnthropicFastModeResolution,
|
AnthropicFastModeResolution,
|
||||||
AnthropicRuntimeProfileSource,
|
AnthropicRuntimeProfileSource,
|
||||||
AnthropicRuntimeReconciliation,
|
AnthropicRuntimeReconciliation,
|
||||||
|
|
@ -6,6 +7,7 @@ export type {
|
||||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||||
export {
|
export {
|
||||||
reconcileAnthropicRuntimeSelections,
|
reconcileAnthropicRuntimeSelections,
|
||||||
|
resolveAnthropicEffortSupport,
|
||||||
resolveAnthropicFastMode,
|
resolveAnthropicFastMode,
|
||||||
resolveAnthropicRuntimeSelection,
|
resolveAnthropicRuntimeSelection,
|
||||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,7 @@ import type {
|
||||||
TeamMessageNotificationData,
|
TeamMessageNotificationData,
|
||||||
TeamProviderBackendId,
|
TeamProviderBackendId,
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
|
TeamProvisioningModelCheckRequest,
|
||||||
TeamProvisioningModelVerificationMode,
|
TeamProvisioningModelVerificationMode,
|
||||||
TeamProvisioningPrepareResult,
|
TeamProvisioningPrepareResult,
|
||||||
TeamProvisioningProgress,
|
TeamProvisioningProgress,
|
||||||
|
|
@ -2366,7 +2367,8 @@ async function handlePrepareProvisioning(
|
||||||
providerIds: unknown,
|
providerIds: unknown,
|
||||||
selectedModels: unknown,
|
selectedModels: unknown,
|
||||||
limitContext: unknown,
|
limitContext: unknown,
|
||||||
modelVerificationMode: unknown
|
modelVerificationMode: unknown,
|
||||||
|
selectedModelChecks: unknown
|
||||||
): Promise<IpcResult<TeamProvisioningPrepareResult>> {
|
): Promise<IpcResult<TeamProvisioningPrepareResult>> {
|
||||||
let validatedCwd: string | undefined;
|
let validatedCwd: string | undefined;
|
||||||
let validatedProviderId: TeamLaunchRequest['providerId'];
|
let validatedProviderId: TeamLaunchRequest['providerId'];
|
||||||
|
|
@ -2374,6 +2376,7 @@ async function handlePrepareProvisioning(
|
||||||
let validatedSelectedModels: string[] | undefined;
|
let validatedSelectedModels: string[] | undefined;
|
||||||
let validatedLimitContext: boolean | undefined;
|
let validatedLimitContext: boolean | undefined;
|
||||||
let validatedModelVerificationMode: TeamProvisioningModelVerificationMode | undefined;
|
let validatedModelVerificationMode: TeamProvisioningModelVerificationMode | undefined;
|
||||||
|
let validatedSelectedModelChecks: TeamProvisioningModelCheckRequest[] | undefined;
|
||||||
if (cwd !== undefined) {
|
if (cwd !== undefined) {
|
||||||
if (typeof cwd !== 'string' || cwd.trim().length === 0) {
|
if (typeof cwd !== 'string' || cwd.trim().length === 0) {
|
||||||
return { success: false, error: 'cwd must be a non-empty string' };
|
return { success: false, error: 'cwd must be a non-empty string' };
|
||||||
|
|
@ -2436,6 +2439,51 @@ async function handlePrepareProvisioning(
|
||||||
}
|
}
|
||||||
validatedModelVerificationMode = modelVerificationMode;
|
validatedModelVerificationMode = modelVerificationMode;
|
||||||
}
|
}
|
||||||
|
if (selectedModelChecks !== undefined) {
|
||||||
|
if (!Array.isArray(selectedModelChecks)) {
|
||||||
|
return { success: false, error: 'selectedModelChecks must be an array when provided' };
|
||||||
|
}
|
||||||
|
const normalized: TeamProvisioningModelCheckRequest[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const entry of selectedModelChecks) {
|
||||||
|
if (!entry || typeof entry !== 'object') {
|
||||||
|
return { success: false, error: 'selectedModelChecks entries must be objects' };
|
||||||
|
}
|
||||||
|
const rawEntry = entry as {
|
||||||
|
providerId?: unknown;
|
||||||
|
model?: unknown;
|
||||||
|
effort?: unknown;
|
||||||
|
};
|
||||||
|
if (!isTeamProviderId(rawEntry.providerId)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'selectedModelChecks entries must include a valid providerId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof rawEntry.model !== 'string' || rawEntry.model.trim().length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'selectedModelChecks entries must include a non-empty model',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const effortValidation = parseOptionalTeamEffort(rawEntry.effort, rawEntry.providerId);
|
||||||
|
if (!effortValidation.valid) {
|
||||||
|
return { success: false, error: `selectedModelChecks ${effortValidation.error}` };
|
||||||
|
}
|
||||||
|
const model = rawEntry.model.trim();
|
||||||
|
const key = `${rawEntry.providerId}\n${model}\n${effortValidation.value ?? ''}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
normalized.push({
|
||||||
|
providerId: rawEntry.providerId,
|
||||||
|
model,
|
||||||
|
...(effortValidation.value ? { effort: effortValidation.value } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
validatedSelectedModelChecks = normalized;
|
||||||
|
}
|
||||||
return wrapTeamHandler('prepareProvisioning', () =>
|
return wrapTeamHandler('prepareProvisioning', () =>
|
||||||
getTeamProvisioningService().prepareForProvisioning(validatedCwd, {
|
getTeamProvisioningService().prepareForProvisioning(validatedCwd, {
|
||||||
providerId: validatedProviderId,
|
providerId: validatedProviderId,
|
||||||
|
|
@ -2443,6 +2491,7 @@ async function handlePrepareProvisioning(
|
||||||
modelIds: validatedSelectedModels,
|
modelIds: validatedSelectedModels,
|
||||||
limitContext: validatedLimitContext,
|
limitContext: validatedLimitContext,
|
||||||
modelVerificationMode: validatedModelVerificationMode,
|
modelVerificationMode: validatedModelVerificationMode,
|
||||||
|
modelChecks: validatedSelectedModelChecks,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -679,6 +679,11 @@ export class ClaudeMultimodelBridgeService {
|
||||||
|
|
||||||
private readonly providerStatusHydrationGenerations = new Map<CliProviderId, number>();
|
private readonly providerStatusHydrationGenerations = new Map<CliProviderId, number>();
|
||||||
|
|
||||||
|
private readonly providerStatusHydrationInFlight = new Map<
|
||||||
|
CliProviderId,
|
||||||
|
{ readonly generation: number; readonly promise: Promise<CliProviderStatus> }
|
||||||
|
>();
|
||||||
|
|
||||||
private beginProviderStatusHydration(providerIds: readonly CliProviderId[]): number {
|
private beginProviderStatusHydration(providerIds: readonly CliProviderId[]): number {
|
||||||
const generation = ++this.providerStatusHydrationGeneration;
|
const generation = ++this.providerStatusHydrationGeneration;
|
||||||
for (const providerId of providerIds) {
|
for (const providerId of providerIds) {
|
||||||
|
|
@ -691,6 +696,38 @@ export class ClaudeMultimodelBridgeService {
|
||||||
return this.providerStatusHydrationGenerations.get(providerId) === generation;
|
return this.providerStatusHydrationGenerations.get(providerId) === generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getProviderCatalogHydration(
|
||||||
|
binaryPath: string,
|
||||||
|
providerId: CliProviderId,
|
||||||
|
generation: number
|
||||||
|
): Promise<CliProviderStatus | null> {
|
||||||
|
const inFlight = this.providerStatusHydrationInFlight.get(providerId);
|
||||||
|
if (inFlight) {
|
||||||
|
if (inFlight.generation === generation) {
|
||||||
|
return inFlight.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inFlight.promise
|
||||||
|
.catch(() => undefined)
|
||||||
|
.then(() => {
|
||||||
|
if (!this.isProviderStatusHydrationCurrent(providerId, generation)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.getProviderCatalogHydration(binaryPath, providerId, generation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = this.getProviderStatusFromScopedRuntimeStatus(binaryPath, providerId).finally(
|
||||||
|
() => {
|
||||||
|
if (this.providerStatusHydrationInFlight.get(providerId)?.promise === request) {
|
||||||
|
this.providerStatusHydrationInFlight.delete(providerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.providerStatusHydrationInFlight.set(providerId, { generation, promise: request });
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
private async buildCliEnv(
|
private async buildCliEnv(
|
||||||
binaryPath: string
|
binaryPath: string
|
||||||
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
||||||
|
|
@ -708,18 +745,23 @@ export class ClaudeMultimodelBridgeService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private isUnifiedRuntimeUnsupported(error: unknown): boolean {
|
private isRuntimeStatusCompatibilityError(error: unknown): boolean {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
const lower = message.toLowerCase();
|
const lower = message.toLowerCase();
|
||||||
return (
|
return (
|
||||||
lower.includes('unknown command') ||
|
lower.includes('unknown command') ||
|
||||||
lower.includes('unknown option') ||
|
lower.includes('unknown option') ||
|
||||||
lower.includes('no such command') ||
|
lower.includes('no such command') ||
|
||||||
lower.includes('did you mean') ||
|
lower.includes('did you mean')
|
||||||
lower.includes('runtime status')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isUnifiedRuntimeUnsupported(error: unknown): boolean {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
const lower = message.toLowerCase();
|
||||||
|
return this.isRuntimeStatusCompatibilityError(error) || lower.includes('runtime status');
|
||||||
|
}
|
||||||
|
|
||||||
private mapRuntimeProviderStatus(
|
private mapRuntimeProviderStatus(
|
||||||
providerId: CliProviderId,
|
providerId: CliProviderId,
|
||||||
runtimeStatus: NonNullable<UnifiedRuntimeStatusResponse['providers']>[string] | undefined
|
runtimeStatus: NonNullable<UnifiedRuntimeStatusResponse['providers']>[string] | undefined
|
||||||
|
|
@ -961,8 +1003,11 @@ export class ClaudeMultimodelBridgeService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.getProviderStatusFromScopedRuntimeStatus(binaryPath, liveProvider.providerId)
|
void this.getProviderCatalogHydration(binaryPath, liveProvider.providerId, generation)
|
||||||
.then((hydratedProvider) => {
|
.then((hydratedProvider) => {
|
||||||
|
if (!hydratedProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.isProviderStatusHydrationCurrent(liveProvider.providerId, generation)) {
|
if (!this.isProviderStatusHydrationCurrent(liveProvider.providerId, generation)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1100,8 +1145,11 @@ export class ClaudeMultimodelBridgeService {
|
||||||
summary: true,
|
summary: true,
|
||||||
});
|
});
|
||||||
if (provider.runtimeCapabilities?.modelCatalog?.dynamic === true && onCatalogUpdate) {
|
if (provider.runtimeCapabilities?.modelCatalog?.dynamic === true && onCatalogUpdate) {
|
||||||
void this.getProviderStatusFromScopedRuntimeStatus(binaryPath, provider.providerId)
|
void this.getProviderCatalogHydration(binaryPath, provider.providerId, generation)
|
||||||
.then((hydratedProvider) => {
|
.then((hydratedProvider) => {
|
||||||
|
if (!hydratedProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.isProviderStatusHydrationCurrent(provider.providerId, generation)) {
|
if (!this.isProviderStatusHydrationCurrent(provider.providerId, generation)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1124,24 +1172,35 @@ export class ClaudeMultimodelBridgeService {
|
||||||
}
|
}
|
||||||
return provider;
|
return provider;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!this.isUnifiedRuntimeUnsupported(error)) {
|
if (providerId === 'gemini' && this.isRuntimeStatusCompatibilityError(error)) {
|
||||||
|
return this.buildGeminiStatus(binaryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRuntimeStatusCompatibilityError(error)) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Provider-scoped summary runtime status unavailable for ${providerId}, falling back to full probe: ${
|
`Provider-scoped summary runtime status unavailable for ${providerId}, falling back to full probe: ${
|
||||||
error instanceof Error ? error.message : String(error)
|
error instanceof Error ? error.message : String(error)
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
|
return await this.getProviderStatusFromScopedRuntimeStatus(binaryPath, providerId);
|
||||||
|
} catch (fullError) {
|
||||||
|
logger.warn(
|
||||||
|
`Provider-scoped full runtime status unavailable for ${providerId}, returning scoped error: ${
|
||||||
|
fullError instanceof Error ? fullError.message : String(fullError)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return createRuntimeStatusErrorProviderStatus(providerId, fullError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (providerId === 'gemini') {
|
logger.warn(
|
||||||
return this.buildGeminiStatus(binaryPath);
|
`Provider-scoped summary runtime status unavailable for ${providerId}, returning scoped error: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return createRuntimeStatusErrorProviderStatus(providerId, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers = await this.getProviderStatuses(binaryPath);
|
|
||||||
return (
|
|
||||||
providers.find((provider) => provider.providerId === providerId) ??
|
|
||||||
createDefaultProviderStatus(providerId)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyProviderStatus(
|
async verifyProviderStatus(
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,33 @@ function firstEvidence(parts: readonly string[], pattern: RegExp): string[] {
|
||||||
const WORKSPACE_TRUST_FAILURE_PATTERN =
|
const WORKSPACE_TRUST_FAILURE_PATTERN =
|
||||||
/workspace trust is not accepted|cannot start in headless process runtime because workspace trust|open that workspace once interactively and accept trust|workspace_trust_preflight_not_confirmed|workspace trust was not confirmed|workspace trust preflight blocked launch/i;
|
/workspace trust is not accepted|cannot start in headless process runtime because workspace trust|open that workspace once interactively and accept trust|workspace_trust_preflight_not_confirmed|workspace trust was not confirmed|workspace trust preflight blocked launch/i;
|
||||||
|
|
||||||
|
const BOOTSTRAP_TRANSPORT_EVIDENCE_PATTERN = new RegExp(
|
||||||
|
[
|
||||||
|
'mailbox_bootstrap_written',
|
||||||
|
'bootstrap_prompt_observed',
|
||||||
|
'bootstrap_submit_attempted',
|
||||||
|
'bootstrap_submitted',
|
||||||
|
'inbox_poller_ready',
|
||||||
|
'runtime_events_log',
|
||||||
|
].join('|'),
|
||||||
|
'i'
|
||||||
|
);
|
||||||
|
|
||||||
|
const MODEL_NO_BOOTSTRAP_PATTERN = new RegExp(
|
||||||
|
[
|
||||||
|
'did not bootstrap-confirm',
|
||||||
|
'bootstrap unconfirmed',
|
||||||
|
'bootstrap-confirm before timeout',
|
||||||
|
'bootstrap was not confirmed',
|
||||||
|
'bootstrap not confirmed',
|
||||||
|
'check-in not yet received',
|
||||||
|
'bootstrap_stalled',
|
||||||
|
'timed out waiting for bootstrap_submitted',
|
||||||
|
'last transport stage:\\s*(?:mailbox_bootstrap_written|bootstrap_prompt_observed|bootstrap_submit_attempted|bootstrap_submitted)',
|
||||||
|
].join('|'),
|
||||||
|
'i'
|
||||||
|
);
|
||||||
|
|
||||||
export function isWorkspaceTrustLaunchFailureText(value: string): boolean {
|
export function isWorkspaceTrustLaunchFailureText(value: string): boolean {
|
||||||
return WORKSPACE_TRUST_FAILURE_PATTERN.test(value);
|
return WORKSPACE_TRUST_FAILURE_PATTERN.test(value);
|
||||||
}
|
}
|
||||||
|
|
@ -228,6 +255,7 @@ export function classifyLaunchFailureArtifact(
|
||||||
): LaunchFailureArtifactClassification {
|
): LaunchFailureArtifactClassification {
|
||||||
const parts = collectLaunchFailureSearchParts(input);
|
const parts = collectLaunchFailureSearchParts(input);
|
||||||
const text = parts.join('\n').toLowerCase();
|
const text = parts.join('\n').toLowerCase();
|
||||||
|
const hasBootstrapTransportEvidence = BOOTSTRAP_TRANSPORT_EVIDENCE_PATTERN.test(text);
|
||||||
const candidates: {
|
const candidates: {
|
||||||
code: LaunchFailureArtifactClassificationCode;
|
code: LaunchFailureArtifactClassificationCode;
|
||||||
confidence: number;
|
confidence: number;
|
||||||
|
|
@ -268,8 +296,7 @@ export function classifyLaunchFailureArtifact(
|
||||||
{
|
{
|
||||||
code: 'model_no_bootstrap',
|
code: 'model_no_bootstrap',
|
||||||
confidence: 0.82,
|
confidence: 0.82,
|
||||||
pattern:
|
pattern: MODEL_NO_BOOTSTRAP_PATTERN,
|
||||||
/did not bootstrap-confirm|bootstrap unconfirmed|bootstrap-confirm before timeout|bootstrap was not confirmed|bootstrap not confirmed|check-in not yet received|bootstrap_stalled/i,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'process_exited',
|
code: 'process_exited',
|
||||||
|
|
@ -279,6 +306,9 @@ export function classifyLaunchFailureArtifact(
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
|
if (candidate.code === 'stdin_missing' && hasBootstrapTransportEvidence) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (candidate.pattern.test(text)) {
|
if (candidate.pattern.test(text)) {
|
||||||
return {
|
return {
|
||||||
code: candidate.code,
|
code: candidate.code,
|
||||||
|
|
@ -305,7 +335,7 @@ export function extractLaunchBootstrapTransportBreadcrumb(
|
||||||
];
|
];
|
||||||
const evidence = firstEvidence(
|
const evidence = firstEvidence(
|
||||||
parts,
|
parts,
|
||||||
/bootstrap_submit_|last transport stage|no stdin data received|local prompt handler/i
|
/bootstrap_submit_|mailbox_bootstrap_written|bootstrap_prompt_observed|bootstrap_submitted|last transport stage|no stdin data received|local prompt handler/i
|
||||||
).map(redactLaunchFailureArtifactText);
|
).map(redactLaunchFailureArtifactText);
|
||||||
const retryableRaw = retryableMatches.at(-1)?.[1]?.toLowerCase();
|
const retryableRaw = retryableMatches.at(-1)?.[1]?.toLowerCase();
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,7 @@ export class TeamMcpConfigBuilder {
|
||||||
[MCP_SERVER_NAME]: {
|
[MCP_SERVER_NAME]: {
|
||||||
command: launchSpec.command,
|
command: launchSpec.command,
|
||||||
args: launchSpec.args,
|
args: launchSpec.args,
|
||||||
|
enabled: true,
|
||||||
env: {
|
env: {
|
||||||
[MCP_CLAUDE_DIR_ENV]: getClaudeBasePath(),
|
[MCP_CLAUDE_DIR_ENV]: getClaudeBasePath(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
type OpenCodeFilePart,
|
type OpenCodeFilePart,
|
||||||
} from '@features/agent-attachments/main';
|
} from '@features/agent-attachments/main';
|
||||||
import {
|
import {
|
||||||
|
resolveAnthropicEffortSupport,
|
||||||
resolveAnthropicFastMode,
|
resolveAnthropicFastMode,
|
||||||
resolveAnthropicRuntimeSelection,
|
resolveAnthropicRuntimeSelection,
|
||||||
} from '@features/anthropic-runtime-profile/main';
|
} from '@features/anthropic-runtime-profile/main';
|
||||||
|
|
@ -568,6 +569,7 @@ import type {
|
||||||
TeamMember,
|
TeamMember,
|
||||||
TeamProviderBackendId,
|
TeamProviderBackendId,
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
|
TeamProvisioningModelCheckRequest,
|
||||||
TeamProvisioningModelVerificationMode,
|
TeamProvisioningModelVerificationMode,
|
||||||
TeamProvisioningPrepareIssue,
|
TeamProvisioningPrepareIssue,
|
||||||
TeamProvisioningPrepareResult,
|
TeamProvisioningPrepareResult,
|
||||||
|
|
@ -1328,6 +1330,11 @@ interface RuntimeProviderLaunchFacts {
|
||||||
| null;
|
| null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProviderSelectedModelCheck {
|
||||||
|
modelId: string;
|
||||||
|
effort?: EffortLevel;
|
||||||
|
}
|
||||||
|
|
||||||
function extractJsonObjectFromCli<T>(raw: string): T {
|
function extractJsonObjectFromCli<T>(raw: string): T {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
try {
|
try {
|
||||||
|
|
@ -1389,6 +1396,58 @@ function normalizeProviderModelListModels(
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProviderSelectedModelChecks(
|
||||||
|
modelIds: readonly string[],
|
||||||
|
modelChecks?: readonly ProviderSelectedModelCheck[]
|
||||||
|
): ProviderSelectedModelCheck[] {
|
||||||
|
const checks: ProviderSelectedModelCheck[] =
|
||||||
|
modelChecks && modelChecks.length > 0
|
||||||
|
? [...modelChecks]
|
||||||
|
: modelIds.map((modelId) => ({ modelId }));
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const normalized: ProviderSelectedModelCheck[] = [];
|
||||||
|
for (const check of checks) {
|
||||||
|
const modelId = check.modelId.trim();
|
||||||
|
if (!modelId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = `${modelId}\n${check.effort ?? ''}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
normalized.push({
|
||||||
|
modelId,
|
||||||
|
...(check.effort ? { effort: check.effort } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProvisioningModelCheckRequests(
|
||||||
|
checks: readonly TeamProvisioningModelCheckRequest[] | undefined
|
||||||
|
): TeamProvisioningModelCheckRequest[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const normalized: TeamProvisioningModelCheckRequest[] = [];
|
||||||
|
for (const check of checks ?? []) {
|
||||||
|
const model = check.model.trim();
|
||||||
|
if (!model) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = `${check.providerId}\n${model}\n${check.effort ?? ''}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
normalized.push({
|
||||||
|
providerId: check.providerId,
|
||||||
|
model,
|
||||||
|
...(check.effort ? { effort: check.effort } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
function addModelCatalogLaunchModels(
|
function addModelCatalogLaunchModels(
|
||||||
modelIds: Set<string>,
|
modelIds: Set<string>,
|
||||||
catalog: CliProviderModelCatalog
|
catalog: CliProviderModelCatalog
|
||||||
|
|
@ -1444,7 +1503,7 @@ function getAnthropicFastModeDefault(): boolean {
|
||||||
function resolveAnthropicSelectionFromFacts(params: {
|
function resolveAnthropicSelectionFromFacts(params: {
|
||||||
selectedModel?: string;
|
selectedModel?: string;
|
||||||
limitContext?: boolean;
|
limitContext?: boolean;
|
||||||
facts: Pick<RuntimeProviderLaunchFacts, 'modelCatalog' | 'runtimeCapabilities'>;
|
facts: Pick<RuntimeProviderLaunchFacts, 'modelCatalog' | 'modelIds' | 'runtimeCapabilities'>;
|
||||||
}) {
|
}) {
|
||||||
return resolveAnthropicRuntimeSelection({
|
return resolveAnthropicRuntimeSelection({
|
||||||
source: {
|
source: {
|
||||||
|
|
@ -1453,9 +1512,33 @@ function resolveAnthropicSelectionFromFacts(params: {
|
||||||
},
|
},
|
||||||
selectedModel: params.selectedModel,
|
selectedModel: params.selectedModel,
|
||||||
limitContext: params.limitContext === true,
|
limitContext: params.limitContext === true,
|
||||||
|
availableLaunchModels: params.facts.modelCatalog ? undefined : params.facts.modelIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAnthropicEffortSupportFailure(params: {
|
||||||
|
effort: EffortLevel;
|
||||||
|
modelLabel: string;
|
||||||
|
supportedEfforts?: readonly EffortLevel[];
|
||||||
|
kind:
|
||||||
|
| 'unsupported-by-catalog'
|
||||||
|
| 'unsupported-by-runtime-capability'
|
||||||
|
| 'unverified-catalog-missing';
|
||||||
|
}): string {
|
||||||
|
if (params.kind === 'unverified-catalog-missing') {
|
||||||
|
return `Anthropic runtime catalog was unavailable, so effort "${params.effort}" for ${params.modelLabel} could not be verified.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supported = params.supportedEfforts?.length
|
||||||
|
? ` Supported efforts: ${params.supportedEfforts.join(', ')}.`
|
||||||
|
: '';
|
||||||
|
const runtimeSuffix =
|
||||||
|
params.kind === 'unsupported-by-runtime-capability'
|
||||||
|
? ' in the current runtime capability data'
|
||||||
|
: ' in the current runtime';
|
||||||
|
return `${params.modelLabel} does not support Anthropic effort "${params.effort}"${runtimeSuffix}.${supported}`;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveCodexSelectionFromFacts(params: {
|
function resolveCodexSelectionFromFacts(params: {
|
||||||
selectedModel?: string;
|
selectedModel?: string;
|
||||||
providerBackendId?: TeamCreateRequest['providerBackendId'];
|
providerBackendId?: TeamCreateRequest['providerBackendId'];
|
||||||
|
|
@ -4467,6 +4550,33 @@ interface RuntimeBootstrapSpec {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DETERMINISTIC_BOOTSTRAP_MIN_TIMEOUT_MS = 120_000;
|
||||||
|
const DETERMINISTIC_BOOTSTRAP_TIMEOUT_PER_MEMBER_MS = 60_000;
|
||||||
|
const DETERMINISTIC_BOOTSTRAP_MAX_TIMEOUT_MS = 300_000;
|
||||||
|
const DETERMINISTIC_BOOTSTRAP_OUTER_TIMEOUT_GRACE_MS = 30_000;
|
||||||
|
|
||||||
|
function getDeterministicBootstrapTimeoutMs(memberCount: number): number {
|
||||||
|
const perMemberBudget = Math.max(0, memberCount) * DETERMINISTIC_BOOTSTRAP_TIMEOUT_PER_MEMBER_MS;
|
||||||
|
return Math.min(
|
||||||
|
DETERMINISTIC_BOOTSTRAP_MAX_TIMEOUT_MS,
|
||||||
|
Math.max(DETERMINISTIC_BOOTSTRAP_MIN_TIMEOUT_MS, perMemberBudget)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProvisioningRunTimeoutMs(
|
||||||
|
run: Pick<ProvisioningRun, 'deterministicBootstrap' | 'effectiveMembers'>
|
||||||
|
): number {
|
||||||
|
if (!run.deterministicBootstrap) {
|
||||||
|
return RUN_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(
|
||||||
|
RUN_TIMEOUT_MS,
|
||||||
|
getDeterministicBootstrapTimeoutMs(run.effectiveMembers.length) +
|
||||||
|
DETERMINISTIC_BOOTSTRAP_OUTER_TIMEOUT_GRACE_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildDeterministicCreateBootstrapSpec(
|
function buildDeterministicCreateBootstrapSpec(
|
||||||
runId: string,
|
runId: string,
|
||||||
request: TeamCreateRequest,
|
request: TeamCreateRequest,
|
||||||
|
|
@ -4516,6 +4626,7 @@ function buildDeterministicCreateBootstrapSpec(
|
||||||
: {}),
|
: {}),
|
||||||
})),
|
})),
|
||||||
launch: {
|
launch: {
|
||||||
|
bootstrapTimeoutMs: getDeterministicBootstrapTimeoutMs(effectiveMembers.length),
|
||||||
continueOnPartialFailure: true,
|
continueOnPartialFailure: true,
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
|
|
@ -4570,6 +4681,7 @@ function buildDeterministicLaunchBootstrapSpec(
|
||||||
: {}),
|
: {}),
|
||||||
})),
|
})),
|
||||||
launch: {
|
launch: {
|
||||||
|
bootstrapTimeoutMs: getDeterministicBootstrapTimeoutMs(effectiveMembers.length),
|
||||||
continueOnPartialFailure: true,
|
continueOnPartialFailure: true,
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
|
|
@ -7342,11 +7454,28 @@ export class TeamProvisioningService {
|
||||||
`${params.actorLabel} resolves to Anthropic model "${resolvedLaunchModel}", but the current runtime does not list it as launchable.`
|
`${params.actorLabel} resolves to Anthropic model "${resolvedLaunchModel}", but the current runtime does not list it as launchable.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (params.effort && !selection.supportedEfforts.includes(params.effort)) {
|
if (params.effort) {
|
||||||
const modelLabel = selection.displayName ?? resolvedLaunchModel;
|
const modelLabel = selection.displayName ?? resolvedLaunchModel;
|
||||||
throw new Error(
|
const effortSupport = resolveAnthropicEffortSupport({
|
||||||
`${params.actorLabel} uses Anthropic effort "${params.effort}", but ${modelLabel} does not support it in the current runtime.`
|
selection,
|
||||||
);
|
effort: params.effort,
|
||||||
|
runtimeCapabilities: params.facts.runtimeCapabilities,
|
||||||
|
});
|
||||||
|
if (effortSupport.kind !== 'supported') {
|
||||||
|
throw new Error(
|
||||||
|
`${params.actorLabel} uses Anthropic effort "${params.effort}", but ${formatAnthropicEffortSupportFailure(
|
||||||
|
{
|
||||||
|
effort: params.effort,
|
||||||
|
modelLabel,
|
||||||
|
kind: effortSupport.kind,
|
||||||
|
supportedEfforts:
|
||||||
|
effortSupport.kind === 'unverified-catalog-missing'
|
||||||
|
? undefined
|
||||||
|
: effortSupport.supportedEfforts,
|
||||||
|
}
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fastResolution = resolveAnthropicFastMode({
|
const fastResolution = resolveAnthropicFastMode({
|
||||||
|
|
@ -19000,6 +19129,7 @@ export class TeamProvisioningService {
|
||||||
providerId?: TeamProviderId;
|
providerId?: TeamProviderId;
|
||||||
providerIds?: TeamProviderId[];
|
providerIds?: TeamProviderId[];
|
||||||
modelIds?: string[];
|
modelIds?: string[];
|
||||||
|
modelChecks?: TeamProvisioningModelCheckRequest[];
|
||||||
limitContext?: boolean;
|
limitContext?: boolean;
|
||||||
modelVerificationMode?: TeamProvisioningModelVerificationMode;
|
modelVerificationMode?: TeamProvisioningModelVerificationMode;
|
||||||
}
|
}
|
||||||
|
|
@ -19026,6 +19156,7 @@ export class TeamProvisioningService {
|
||||||
providerId?: TeamProviderId;
|
providerId?: TeamProviderId;
|
||||||
providerIds?: TeamProviderId[];
|
providerIds?: TeamProviderId[];
|
||||||
modelIds?: string[];
|
modelIds?: string[];
|
||||||
|
modelChecks?: TeamProvisioningModelCheckRequest[];
|
||||||
limitContext?: boolean;
|
limitContext?: boolean;
|
||||||
modelVerificationMode?: TeamProvisioningModelVerificationMode;
|
modelVerificationMode?: TeamProvisioningModelVerificationMode;
|
||||||
}
|
}
|
||||||
|
|
@ -19040,11 +19171,24 @@ export class TeamProvisioningService {
|
||||||
const modelIds = Array.from(
|
const modelIds = Array.from(
|
||||||
new Set((opts?.modelIds ?? []).map((modelId) => modelId.trim()).filter(Boolean))
|
new Set((opts?.modelIds ?? []).map((modelId) => modelId.trim()).filter(Boolean))
|
||||||
);
|
);
|
||||||
|
const modelChecks = normalizeProvisioningModelCheckRequests(opts?.modelChecks)
|
||||||
|
.map((check) => ({
|
||||||
|
providerId: check.providerId,
|
||||||
|
model: check.model,
|
||||||
|
effort: check.effort ?? null,
|
||||||
|
}))
|
||||||
|
.sort(
|
||||||
|
(left, right) =>
|
||||||
|
left.providerId.localeCompare(right.providerId) ||
|
||||||
|
left.model.localeCompare(right.model) ||
|
||||||
|
(left.effort ?? '').localeCompare(right.effort ?? '')
|
||||||
|
);
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
cwd: cwd?.trim() || process.cwd(),
|
cwd: cwd?.trim() || process.cwd(),
|
||||||
forceFresh: opts?.forceFresh === true,
|
forceFresh: opts?.forceFresh === true,
|
||||||
providerIds,
|
providerIds,
|
||||||
modelIds,
|
modelIds,
|
||||||
|
modelChecks,
|
||||||
limitContext: opts?.limitContext === true,
|
limitContext: opts?.limitContext === true,
|
||||||
modelVerificationMode: opts?.modelVerificationMode ?? null,
|
modelVerificationMode: opts?.modelVerificationMode ?? null,
|
||||||
});
|
});
|
||||||
|
|
@ -19068,6 +19212,7 @@ export class TeamProvisioningService {
|
||||||
providerId?: TeamProviderId;
|
providerId?: TeamProviderId;
|
||||||
providerIds?: TeamProviderId[];
|
providerIds?: TeamProviderId[];
|
||||||
modelIds?: string[];
|
modelIds?: string[];
|
||||||
|
modelChecks?: TeamProvisioningModelCheckRequest[];
|
||||||
limitContext?: boolean;
|
limitContext?: boolean;
|
||||||
modelVerificationMode?: TeamProvisioningModelVerificationMode;
|
modelVerificationMode?: TeamProvisioningModelVerificationMode;
|
||||||
}
|
}
|
||||||
|
|
@ -19104,8 +19249,20 @@ export class TeamProvisioningService {
|
||||||
const selectedModelIds = Array.from(
|
const selectedModelIds = Array.from(
|
||||||
new Set((opts?.modelIds ?? []).map((modelId) => modelId.trim()).filter(Boolean))
|
new Set((opts?.modelIds ?? []).map((modelId) => modelId.trim()).filter(Boolean))
|
||||||
);
|
);
|
||||||
|
const selectedModelChecks = normalizeProvisioningModelCheckRequests(opts?.modelChecks);
|
||||||
|
const useStructuredModelChecks = selectedModelChecks.length > 0;
|
||||||
|
|
||||||
for (const providerId of providerIds) {
|
for (const providerId of providerIds) {
|
||||||
|
const providerModelChecks = selectedModelChecks
|
||||||
|
.filter((check) => check.providerId === providerId)
|
||||||
|
.map((check) => ({
|
||||||
|
modelId: check.model,
|
||||||
|
...(check.effort ? { effort: check.effort } : {}),
|
||||||
|
}));
|
||||||
|
const providerSelectedModelIds = useStructuredModelChecks
|
||||||
|
? Array.from(new Set(providerModelChecks.map((check) => check.modelId)))
|
||||||
|
: selectedModelIds;
|
||||||
|
|
||||||
if (providerId === 'opencode') {
|
if (providerId === 'opencode') {
|
||||||
const adapter = this.getOpenCodeRuntimeAdapter();
|
const adapter = this.getOpenCodeRuntimeAdapter();
|
||||||
if (!adapter) {
|
if (!adapter) {
|
||||||
|
|
@ -19115,7 +19272,7 @@ export class TeamProvisioningService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedModelIds.length === 0) {
|
if (providerSelectedModelIds.length === 0) {
|
||||||
const prepare = await adapter.prepare({
|
const prepare = await adapter.prepare({
|
||||||
runId: `prepare-${randomUUID()}`,
|
runId: `prepare-${randomUUID()}`,
|
||||||
teamName: '__prepare_opencode__',
|
teamName: '__prepare_opencode__',
|
||||||
|
|
@ -19149,7 +19306,7 @@ export class TeamProvisioningService {
|
||||||
const openCodeModelPrepare = await this.prepareSelectedOpenCodeModels({
|
const openCodeModelPrepare = await this.prepareSelectedOpenCodeModels({
|
||||||
adapter,
|
adapter,
|
||||||
cwd: targetCwd,
|
cwd: targetCwd,
|
||||||
modelIds: selectedModelIds,
|
modelIds: providerSelectedModelIds,
|
||||||
verificationMode: opts?.modelVerificationMode ?? 'deep',
|
verificationMode: opts?.modelVerificationMode ?? 'deep',
|
||||||
});
|
});
|
||||||
details.push(...openCodeModelPrepare.details);
|
details.push(...openCodeModelPrepare.details);
|
||||||
|
|
@ -19176,7 +19333,7 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const appendSelectedModelVerification = async (): Promise<void> => {
|
const appendSelectedModelVerification = async (): Promise<void> => {
|
||||||
if (selectedModelIds.length === 0) {
|
if (providerSelectedModelIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19184,12 +19341,14 @@ export class TeamProvisioningService {
|
||||||
claudePath: probeResult.claudePath,
|
claudePath: probeResult.claudePath,
|
||||||
cwd: targetCwd,
|
cwd: targetCwd,
|
||||||
providerId,
|
providerId,
|
||||||
modelIds: selectedModelIds,
|
modelIds: providerSelectedModelIds,
|
||||||
|
modelChecks: providerModelChecks,
|
||||||
limitContext: opts?.limitContext === true,
|
limitContext: opts?.limitContext === true,
|
||||||
});
|
});
|
||||||
details.push(...modelVerification.details);
|
details.push(...modelVerification.details);
|
||||||
warnings.push(...modelVerification.warnings);
|
warnings.push(...modelVerification.warnings);
|
||||||
blockingMessages.push(...modelVerification.blockingMessages);
|
blockingMessages.push(...modelVerification.blockingMessages);
|
||||||
|
issues.push(...(modelVerification.issues ?? []));
|
||||||
};
|
};
|
||||||
|
|
||||||
const appendOneShotDiagnostic = async (): Promise<void> => {
|
const appendOneShotDiagnostic = async (): Promise<void> => {
|
||||||
|
|
@ -19298,7 +19457,7 @@ export class TeamProvisioningService {
|
||||||
// Preflight warnings (including timeouts) should not block provisioning.
|
// Preflight warnings (including timeouts) should not block provisioning.
|
||||||
warnings.push(prefixedWarning);
|
warnings.push(prefixedWarning);
|
||||||
const blockingCountBeforeModelChecks = blockingMessages.length;
|
const blockingCountBeforeModelChecks = blockingMessages.length;
|
||||||
if (!isBlockingPreflightWarning && selectedModelIds.length > 0) {
|
if (!isBlockingPreflightWarning && providerSelectedModelIds.length > 0) {
|
||||||
await appendSelectedModelVerification();
|
await appendSelectedModelVerification();
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|
@ -19964,24 +20123,29 @@ export class TeamProvisioningService {
|
||||||
cwd,
|
cwd,
|
||||||
providerId,
|
providerId,
|
||||||
modelIds,
|
modelIds,
|
||||||
|
modelChecks,
|
||||||
limitContext,
|
limitContext,
|
||||||
}: {
|
}: {
|
||||||
claudePath: string;
|
claudePath: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
modelIds: string[];
|
modelIds: string[];
|
||||||
|
modelChecks?: ProviderSelectedModelCheck[];
|
||||||
limitContext: boolean;
|
limitContext: boolean;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
details: string[];
|
details: string[];
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
blockingMessages: string[];
|
blockingMessages: string[];
|
||||||
|
issues?: TeamProvisioningPrepareIssue[];
|
||||||
}> {
|
}> {
|
||||||
const details: string[] = [];
|
const details: string[] = [];
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const blockingMessages: string[] = [];
|
const blockingMessages: string[] = [];
|
||||||
|
const issues: TeamProvisioningPrepareIssue[] = [];
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
const selectedModelChecks = normalizeProviderSelectedModelChecks(modelIds, modelChecks);
|
||||||
|
|
||||||
if (modelIds.length === 0) {
|
if (selectedModelChecks.length === 0) {
|
||||||
return { details, warnings, blockingMessages };
|
return { details, warnings, blockingMessages };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20013,35 +20177,99 @@ export class TeamProvisioningService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
blockingMessages.push(`Selected model ${requestedModelId} is unavailable. ${outcome.reason}`);
|
blockingMessages.push(`Selected model ${requestedModelId} is unavailable. ${outcome.reason}`);
|
||||||
|
issues.push({
|
||||||
|
providerId,
|
||||||
|
modelId: requestedModelId,
|
||||||
|
scope: 'model',
|
||||||
|
severity: 'blocking',
|
||||||
|
code: 'model_unavailable',
|
||||||
|
message: outcome.reason,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordAnthropicEffortOutcome = (
|
||||||
|
requestedModelId: string,
|
||||||
|
effort: EffortLevel
|
||||||
|
): boolean => {
|
||||||
|
const selection = resolveAnthropicSelectionFromFacts({
|
||||||
|
selectedModel: requestedModelId,
|
||||||
|
limitContext,
|
||||||
|
facts: runtimeFacts,
|
||||||
|
});
|
||||||
|
const modelLabel = selection.displayName ?? selection.resolvedLaunchModel ?? requestedModelId;
|
||||||
|
const effortSupport = resolveAnthropicEffortSupport({
|
||||||
|
selection,
|
||||||
|
effort,
|
||||||
|
runtimeCapabilities: runtimeFacts.runtimeCapabilities,
|
||||||
|
});
|
||||||
|
if (effortSupport.kind === 'supported') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reason = formatAnthropicEffortSupportFailure({
|
||||||
|
effort,
|
||||||
|
modelLabel,
|
||||||
|
kind: effortSupport.kind,
|
||||||
|
supportedEfforts:
|
||||||
|
effortSupport.kind === 'unverified-catalog-missing'
|
||||||
|
? undefined
|
||||||
|
: effortSupport.supportedEfforts,
|
||||||
|
});
|
||||||
|
blockingMessages.push(`Selected model ${requestedModelId} is unavailable. ${reason}`);
|
||||||
|
issues.push({
|
||||||
|
providerId,
|
||||||
|
modelId: requestedModelId,
|
||||||
|
scope: 'model',
|
||||||
|
severity: 'blocking',
|
||||||
|
code:
|
||||||
|
effortSupport.kind === 'unverified-catalog-missing'
|
||||||
|
? 'effort_unverified'
|
||||||
|
: 'effort_unsupported',
|
||||||
|
message: reason,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
appendPreflightDebugLog('provider_model_catalog_check_start', {
|
appendPreflightDebugLog('provider_model_catalog_check_start', {
|
||||||
providerId,
|
providerId,
|
||||||
cwd,
|
cwd,
|
||||||
modelIds,
|
modelIds: selectedModelChecks.map((check) => check.modelId),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const modelId of modelIds) {
|
const checksByModelId = new Map<string, ProviderSelectedModelCheck[]>();
|
||||||
const label = modelId.trim();
|
for (const check of selectedModelChecks) {
|
||||||
|
const label = check.modelId.trim();
|
||||||
if (!label) {
|
if (!label) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
checksByModelId.set(label, [...(checksByModelId.get(label) ?? []), check]);
|
||||||
|
}
|
||||||
|
|
||||||
recordOutcome(
|
for (const [label, checks] of checksByModelId.entries()) {
|
||||||
label,
|
const outcome = this.resolveProviderCompatibilityModel({
|
||||||
this.resolveProviderCompatibilityModel({
|
providerId,
|
||||||
providerId,
|
requestedModelId: label,
|
||||||
requestedModelId: label,
|
runtimeFacts,
|
||||||
runtimeFacts,
|
limitContext,
|
||||||
limitContext,
|
});
|
||||||
})
|
let effortSupported = true;
|
||||||
);
|
if (outcome.kind !== 'unavailable' && providerId === 'anthropic') {
|
||||||
|
for (const check of checks) {
|
||||||
|
if (check.effort && !recordAnthropicEffortOutcome(label, check.effort)) {
|
||||||
|
effortSupported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!effortSupported) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
recordOutcome(label, outcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
appendPreflightDebugLog('provider_model_catalog_check_complete', {
|
appendPreflightDebugLog('provider_model_catalog_check_complete', {
|
||||||
providerId,
|
providerId,
|
||||||
cwd,
|
cwd,
|
||||||
modelIds,
|
modelIds: selectedModelChecks.map((check) => check.modelId),
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
modelCount: runtimeFacts.modelIds.size,
|
modelCount: runtimeFacts.modelIds.size,
|
||||||
details,
|
details,
|
||||||
|
|
@ -20049,7 +20277,12 @@ export class TeamProvisioningService {
|
||||||
blockingMessages,
|
blockingMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { details, warnings, blockingMessages };
|
return {
|
||||||
|
details,
|
||||||
|
warnings,
|
||||||
|
blockingMessages,
|
||||||
|
...(issues.length > 0 ? { issues } : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveProviderDefaultModel(
|
private async resolveProviderDefaultModel(
|
||||||
|
|
@ -20999,7 +21232,7 @@ export class TeamProvisioningService {
|
||||||
this.cleanupRun(run);
|
this.cleanupRun(run);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}, RUN_TIMEOUT_MS);
|
}, getProvisioningRunTimeoutMs(run));
|
||||||
|
|
||||||
child.once('error', (error) => {
|
child.once('error', (error) => {
|
||||||
const hint = run.isLaunch ? ' (launch)' : '';
|
const hint = run.isLaunch ? ' (launch)' : '';
|
||||||
|
|
@ -21795,7 +22028,7 @@ export class TeamProvisioningService {
|
||||||
this.cleanupRun(run);
|
this.cleanupRun(run);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}, RUN_TIMEOUT_MS);
|
}, getProvisioningRunTimeoutMs(run));
|
||||||
|
|
||||||
child.once('error', (error) => {
|
child.once('error', (error) => {
|
||||||
const progress = updateProgress(run, 'failed', 'Failed to start Claude CLI', {
|
const progress = updateProgress(run, 'failed', 'Failed to start Claude CLI', {
|
||||||
|
|
@ -23098,7 +23331,7 @@ export class TeamProvisioningService {
|
||||||
this.cleanupRun(run);
|
this.cleanupRun(run);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}, RUN_TIMEOUT_MS);
|
}, getProvisioningRunTimeoutMs(run));
|
||||||
|
|
||||||
child.once('error', (error) => {
|
child.once('error', (error) => {
|
||||||
const progress = updateProgress(run, 'failed', 'Failed to start Claude CLI (launch)', {
|
const progress = updateProgress(run, 'failed', 'Failed to start Claude CLI (launch)', {
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,7 @@ import type {
|
||||||
TeamLaunchResponse,
|
TeamLaunchResponse,
|
||||||
TeamMemberActivityMeta,
|
TeamMemberActivityMeta,
|
||||||
TeamMessageNotificationData,
|
TeamMessageNotificationData,
|
||||||
|
TeamProvisioningModelCheckRequest,
|
||||||
TeamProvisioningModelVerificationMode,
|
TeamProvisioningModelVerificationMode,
|
||||||
TeamProvisioningPrepareResult,
|
TeamProvisioningPrepareResult,
|
||||||
TeamProvisioningProgress,
|
TeamProvisioningProgress,
|
||||||
|
|
@ -927,7 +928,8 @@ const electronAPI: ElectronAPI = {
|
||||||
providerIds?: TeamLaunchRequest['providerId'][],
|
providerIds?: TeamLaunchRequest['providerId'][],
|
||||||
selectedModels?: string[],
|
selectedModels?: string[],
|
||||||
limitContext?: boolean,
|
limitContext?: boolean,
|
||||||
modelVerificationMode?: TeamProvisioningModelVerificationMode
|
modelVerificationMode?: TeamProvisioningModelVerificationMode,
|
||||||
|
selectedModelChecks?: TeamProvisioningModelCheckRequest[]
|
||||||
) => {
|
) => {
|
||||||
return invokeIpcWithResult<TeamProvisioningPrepareResult>(
|
return invokeIpcWithResult<TeamProvisioningPrepareResult>(
|
||||||
TEAM_PREPARE_PROVISIONING,
|
TEAM_PREPARE_PROVISIONING,
|
||||||
|
|
@ -936,7 +938,8 @@ const electronAPI: ElectronAPI = {
|
||||||
providerIds,
|
providerIds,
|
||||||
selectedModels,
|
selectedModels,
|
||||||
limitContext,
|
limitContext,
|
||||||
modelVerificationMode
|
modelVerificationMode,
|
||||||
|
selectedModelChecks
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getWorktreeGitStatus: async (projectPath: string) => {
|
getWorktreeGitStatus: async (projectPath: string) => {
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ export const CollapsibleTeamSection = memo(function CollapsibleTeamSection({
|
||||||
{headerExtra}
|
{headerExtra}
|
||||||
</div>
|
</div>
|
||||||
{action && (
|
{action && (
|
||||||
<div className="relative z-10 flex shrink-0 items-center self-start">{action}</div>
|
<div className="relative z-10 flex shrink-0 items-center self-stretch">{action}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{keepMounted ? (
|
{keepMounted ? (
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ import {
|
||||||
} from './providerPrepareDiagnostics';
|
} from './providerPrepareDiagnostics';
|
||||||
import {
|
import {
|
||||||
buildProviderPrepareMembersSignature,
|
buildProviderPrepareMembersSignature,
|
||||||
|
buildProviderPrepareModelChecksSignature,
|
||||||
buildProviderPrepareRequestSignature,
|
buildProviderPrepareRequestSignature,
|
||||||
buildProviderPrepareRuntimeStatusSignature,
|
buildProviderPrepareRuntimeStatusSignature,
|
||||||
} from './providerPrepareRequestSignature';
|
} from './providerPrepareRequestSignature';
|
||||||
|
|
@ -146,6 +147,14 @@ import {
|
||||||
} from './WorktreeGitReadinessBanner';
|
} from './WorktreeGitReadinessBanner';
|
||||||
|
|
||||||
import type { MemberDraft } from '@renderer/components/team/members/MembersEditorSection';
|
import type { MemberDraft } from '@renderer/components/team/members/MembersEditorSection';
|
||||||
|
import type {
|
||||||
|
EffortLevel,
|
||||||
|
TeamCreateRequest,
|
||||||
|
TeamFastMode,
|
||||||
|
TeamProviderId,
|
||||||
|
TeamProvisioningMemberInput,
|
||||||
|
TeamProvisioningModelCheckRequest,
|
||||||
|
} from '@shared/types';
|
||||||
|
|
||||||
const TEAM_COLOR_NAMES = [
|
const TEAM_COLOR_NAMES = [
|
||||||
'blue',
|
'blue',
|
||||||
|
|
@ -160,14 +169,6 @@ const TEAM_COLOR_NAMES = [
|
||||||
|
|
||||||
const APP_TEAM_RUNTIME_DISALLOWED_TOOLS = 'TeamDelete,TodoWrite,TaskCreate,TaskUpdate';
|
const APP_TEAM_RUNTIME_DISALLOWED_TOOLS = 'TeamDelete,TodoWrite,TaskCreate,TaskUpdate';
|
||||||
|
|
||||||
import type {
|
|
||||||
EffortLevel,
|
|
||||||
TeamCreateRequest,
|
|
||||||
TeamFastMode,
|
|
||||||
TeamProviderId,
|
|
||||||
TeamProvisioningMemberInput,
|
|
||||||
} from '@shared/types';
|
|
||||||
|
|
||||||
function getProviderLabel(providerId: TeamProviderId): string {
|
function getProviderLabel(providerId: TeamProviderId): string {
|
||||||
return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic';
|
return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic';
|
||||||
}
|
}
|
||||||
|
|
@ -737,6 +738,85 @@ export const CreateTeamDialog = ({
|
||||||
() => buildProviderPrepareMembersSignature(effectiveMemberDrafts),
|
() => buildProviderPrepareMembersSignature(effectiveMemberDrafts),
|
||||||
[effectiveMemberDrafts]
|
[effectiveMemberDrafts]
|
||||||
);
|
);
|
||||||
|
const selectedModelChecksByProvider = useMemo(() => {
|
||||||
|
const modelsByProvider = new Map<TeamProviderId, TeamProvisioningModelCheckRequest[]>();
|
||||||
|
const leadEffort = (selectedEffort as EffortLevel | '') || undefined;
|
||||||
|
const addModel = (
|
||||||
|
providerId: TeamProviderId,
|
||||||
|
model: string | undefined,
|
||||||
|
effort?: EffortLevel
|
||||||
|
): void => {
|
||||||
|
const trimmed = model?.trim() ?? '';
|
||||||
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existing = modelsByProvider.get(providerId) ?? [];
|
||||||
|
if (!existing.some((entry) => entry.model === trimmed && entry.effort === effort)) {
|
||||||
|
modelsByProvider.set(providerId, [
|
||||||
|
...existing,
|
||||||
|
{
|
||||||
|
providerId,
|
||||||
|
model: trimmed,
|
||||||
|
...(effort ? { effort } : {}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const addDefaultSelection = (providerId: TeamProviderId, effort?: EffortLevel): void => {
|
||||||
|
if (
|
||||||
|
providerId === 'codex' ||
|
||||||
|
providerId === 'gemini' ||
|
||||||
|
(providerId === 'anthropic' && selectedProviderId === 'anthropic')
|
||||||
|
) {
|
||||||
|
addModel(providerId, DEFAULT_PROVIDER_MODEL_SELECTION, effort);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const leadModel = computeEffectiveTeamModel(
|
||||||
|
selectedModel,
|
||||||
|
effectiveAnthropicRuntimeLimitContext,
|
||||||
|
selectedProviderId
|
||||||
|
);
|
||||||
|
if (selectedModel.trim()) {
|
||||||
|
addModel(selectedProviderId, leadModel, leadEffort);
|
||||||
|
} else {
|
||||||
|
addDefaultSelection(selectedProviderId, leadEffort);
|
||||||
|
}
|
||||||
|
for (const member of effectiveMemberDrafts) {
|
||||||
|
if (member.removedAt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const memberProviderId = normalizeOptionalTeamProviderId(member.providerId);
|
||||||
|
const inheritsDefaultRuntime = !memberProviderId || memberProviderId === selectedProviderId;
|
||||||
|
const explicitMemberModel = member.model?.trim() ?? '';
|
||||||
|
const memberEffort =
|
||||||
|
member.effort ?? (inheritsDefaultRuntime && !explicitMemberModel ? leadEffort : undefined);
|
||||||
|
const scopedModel = resolveProviderScopedMemberModel({
|
||||||
|
memberProviderId: member.providerId,
|
||||||
|
memberModel: member.model,
|
||||||
|
selectedProviderId,
|
||||||
|
runtimeProviderStatusById,
|
||||||
|
});
|
||||||
|
if (scopedModel.model) {
|
||||||
|
addModel(scopedModel.providerId, scopedModel.model, memberEffort);
|
||||||
|
} else {
|
||||||
|
addDefaultSelection(scopedModel.providerId, memberEffort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelsByProvider;
|
||||||
|
}, [
|
||||||
|
effectiveAnthropicRuntimeLimitContext,
|
||||||
|
effectiveMemberDrafts,
|
||||||
|
runtimeProviderStatusById,
|
||||||
|
selectedEffort,
|
||||||
|
selectedModel,
|
||||||
|
selectedProviderId,
|
||||||
|
]);
|
||||||
|
const selectedModelChecksByProviderSignature = useMemo(
|
||||||
|
() => buildProviderPrepareModelChecksSignature(selectedModelChecksByProvider),
|
||||||
|
[selectedModelChecksByProvider]
|
||||||
|
);
|
||||||
const prepareRequestSignature = useMemo(
|
const prepareRequestSignature = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildProviderPrepareRequestSignature({
|
buildProviderPrepareRequestSignature({
|
||||||
|
|
@ -747,6 +827,7 @@ export const CreateTeamDialog = ({
|
||||||
limitContext: effectiveAnthropicRuntimeLimitContext,
|
limitContext: effectiveAnthropicRuntimeLimitContext,
|
||||||
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
||||||
membersSignature: prepareMembersSignature,
|
membersSignature: prepareMembersSignature,
|
||||||
|
modelChecksSignature: selectedModelChecksByProviderSignature,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
effectiveCwd,
|
effectiveCwd,
|
||||||
|
|
@ -755,6 +836,7 @@ export const CreateTeamDialog = ({
|
||||||
prepareRuntimeStatusSignature,
|
prepareRuntimeStatusSignature,
|
||||||
selectedMemberProviders,
|
selectedMemberProviders,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
|
selectedModelChecksByProviderSignature,
|
||||||
selectedProviderId,
|
selectedProviderId,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
@ -775,6 +857,7 @@ export const CreateTeamDialog = ({
|
||||||
backendSummary,
|
backendSummary,
|
||||||
limitContext: effectiveAnthropicRuntimeLimitContext,
|
limitContext: effectiveAnthropicRuntimeLimitContext,
|
||||||
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
||||||
|
modelChecksSignature: selectedModelChecksByProviderSignature,
|
||||||
});
|
});
|
||||||
const issueReasons = getShortLivedProviderPrepareModelIssueReasons({
|
const issueReasons = getShortLivedProviderPrepareModelIssueReasons({
|
||||||
providerId,
|
providerId,
|
||||||
|
|
@ -802,6 +885,7 @@ export const CreateTeamDialog = ({
|
||||||
prepareChecks,
|
prepareChecks,
|
||||||
prepareRuntimeStatusSignature,
|
prepareRuntimeStatusSignature,
|
||||||
runtimeBackendSummaryByProvider,
|
runtimeBackendSummaryByProvider,
|
||||||
|
selectedModelChecksByProviderSignature,
|
||||||
selectedMemberProviders,
|
selectedMemberProviders,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -903,49 +987,8 @@ export const CreateTeamDialog = ({
|
||||||
void (async () => {
|
void (async () => {
|
||||||
let checks = initialChecks;
|
let checks = initialChecks;
|
||||||
const providerPlans = selectedMemberProviders.map((providerId) => {
|
const providerPlans = selectedMemberProviders.map((providerId) => {
|
||||||
const selectedModelChecks = (() => {
|
const selectedModelChecks = selectedModelChecksByProvider.get(providerId) ?? [];
|
||||||
const next = new Set<string>();
|
const selectedModelIds = selectedModelChecks.map((check) => check.model);
|
||||||
let hasDefaultSelection = false;
|
|
||||||
const supportsProviderDefaultCheck =
|
|
||||||
providerId === 'codex' ||
|
|
||||||
providerId === 'gemini' ||
|
|
||||||
(providerId === 'anthropic' && selectedProviderId === 'anthropic');
|
|
||||||
const leadModel = computeEffectiveTeamModel(
|
|
||||||
selectedModel,
|
|
||||||
effectiveAnthropicRuntimeLimitContext,
|
|
||||||
selectedProviderId
|
|
||||||
);
|
|
||||||
if (selectedProviderId === providerId && selectedModel.trim()) {
|
|
||||||
if (leadModel?.trim()) {
|
|
||||||
next.add(leadModel.trim());
|
|
||||||
}
|
|
||||||
} else if (selectedProviderId === providerId && supportsProviderDefaultCheck) {
|
|
||||||
hasDefaultSelection = true;
|
|
||||||
}
|
|
||||||
for (const member of effectiveMemberDrafts) {
|
|
||||||
if (member.removedAt) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const scopedModel = resolveProviderScopedMemberModel({
|
|
||||||
memberProviderId: member.providerId,
|
|
||||||
memberModel: member.model,
|
|
||||||
selectedProviderId,
|
|
||||||
runtimeProviderStatusById,
|
|
||||||
});
|
|
||||||
if (scopedModel.providerId !== providerId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (scopedModel.model) {
|
|
||||||
next.add(scopedModel.model);
|
|
||||||
} else if (supportsProviderDefaultCheck) {
|
|
||||||
hasDefaultSelection = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (supportsProviderDefaultCheck && hasDefaultSelection) {
|
|
||||||
next.add(DEFAULT_PROVIDER_MODEL_SELECTION);
|
|
||||||
}
|
|
||||||
return Array.from(next);
|
|
||||||
})();
|
|
||||||
const backendSummary = runtimeBackendSummaryByProviderRef.current.get(providerId) ?? null;
|
const backendSummary = runtimeBackendSummaryByProviderRef.current.get(providerId) ?? null;
|
||||||
const cacheKey = buildProviderPrepareModelCacheKey({
|
const cacheKey = buildProviderPrepareModelCacheKey({
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
|
|
@ -953,6 +996,7 @@ export const CreateTeamDialog = ({
|
||||||
backendSummary,
|
backendSummary,
|
||||||
limitContext: effectiveAnthropicRuntimeLimitContext,
|
limitContext: effectiveAnthropicRuntimeLimitContext,
|
||||||
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
||||||
|
modelChecksSignature: selectedModelChecksByProviderSignature,
|
||||||
});
|
});
|
||||||
const cachedModelResultsById = {
|
const cachedModelResultsById = {
|
||||||
...getShortLivedProviderPrepareModelResults({
|
...getShortLivedProviderPrepareModelResults({
|
||||||
|
|
@ -963,12 +1007,13 @@ export const CreateTeamDialog = ({
|
||||||
};
|
};
|
||||||
const cachedSnapshot = getProviderPrepareCachedSnapshot({
|
const cachedSnapshot = getProviderPrepareCachedSnapshot({
|
||||||
providerId,
|
providerId,
|
||||||
selectedModelIds: selectedModelChecks,
|
selectedModelIds,
|
||||||
cachedModelResultsById,
|
cachedModelResultsById,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
providerId,
|
providerId,
|
||||||
selectedModelChecks,
|
selectedModelChecks,
|
||||||
|
selectedModelIds,
|
||||||
backendSummary,
|
backendSummary,
|
||||||
cacheKey,
|
cacheKey,
|
||||||
cachedModelResultsById,
|
cachedModelResultsById,
|
||||||
|
|
@ -979,7 +1024,7 @@ export const CreateTeamDialog = ({
|
||||||
try {
|
try {
|
||||||
for (const plan of providerPlans) {
|
for (const plan of providerPlans) {
|
||||||
checks = updateProviderCheck(checks, plan.providerId, {
|
checks = updateProviderCheck(checks, plan.providerId, {
|
||||||
status: plan.selectedModelChecks.length > 0 ? plan.cachedSnapshot.status : 'checking',
|
status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking',
|
||||||
backendSummary: plan.backendSummary,
|
backendSummary: plan.backendSummary,
|
||||||
details: plan.cachedSnapshot.details,
|
details: plan.cachedSnapshot.details,
|
||||||
});
|
});
|
||||||
|
|
@ -992,7 +1037,8 @@ export const CreateTeamDialog = ({
|
||||||
const prepResult = await runProviderPrepareDiagnostics({
|
const prepResult = await runProviderPrepareDiagnostics({
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
providerId: plan.providerId,
|
providerId: plan.providerId,
|
||||||
selectedModelIds: plan.selectedModelChecks,
|
selectedModelIds: plan.selectedModelIds,
|
||||||
|
selectedModelChecks: plan.selectedModelChecks,
|
||||||
prepareProvisioning: api.teams.prepareProvisioning,
|
prepareProvisioning: api.teams.prepareProvisioning,
|
||||||
limitContext: effectiveAnthropicRuntimeLimitContext,
|
limitContext: effectiveAnthropicRuntimeLimitContext,
|
||||||
cachedModelResultsById: plan.cachedModelResultsById,
|
cachedModelResultsById: plan.cachedModelResultsById,
|
||||||
|
|
@ -1059,14 +1105,14 @@ export const CreateTeamDialog = ({
|
||||||
anyFailure
|
anyFailure
|
||||||
? failureMessage
|
? failureMessage
|
||||||
: anyNotes
|
: anyNotes
|
||||||
? 'Selected providers are ready with notes.'
|
? 'All selected providers are ready, with notes.'
|
||||||
: 'Selected providers are ready.'
|
: 'All selected providers are ready.'
|
||||||
);
|
);
|
||||||
setPrepareWarnings(collectedWarnings);
|
setPrepareWarnings(collectedWarnings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (prepareRequestSeqRef.current !== requestSeq) return;
|
if (prepareRequestSeqRef.current !== requestSeq) return;
|
||||||
const failureMessage =
|
const failureMessage =
|
||||||
error instanceof Error ? error.message : 'Failed to warm up Claude CLI environment';
|
error instanceof Error ? error.message : 'Failed to prepare selected providers';
|
||||||
setPrepareState('failed');
|
setPrepareState('failed');
|
||||||
setPrepareWarnings([]);
|
setPrepareWarnings([]);
|
||||||
setPrepareChecks(failIncompleteProviderChecks(checks, failureMessage));
|
setPrepareChecks(failIncompleteProviderChecks(checks, failureMessage));
|
||||||
|
|
@ -1085,6 +1131,8 @@ export const CreateTeamDialog = ({
|
||||||
prepareRuntimeStatusSignature,
|
prepareRuntimeStatusSignature,
|
||||||
runtimeProviderStatusById,
|
runtimeProviderStatusById,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
|
selectedModelChecksByProvider,
|
||||||
|
selectedModelChecksByProviderSignature,
|
||||||
selectedProviderId,
|
selectedProviderId,
|
||||||
selectedMemberProviders,
|
selectedMemberProviders,
|
||||||
]);
|
]);
|
||||||
|
|
@ -2325,7 +2373,7 @@ export const CreateTeamDialog = ({
|
||||||
<span>
|
<span>
|
||||||
{effectivePrepare.message ??
|
{effectivePrepare.message ??
|
||||||
(effectivePrepare.state === 'idle'
|
(effectivePrepare.state === 'idle'
|
||||||
? 'Warming up CLI environment...'
|
? 'Checking selected providers...'
|
||||||
: 'Preparing environment...')}
|
: 'Preparing environment...')}
|
||||||
</span>
|
</span>
|
||||||
<p className="mt-0.5 text-[10px] text-[var(--color-text-muted)] opacity-70">
|
<p className="mt-0.5 text-[10px] text-[var(--color-text-muted)] opacity-70">
|
||||||
|
|
@ -2344,8 +2392,8 @@ export const CreateTeamDialog = ({
|
||||||
<span>
|
<span>
|
||||||
{prepareChecks.some((check) => check.status === 'notes') ||
|
{prepareChecks.some((check) => check.status === 'notes') ||
|
||||||
prepareWarnings.length > 0
|
prepareWarnings.length > 0
|
||||||
? 'CLI environment ready (with notes)'
|
? 'Selected providers ready (with notes)'
|
||||||
: 'CLI environment ready'}
|
: 'Selected providers ready'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{effectivePrepare.message ? (
|
{effectivePrepare.message ? (
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ import type {
|
||||||
TeamFastMode,
|
TeamFastMode,
|
||||||
TeamLaunchRequest,
|
TeamLaunchRequest,
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
|
TeamProvisioningModelCheckRequest,
|
||||||
UpdateSchedulePatch,
|
UpdateSchedulePatch,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -1133,37 +1134,53 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const selectedModelChecksByProvider = useMemo(() => {
|
const selectedModelChecksByProvider = useMemo(() => {
|
||||||
const modelsByProvider = new Map<TeamProviderId, string[]>();
|
const modelsByProvider = new Map<TeamProviderId, TeamProvisioningModelCheckRequest[]>();
|
||||||
const defaultSelectionByProvider = new Map<TeamProviderId, boolean>();
|
const leadEffort = (selectedEffort as EffortLevel | '') || undefined;
|
||||||
const addModel = (providerId: TeamProviderId, model: string | undefined): void => {
|
const addModel = (
|
||||||
|
providerId: TeamProviderId,
|
||||||
|
model: string | undefined,
|
||||||
|
effort?: EffortLevel
|
||||||
|
): void => {
|
||||||
const trimmed = model?.trim() ?? '';
|
const trimmed = model?.trim() ?? '';
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const existing = modelsByProvider.get(providerId) ?? [];
|
const existing = modelsByProvider.get(providerId) ?? [];
|
||||||
if (!existing.includes(trimmed)) {
|
if (!existing.some((entry) => entry.model === trimmed && entry.effort === effort)) {
|
||||||
modelsByProvider.set(providerId, [...existing, trimmed]);
|
modelsByProvider.set(providerId, [
|
||||||
|
...existing,
|
||||||
|
{
|
||||||
|
providerId,
|
||||||
|
model: trimmed,
|
||||||
|
...(effort ? { effort } : {}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const addDefaultSelection = (providerId: TeamProviderId): void => {
|
const addDefaultSelection = (providerId: TeamProviderId, effort?: EffortLevel): void => {
|
||||||
if (
|
if (
|
||||||
providerId === 'codex' ||
|
providerId === 'codex' ||
|
||||||
providerId === 'gemini' ||
|
providerId === 'gemini' ||
|
||||||
(providerId === 'anthropic' && selectedProviderId === 'anthropic')
|
(providerId === 'anthropic' && selectedProviderId === 'anthropic')
|
||||||
) {
|
) {
|
||||||
defaultSelectionByProvider.set(providerId, true);
|
addModel(providerId, DEFAULT_PROVIDER_MODEL_SELECTION, effort);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedModel.trim()) {
|
if (selectedModel.trim()) {
|
||||||
addModel(selectedProviderId, effectiveLeadRuntimeModel);
|
addModel(selectedProviderId, effectiveLeadRuntimeModel, leadEffort);
|
||||||
} else {
|
} else {
|
||||||
addDefaultSelection(selectedProviderId);
|
addDefaultSelection(selectedProviderId, leadEffort);
|
||||||
}
|
}
|
||||||
for (const member of effectiveMemberDrafts) {
|
for (const member of effectiveMemberDrafts) {
|
||||||
if (member.removedAt) {
|
if (member.removedAt) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const memberProviderId = normalizeOptionalTeamProviderId(member.providerId);
|
||||||
|
const inheritsDefaultRuntime = !memberProviderId || memberProviderId === selectedProviderId;
|
||||||
|
const explicitMemberModel = member.model?.trim() ?? '';
|
||||||
|
const memberEffort =
|
||||||
|
member.effort ?? (inheritsDefaultRuntime && !explicitMemberModel ? leadEffort : undefined);
|
||||||
const scopedModel = resolveProviderScopedMemberModel({
|
const scopedModel = resolveProviderScopedMemberModel({
|
||||||
memberProviderId: member.providerId,
|
memberProviderId: member.providerId,
|
||||||
memberModel: member.model,
|
memberModel: member.model,
|
||||||
|
|
@ -1171,20 +1188,18 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
runtimeProviderStatusById,
|
runtimeProviderStatusById,
|
||||||
});
|
});
|
||||||
if (scopedModel.model) {
|
if (scopedModel.model) {
|
||||||
addModel(scopedModel.providerId, scopedModel.model);
|
addModel(scopedModel.providerId, scopedModel.model, memberEffort);
|
||||||
} else {
|
} else {
|
||||||
addDefaultSelection(scopedModel.providerId);
|
addDefaultSelection(scopedModel.providerId, memberEffort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const providerId of defaultSelectionByProvider.keys()) {
|
|
||||||
addModel(providerId, DEFAULT_PROVIDER_MODEL_SELECTION);
|
|
||||||
}
|
|
||||||
|
|
||||||
return modelsByProvider;
|
return modelsByProvider;
|
||||||
}, [
|
}, [
|
||||||
effectiveLeadRuntimeModel,
|
effectiveLeadRuntimeModel,
|
||||||
effectiveMemberDrafts,
|
effectiveMemberDrafts,
|
||||||
runtimeProviderStatusById,
|
runtimeProviderStatusById,
|
||||||
|
selectedEffort,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
selectedProviderId,
|
selectedProviderId,
|
||||||
]);
|
]);
|
||||||
|
|
@ -1470,6 +1485,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
backendSummary,
|
backendSummary,
|
||||||
limitContext: effectiveAnthropicRuntimeLimitContext,
|
limitContext: effectiveAnthropicRuntimeLimitContext,
|
||||||
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
||||||
|
modelChecksSignature: selectedModelChecksByProviderSignature,
|
||||||
});
|
});
|
||||||
const issueReasons = getShortLivedProviderPrepareModelIssueReasons({
|
const issueReasons = getShortLivedProviderPrepareModelIssueReasons({
|
||||||
providerId,
|
providerId,
|
||||||
|
|
@ -1498,6 +1514,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
prepareChecks,
|
prepareChecks,
|
||||||
prepareRuntimeStatusSignature,
|
prepareRuntimeStatusSignature,
|
||||||
runtimeBackendSummaryByProvider,
|
runtimeBackendSummaryByProvider,
|
||||||
|
selectedModelChecksByProviderSignature,
|
||||||
selectedMemberProviders,
|
selectedMemberProviders,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -1557,6 +1574,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
let checks = initialChecks;
|
let checks = initialChecks;
|
||||||
const providerPlans = selectedMemberProviders.map((providerId) => {
|
const providerPlans = selectedMemberProviders.map((providerId) => {
|
||||||
const selectedModelChecks = selectedModelChecksByProvider.get(providerId) ?? [];
|
const selectedModelChecks = selectedModelChecksByProvider.get(providerId) ?? [];
|
||||||
|
const selectedModelIds = selectedModelChecks.map((check) => check.model);
|
||||||
const backendSummary = runtimeBackendSummaryByProviderRef.current.get(providerId) ?? null;
|
const backendSummary = runtimeBackendSummaryByProviderRef.current.get(providerId) ?? null;
|
||||||
const cacheKey = buildProviderPrepareModelCacheKey({
|
const cacheKey = buildProviderPrepareModelCacheKey({
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
|
|
@ -1564,6 +1582,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
backendSummary,
|
backendSummary,
|
||||||
limitContext: effectiveAnthropicRuntimeLimitContext,
|
limitContext: effectiveAnthropicRuntimeLimitContext,
|
||||||
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
runtimeStatusSignature: prepareRuntimeStatusSignature,
|
||||||
|
modelChecksSignature: selectedModelChecksByProviderSignature,
|
||||||
});
|
});
|
||||||
const cachedModelResultsById = {
|
const cachedModelResultsById = {
|
||||||
...getShortLivedProviderPrepareModelResults({
|
...getShortLivedProviderPrepareModelResults({
|
||||||
|
|
@ -1574,12 +1593,13 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
};
|
};
|
||||||
const cachedSnapshot = getProviderPrepareCachedSnapshot({
|
const cachedSnapshot = getProviderPrepareCachedSnapshot({
|
||||||
providerId,
|
providerId,
|
||||||
selectedModelIds: selectedModelChecks,
|
selectedModelIds,
|
||||||
cachedModelResultsById,
|
cachedModelResultsById,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
providerId,
|
providerId,
|
||||||
selectedModelChecks,
|
selectedModelChecks,
|
||||||
|
selectedModelIds,
|
||||||
backendSummary,
|
backendSummary,
|
||||||
cacheKey,
|
cacheKey,
|
||||||
cachedModelResultsById,
|
cachedModelResultsById,
|
||||||
|
|
@ -1590,7 +1610,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
try {
|
try {
|
||||||
for (const plan of providerPlans) {
|
for (const plan of providerPlans) {
|
||||||
checks = updateProviderCheck(checks, plan.providerId, {
|
checks = updateProviderCheck(checks, plan.providerId, {
|
||||||
status: plan.selectedModelChecks.length > 0 ? plan.cachedSnapshot.status : 'checking',
|
status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking',
|
||||||
backendSummary: plan.backendSummary,
|
backendSummary: plan.backendSummary,
|
||||||
details: plan.cachedSnapshot.details,
|
details: plan.cachedSnapshot.details,
|
||||||
});
|
});
|
||||||
|
|
@ -1603,7 +1623,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
const prepResult = await runProviderPrepareDiagnostics({
|
const prepResult = await runProviderPrepareDiagnostics({
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
providerId: plan.providerId,
|
providerId: plan.providerId,
|
||||||
selectedModelIds: plan.selectedModelChecks,
|
selectedModelIds: plan.selectedModelIds,
|
||||||
|
selectedModelChecks: plan.selectedModelChecks,
|
||||||
prepareProvisioning: api.teams.prepareProvisioning,
|
prepareProvisioning: api.teams.prepareProvisioning,
|
||||||
limitContext: effectiveAnthropicRuntimeLimitContext,
|
limitContext: effectiveAnthropicRuntimeLimitContext,
|
||||||
cachedModelResultsById: plan.cachedModelResultsById,
|
cachedModelResultsById: plan.cachedModelResultsById,
|
||||||
|
|
@ -1669,14 +1690,14 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
anyFailure
|
anyFailure
|
||||||
? failureMessage
|
? failureMessage
|
||||||
: anyNotes
|
: anyNotes
|
||||||
? 'Selected providers are ready with notes.'
|
? 'All selected providers are ready, with notes.'
|
||||||
: 'Selected providers are ready.'
|
: 'All selected providers are ready.'
|
||||||
);
|
);
|
||||||
setPrepareWarnings(collectedWarnings);
|
setPrepareWarnings(collectedWarnings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (prepareRequestSeqRef.current !== requestSeq) return;
|
if (prepareRequestSeqRef.current !== requestSeq) return;
|
||||||
const failureMessage =
|
const failureMessage =
|
||||||
error instanceof Error ? error.message : 'Failed to warm up Claude CLI environment';
|
error instanceof Error ? error.message : 'Failed to prepare selected providers';
|
||||||
setPrepareState('failed');
|
setPrepareState('failed');
|
||||||
setPrepareWarnings([]);
|
setPrepareWarnings([]);
|
||||||
setPrepareChecks(failIncompleteProviderChecks(checks, failureMessage));
|
setPrepareChecks(failIncompleteProviderChecks(checks, failureMessage));
|
||||||
|
|
@ -1692,6 +1713,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
selectedProviderId,
|
selectedProviderId,
|
||||||
selectedMemberProviders,
|
selectedMemberProviders,
|
||||||
selectedModelChecksByProvider,
|
selectedModelChecksByProvider,
|
||||||
|
selectedModelChecksByProviderSignature,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -2832,7 +2854,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
id="dialog-effort"
|
id="dialog-effort"
|
||||||
providerId={selectedProviderId}
|
providerId={selectedProviderId}
|
||||||
model={selectedModel}
|
model={selectedModel}
|
||||||
limitContext={false}
|
limitContext={effectiveAnthropicRuntimeLimitContext}
|
||||||
/>
|
/>
|
||||||
{selectedProviderId === 'anthropic' ? (
|
{selectedProviderId === 'anthropic' ? (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
|
@ -2841,7 +2863,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
onValueChange={setSelectedFastMode}
|
onValueChange={setSelectedFastMode}
|
||||||
providerFastModeDefault={anthropicProviderFastModeDefault}
|
providerFastModeDefault={anthropicProviderFastModeDefault}
|
||||||
model={selectedModel}
|
model={selectedModel}
|
||||||
limitContext={false}
|
limitContext={effectiveAnthropicRuntimeLimitContext}
|
||||||
id="dialog-fast-mode"
|
id="dialog-fast-mode"
|
||||||
/>
|
/>
|
||||||
{anthropicRuntimeNotice ? (
|
{anthropicRuntimeNotice ? (
|
||||||
|
|
@ -2951,7 +2973,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
<span>
|
<span>
|
||||||
{effectivePrepare.message ??
|
{effectivePrepare.message ??
|
||||||
(effectivePrepare.state === 'idle'
|
(effectivePrepare.state === 'idle'
|
||||||
? 'Warming up CLI environment...'
|
? 'Checking selected providers...'
|
||||||
: 'Preparing environment...')}
|
: 'Preparing environment...')}
|
||||||
</span>
|
</span>
|
||||||
<p className="mt-0.5 flex items-center gap-1.5 text-[10px] text-[var(--color-text-muted)] opacity-70">
|
<p className="mt-0.5 flex items-center gap-1.5 text-[10px] text-[var(--color-text-muted)] opacity-70">
|
||||||
|
|
@ -2973,8 +2995,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
<span>
|
<span>
|
||||||
{prepareChecks.some((check) => check.status === 'notes') ||
|
{prepareChecks.some((check) => check.status === 'notes') ||
|
||||||
prepareWarnings.length > 0
|
prepareWarnings.length > 0
|
||||||
? 'CLI environment ready (with notes)'
|
? 'Selected providers ready (with notes)'
|
||||||
: 'CLI environment ready'}
|
: 'Selected providers ready'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{effectivePrepare.message ? (
|
{effectivePrepare.message ? (
|
||||||
|
|
|
||||||
|
|
@ -610,8 +610,8 @@ export function deriveEffectiveProvisioningPrepareState(params: {
|
||||||
return {
|
return {
|
||||||
state: 'ready',
|
state: 'ready',
|
||||||
message: hasNotes
|
message: hasNotes
|
||||||
? 'Selected providers are ready with notes.'
|
? 'All selected providers are ready, with notes.'
|
||||||
: 'Selected providers are ready.',
|
: 'All selected providers are ready.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ export function buildProviderPrepareModelCacheKey({
|
||||||
backendSummary,
|
backendSummary,
|
||||||
limitContext,
|
limitContext,
|
||||||
runtimeStatusSignature,
|
runtimeStatusSignature,
|
||||||
|
modelChecksSignature,
|
||||||
}: {
|
}: {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
backendSummary: string | null | undefined;
|
backendSummary: string | null | undefined;
|
||||||
limitContext: boolean;
|
limitContext: boolean;
|
||||||
runtimeStatusSignature?: string | null;
|
runtimeStatusSignature?: string | null;
|
||||||
|
modelChecksSignature?: string | null;
|
||||||
}): string {
|
}): string {
|
||||||
return [
|
return [
|
||||||
cwd,
|
cwd,
|
||||||
|
|
@ -19,5 +21,6 @@ export function buildProviderPrepareModelCacheKey({
|
||||||
backendSummary ?? '',
|
backendSummary ?? '',
|
||||||
limitContext ? 'limit-context:on' : 'limit-context:off',
|
limitContext ? 'limit-context:on' : 'limit-context:off',
|
||||||
runtimeStatusSignature ?? '',
|
runtimeStatusSignature ?? '',
|
||||||
|
modelChecksSignature ?? '',
|
||||||
].join('::');
|
].join('::');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSele
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
|
TeamProvisioningModelCheckRequest,
|
||||||
TeamProvisioningModelVerificationMode,
|
TeamProvisioningModelVerificationMode,
|
||||||
TeamProvisioningPrepareResult,
|
TeamProvisioningPrepareResult,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
|
|
@ -15,7 +16,8 @@ type PrepareProvisioningFn = (
|
||||||
providerIds?: TeamProviderId[],
|
providerIds?: TeamProviderId[],
|
||||||
selectedModels?: string[],
|
selectedModels?: string[],
|
||||||
limitContext?: boolean,
|
limitContext?: boolean,
|
||||||
modelVerificationMode?: TeamProvisioningModelVerificationMode
|
modelVerificationMode?: TeamProvisioningModelVerificationMode,
|
||||||
|
selectedModelChecks?: TeamProvisioningModelCheckRequest[]
|
||||||
) => Promise<TeamProvisioningPrepareResult>;
|
) => Promise<TeamProvisioningPrepareResult>;
|
||||||
|
|
||||||
interface ProviderPrepareDiagnosticsProgress {
|
interface ProviderPrepareDiagnosticsProgress {
|
||||||
|
|
@ -109,6 +111,44 @@ function buildModelSuccessLine(providerId: TeamProviderId, modelId: string): str
|
||||||
return `${getModelLabel(providerId, modelId)} - verified`;
|
return `${getModelLabel(providerId, modelId)} - verified`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSelectedModelChecks(
|
||||||
|
providerId: TeamProviderId,
|
||||||
|
selectedModelIds: readonly string[],
|
||||||
|
selectedModelChecks?: readonly TeamProvisioningModelCheckRequest[]
|
||||||
|
): TeamProvisioningModelCheckRequest[] {
|
||||||
|
const rawChecks: TeamProvisioningModelCheckRequest[] =
|
||||||
|
selectedModelChecks && selectedModelChecks.length > 0
|
||||||
|
? [...selectedModelChecks]
|
||||||
|
: selectedModelIds.map((model) => ({ providerId, model }));
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const normalized: TeamProvisioningModelCheckRequest[] = [];
|
||||||
|
for (const check of rawChecks) {
|
||||||
|
const model = check.model.trim();
|
||||||
|
if (!model) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = `${check.providerId}\n${model}\n${check.effort ?? ''}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
normalized.push({
|
||||||
|
providerId: check.providerId,
|
||||||
|
model,
|
||||||
|
...(check.effort ? { effort: check.effort } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectModelChecksForIds(
|
||||||
|
modelChecks: readonly TeamProvisioningModelCheckRequest[],
|
||||||
|
modelIds: readonly string[]
|
||||||
|
): TeamProvisioningModelCheckRequest[] {
|
||||||
|
const modelIdSet = new Set(modelIds);
|
||||||
|
return modelChecks.filter((check) => modelIdSet.has(check.model));
|
||||||
|
}
|
||||||
|
|
||||||
function buildModelAvailableLine(providerId: TeamProviderId, modelId: string): string {
|
function buildModelAvailableLine(providerId: TeamProviderId, modelId: string): string {
|
||||||
return `${getModelLabel(providerId, modelId)} - available for launch`;
|
return `${getModelLabel(providerId, modelId)} - available for launch`;
|
||||||
}
|
}
|
||||||
|
|
@ -899,16 +939,25 @@ export async function runProviderPrepareDiagnostics({
|
||||||
limitContext,
|
limitContext,
|
||||||
onModelProgress,
|
onModelProgress,
|
||||||
cachedModelResultsById,
|
cachedModelResultsById,
|
||||||
|
selectedModelChecks,
|
||||||
}: {
|
}: {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
selectedModelIds: string[];
|
selectedModelIds: string[];
|
||||||
|
selectedModelChecks?: TeamProvisioningModelCheckRequest[];
|
||||||
prepareProvisioning: PrepareProvisioningFn;
|
prepareProvisioning: PrepareProvisioningFn;
|
||||||
limitContext?: boolean;
|
limitContext?: boolean;
|
||||||
onModelProgress?: (progress: ProviderPrepareDiagnosticsProgress) => void;
|
onModelProgress?: (progress: ProviderPrepareDiagnosticsProgress) => void;
|
||||||
cachedModelResultsById?: Record<string, ProviderPrepareDiagnosticsModelResult>;
|
cachedModelResultsById?: Record<string, ProviderPrepareDiagnosticsModelResult>;
|
||||||
}): Promise<ProviderPrepareDiagnosticsResult> {
|
}): Promise<ProviderPrepareDiagnosticsResult> {
|
||||||
if (selectedModelIds.length === 0) {
|
const normalizedModelChecks = normalizeSelectedModelChecks(
|
||||||
|
providerId,
|
||||||
|
selectedModelIds,
|
||||||
|
selectedModelChecks
|
||||||
|
);
|
||||||
|
const hasExplicitModelChecks = (selectedModelChecks?.length ?? 0) > 0;
|
||||||
|
const orderedModelIds = Array.from(new Set(normalizedModelChecks.map((check) => check.model)));
|
||||||
|
if (orderedModelIds.length === 0) {
|
||||||
const runtimeResult = await prepareProvisioning(
|
const runtimeResult = await prepareProvisioning(
|
||||||
cwd,
|
cwd,
|
||||||
providerId,
|
providerId,
|
||||||
|
|
@ -936,9 +985,6 @@ export async function runProviderPrepareDiagnostics({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderedModelIds = Array.from(
|
|
||||||
new Set(selectedModelIds.map((modelId) => modelId.trim()).filter(Boolean))
|
|
||||||
);
|
|
||||||
const reusableModelResultsById = cachedModelResultsById ?? {};
|
const reusableModelResultsById = cachedModelResultsById ?? {};
|
||||||
const modelResultsById = new Map<string, ProviderPrepareDiagnosticsModelResult>();
|
const modelResultsById = new Map<string, ProviderPrepareDiagnosticsModelResult>();
|
||||||
const modelLines = new Map<string, string>();
|
const modelLines = new Map<string, string>();
|
||||||
|
|
@ -1039,7 +1085,10 @@ export async function runProviderPrepareDiagnostics({
|
||||||
[providerId],
|
[providerId],
|
||||||
uncachedModelIds,
|
uncachedModelIds,
|
||||||
limitContext,
|
limitContext,
|
||||||
'compatibility'
|
'compatibility',
|
||||||
|
...(hasExplicitModelChecks
|
||||||
|
? [selectModelChecksForIds(normalizedModelChecks, uncachedModelIds)]
|
||||||
|
: [])
|
||||||
);
|
);
|
||||||
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||||
|
|
@ -1177,7 +1226,10 @@ export async function runProviderPrepareDiagnostics({
|
||||||
[providerId],
|
[providerId],
|
||||||
compatibilityPassedModelIds,
|
compatibilityPassedModelIds,
|
||||||
limitContext,
|
limitContext,
|
||||||
'deep'
|
'deep',
|
||||||
|
...(hasExplicitModelChecks
|
||||||
|
? [selectModelChecksForIds(normalizedModelChecks, compatibilityPassedModelIds)]
|
||||||
|
: [])
|
||||||
);
|
);
|
||||||
runtimeDetailLines = createRuntimeDetailLines(batchedModelResult).filter(
|
runtimeDetailLines = createRuntimeDetailLines(batchedModelResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
|
||||||
|
|
@ -1328,7 +1380,10 @@ export async function runProviderPrepareDiagnostics({
|
||||||
[providerId],
|
[providerId],
|
||||||
uncachedModelIds,
|
uncachedModelIds,
|
||||||
limitContext,
|
limitContext,
|
||||||
'compatibility'
|
'compatibility',
|
||||||
|
...(hasExplicitModelChecks
|
||||||
|
? [selectModelChecksForIds(normalizedModelChecks, uncachedModelIds)]
|
||||||
|
: [])
|
||||||
);
|
);
|
||||||
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
import type { MemberDraft } from '@renderer/components/team/members/membersEditorTypes';
|
import type { MemberDraft } from '@renderer/components/team/members/membersEditorTypes';
|
||||||
import type { CliProviderStatus, TeamProviderId } from '@shared/types';
|
import type {
|
||||||
|
CliProviderStatus,
|
||||||
|
TeamProviderId,
|
||||||
|
TeamProvisioningModelCheckRequest,
|
||||||
|
} from '@shared/types';
|
||||||
|
|
||||||
type RuntimeProviderStatusById = ReadonlyMap<TeamProviderId, CliProviderStatus | null | undefined>;
|
type RuntimeProviderStatusById = ReadonlyMap<TeamProviderId, CliProviderStatus | null | undefined>;
|
||||||
type SelectedModelChecksByProvider = ReadonlyMap<TeamProviderId, readonly string[]>;
|
type ProviderModelCheckSignatureInput =
|
||||||
|
| string
|
||||||
|
| Pick<TeamProvisioningModelCheckRequest, 'model' | 'effort'>;
|
||||||
|
type SelectedModelChecksByProvider = ReadonlyMap<
|
||||||
|
TeamProviderId,
|
||||||
|
readonly ProviderModelCheckSignatureInput[]
|
||||||
|
>;
|
||||||
|
|
||||||
function normalizeModelIds(modelIds: readonly string[] | null | undefined): string[] {
|
function normalizeModelIds(modelIds: readonly string[] | null | undefined): string[] {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
|
|
@ -10,6 +20,30 @@ function normalizeModelIds(modelIds: readonly string[] | null | undefined): stri
|
||||||
).sort();
|
).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeModelChecks(
|
||||||
|
checks: readonly ProviderModelCheckSignatureInput[] | null | undefined
|
||||||
|
): { model: string; effort: string | null }[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const normalized: { model: string; effort: string | null }[] = [];
|
||||||
|
for (const check of checks ?? []) {
|
||||||
|
const model = (typeof check === 'string' ? check : check.model).trim();
|
||||||
|
if (!model) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const effort = typeof check === 'string' ? null : (check.effort ?? null);
|
||||||
|
const key = `${model}\n${effort ?? ''}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
normalized.push({ model, effort });
|
||||||
|
}
|
||||||
|
return normalized.sort(
|
||||||
|
(left, right) =>
|
||||||
|
left.model.localeCompare(right.model) || (left.effort ?? '').localeCompare(right.effort ?? '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function buildProviderPrepareMembersSignature(members: readonly MemberDraft[]): string {
|
export function buildProviderPrepareMembersSignature(members: readonly MemberDraft[]): string {
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
members.map((member) => ({
|
members.map((member) => ({
|
||||||
|
|
@ -29,7 +63,10 @@ export function buildProviderPrepareModelChecksSignature(
|
||||||
Array.from(modelChecksByProvider.entries())
|
Array.from(modelChecksByProvider.entries())
|
||||||
.map(([providerId, modelIds]) => ({
|
.map(([providerId, modelIds]) => ({
|
||||||
providerId,
|
providerId,
|
||||||
modelIds: normalizeModelIds(modelIds),
|
modelIds: normalizeModelIds(
|
||||||
|
modelIds.map((modelId) => (typeof modelId === 'string' ? modelId : modelId.model))
|
||||||
|
),
|
||||||
|
modelChecks: normalizeModelChecks(modelIds),
|
||||||
}))
|
}))
|
||||||
.sort((left, right) => left.providerId.localeCompare(right.providerId))
|
.sort((left, right) => left.providerId.localeCompare(right.providerId))
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ vi.mock('@renderer/components/team/dialogs/LimitContextCheckbox', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@renderer/components/team/dialogs/TeamModelSelector', () => ({
|
vi.mock('@renderer/components/team/dialogs/TeamModelSelector', () => ({
|
||||||
|
formatTeamModelSummary: (providerId: string, model: string, effort?: string) =>
|
||||||
|
[providerId, model || 'Default', effort].filter(Boolean).join(' · '),
|
||||||
getProviderScopedTeamModelLabel: (_providerId: string, model: string) => model || 'Default',
|
getProviderScopedTeamModelLabel: (_providerId: string, model: string) => model || 'Default',
|
||||||
|
getTeamEffortLabel: (effort: string) => effort || 'Default',
|
||||||
getTeamProviderLabel: (providerId: string) => providerId,
|
getTeamProviderLabel: (providerId: string) => providerId,
|
||||||
TeamModelSelector: () => React.createElement('div', null, 'team-model-selector'),
|
TeamModelSelector: () => React.createElement('div', null, 'team-model-selector'),
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import {
|
||||||
import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLevelSelector';
|
import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLevelSelector';
|
||||||
import { LimitContextCheckbox } from '@renderer/components/team/dialogs/LimitContextCheckbox';
|
import { LimitContextCheckbox } from '@renderer/components/team/dialogs/LimitContextCheckbox';
|
||||||
import {
|
import {
|
||||||
|
formatTeamModelSummary,
|
||||||
getProviderScopedTeamModelLabel,
|
getProviderScopedTeamModelLabel,
|
||||||
|
getTeamEffortLabel,
|
||||||
getTeamProviderLabel,
|
getTeamProviderLabel,
|
||||||
TeamModelSelector,
|
TeamModelSelector,
|
||||||
} from '@renderer/components/team/dialogs/TeamModelSelector';
|
} from '@renderer/components/team/dialogs/TeamModelSelector';
|
||||||
|
|
@ -111,6 +113,11 @@ export const LeadModelRow = ({
|
||||||
const contextLimitDisabled =
|
const contextLimitDisabled =
|
||||||
disableAnthropicContextLimit ??
|
disableAnthropicContextLimit ??
|
||||||
(providerId === 'anthropic' && isAnthropicHaikuTeamModel(model));
|
(providerId === 'anthropic' && isAnthropicHaikuTeamModel(model));
|
||||||
|
const runtimeSummary = formatTeamModelSummary(providerId, model, effort);
|
||||||
|
const runtimeMeta = [
|
||||||
|
effort || providerId === 'anthropic' ? `Effort ${getTeamEffortLabel(effort ?? '')}` : null,
|
||||||
|
providerId === 'anthropic' ? (limitContext ? '200K context' : '1M-capable context') : null,
|
||||||
|
].filter((item): item is string => Boolean(item));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasActiveProviderNotice && !modelExpanded) {
|
if (hasActiveProviderNotice && !modelExpanded) {
|
||||||
|
|
@ -189,6 +196,12 @@ export const LeadModelRow = ({
|
||||||
{hasModelIssue ? <AlertTriangle className="size-3.5 shrink-0 text-red-300" /> : null}
|
{hasModelIssue ? <AlertTriangle className="size-3.5 shrink-0 text-red-300" /> : null}
|
||||||
{hasModelAdvisory ? <Info className="size-3.5 shrink-0 text-amber-300" /> : null}
|
{hasModelAdvisory ? <Info className="size-3.5 shrink-0 text-amber-300" /> : null}
|
||||||
</Button>
|
</Button>
|
||||||
|
{runtimeMeta.length > 0 ? (
|
||||||
|
<p className="truncate px-1 text-[10px] leading-tight text-[var(--color-text-muted)]">
|
||||||
|
{runtimeMeta.join(' · ')}
|
||||||
|
<span className="sr-only">. {runtimeSummary}</span>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasWarnings ? (
|
{hasWarnings ? (
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ vi.mock('@renderer/components/team/dialogs/TeamModelSelector', () => ({
|
||||||
formatTeamModelSummary: (providerId: string, model: string, effort?: string) =>
|
formatTeamModelSummary: (providerId: string, model: string, effort?: string) =>
|
||||||
[providerId, model || 'Default', effort].filter(Boolean).join(' · '),
|
[providerId, model || 'Default', effort].filter(Boolean).join(' · '),
|
||||||
getProviderScopedTeamModelLabel: (_providerId: string, model: string) => model || 'Default',
|
getProviderScopedTeamModelLabel: (_providerId: string, model: string) => model || 'Default',
|
||||||
|
getTeamEffortLabel: (effort: string) => effort || 'Default',
|
||||||
getTeamProviderLabel: (providerId: string) => providerId,
|
getTeamProviderLabel: (providerId: string) => providerId,
|
||||||
TeamModelSelector: () => React.createElement('div', null, 'team-model-selector'),
|
TeamModelSelector: () => React.createElement('div', null, 'team-model-selector'),
|
||||||
}));
|
}));
|
||||||
|
|
@ -32,6 +33,7 @@ vi.mock('@renderer/components/ui/button', () => ({
|
||||||
disabled,
|
disabled,
|
||||||
title,
|
title,
|
||||||
'aria-describedby': ariaDescribedBy,
|
'aria-describedby': ariaDescribedBy,
|
||||||
|
'aria-expanded': ariaExpanded,
|
||||||
'aria-label': ariaLabel,
|
'aria-label': ariaLabel,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -40,6 +42,7 @@ vi.mock('@renderer/components/ui/button', () => ({
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
'aria-describedby'?: string;
|
'aria-describedby'?: string;
|
||||||
|
'aria-expanded'?: boolean;
|
||||||
'aria-label'?: string;
|
'aria-label'?: string;
|
||||||
}) =>
|
}) =>
|
||||||
React.createElement(
|
React.createElement(
|
||||||
|
|
@ -51,6 +54,7 @@ vi.mock('@renderer/components/ui/button', () => ({
|
||||||
disabled,
|
disabled,
|
||||||
title,
|
title,
|
||||||
'aria-describedby': ariaDescribedBy,
|
'aria-describedby': ariaDescribedBy,
|
||||||
|
'aria-expanded': ariaExpanded,
|
||||||
'aria-label': ariaLabel,
|
'aria-label': ariaLabel,
|
||||||
},
|
},
|
||||||
children
|
children
|
||||||
|
|
@ -174,6 +178,33 @@ describe('MemberDraftRow', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders the workflow control as an icon-only button with tooltip text', () => {
|
||||||
|
const { host, root } = renderMemberDraftRow({
|
||||||
|
showWorkflow: true,
|
||||||
|
onWorkflowChange: () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflowButton = host.querySelector<HTMLButtonElement>(
|
||||||
|
'button[aria-label="Add teammate workflow"]'
|
||||||
|
)!;
|
||||||
|
|
||||||
|
expect(workflowButton).toBeTruthy();
|
||||||
|
expect(workflowButton.textContent).not.toContain('Workflow');
|
||||||
|
expect(workflowButton.closest('[title]')?.getAttribute('title')).toBe('Add teammate workflow');
|
||||||
|
expect(host.textContent).not.toContain('Workflow (optional)');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
workflowButton.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(workflowButton.getAttribute('aria-expanded')).toBe('true');
|
||||||
|
expect(host.textContent).toContain('Workflow (optional)');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('shows inherited model copy when sync is enabled', () => {
|
it('shows inherited model copy when sync is enabled', () => {
|
||||||
const { host, root } = renderMemberDraftRow({
|
const { host, root } = renderMemberDraftRow({
|
||||||
lockProviderModel: true,
|
lockProviderModel: true,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLev
|
||||||
import {
|
import {
|
||||||
formatTeamModelSummary,
|
formatTeamModelSummary,
|
||||||
getProviderScopedTeamModelLabel,
|
getProviderScopedTeamModelLabel,
|
||||||
|
getTeamEffortLabel,
|
||||||
getTeamProviderLabel,
|
getTeamProviderLabel,
|
||||||
TeamModelSelector,
|
TeamModelSelector,
|
||||||
} from '@renderer/components/team/dialogs/TeamModelSelector';
|
} from '@renderer/components/team/dialogs/TeamModelSelector';
|
||||||
|
|
@ -32,6 +33,7 @@ import {
|
||||||
Info,
|
Info,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
Workflow as WorkflowIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import type { MemberDraft } from './membersEditorTypes';
|
import type { MemberDraft } from './membersEditorTypes';
|
||||||
|
|
@ -310,11 +312,24 @@ export const MemberDraftRow = ({
|
||||||
const anthropicContextModeLabel = limitContext
|
const anthropicContextModeLabel = limitContext
|
||||||
? '200K limit enabled'
|
? '200K limit enabled'
|
||||||
: '1M-capable context allowed';
|
: '1M-capable context allowed';
|
||||||
|
const workflowTooltipText = workflowDraft.value.trim()
|
||||||
|
? 'Edit teammate workflow'
|
||||||
|
: 'Add teammate workflow';
|
||||||
const runtimeSummary = formatTeamModelSummary(
|
const runtimeSummary = formatTeamModelSummary(
|
||||||
effectiveProviderId,
|
effectiveProviderId,
|
||||||
effectiveModel?.trim() ?? '',
|
effectiveModel?.trim() ?? '',
|
||||||
effectiveEffort
|
effectiveEffort
|
||||||
);
|
);
|
||||||
|
const runtimeMeta = [
|
||||||
|
effectiveEffort || effectiveProviderId === 'anthropic'
|
||||||
|
? `Effort ${getTeamEffortLabel(effectiveEffort ?? '')}`
|
||||||
|
: null,
|
||||||
|
effectiveProviderId === 'anthropic'
|
||||||
|
? limitContext
|
||||||
|
? '200K context'
|
||||||
|
: '1M-capable context'
|
||||||
|
: null,
|
||||||
|
].filter((item): item is string => Boolean(item));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -346,6 +361,7 @@ export const MemberDraftRow = ({
|
||||||
value={member.name}
|
value={member.name}
|
||||||
aria-label={`Member ${index + 1} name`}
|
aria-label={`Member ${index + 1} name`}
|
||||||
disabled={isRemoved || lockIdentity}
|
disabled={isRemoved || lockIdentity}
|
||||||
|
title={lockIdentity ? identityLockReason : undefined}
|
||||||
onChange={(event) => onNameChange(member.id, event.target.value)}
|
onChange={(event) => onNameChange(member.id, event.target.value)}
|
||||||
placeholder="member-name"
|
placeholder="member-name"
|
||||||
/>
|
/>
|
||||||
|
|
@ -372,23 +388,31 @@ export const MemberDraftRow = ({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-start">
|
||||||
{showWorkflow && onWorkflowChange ? (
|
{showWorkflow && onWorkflowChange ? (
|
||||||
<Button
|
<HoverTooltip
|
||||||
variant="outline"
|
content={workflowTooltipText}
|
||||||
size="sm"
|
title={workflowTooltipText}
|
||||||
className="relative h-8 shrink-0 gap-1"
|
className="shrink-0"
|
||||||
disabled={isRemoved}
|
contentClassName="max-w-64"
|
||||||
onClick={() => setWorkflowExpanded((prev) => !prev)}
|
|
||||||
>
|
>
|
||||||
{workflowExpanded ? (
|
<Button
|
||||||
<ChevronDown className="size-3.5" />
|
variant="outline"
|
||||||
) : (
|
size="sm"
|
||||||
<ChevronRight className="size-3.5" />
|
className={cn(
|
||||||
)}
|
'relative size-8 shrink-0 px-0',
|
||||||
Workflow
|
workflowExpanded &&
|
||||||
{!workflowExpanded && workflowDraft.value.trim() ? (
|
'border-blue-400/50 bg-blue-500/10 text-blue-100 hover:bg-blue-500/15'
|
||||||
<span className="absolute -right-1 -top-1 size-2 rounded-full bg-blue-500" />
|
)}
|
||||||
) : null}
|
aria-label={workflowTooltipText}
|
||||||
</Button>
|
aria-expanded={workflowExpanded}
|
||||||
|
disabled={isRemoved}
|
||||||
|
onClick={() => setWorkflowExpanded((prev) => !prev)}
|
||||||
|
>
|
||||||
|
<WorkflowIcon className="size-3.5" />
|
||||||
|
{!workflowExpanded && workflowDraft.value.trim() ? (
|
||||||
|
<span className="absolute -right-1 -top-1 size-2 rounded-full bg-blue-500" />
|
||||||
|
) : null}
|
||||||
|
</Button>
|
||||||
|
</HoverTooltip>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="w-full min-w-0 space-y-1 sm:w-[150px] sm:min-w-[150px]">
|
<div className="w-full min-w-0 space-y-1 sm:w-[150px] sm:min-w-[150px]">
|
||||||
<HoverTooltip
|
<HoverTooltip
|
||||||
|
|
@ -426,6 +450,12 @@ export const MemberDraftRow = ({
|
||||||
{hasModelAdvisory ? <Info className="size-3.5 shrink-0 text-amber-300" /> : null}
|
{hasModelAdvisory ? <Info className="size-3.5 shrink-0 text-amber-300" /> : null}
|
||||||
</Button>
|
</Button>
|
||||||
</HoverTooltip>
|
</HoverTooltip>
|
||||||
|
{runtimeMeta.length > 0 ? (
|
||||||
|
<p className="truncate px-1 text-[10px] leading-tight text-[var(--color-text-muted)]">
|
||||||
|
{runtimeMeta.join(' · ')}
|
||||||
|
<span className="sr-only">. {runtimeSummary}</span>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
{modelTooltipText ? (
|
{modelTooltipText ? (
|
||||||
<span id={modelHelpDescriptionId} className="sr-only">
|
<span id={modelHelpDescriptionId} className="sr-only">
|
||||||
{modelTooltipText}
|
{modelTooltipText}
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ export const CronScheduleInput = ({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-[11px] text-[var(--color-text-muted)]">
|
<p className="text-[11px] text-[var(--color-text-muted)]">
|
||||||
Pre-warms CLI environment before scheduled execution
|
Prepares selected providers before scheduled execution
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -652,7 +652,7 @@
|
||||||
var TEAM_MEMBER_OFFSETS = [0, 4, 7];
|
var TEAM_MEMBER_OFFSETS = [0, 4, 7];
|
||||||
var TEAM_LABELS = ['Marketing', 'Researchers', 'Coding'];
|
var TEAM_LABELS = ['Marketing', 'Researchers', 'Coding'];
|
||||||
var MAX_DPR = 2;
|
var MAX_DPR = 2;
|
||||||
var AVATAR_URLS = [
|
var FALLBACK_AVATAR_URLS = [
|
||||||
'./assets/participant-avatars/01.png',
|
'./assets/participant-avatars/01.png',
|
||||||
'./assets/participant-avatars/02.png',
|
'./assets/participant-avatars/02.png',
|
||||||
'./assets/participant-avatars/03.png',
|
'./assets/participant-avatars/03.png',
|
||||||
|
|
@ -667,9 +667,36 @@
|
||||||
'./assets/participant-avatars/12.png',
|
'./assets/participant-avatars/12.png',
|
||||||
'./assets/participant-avatars/13.png',
|
'./assets/participant-avatars/13.png',
|
||||||
];
|
];
|
||||||
|
var AVATAR_URLS = resolveAvatarUrls();
|
||||||
var avatarCache = new Map();
|
var avatarCache = new Map();
|
||||||
var avatarLoading = new Map();
|
var avatarLoading = new Map();
|
||||||
|
|
||||||
|
function resolveAvatarUrls() {
|
||||||
|
var links = document.querySelectorAll(
|
||||||
|
'link[rel="preload"][as="image"][type="image/png"]'
|
||||||
|
);
|
||||||
|
var urlsByIndex = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < links.length; i++) {
|
||||||
|
var href = links[i].getAttribute('href');
|
||||||
|
if (!href) continue;
|
||||||
|
|
||||||
|
var match = href.match(/(?:^|\/)(0[1-9]|1[0-3])(?:-[^/?#]+)?\.png(?:[?#].*)?$/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
urlsByIndex[Number(match[1]) - 1] = href;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved = [];
|
||||||
|
for (var avatarIndex = 0; avatarIndex < FALLBACK_AVATAR_URLS.length; avatarIndex++) {
|
||||||
|
var url = urlsByIndex[avatarIndex];
|
||||||
|
if (!url) return FALLBACK_AVATAR_URLS;
|
||||||
|
resolved.push(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
function startFileSplashScene(splash) {
|
function startFileSplashScene(splash) {
|
||||||
if (window.__claudeTeamsSplashScene && splash.querySelector('#splash-enhanced-canvas')) {
|
if (window.__claudeTeamsSplashScene && splash.querySelector('#splash-enhanced-canvas')) {
|
||||||
return window.__claudeTeamsSplashScene;
|
return window.__claudeTeamsSplashScene;
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ import type {
|
||||||
TeamLaunchResponse,
|
TeamLaunchResponse,
|
||||||
TeamMemberActivityMeta,
|
TeamMemberActivityMeta,
|
||||||
TeamMessageNotificationData,
|
TeamMessageNotificationData,
|
||||||
|
TeamProvisioningModelCheckRequest,
|
||||||
TeamProvisioningModelVerificationMode,
|
TeamProvisioningModelVerificationMode,
|
||||||
TeamProvisioningPrepareResult,
|
TeamProvisioningPrepareResult,
|
||||||
TeamProvisioningProgress,
|
TeamProvisioningProgress,
|
||||||
|
|
@ -494,7 +495,8 @@ export interface TeamsAPI {
|
||||||
providerIds?: TeamLaunchRequest['providerId'][],
|
providerIds?: TeamLaunchRequest['providerId'][],
|
||||||
selectedModels?: string[],
|
selectedModels?: string[],
|
||||||
limitContext?: boolean,
|
limitContext?: boolean,
|
||||||
modelVerificationMode?: TeamProvisioningModelVerificationMode
|
modelVerificationMode?: TeamProvisioningModelVerificationMode,
|
||||||
|
selectedModelChecks?: TeamProvisioningModelCheckRequest[]
|
||||||
) => Promise<TeamProvisioningPrepareResult>;
|
) => Promise<TeamProvisioningPrepareResult>;
|
||||||
getWorktreeGitStatus: (projectPath: string) => Promise<TeamWorktreeGitStatus>;
|
getWorktreeGitStatus: (projectPath: string) => Promise<TeamWorktreeGitStatus>;
|
||||||
initializeGitRepository: (projectPath: string) => Promise<TeamWorktreeGitStatus>;
|
initializeGitRepository: (projectPath: string) => Promise<TeamWorktreeGitStatus>;
|
||||||
|
|
|
||||||
|
|
@ -1466,6 +1466,12 @@ export interface TeamCreateResponse {
|
||||||
|
|
||||||
export type TeamProvisioningModelVerificationMode = 'compatibility' | 'deep';
|
export type TeamProvisioningModelVerificationMode = 'compatibility' | 'deep';
|
||||||
|
|
||||||
|
export interface TeamProvisioningModelCheckRequest {
|
||||||
|
providerId: TeamProviderId;
|
||||||
|
model: string;
|
||||||
|
effort?: EffortLevel;
|
||||||
|
}
|
||||||
|
|
||||||
export type TeamProvisioningPrepareIssueScope = 'provider' | 'model';
|
export type TeamProvisioningPrepareIssueScope = 'provider' | 'model';
|
||||||
export type TeamProvisioningPrepareIssueSeverity = 'blocking' | 'warning';
|
export type TeamProvisioningPrepareIssueSeverity = 'blocking' | 'warning';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type AnthropicRuntimeProfileSource,
|
||||||
reconcileAnthropicRuntimeSelections,
|
reconcileAnthropicRuntimeSelections,
|
||||||
|
resolveAnthropicEffortSupport,
|
||||||
resolveAnthropicFastMode,
|
resolveAnthropicFastMode,
|
||||||
resolveAnthropicRuntimeSelection,
|
resolveAnthropicRuntimeSelection,
|
||||||
} from '@features/anthropic-runtime-profile/renderer';
|
} from '@features/anthropic-runtime-profile/renderer';
|
||||||
import type { CliProviderModelCatalog, CliProviderRuntimeCapabilities } from '@shared/types';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import type { AnthropicRuntimeProfileSource } from '@features/anthropic-runtime-profile/renderer';
|
import type { CliProviderModelCatalog, CliProviderRuntimeCapabilities } from '@shared/types';
|
||||||
|
|
||||||
function createAnthropicSource(options: {
|
function createAnthropicSource(options: {
|
||||||
models: CliProviderModelCatalog['models'];
|
models: CliProviderModelCatalog['models'];
|
||||||
|
|
@ -260,6 +260,77 @@ describe('resolveAnthropicRuntimeProfile', () => {
|
||||||
).toBe('Anthropic runtime capability data is still loading.');
|
).toBe('Anthropic runtime capability data is still loading.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows known Opus 1M effort when catalog is unavailable but runtime capability passthrough is present', () => {
|
||||||
|
const selection = resolveAnthropicRuntimeSelection({
|
||||||
|
source: {
|
||||||
|
modelCatalog: null,
|
||||||
|
runtimeCapabilities: {
|
||||||
|
reasoningEffort: {
|
||||||
|
supported: true,
|
||||||
|
values: ['low', 'medium', 'high', 'max'],
|
||||||
|
configPassthrough: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedModel: 'claude-opus-4-6[1m]',
|
||||||
|
limitContext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveAnthropicEffortSupport({
|
||||||
|
selection,
|
||||||
|
effort: 'medium',
|
||||||
|
runtimeCapabilities: {
|
||||||
|
reasoningEffort: {
|
||||||
|
supported: true,
|
||||||
|
values: ['low', 'medium', 'high', 'max'],
|
||||||
|
configPassthrough: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual({ kind: 'supported', source: 'runtime-capability' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to runtime launch model ids when catalog is unavailable', () => {
|
||||||
|
const selection = resolveAnthropicRuntimeSelection({
|
||||||
|
source: {
|
||||||
|
modelCatalog: null,
|
||||||
|
runtimeCapabilities: null,
|
||||||
|
},
|
||||||
|
selectedModel: 'claude-opus-4-6[1m]',
|
||||||
|
limitContext: false,
|
||||||
|
availableLaunchModels: ['claude-opus-4-6'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(selection.resolvedLaunchModel).toBe('claude-opus-4-6');
|
||||||
|
expect(
|
||||||
|
resolveAnthropicEffortSupport({
|
||||||
|
selection,
|
||||||
|
effort: 'medium',
|
||||||
|
runtimeCapabilities: null,
|
||||||
|
})
|
||||||
|
).toEqual({ kind: 'supported', source: 'static-fallback' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not infer effort support for unknown Anthropic models without catalog truth', () => {
|
||||||
|
const selection = resolveAnthropicRuntimeSelection({
|
||||||
|
source: {
|
||||||
|
modelCatalog: null,
|
||||||
|
runtimeCapabilities: null,
|
||||||
|
},
|
||||||
|
selectedModel: 'claude-experimental-5',
|
||||||
|
limitContext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveAnthropicEffortSupport({
|
||||||
|
selection,
|
||||||
|
effort: 'medium',
|
||||||
|
runtimeCapabilities: null,
|
||||||
|
})
|
||||||
|
).toEqual({ kind: 'unverified-catalog-missing' });
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps the fast control visible in degraded states and surfaces the provider reason', () => {
|
it('keeps the fast control visible in degraded states and surfaces the provider reason', () => {
|
||||||
const selection = resolveAnthropicRuntimeSelection({
|
const selection = resolveAnthropicRuntimeSelection({
|
||||||
source: createAnthropicSource({
|
source: createAnthropicSource({
|
||||||
|
|
|
||||||
|
|
@ -462,6 +462,65 @@ describe('ipc teams handlers', () => {
|
||||||
expect(handlers.has(TEAM_DELETE_TASK_ATTACHMENT)).toBe(true);
|
expect(handlers.has(TEAM_DELETE_TASK_ATTACHMENT)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('forwards selected model checks with effort to prepareProvisioning', async () => {
|
||||||
|
const handler = handlers.get(TEAM_PREPARE_PROVISIONING)!;
|
||||||
|
const result = (await handler(
|
||||||
|
{ sender: { send: vi.fn() } } as never,
|
||||||
|
os.tmpdir(),
|
||||||
|
'anthropic',
|
||||||
|
['anthropic'],
|
||||||
|
['claude-opus-4-6[1m]'],
|
||||||
|
false,
|
||||||
|
'compatibility',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-6[1m]',
|
||||||
|
effort: 'medium',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)) as { success: boolean };
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(provisioningService.prepareForProvisioning).toHaveBeenCalledWith(os.tmpdir(), {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
providerIds: ['anthropic'],
|
||||||
|
modelIds: ['claude-opus-4-6[1m]'],
|
||||||
|
limitContext: false,
|
||||||
|
modelVerificationMode: 'compatibility',
|
||||||
|
modelChecks: [
|
||||||
|
{
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-6[1m]',
|
||||||
|
effort: 'medium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid selected model check effort for the provider', async () => {
|
||||||
|
const handler = handlers.get(TEAM_PREPARE_PROVISIONING)!;
|
||||||
|
const result = (await handler(
|
||||||
|
{ sender: { send: vi.fn() } } as never,
|
||||||
|
os.tmpdir(),
|
||||||
|
'anthropic',
|
||||||
|
['anthropic'],
|
||||||
|
['claude-opus-4-6[1m]'],
|
||||||
|
false,
|
||||||
|
'compatibility',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-6[1m]',
|
||||||
|
effort: 'xhigh',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)) as { success: boolean; error: string };
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('selectedModelChecks effort must be one of');
|
||||||
|
});
|
||||||
|
|
||||||
it('updates change presence tracking for a team', async () => {
|
it('updates change presence tracking for a team', async () => {
|
||||||
const handler = handlers.get(TEAM_SET_CHANGE_PRESENCE_TRACKING);
|
const handler = handlers.get(TEAM_SET_CHANGE_PRESENCE_TRACKING);
|
||||||
expect(handler).toBeDefined();
|
expect(handler).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ const execCliMock = vi.fn();
|
||||||
const buildProviderAwareCliEnvMock = vi.fn();
|
const buildProviderAwareCliEnvMock = vi.fn();
|
||||||
const resolveInteractiveShellEnvMock = vi.fn<() => Promise<NodeJS.ProcessEnv>>();
|
const resolveInteractiveShellEnvMock = vi.fn<() => Promise<NodeJS.ProcessEnv>>();
|
||||||
const readFileMock = vi.fn<(path: PathLike, encoding: BufferEncoding) => Promise<string>>();
|
const readFileMock = vi.fn<(path: PathLike, encoding: BufferEncoding) => Promise<string>>();
|
||||||
const enrichProviderStatusMock = vi.fn(
|
const enrichProviderStatusMock = vi.fn((provider, _options?: { hydrateModelCatalog?: boolean }) =>
|
||||||
(provider, _options?: { hydrateModelCatalog?: boolean }) => Promise.resolve(provider)
|
Promise.resolve(provider)
|
||||||
);
|
);
|
||||||
const enrichProviderStatusesMock = vi.fn((providers) => Promise.resolve(providers));
|
const enrichProviderStatusesMock = vi.fn((providers) => Promise.resolve(providers));
|
||||||
|
|
||||||
|
|
@ -343,6 +343,42 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
expect(calls).not.toContain('model list --json --provider all');
|
expect(calls).not.toContain('model list --json --provider all');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns a scoped provider error when single-provider summary status times out', async () => {
|
||||||
|
execCliMock.mockImplementation((_binaryPath, args) => {
|
||||||
|
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||||
|
if (normalizedArgs === 'runtime status --json --provider codex --summary') {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
'Command timed out after 25000ms: /mock/agent_teams_orchestrator runtime status --json --provider codex --summary'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ClaudeMultimodelBridgeService } =
|
||||||
|
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
|
||||||
|
const service = new ClaudeMultimodelBridgeService();
|
||||||
|
|
||||||
|
const provider = await service.getProviderStatus('/mock/agent_teams_orchestrator', 'codex');
|
||||||
|
const calls = execCliMock.mock.calls.map((call) => call[1].join(' '));
|
||||||
|
|
||||||
|
expect(provider).toMatchObject({
|
||||||
|
providerId: 'codex',
|
||||||
|
verificationState: 'error',
|
||||||
|
statusMessage: 'Provider status unavailable',
|
||||||
|
});
|
||||||
|
expect(provider.detailMessage).toContain('Command timed out after 25000ms');
|
||||||
|
expect(calls).toEqual(['runtime status --json --provider codex --summary']);
|
||||||
|
expect(vi.mocked(console.warn).mock.calls.map((call) => call.join(' '))).toEqual([
|
||||||
|
expect.stringContaining(
|
||||||
|
'Provider-scoped summary runtime status unavailable for codex, returning scoped error'
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
vi.mocked(console.warn).mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
it('loads frontend providers with parallel provider-scoped runtime status probes', async () => {
|
it('loads frontend providers with parallel provider-scoped runtime status probes', async () => {
|
||||||
const providerPayloads = {
|
const providerPayloads = {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
|
|
@ -439,9 +475,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
).toEqual([8 * 1024 * 1024, 8 * 1024 * 1024, 8 * 1024 * 1024]);
|
).toEqual([8 * 1024 * 1024, 8 * 1024 * 1024, 8 * 1024 * 1024]);
|
||||||
expect(enrichProviderStatusMock).toHaveBeenCalledTimes(3);
|
expect(enrichProviderStatusMock).toHaveBeenCalledTimes(3);
|
||||||
expect(
|
expect(
|
||||||
enrichProviderStatusMock.mock.calls.every(
|
enrichProviderStatusMock.mock.calls.every((call) => call[1]?.hydrateModelCatalog === false)
|
||||||
(call) => call[1]?.hydrateModelCatalog === false
|
|
||||||
)
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(providers.map((provider) => provider.providerId)).toEqual([
|
expect(providers.map((provider) => provider.providerId)).toEqual([
|
||||||
'anthropic',
|
'anthropic',
|
||||||
|
|
@ -506,10 +540,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
? (args[providerArgIndex + 1] as keyof typeof summaryPayloads)
|
? (args[providerArgIndex + 1] as keyof typeof summaryPayloads)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (
|
if (normalizedArgs === 'runtime status --json --provider codex' && providerId === 'codex') {
|
||||||
normalizedArgs === 'runtime status --json --provider codex' &&
|
|
||||||
providerId === 'codex'
|
|
||||||
) {
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
stdout: JSON.stringify({
|
stdout: JSON.stringify({
|
||||||
schemaVersion: 2,
|
schemaVersion: 2,
|
||||||
|
|
@ -711,6 +742,157 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('queues fresh single-provider catalog hydration behind an in-flight one', async () => {
|
||||||
|
let resolveHydration!: (value: { stdout: string; stderr: string; exitCode: number }) => void;
|
||||||
|
const hydration = new Promise<{ stdout: string; stderr: string; exitCode: number }>(
|
||||||
|
(resolve) => {
|
||||||
|
resolveHydration = resolve;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let fullStatusCalls = 0;
|
||||||
|
|
||||||
|
execCliMock.mockImplementation((_binaryPath, args) => {
|
||||||
|
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||||
|
|
||||||
|
if (normalizedArgs === 'runtime status --json --provider codex --summary') {
|
||||||
|
return Promise.resolve({
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
schemaVersion: 2,
|
||||||
|
providers: {
|
||||||
|
codex: {
|
||||||
|
providerId: 'codex',
|
||||||
|
displayName: 'Codex',
|
||||||
|
supported: true,
|
||||||
|
authenticated: true,
|
||||||
|
authMethod: 'api_key',
|
||||||
|
verificationState: 'verified',
|
||||||
|
canLoginFromUi: false,
|
||||||
|
statusMessage: null,
|
||||||
|
models: ['gpt-5.4'],
|
||||||
|
capabilities: { teamLaunch: true, oneShot: true },
|
||||||
|
runtimeCapabilities: { modelCatalog: { dynamic: true, source: 'app-server' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedArgs === 'runtime status --json --provider codex') {
|
||||||
|
fullStatusCalls += 1;
|
||||||
|
if (fullStatusCalls === 1) {
|
||||||
|
return hydration;
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
schemaVersion: 2,
|
||||||
|
providers: {
|
||||||
|
codex: {
|
||||||
|
providerId: 'codex',
|
||||||
|
displayName: 'Codex',
|
||||||
|
supported: true,
|
||||||
|
authenticated: false,
|
||||||
|
authMethod: null,
|
||||||
|
verificationState: 'unknown',
|
||||||
|
canLoginFromUi: false,
|
||||||
|
statusMessage: 'fresh full status should not overwrite live summary',
|
||||||
|
models: ['gpt-5.4'],
|
||||||
|
capabilities: { teamLaunch: true, oneShot: true },
|
||||||
|
runtimeCapabilities: { modelCatalog: { dynamic: true, source: 'app-server' } },
|
||||||
|
modelCatalog: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
providerId: 'codex',
|
||||||
|
source: 'app-server',
|
||||||
|
status: 'ready',
|
||||||
|
fetchedAt: '2026-05-17T00:01:00.000Z',
|
||||||
|
staleAt: '2026-05-17T00:11:00.000Z',
|
||||||
|
defaultModelId: 'fresh-model',
|
||||||
|
defaultLaunchModel: 'fresh-model',
|
||||||
|
models: [],
|
||||||
|
diagnostics: {
|
||||||
|
configReadState: 'skipped',
|
||||||
|
appServerState: 'healthy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ClaudeMultimodelBridgeService } =
|
||||||
|
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
|
||||||
|
const service = new ClaudeMultimodelBridgeService();
|
||||||
|
const firstUpdate = vi.fn();
|
||||||
|
const secondUpdate = vi.fn();
|
||||||
|
|
||||||
|
await service.getProviderStatus('/mock/agent_teams_orchestrator', 'codex', firstUpdate);
|
||||||
|
await service.getProviderStatus('/mock/agent_teams_orchestrator', 'codex', secondUpdate);
|
||||||
|
expect(
|
||||||
|
execCliMock.mock.calls.filter(
|
||||||
|
(call) => call[1].join(' ') === 'runtime status --json --provider codex'
|
||||||
|
)
|
||||||
|
).toHaveLength(1);
|
||||||
|
|
||||||
|
resolveHydration({
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
schemaVersion: 2,
|
||||||
|
providers: {
|
||||||
|
codex: {
|
||||||
|
providerId: 'codex',
|
||||||
|
displayName: 'Codex',
|
||||||
|
supported: true,
|
||||||
|
authenticated: false,
|
||||||
|
authMethod: null,
|
||||||
|
verificationState: 'unknown',
|
||||||
|
canLoginFromUi: false,
|
||||||
|
statusMessage: 'full status should not overwrite live summary',
|
||||||
|
models: ['gpt-5.4'],
|
||||||
|
capabilities: { teamLaunch: true, oneShot: true },
|
||||||
|
runtimeCapabilities: { modelCatalog: { dynamic: true, source: 'app-server' } },
|
||||||
|
modelCatalog: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
providerId: 'codex',
|
||||||
|
source: 'app-server',
|
||||||
|
status: 'ready',
|
||||||
|
fetchedAt: '2026-05-17T00:00:00.000Z',
|
||||||
|
staleAt: '2026-05-17T00:10:00.000Z',
|
||||||
|
defaultModelId: 'gpt-5.4',
|
||||||
|
defaultLaunchModel: 'gpt-5.4',
|
||||||
|
models: [],
|
||||||
|
diagnostics: {
|
||||||
|
configReadState: 'skipped',
|
||||||
|
appServerState: 'healthy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stderr: '',
|
||||||
|
exitCode: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(secondUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
expect(fullStatusCalls).toBe(2);
|
||||||
|
expect(firstUpdate).not.toHaveBeenCalled();
|
||||||
|
expect(secondUpdate.mock.calls[0]?.[0]).toMatchObject({
|
||||||
|
authenticated: true,
|
||||||
|
authMethod: 'api_key',
|
||||||
|
statusMessage: null,
|
||||||
|
modelCatalog: {
|
||||||
|
defaultModelId: 'fresh-model',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('hydrates Anthropic subscription rate limits after the live summary status', async () => {
|
it('hydrates Anthropic subscription rate limits after the live summary status', async () => {
|
||||||
execCliMock.mockImplementation((_binaryPath, args) => {
|
execCliMock.mockImplementation((_binaryPath, args) => {
|
||||||
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||||
|
|
@ -1161,9 +1343,8 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
|
|
||||||
const hasOldCatalogUpdate = [...firstUpdates.mock.calls, ...secondUpdates.mock.calls].some(
|
const hasOldCatalogUpdate = [...firstUpdates.mock.calls, ...secondUpdates.mock.calls].some(
|
||||||
([providers]) =>
|
([providers]) =>
|
||||||
providers
|
providers.find((provider) => provider.providerId === 'codex')?.modelCatalog
|
||||||
.find((provider) => provider.providerId === 'codex')
|
?.defaultModelId === 'old-model'
|
||||||
?.modelCatalog?.defaultModelId === 'old-model'
|
|
||||||
);
|
);
|
||||||
expect(hasOldCatalogUpdate).toBe(false);
|
expect(hasOldCatalogUpdate).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,11 @@ const liveDescribe =
|
||||||
? describe
|
? describe
|
||||||
: describe.skip;
|
: describe.skip;
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
const DEFAULT_LEAD_MODEL = 'sonnet';
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
|
const DEFAULT_LEAD_MODEL = 'claude-opus-4-6[1m]';
|
||||||
const DEFAULT_MEMBER_MODEL = 'haiku';
|
const DEFAULT_MEMBER_MODEL = 'haiku';
|
||||||
const DEFAULT_LEAD_EFFORT = 'low' as const;
|
const DEFAULT_LEAD_EFFORT = 'medium' as const;
|
||||||
|
|
||||||
liveDescribe('Anthropic launch selection live e2e', () => {
|
liveDescribe('Anthropic launch selection live e2e', () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
|
|
@ -165,9 +166,10 @@ liveDescribe('Anthropic launch selection live e2e', () => {
|
||||||
if (subscriptionAuth && teamName) {
|
if (subscriptionAuth && teamName) {
|
||||||
await removeTeamArtifacts(teamName);
|
await removeTeamArtifacts(teamName);
|
||||||
}
|
}
|
||||||
|
discardKnownAnthropicLaunchSelectionWarnings();
|
||||||
}, 180_000);
|
}, 180_000);
|
||||||
|
|
||||||
it('launches Sonnet low lead with explicit Haiku teammate without inherited effort', async () => {
|
it('launches Opus 4.6 1M medium lead with explicit Haiku teammate without inherited effort', async () => {
|
||||||
const orchestratorCli = process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim();
|
const orchestratorCli = process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim();
|
||||||
expect(orchestratorCli).toBeTruthy();
|
expect(orchestratorCli).toBeTruthy();
|
||||||
await assertExecutable(orchestratorCli!);
|
await assertExecutable(orchestratorCli!);
|
||||||
|
|
@ -191,6 +193,7 @@ liveDescribe('Anthropic launch selection live e2e', () => {
|
||||||
model: leadModel,
|
model: leadModel,
|
||||||
effort: leadEffort,
|
effort: leadEffort,
|
||||||
skipPermissions: true,
|
skipPermissions: true,
|
||||||
|
extraCliArgs: "--settings '{\"disableAllHooks\":true}'",
|
||||||
prompt: 'Keep the team idle after bootstrap. Do not start extra work.',
|
prompt: 'Keep the team idle after bootstrap. Do not start extra work.',
|
||||||
members: [
|
members: [
|
||||||
{
|
{
|
||||||
|
|
@ -286,6 +289,18 @@ function restoreEnv(name: string, previous: string | undefined): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function discardKnownAnthropicLaunchSelectionWarnings(): void {
|
||||||
|
const warn = vi.mocked(console.warn);
|
||||||
|
if (!warn.mock) return;
|
||||||
|
const calls = warn.mock.calls;
|
||||||
|
for (let index = calls.length - 1; index >= 0; index -= 1) {
|
||||||
|
const text = calls[index]?.map((value) => String(value)).join(' ') ?? '';
|
||||||
|
if (text.includes('Failed to resolve login shell env: shell env resolve timeout')) {
|
||||||
|
calls.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function assertExecutable(filePath: string): Promise<void> {
|
async function assertExecutable(filePath: string): Promise<void> {
|
||||||
await fs.access(filePath, fsConstants.X_OK);
|
await fs.access(filePath, fsConstants.X_OK);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ const liveDescribe =
|
||||||
? describe
|
? describe
|
||||||
: describe.skip;
|
: describe.skip;
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
const DEFAULT_MODEL = 'haiku';
|
const DEFAULT_MODEL = 'haiku';
|
||||||
|
|
||||||
liveDescribe('Anthropic runtime memory live e2e', () => {
|
liveDescribe('Anthropic runtime memory live e2e', () => {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ const liveDescribe =
|
||||||
? describe
|
? describe
|
||||||
: describe.skip;
|
: describe.skip;
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
const DEFAULT_MODEL = 'sonnet';
|
const DEFAULT_MODEL = 'sonnet';
|
||||||
const DEFAULT_EFFORT = 'low' as const;
|
const DEFAULT_EFFORT = 'low' as const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ const liveDescribe =
|
||||||
? describe
|
? describe
|
||||||
: describe.skip;
|
: describe.skip;
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
const DEFAULT_MODEL = 'gpt-5.4-mini';
|
const DEFAULT_MODEL = 'gpt-5.4-mini';
|
||||||
const DEFAULT_EFFORT = 'low' as const;
|
const DEFAULT_EFFORT = 'low' as const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ const liveDescribe =
|
||||||
? describe
|
? describe
|
||||||
: describe.skip;
|
: describe.skip;
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
const DEFAULT_ANTHROPIC_MODEL = 'haiku';
|
const DEFAULT_ANTHROPIC_MODEL = 'haiku';
|
||||||
const DEFAULT_CODEX_MODEL = 'gpt-5.4-mini';
|
const DEFAULT_CODEX_MODEL = 'gpt-5.4-mini';
|
||||||
const DEFAULT_OPENCODE_MODEL = 'openai/gpt-5.4-mini';
|
const DEFAULT_OPENCODE_MODEL = 'openai/gpt-5.4-mini';
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ const liveDescribe =
|
||||||
? describe
|
? describe
|
||||||
: describe.skip;
|
: describe.skip;
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
const DEFAULT_MODEL = 'opencode/big-pickle';
|
const DEFAULT_MODEL = 'opencode/big-pickle';
|
||||||
|
|
||||||
liveDescribe('OpenCode accept-fast delivery live e2e', () => {
|
liveDescribe('OpenCode accept-fast delivery live e2e', () => {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ const liveDescribe =
|
||||||
const liveMultiLaneIt = process.env.OPENCODE_E2E_MIXED_RECOVERY_MULTI === '1' ? it : it.skip;
|
const liveMultiLaneIt = process.env.OPENCODE_E2E_MIXED_RECOVERY_MULTI === '1' ? it : it.skip;
|
||||||
|
|
||||||
const PROJECT_PATH = process.env.OPENCODE_E2E_PROJECT_PATH?.trim() || process.cwd();
|
const PROJECT_PATH = process.env.OPENCODE_E2E_PROJECT_PATH?.trim() || process.cwd();
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
const DEFAULT_MODEL = 'opencode/big-pickle';
|
const DEFAULT_MODEL = 'opencode/big-pickle';
|
||||||
|
|
||||||
liveDescribe('OpenCode mixed recovery live e2e', () => {
|
liveDescribe('OpenCode mixed recovery live e2e', () => {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@ const liveDescribe =
|
||||||
: describe.skip;
|
: describe.skip;
|
||||||
|
|
||||||
const PROJECT_PATH = process.env.OPENCODE_E2E_PROJECT_PATH?.trim() || process.cwd();
|
const PROJECT_PATH = process.env.OPENCODE_E2E_PROJECT_PATH?.trim() || process.cwd();
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
const DEFAULT_MODEL = 'opencode/big-pickle';
|
const DEFAULT_MODEL = 'opencode/big-pickle';
|
||||||
|
|
||||||
liveDescribe('OpenCode team provisioning live e2e', () => {
|
liveDescribe('OpenCode team provisioning live e2e', () => {
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,17 @@ import * as path from 'node:path';
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { createOpenCodeLiveHarness, waitForOpenCodeLanesStopped, waitUntil } from './openCodeLiveTestHarness';
|
import { TeamDataService } from '../../../../src/main/services/team/TeamDataService';
|
||||||
|
import { TeamProvisioningService } from '../../../../src/main/services/team/TeamProvisioningService';
|
||||||
|
import { TeamTaskReader } from '../../../../src/main/services/team/TeamTaskReader';
|
||||||
import {
|
import {
|
||||||
getTasksBasePath,
|
getTasksBasePath,
|
||||||
getTeamsBasePath,
|
getTeamsBasePath,
|
||||||
setClaudeBasePathOverride,
|
setClaudeBasePathOverride,
|
||||||
} from '../../../../src/main/utils/pathDecoder';
|
} from '../../../../src/main/utils/pathDecoder';
|
||||||
import { killProcessByPid } from '../../../../src/main/utils/processKill';
|
import { killProcessByPid } from '../../../../src/main/utils/processKill';
|
||||||
import { TeamDataService } from '../../../../src/main/services/team/TeamDataService';
|
|
||||||
import { TeamProvisioningService } from '../../../../src/main/services/team/TeamProvisioningService';
|
import { createOpenCodeLiveHarness, waitForOpenCodeLanesStopped, waitUntil } from './openCodeLiveTestHarness';
|
||||||
import { TeamTaskReader } from '../../../../src/main/services/team/TeamTaskReader';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TeamAgentRuntimeSnapshot,
|
TeamAgentRuntimeSnapshot,
|
||||||
|
|
@ -37,13 +38,35 @@ const liveDescribe =
|
||||||
? describe
|
? describe
|
||||||
: describe.skip;
|
: describe.skip;
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
const DEFAULT_ANTHROPIC_MODEL = 'haiku';
|
const DEFAULT_ANTHROPIC_MODEL = 'haiku';
|
||||||
const DEFAULT_CODEX_MODEL = 'gpt-5.4-mini';
|
const DEFAULT_CODEX_MODEL = 'gpt-5.4-mini';
|
||||||
const DEFAULT_CODEX_EFFORT = 'low' as const;
|
const DEFAULT_CODEX_EFFORT = 'low' as const;
|
||||||
const DEFAULT_OPENCODE_MODEL = 'openai/gpt-5.4-mini';
|
const DEFAULT_OPENCODE_MODEL = 'openai/gpt-5.4-mini';
|
||||||
const DEFAULT_ORDER: ProviderLaunchStressScenario[] = ['anthropic', 'codex', 'opencode', 'mixed'];
|
const DEFAULT_ORDER: ProviderLaunchStressScenario[] = ['anthropic', 'codex', 'opencode', 'mixed'];
|
||||||
const MEMBER_NAMES = ['alice', 'bob', 'jack', 'tom', 'atlas', 'nova', 'cody', 'oscar'];
|
const MEMBER_NAMES = [
|
||||||
|
'alice',
|
||||||
|
'bob',
|
||||||
|
'jack',
|
||||||
|
'tom',
|
||||||
|
'atlas',
|
||||||
|
'nova',
|
||||||
|
'cody',
|
||||||
|
'oscar',
|
||||||
|
'maya',
|
||||||
|
'liam',
|
||||||
|
'ivy',
|
||||||
|
'noah',
|
||||||
|
'zoe',
|
||||||
|
'ryan',
|
||||||
|
'emma',
|
||||||
|
'owen',
|
||||||
|
'luna',
|
||||||
|
'finn',
|
||||||
|
'aria',
|
||||||
|
'milo',
|
||||||
|
];
|
||||||
const RESTART_CONFIRM_TIMEOUT_MS = 300_000;
|
const RESTART_CONFIRM_TIMEOUT_MS = 300_000;
|
||||||
const POST_LAUNCH_WORK_TIMEOUT_MS = 300_000;
|
const POST_LAUNCH_WORK_TIMEOUT_MS = 300_000;
|
||||||
let currentStressTempDir = '';
|
let currentStressTempDir = '';
|
||||||
|
|
@ -159,7 +182,7 @@ liveDescribe('provider launch stress live e2e', () => {
|
||||||
}, 240_000);
|
}, 240_000);
|
||||||
|
|
||||||
it(
|
it(
|
||||||
'launches, restarts, and exercises post-launch work for provider teams with five teammates each',
|
'launches, restarts, and exercises post-launch work for provider teams with the requested teammate count',
|
||||||
async () => {
|
async () => {
|
||||||
const orchestratorCli = process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim();
|
const orchestratorCli = process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim();
|
||||||
expect(orchestratorCli).toBeTruthy();
|
expect(orchestratorCli).toBeTruthy();
|
||||||
|
|
@ -495,6 +518,7 @@ function buildStressCreateRequest(input: {
|
||||||
effort: providerId === 'codex' ? input.selection.codexEffort : undefined,
|
effort: providerId === 'codex' ? input.selection.codexEffort : undefined,
|
||||||
fastMode: providerId === 'codex' ? 'off' : undefined,
|
fastMode: providerId === 'codex' ? 'off' : undefined,
|
||||||
skipPermissions: true,
|
skipPermissions: true,
|
||||||
|
extraCliArgs: process.env.PROVIDER_LAUNCH_STRESS_EXTRA_CLI_ARGS?.trim() || undefined,
|
||||||
prompt: 'Keep the team idle after bootstrap. Do not start extra work.',
|
prompt: 'Keep the team idle after bootstrap. Do not start extra work.',
|
||||||
members,
|
members,
|
||||||
};
|
};
|
||||||
|
|
@ -534,7 +558,7 @@ function resolveStressMemberProvider(
|
||||||
return providers[index % providers.length] ?? 'anthropic';
|
return providers[index % providers.length] ?? 'anthropic';
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveScenarioSelection(scenario: ProviderLaunchStressScenario): {
|
function resolveScenarioSelection(_scenario: ProviderLaunchStressScenario): {
|
||||||
anthropicModel: string;
|
anthropicModel: string;
|
||||||
codexModel: string;
|
codexModel: string;
|
||||||
codexEffort: 'low' | 'medium' | 'high' | 'xhigh';
|
codexEffort: 'low' | 'medium' | 'high' | 'xhigh';
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,25 @@ describe('TeamLaunchFailureArtifactPack', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not classify stdin warning as root cause after bootstrap transport evidence', () => {
|
||||||
|
const input = {
|
||||||
|
teamName: 'artifact-team',
|
||||||
|
runId: 'run-mailbox-written',
|
||||||
|
reason:
|
||||||
|
'atlas: Teammate process atlas@signal-ops did not submit bootstrap prompt: timed out waiting for bootstrap_submitted; last transport stage: mailbox_bootstrap_written Last stderr: Warning: no stdin data received in 3s, proceeding without it.',
|
||||||
|
progressTraceLines: [
|
||||||
|
'mailbox_bootstrap_written detail=messageId=bootstrap-atlas-1',
|
||||||
|
'Warning: no stdin data received in 3s, proceeding without it.',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(classifyLaunchFailureArtifact(input).code).toBe('model_no_bootstrap');
|
||||||
|
expect(extractLaunchBootstrapTransportBreadcrumb(input)).toMatchObject({
|
||||||
|
noStdinWarning: true,
|
||||||
|
bootstrapSubmitted: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('classifies provider quota separately from protocol errors', () => {
|
it('classifies provider quota separately from protocol errors', () => {
|
||||||
expect(
|
expect(
|
||||||
classifyLaunchFailureArtifact({
|
classifyLaunchFailureArtifact({
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import Module from 'module';
|
import Module from 'module';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
type ExecCliMock = (
|
type ExecCliMock = (
|
||||||
binaryPath: string | null,
|
binaryPath: string | null,
|
||||||
|
|
@ -58,11 +58,11 @@ vi.mock('@main/utils/shellEnv', async (importOriginal) => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import { setAppDataBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder';
|
|
||||||
import {
|
import {
|
||||||
TeamMcpConfigBuilder,
|
|
||||||
clearResolvedNodePathForTests,
|
clearResolvedNodePathForTests,
|
||||||
|
TeamMcpConfigBuilder,
|
||||||
} from '@main/services/team/TeamMcpConfigBuilder';
|
} from '@main/services/team/TeamMcpConfigBuilder';
|
||||||
|
import { setAppDataBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder';
|
||||||
|
|
||||||
describe('TeamMcpConfigBuilder', () => {
|
describe('TeamMcpConfigBuilder', () => {
|
||||||
const createdPaths: string[] = [];
|
const createdPaths: string[] = [];
|
||||||
|
|
@ -495,6 +495,58 @@ describe('TeamMcpConfigBuilder', () => {
|
||||||
expectNodeTsxSourceEntry(parsed.mcpServers['agent-teams'], tsxCli, sourceEntry);
|
expectNodeTsxSourceEntry(parsed.mcpServers['agent-teams'], tsxCli, sourceEntry);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('forces the generated agent-teams MCP server on regardless of user, local, or project settings', async () => {
|
||||||
|
const { sourceEntry, tsxCli } = mockSourceWorkspaceEntryAvailable();
|
||||||
|
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-'));
|
||||||
|
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-project-'));
|
||||||
|
createdDirs.push(homeDir, projectDir);
|
||||||
|
mockHomeDir = homeDir;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(homeDir, '.claude.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
mcpServers: {
|
||||||
|
'agent-teams': { command: 'node', args: ['user-disabled.js'], enabled: false },
|
||||||
|
},
|
||||||
|
projects: {
|
||||||
|
[projectDir]: {
|
||||||
|
mcpServers: {
|
||||||
|
'agent-teams': { command: 'node', args: ['local-disabled.js'], enabled: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(projectDir, '.mcp.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
mcpServers: {
|
||||||
|
'agent-teams': { command: 'node', args: ['project-disabled.js'], enabled: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const builder = new TeamMcpConfigBuilder();
|
||||||
|
const configPath = await builder.writeConfigFile(projectDir);
|
||||||
|
createdPaths.push(configPath);
|
||||||
|
|
||||||
|
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
|
||||||
|
mcpServers: Record<string, { command?: string; args?: string[]; enabled?: boolean }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(Object.keys(parsed.mcpServers)).toEqual(['agent-teams']);
|
||||||
|
expect(parsed.mcpServers['agent-teams']?.enabled).toBe(true);
|
||||||
|
expectNodeTsxSourceEntry(parsed.mcpServers['agent-teams'], tsxCli, sourceEntry);
|
||||||
|
});
|
||||||
|
|
||||||
it('passes the configured Claude root to the MCP server', async () => {
|
it('passes the configured Claude root to the MCP server', async () => {
|
||||||
const claudeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-claude-root-'));
|
const claudeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-claude-root-'));
|
||||||
createdDirs.push(claudeRoot);
|
createdDirs.push(claudeRoot);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
|
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
|
||||||
|
|
||||||
vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
|
vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
|
||||||
ClaudeBinaryResolver: { resolve: vi.fn() },
|
ClaudeBinaryResolver: { resolve: vi.fn() },
|
||||||
|
|
@ -95,13 +94,13 @@ vi.mock('@main/utils/childProcess', () => ({
|
||||||
killProcessTree: vi.fn(),
|
killProcessTree: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import { ProviderConnectionService } from '@main/services/runtime/ProviderConnectionService';
|
||||||
TeamProvisioningService,
|
|
||||||
buildDirectTmuxRestartEnvAssignments,
|
|
||||||
} from '@main/services/team/TeamProvisioningService';
|
|
||||||
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
||||||
import { TeamRuntimeAdapterRegistry } from '@main/services/team/runtime';
|
import { TeamRuntimeAdapterRegistry } from '@main/services/team/runtime';
|
||||||
import { ProviderConnectionService } from '@main/services/runtime/ProviderConnectionService';
|
import {
|
||||||
|
buildDirectTmuxRestartEnvAssignments,
|
||||||
|
TeamProvisioningService,
|
||||||
|
} from '@main/services/team/TeamProvisioningService';
|
||||||
import { spawnCli } from '@main/utils/childProcess';
|
import { spawnCli } from '@main/utils/childProcess';
|
||||||
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
|
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
|
||||||
|
|
||||||
|
|
@ -667,7 +666,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
facts,
|
facts,
|
||||||
})
|
})
|
||||||
).toThrow('does not support it in the current runtime');
|
).toThrow('does not support Anthropic effort "low" in the current runtime');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
@ -2394,6 +2393,81 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
expect(spawnProbe).not.toHaveBeenCalled();
|
expect(spawnProbe).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows selected Anthropic effort checks when model catalog is missing but model is known', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
||||||
|
env: {
|
||||||
|
PATH: '/usr/bin',
|
||||||
|
SHELL: '/bin/zsh',
|
||||||
|
},
|
||||||
|
authSource: 'none',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: [],
|
||||||
|
});
|
||||||
|
vi.spyOn(svc as any, 'readRuntimeProviderLaunchFacts').mockResolvedValue({
|
||||||
|
defaultModel: null,
|
||||||
|
modelIds: new Set(['claude-opus-4-6[1m]']),
|
||||||
|
modelCatalog: null,
|
||||||
|
runtimeCapabilities: null,
|
||||||
|
providerStatus: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await (svc as any).verifySelectedProviderModels({
|
||||||
|
claudePath: '/fake/claude',
|
||||||
|
cwd: tempRoot,
|
||||||
|
providerId: 'anthropic',
|
||||||
|
modelIds: ['claude-opus-4-6[1m]'],
|
||||||
|
modelChecks: [{ modelId: 'claude-opus-4-6[1m]', effort: 'medium' }],
|
||||||
|
limitContext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.details).toEqual([
|
||||||
|
'Selected model claude-opus-4-6[1m] is available for launch.',
|
||||||
|
]);
|
||||||
|
expect(result.blockingMessages).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blocks selected Anthropic effort checks when model catalog cannot verify an unknown model', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
||||||
|
env: {
|
||||||
|
PATH: '/usr/bin',
|
||||||
|
SHELL: '/bin/zsh',
|
||||||
|
},
|
||||||
|
authSource: 'none',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: [],
|
||||||
|
});
|
||||||
|
vi.spyOn(svc as any, 'readRuntimeProviderLaunchFacts').mockResolvedValue({
|
||||||
|
defaultModel: null,
|
||||||
|
modelIds: new Set(['claude-experimental-5']),
|
||||||
|
modelCatalog: null,
|
||||||
|
runtimeCapabilities: null,
|
||||||
|
providerStatus: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await (svc as any).verifySelectedProviderModels({
|
||||||
|
claudePath: '/fake/claude',
|
||||||
|
cwd: tempRoot,
|
||||||
|
providerId: 'anthropic',
|
||||||
|
modelIds: ['claude-experimental-5'],
|
||||||
|
modelChecks: [{ modelId: 'claude-experimental-5', effort: 'medium' }],
|
||||||
|
limitContext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.details).toEqual([]);
|
||||||
|
expect(result.blockingMessages).toEqual([
|
||||||
|
'Selected model claude-experimental-5 is unavailable. Anthropic runtime catalog was unavailable, so effort "medium" for claude-experimental-5 could not be verified.',
|
||||||
|
]);
|
||||||
|
expect(result.issues).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
providerId: 'anthropic',
|
||||||
|
modelId: 'claude-experimental-5',
|
||||||
|
code: 'effort_unverified',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('augments dynamic Codex compatibility checks with the app-server catalog', async () => {
|
it('augments dynamic Codex compatibility checks with the app-server catalog', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
||||||
|
|
@ -3422,7 +3496,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
facts,
|
facts,
|
||||||
})
|
})
|
||||||
).toThrow('does not support it in the current runtime');
|
).toThrow('does not support Anthropic effort "max" in the current runtime');
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
(svc as any).validateRuntimeLaunchSelection({
|
(svc as any).validateRuntimeLaunchSelection({
|
||||||
|
|
@ -3436,6 +3510,75 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
).toThrow('enables Anthropic Fast mode');
|
).toThrow('enables Anthropic Fast mode');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows known Anthropic effort when runtime catalog is unavailable', () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const facts = {
|
||||||
|
defaultModel: null,
|
||||||
|
modelIds: new Set<string>(),
|
||||||
|
modelCatalog: null,
|
||||||
|
runtimeCapabilities: {
|
||||||
|
reasoningEffort: {
|
||||||
|
supported: true,
|
||||||
|
values: ['low', 'medium', 'high', 'max'],
|
||||||
|
configPassthrough: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
(svc as any).validateRuntimeLaunchSelection({
|
||||||
|
actorLabel: 'Team lead',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-6[1m]',
|
||||||
|
effort: 'medium',
|
||||||
|
limitContext: false,
|
||||||
|
facts,
|
||||||
|
})
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows known Anthropic effort when catalog is missing and model list only exposes the base launch id', () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const facts = {
|
||||||
|
defaultModel: null,
|
||||||
|
modelIds: new Set(['claude-opus-4-6']),
|
||||||
|
modelCatalog: null,
|
||||||
|
runtimeCapabilities: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
(svc as any).validateRuntimeLaunchSelection({
|
||||||
|
actorLabel: 'Team lead',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-6[1m]',
|
||||||
|
effort: 'medium',
|
||||||
|
limitContext: false,
|
||||||
|
facts,
|
||||||
|
})
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports unknown Anthropic effort support as unverified when runtime catalog is unavailable', () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const facts = {
|
||||||
|
defaultModel: null,
|
||||||
|
modelIds: new Set<string>(),
|
||||||
|
modelCatalog: null,
|
||||||
|
runtimeCapabilities: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
(svc as any).validateRuntimeLaunchSelection({
|
||||||
|
actorLabel: 'Team lead',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-experimental-5',
|
||||||
|
effort: 'medium',
|
||||||
|
limitContext: false,
|
||||||
|
facts,
|
||||||
|
})
|
||||||
|
).toThrow('could not be verified');
|
||||||
|
});
|
||||||
|
|
||||||
it('emits a lead-message refresh after provisioning reaches ready', async () => {
|
it('emits a lead-message refresh after provisioning reaches ready', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
const emitter = vi.fn();
|
const emitter = vi.fn();
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@ function extractBootstrapSpec(callIndex = 0): {
|
||||||
team?: { name?: string; cwd?: string };
|
team?: { name?: string; cwd?: string };
|
||||||
lead?: { permissionSeedTools?: string[] };
|
lead?: { permissionSeedTools?: string[] };
|
||||||
members?: Array<Record<string, unknown>>;
|
members?: Array<Record<string, unknown>>;
|
||||||
|
launch?: { bootstrapTimeoutMs?: number; continueOnPartialFailure?: boolean };
|
||||||
} {
|
} {
|
||||||
const args = vi.mocked(spawnCli).mock.calls[callIndex]?.[1] as string[] | undefined;
|
const args = vi.mocked(spawnCli).mock.calls[callIndex]?.[1] as string[] | undefined;
|
||||||
const specFlagIndex = args?.indexOf('--team-bootstrap-spec') ?? -1;
|
const specFlagIndex = args?.indexOf('--team-bootstrap-spec') ?? -1;
|
||||||
|
|
@ -158,6 +159,7 @@ function extractBootstrapSpec(callIndex = 0): {
|
||||||
team?: { name?: string; cwd?: string };
|
team?: { name?: string; cwd?: string };
|
||||||
lead?: { permissionSeedTools?: string[] };
|
lead?: { permissionSeedTools?: string[] };
|
||||||
members?: Array<Record<string, unknown>>;
|
members?: Array<Record<string, unknown>>;
|
||||||
|
launch?: { bootstrapTimeoutMs?: number; continueOnPartialFailure?: boolean };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,10 +350,61 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
expect(bootstrapSpec.launch).toMatchObject({
|
||||||
|
bootstrapTimeoutMs: 120_000,
|
||||||
|
continueOnPartialFailure: true,
|
||||||
|
});
|
||||||
|
|
||||||
await svc.cancelProvisioning(runId);
|
await svc.cancelProvisioning(runId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('createTeam scales deterministic bootstrap timeout with member count', async () => {
|
||||||
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude');
|
||||||
|
const { child } = createFakeChild();
|
||||||
|
vi.mocked(spawnCli).mockReturnValue(child as any);
|
||||||
|
const setTimeoutSpy = vi.spyOn(global, 'setTimeout');
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
(svc as any).buildProvisioningEnv = vi.fn(async () => ({
|
||||||
|
env: { ANTHROPIC_API_KEY: 'test' },
|
||||||
|
authSource: 'anthropic_api_key',
|
||||||
|
}));
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||||
|
(svc as any).startFilesystemMonitor = vi.fn();
|
||||||
|
(svc as any).pathExists = vi.fn(async () => false);
|
||||||
|
|
||||||
|
let runId: string | undefined;
|
||||||
|
try {
|
||||||
|
const created = await svc.createTeam(
|
||||||
|
{
|
||||||
|
teamName: 'large-team',
|
||||||
|
cwd: process.cwd(),
|
||||||
|
members: [
|
||||||
|
{ name: 'alice' },
|
||||||
|
{ name: 'atlas' },
|
||||||
|
{ name: 'bob' },
|
||||||
|
{ name: 'jack' },
|
||||||
|
{ name: 'tom' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
runId = created.runId;
|
||||||
|
|
||||||
|
expect(extractBootstrapSpec().launch).toMatchObject({
|
||||||
|
bootstrapTimeoutMs: 300_000,
|
||||||
|
continueOnPartialFailure: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setTimeoutSpy.mock.calls.some((call) => call[1] === 330_000)).toBe(true);
|
||||||
|
} finally {
|
||||||
|
if (runId) {
|
||||||
|
await svc.cancelProvisioning(runId);
|
||||||
|
}
|
||||||
|
setTimeoutSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('createTeam bootstrap spec includes worktree isolation only for selected teammates', async () => {
|
it('createTeam bootstrap spec includes worktree isolation only for selected teammates', async () => {
|
||||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude');
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude');
|
||||||
const { child } = createFakeChild();
|
const { child } = createFakeChild();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ import { getClaudeBasePath, getTeamsBasePath } from '../../../../src/main/utils/
|
||||||
import type { HttpServices } from '../../../../src/main/http';
|
import type { HttpServices } from '../../../../src/main/http';
|
||||||
import type { TaskRef } from '../../../../src/shared/types';
|
import type { TaskRef } from '../../../../src/shared/types';
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli';
|
const DEFAULT_ORCHESTRATOR_CLI =
|
||||||
|
'/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
|
||||||
|
|
||||||
export interface InboxMessage {
|
export interface InboxMessage {
|
||||||
from?: string;
|
from?: string;
|
||||||
|
|
|
||||||
|
|
@ -546,6 +546,44 @@ describe('cli child process helpers', () => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('kills a POSIX launcher, Bun child, and nested shell on execFile timeout', async () => {
|
||||||
|
setPlatform('darwin');
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const execFileMock = child.execFile as unknown as Mock;
|
||||||
|
const spawnSyncMock = child.spawnSync as unknown as Mock;
|
||||||
|
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
|
||||||
|
const childProcess = new EventEmitter() as EventEmitter & {
|
||||||
|
pid: number;
|
||||||
|
stdout: EventEmitter;
|
||||||
|
stderr: EventEmitter;
|
||||||
|
};
|
||||||
|
childProcess.pid = 500;
|
||||||
|
childProcess.stdout = new EventEmitter();
|
||||||
|
childProcess.stderr = new EventEmitter();
|
||||||
|
spawnSyncMock.mockReturnValue({
|
||||||
|
status: 0,
|
||||||
|
stdout: ['500 1', '501 500', '502 501'].join('\n'),
|
||||||
|
});
|
||||||
|
execFileMock.mockImplementation(() => childProcess);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = execCli('/tmp/cli-dev', ['runtime', 'status', '--json'], { timeout: 100 });
|
||||||
|
const expectation = expect(result).rejects.toMatchObject({
|
||||||
|
killed: true,
|
||||||
|
signal: 'SIGTERM',
|
||||||
|
});
|
||||||
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
|
|
||||||
|
await expectation;
|
||||||
|
expect(killSpy.mock.calls.map(([pid]) => pid)).toEqual(
|
||||||
|
expect.arrayContaining([500, 501, 502])
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
killSpy.mockRestore();
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('killProcessTree', () => {
|
describe('killProcessTree', () => {
|
||||||
|
|
|
||||||
|
|
@ -1896,7 +1896,7 @@ describe('LaunchTeamDialog', () => {
|
||||||
.mocked(runProviderPrepareDiagnostics)
|
.mocked(runProviderPrepareDiagnostics)
|
||||||
.mock.calls.filter((call) => call[0]?.providerId === 'opencode');
|
.mock.calls.filter((call) => call[0]?.providerId === 'opencode');
|
||||||
expect(inFlightOpencodePrepareCalls).toHaveLength(1);
|
expect(inFlightOpencodePrepareCalls).toHaveLength(1);
|
||||||
expect(host.textContent).toContain('Selected providers are ready.');
|
expect(host.textContent).toContain('All selected providers are ready.');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
@ -2046,7 +2046,7 @@ describe('LaunchTeamDialog', () => {
|
||||||
expect(vi.mocked(runProviderPrepareDiagnostics)).toHaveBeenCalledTimes(
|
expect(vi.mocked(runProviderPrepareDiagnostics)).toHaveBeenCalledTimes(
|
||||||
callsAfterSameSignatureRerender
|
callsAfterSameSignatureRerender
|
||||||
);
|
);
|
||||||
expect(host.textContent).toContain('Selected providers are ready.');
|
expect(host.textContent).toContain('All selected providers are ready.');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
|
||||||
|
|
@ -520,7 +520,7 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
state: 'ready',
|
state: 'ready',
|
||||||
message: 'Selected providers are ready.',
|
message: 'All selected providers are ready.',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildReusableProviderPrepareModelResults,
|
buildReusableProviderPrepareModelResults,
|
||||||
mergeReusableProviderPrepareModelResults,
|
mergeReusableProviderPrepareModelResults,
|
||||||
runProviderPrepareDiagnostics,
|
runProviderPrepareDiagnostics,
|
||||||
} from '@renderer/components/team/dialogs/providerPrepareDiagnostics';
|
} from '@renderer/components/team/dialogs/providerPrepareDiagnostics';
|
||||||
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type { TeamProviderId, TeamProvisioningPrepareResult } from '@shared/types';
|
import type { TeamProviderId, TeamProvisioningPrepareResult } from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -96,6 +95,47 @@ describe('runProviderPrepareDiagnostics', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes selected model effort checks through compatibility preflight', async () => {
|
||||||
|
const prepareProvisioning = vi.fn(async (): Promise<TeamProvisioningPrepareResult> => ({
|
||||||
|
ready: true,
|
||||||
|
message: 'ready',
|
||||||
|
details: ['Selected model claude-opus-4-6[1m] is available for launch.'],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await runProviderPrepareDiagnostics({
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
selectedModelIds: ['claude-opus-4-6[1m]'],
|
||||||
|
selectedModelChecks: [
|
||||||
|
{
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-6[1m]',
|
||||||
|
effort: 'medium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prepareProvisioning,
|
||||||
|
limitContext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.status).toBe('ready');
|
||||||
|
expect(prepareProvisioning).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
'/tmp/project',
|
||||||
|
'anthropic',
|
||||||
|
['anthropic'],
|
||||||
|
['claude-opus-4-6[1m]'],
|
||||||
|
false,
|
||||||
|
'compatibility',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-6[1m]',
|
||||||
|
effort: 'medium',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('removes a stale reusable model result when the latest result is advisory', () => {
|
it('removes a stale reusable model result when the latest result is advisory', () => {
|
||||||
expect(
|
expect(
|
||||||
mergeReusableProviderPrepareModelResults(
|
mergeReusableProviderPrepareModelResults(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildProviderPrepareMembersSignature,
|
buildProviderPrepareMembersSignature,
|
||||||
buildProviderPrepareModelChecksSignature,
|
buildProviderPrepareModelChecksSignature,
|
||||||
buildProviderPrepareRequestSignature,
|
buildProviderPrepareRequestSignature,
|
||||||
buildProviderPrepareRuntimeStatusSignature,
|
buildProviderPrepareRuntimeStatusSignature,
|
||||||
} from '@renderer/components/team/dialogs/providerPrepareRequestSignature';
|
} from '@renderer/components/team/dialogs/providerPrepareRequestSignature';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('providerPrepareRequestSignature', () => {
|
describe('providerPrepareRequestSignature', () => {
|
||||||
it('stays stable for semantically identical provider runtime snapshots', () => {
|
it('stays stable for semantically identical provider runtime snapshots', () => {
|
||||||
|
|
@ -573,4 +572,17 @@ describe('providerPrepareRequestSignature', () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('changes model checks signature when selected effort changes', () => {
|
||||||
|
const medium = buildProviderPrepareModelChecksSignature(
|
||||||
|
new Map([['anthropic', [{ model: 'claude-opus-4-6[1m]', effort: 'medium' }]]])
|
||||||
|
);
|
||||||
|
const high = buildProviderPrepareModelChecksSignature(
|
||||||
|
new Map([['anthropic', [{ model: 'claude-opus-4-6[1m]', effort: 'high' }]]])
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(medium).not.toBe(high);
|
||||||
|
expect(medium).toContain('"effort":"medium"');
|
||||||
|
expect(high).toContain('"effort":"high"');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue