Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ import {
resolveSelectableProvider,
} from "../providerModels";
import { useSettings } from "../hooks/useSettings";
import { getComposerProviderState } from "../composerProviderState";
import { resolveAppModelSelection } from "../modelSelection";
import { isTerminalFocused } from "../lib/terminalFocus";
import {
Expand Down Expand Up @@ -166,7 +167,6 @@ import { ComposerPendingApprovalPanel } from "./chat/ComposerPendingApprovalPane
import { ComposerPendingUserInputPanel } from "./chat/ComposerPendingUserInputPanel";
import { ComposerPlanFollowUpBanner } from "./chat/ComposerPlanFollowUpBanner";
import {
getComposerProviderState,
renderProviderTraitsMenuContent,
renderProviderTraitsPicker,
} from "./chat/composerProviderRegistry";
Expand Down
70 changes: 0 additions & 70 deletions apps/web/src/components/chat/composerProviderRegistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,10 @@ import {
type ServerProviderModel,
type ThreadId,
} from "@t3tools/contracts";
import { isClaudeUltrathinkPrompt, resolveEffort } from "@t3tools/shared/model";
import type { ReactNode } from "react";
import { getProviderModelCapabilities } from "../../providerModels";
import { TraitsMenuContent, TraitsPicker } from "./TraitsPicker";
import {
normalizeClaudeModelOptionsWithCapabilities,
normalizeCodexModelOptionsWithCapabilities,
} from "@t3tools/shared/model";

export type ComposerProviderStateInput = {
provider: ProviderKind;
model: string;
models: ReadonlyArray<ServerProviderModel>;
prompt: string;
modelOptions: ProviderModelOptions | null | undefined;
};

export type ComposerProviderState = {
provider: ProviderKind;
promptEffort: string | null;
modelOptionsForDispatch: ProviderModelOptions[ProviderKind] | undefined;
composerFrameClassName?: string;
composerSurfaceClassName?: string;
modelPickerIconClassName?: string;
};

type ProviderRegistryEntry = {
getState: (input: ComposerProviderStateInput) => ComposerProviderState;
renderTraitsMenuContent: (input: {
threadId: ThreadId;
model: string;
Expand All @@ -50,49 +26,8 @@ type ProviderRegistryEntry = {
}) => ReactNode;
};

function getProviderStateFromCapabilities(
input: ComposerProviderStateInput,
): ComposerProviderState {
const { provider, model, models, prompt, modelOptions } = input;
const caps = getProviderModelCapabilities(models, model, provider);
const providerOptions = modelOptions?.[provider];

// Resolve effort
const rawEffort = providerOptions
? "effort" in providerOptions
? providerOptions.effort
: "reasoningEffort" in providerOptions
? providerOptions.reasoningEffort
: null
: null;

const promptEffort = resolveEffort(caps, rawEffort) ?? null;

// Normalize options for dispatch
const normalizedOptions =
provider === "codex"
? normalizeCodexModelOptionsWithCapabilities(caps, providerOptions)
: normalizeClaudeModelOptionsWithCapabilities(caps, providerOptions);

// Ultrathink styling (driven by capabilities data, not provider identity)
const ultrathinkActive =
caps.promptInjectedEffortLevels.length > 0 && isClaudeUltrathinkPrompt(prompt);

return {
provider,
promptEffort,
modelOptionsForDispatch: normalizedOptions,
...(ultrathinkActive ? { composerFrameClassName: "ultrathink-frame" } : {}),
...(ultrathinkActive
? { composerSurfaceClassName: "shadow-[0_0_0_1px_rgba(255,255,255,0.04)_inset]" }
: {}),
...(ultrathinkActive ? { modelPickerIconClassName: "ultrathink-chroma" } : {}),
};
}

const composerProviderRegistry: Record<ProviderKind, ProviderRegistryEntry> = {
codex: {
getState: (input) => getProviderStateFromCapabilities(input),
renderTraitsMenuContent: ({
threadId,
model,
Expand Down Expand Up @@ -124,7 +59,6 @@ const composerProviderRegistry: Record<ProviderKind, ProviderRegistryEntry> = {
),
},
claudeAgent: {
getState: (input) => getProviderStateFromCapabilities(input),
renderTraitsMenuContent: ({
threadId,
model,
Expand Down Expand Up @@ -157,10 +91,6 @@ const composerProviderRegistry: Record<ProviderKind, ProviderRegistryEntry> = {
},
};

export function getComposerProviderState(input: ComposerProviderStateInput): ComposerProviderState {
return composerProviderRegistry[input.provider].getState(input);
}

export function renderProviderTraitsMenuContent(input: {
provider: ProviderKind;
threadId: ThreadId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { ServerProviderModel } from "@t3tools/contracts";
import { getComposerProviderState } from "./composerProviderRegistry";
import { getComposerProviderState } from "./composerProviderState";

const CODEX_MODELS: ReadonlyArray<ServerProviderModel> = [
{
Expand Down
62 changes: 62 additions & 0 deletions apps/web/src/composerProviderState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
type ProviderKind,
type ProviderModelOptions,
type ServerProviderModel,
} from "@t3tools/contracts";
import {
isClaudeUltrathinkPrompt,
normalizeClaudeModelOptionsWithCapabilities,
normalizeCodexModelOptionsWithCapabilities,
resolveEffort,
} from "@t3tools/shared/model";
import { getProviderModelCapabilities } from "./providerModels";

export type ComposerProviderStateInput = {
provider: ProviderKind;
model: string;
models: ReadonlyArray<ServerProviderModel>;
prompt: string;
modelOptions: ProviderModelOptions | null | undefined;
};

export type ComposerProviderState = {
provider: ProviderKind;
promptEffort: string | null;
modelOptionsForDispatch: ProviderModelOptions[ProviderKind] | undefined;
composerFrameClassName?: string;
composerSurfaceClassName?: string;
modelPickerIconClassName?: string;
};

export function getComposerProviderState(input: ComposerProviderStateInput): ComposerProviderState {
const { provider, model, models, prompt, modelOptions } = input;
const caps = getProviderModelCapabilities(models, model, provider);
const providerOptions = modelOptions?.[provider];

const rawEffort = providerOptions
? "effort" in providerOptions
? providerOptions.effort
: "reasoningEffort" in providerOptions
? providerOptions.reasoningEffort
: null
: null;

const promptEffort = resolveEffort(caps, rawEffort) ?? null;
const modelOptionsForDispatch =
provider === "codex"
? normalizeCodexModelOptionsWithCapabilities(caps, providerOptions)
: normalizeClaudeModelOptionsWithCapabilities(caps, providerOptions);
const ultrathinkActive =
caps.promptInjectedEffortLevels.length > 0 && isClaudeUltrathinkPrompt(prompt);

return {
provider,
promptEffort,
modelOptionsForDispatch,
...(ultrathinkActive ? { composerFrameClassName: "ultrathink-frame" } : {}),
...(ultrathinkActive
? { composerSurfaceClassName: "shadow-[0_0_0_1px_rgba(255,255,255,0.04)_inset]" }
: {}),
...(ultrathinkActive ? { modelPickerIconClassName: "ultrathink-chroma" } : {}),
};
}
2 changes: 1 addition & 1 deletion apps/web/src/modelSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
type ServerProvider,
} from "@t3tools/contracts";
import { normalizeModelSlug, resolveSelectableModel } from "@t3tools/shared/model";
import { getComposerProviderState } from "./components/chat/composerProviderRegistry";
import { UnifiedSettings } from "@t3tools/contracts/settings";
import { getComposerProviderState } from "./composerProviderState";
import {
getDefaultServerModel,
getProviderModels,
Expand Down
60 changes: 60 additions & 0 deletions docs/web-circular-deps-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Web Circular Dependency Cleanup Plan

## Goal

Break the verified web import cycle reported in [`ctx-analysis.md`](../ctx-analysis.md) without changing composer behavior.

## Evidence

### ctx report

- [`ctx-analysis.md`](../ctx-analysis.md) identifies this cycle:
- `apps/web/src/components/chat/TraitsPicker.tsx`
- `apps/web/src/composerDraftStore.ts`
- `apps/web/src/modelSelection.ts`
- `apps/web/src/components/chat/composerProviderRegistry.tsx`

### Live source references

- `apps/web/src/modelSelection.ts:171` resolves settings-backed model selection and currently reaches into the chat registry for provider-state normalization.
- `apps/web/src/components/chat/composerProviderRegistry.tsx:53` contains pure provider-state logic, while `apps/web/src/components/chat/composerProviderRegistry.tsx:93` also owns React rendering for traits controls.
- `apps/web/src/components/chat/TraitsPicker.tsx:163` reads the composer store during render.
- `apps/web/src/composerDraftStore.ts:628` calls back into model selection when deriving effective draft state.

### SCIP checks

The local SCIP index confirms the same edge chain:

```text
tools/scip-query deps apps/web/src/modelSelection.ts
-> apps/web/src/components/chat/composerProviderRegistry.tsx

tools/scip-query deps apps/web/src/components/chat/composerProviderRegistry.tsx
-> apps/web/src/components/chat/TraitsPicker.tsx

tools/scip-query deps apps/web/src/components/chat/TraitsPicker.tsx
-> apps/web/src/composerDraftStore.ts

tools/scip-query deps apps/web/src/composerDraftStore.ts
-> apps/web/src/modelSelection.ts
```

`tools/scip-query refs getComposerProviderState` also shows that the pure provider-state helper is consumed outside the registry, which is the coupling we want to undo.

## Checklist

- [x] Extract the pure provider-state normalization from `apps/web/src/components/chat/composerProviderRegistry.tsx:53` into a non-React helper module that does not import traits UI.
- [x] Update `apps/web/src/modelSelection.ts:171` to use the new pure helper directly.
- [x] Keep `apps/web/src/components/chat/composerProviderRegistry.tsx:93` focused on rendering traits UI and provider-specific composition.
- [x] Move or update the provider-state tests so they exercise the extracted helper directly.
- [x] Re-run graph checks plus `bun fmt`, `bun lint`, and `bun typecheck`, and confirm the web cycle no longer appears.

## Verification

- `bun fmt`
- `bun lint`
- `bun typecheck`
- `mcp__socraticode__codebase_graph_circular`
- Remaining cycle: `packages/contracts/src/model.ts -> packages/contracts/src/orchestration.ts -> packages/contracts/src/model.ts`
- `tools/scip-query deps apps/web/src/modelSelection.ts`
- Now points to `apps/web/src/composerProviderState.ts` instead of `apps/web/src/components/chat/composerProviderRegistry.tsx`
Loading