From ff6de2b411f4b19cb0a7673e5f6852b3dde628d8 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sun, 8 Nov 2026 15:43:19 -0800 Subject: [PATCH 1/4] feat(settings): promote tool call visibility, drop the context meter and legacy sidebar flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tool calls in chat moves out of Labs: Settings → Chat on web and desktop, Appearance on mobile. - The composer context meter and its Legacy setting are gone. Thread details shows context usage. - The legacy sidebar opt-in and its new-thread behavior are gone. The boring sidebar replaces it. Co-Authored-By: Claude Opus 5.5 --- .../settings/DesktopClientSettings.test.ts | 2 - .../SettingsAppearanceRouteScreen.tsx | 2 + .../settings/ThreadSettingsSections.tsx | 42 ---- .../sections/ToolCallsAppearanceSection.tsx | 48 +++++ .../src/features/settings/settings-search.ts | 12 +- apps/web/src/components/ChatView.logic.ts | 18 +- apps/web/src/components/ChatView.tsx | 19 +- .../src/components/Sidebar.browser.test.tsx | 2 - apps/web/src/components/Sidebar.tsx | 7 +- apps/web/src/components/chat/ChatComposer.tsx | 51 +---- .../chat/ComposerFooterControls.tsx | 19 -- .../chat/ContextWindowMeter.logic.ts | 85 --------- .../components/chat/ContextWindowMeter.tsx | 180 ------------------ .../components/chat/ThreadDetailsUsage.tsx | 4 +- ...logic.test.ts => compaction.logic.test.ts} | 52 +---- .../src/components/chat/compaction.logic.ts | 42 ++++ .../components/settings/LabsSettingsPanel.tsx | 55 +----- .../components/settings/SettingsPanels.tsx | 96 ++++++---- .../settings/settingsSearch.test.ts | 2 +- .../src/components/settings/settingsSearch.ts | 19 +- apps/web/src/hooks/useSettings.ts | 7 - apps/web/src/lib/contextWindow.test.ts | 8 +- .../src/shortcuts/chatRouteCommands.test.ts | 2 - apps/web/src/shortcuts/chatRouteCommands.ts | 2 - docs/user/activity-log.md | 3 +- docs/user/composer.md | 9 +- docs/user/providers-acp.md | 6 +- docs/user/providers-claude.md | 7 +- docs/user/providers-pi.md | 2 +- docs/user/thread-sidebar.md | 3 +- packages/contracts/src/settings.ts | 6 - 31 files changed, 188 insertions(+), 624 deletions(-) create mode 100644 apps/mobile/src/features/settings/appearance/sections/ToolCallsAppearanceSection.tsx delete mode 100644 apps/web/src/components/chat/ContextWindowMeter.logic.ts delete mode 100644 apps/web/src/components/chat/ContextWindowMeter.tsx rename apps/web/src/components/chat/{ContextWindowMeter.logic.test.ts => compaction.logic.test.ts} (67%) create mode 100644 apps/web/src/components/chat/compaction.logic.ts diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 0dab1c8d83cc6..7ff1d262857d5 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -34,7 +34,6 @@ const clientSettings: ClientSettings = { confirmThreadArchive: true, confirmThreadDelete: false, confirmThreadUnpin: false, - contextWindowMeterEnabled: false, diffFilesCollapsed: true, diffIgnoreWhitespace: true, diffLayout: "stacked", @@ -67,7 +66,6 @@ const clientSettings: ClientSettings = { boringSidebarMultilineRows: false, threadDetailsUsageEnabled: false, threadDetailsItems: {}, - legacySidebarEnabled: false, sidebarWorkingShelfEnabled: false, loadBalancingEnabled: false, loadBalancingWeights: { "environment-1": 75, "environment-2": 0 }, diff --git a/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx index 8045fb3dd3d4e..717e71362a593 100644 --- a/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx @@ -8,6 +8,7 @@ import { CodeAppearanceSection } from "./appearance/sections/CodeAppearanceSecti import { TerminalAppearanceSection } from "./appearance/sections/TerminalAppearanceSection"; import { TextAppearanceSection } from "./appearance/sections/TextAppearanceSection"; import { ThemeAppearanceSection } from "./appearance/sections/ThemeAppearanceSection"; +import { ToolCallsAppearanceSection } from "./appearance/sections/ToolCallsAppearanceSection"; export function SettingsAppearanceRouteScreen() { const insets = useSafeAreaInsets(); @@ -28,6 +29,7 @@ export function SettingsAppearanceRouteScreen() { + ); diff --git a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx index 6ba284dc513db..85dbee06143a1 100644 --- a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx +++ b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx @@ -22,8 +22,6 @@ import { serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; import { SettingsSection } from "./components/SettingsSection"; import { SettingsSwitchRow } from "./components/SettingsSwitchRow"; -import { ListChoiceRow } from "../../components/ui/ListChoiceRow"; -import type { ToolCallVisibility } from "@t3tools/contracts/settings"; import { SettingsScreen } from "./components/SettingsScreen"; import { planAutoSettleSettingsSync, type AutoSettleSettings } from "./autoSettleSettingsSync"; import { useSettingsEnvironmentFilter } from "./settings-environment-filter"; @@ -45,7 +43,6 @@ export function SettingsLabsRouteScreen() { contentContainerClassName="gap-6 px-5 pt-4" contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 18) + 18 }} > - = [ - { value: "showAll", label: "Show all" }, - { value: "hideSuccessful", label: "Hide successful" }, - { value: "hideAll", label: "Hide all" }, - { value: "zen", label: "Zen" }, -]; - -function ToolCallVisibilitySection() { - const savePreferences = useAtomSet(updateMobilePreferencesAtom); - const preferences = useAtomValue(mobilePreferencesAtom); - const selected = AsyncResult.isSuccess(preferences) - ? (preferences.value.toolCallVisibility ?? "showAll") - : null; - - return ( - - - {TOOL_CALL_VISIBILITY_OPTIONS.map((option, index) => ( - 0} - disabled={selected === null} - onPress={() => savePreferences({ toolCallVisibility: option.value })} - /> - ))} - - - Hide the commands, edits, and searches agents run. Hide successful keeps failures. Zen shows - only final answers. Approvals, questions, and errors always show. - - - ); -} - function LegacySettingsSection() { const savePreferences = useAtomSet(updateMobilePreferencesAtom); const preferences = useAtomValue(mobilePreferencesAtom); diff --git a/apps/mobile/src/features/settings/appearance/sections/ToolCallsAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/ToolCallsAppearanceSection.tsx new file mode 100644 index 0000000000000..aa1c46595d697 --- /dev/null +++ b/apps/mobile/src/features/settings/appearance/sections/ToolCallsAppearanceSection.tsx @@ -0,0 +1,48 @@ +import { useAtomSet, useAtomValue } from "@effect/atom-react"; +import type { ToolCallVisibility } from "@t3tools/contracts/settings"; +import { AsyncResult } from "effect/reactivity"; +import { View } from "react-native"; + +import { AppText as Text } from "../../../../components/ui/AppText"; +import { ListChoiceRow } from "../../../../components/ui/ListChoiceRow"; +import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../../../state/preferences"; +import { SettingsSection } from "../../components/SettingsSection"; + +const TOOL_CALL_VISIBILITY_OPTIONS: ReadonlyArray<{ + readonly value: ToolCallVisibility; + readonly label: string; +}> = [ + { value: "showAll", label: "Show all" }, + { value: "hideSuccessful", label: "Hide successful" }, + { value: "hideAll", label: "Hide all" }, + { value: "zen", label: "Zen" }, +]; + +export function ToolCallsAppearanceSection() { + const savePreferences = useAtomSet(updateMobilePreferencesAtom); + const preferences = useAtomValue(mobilePreferencesAtom); + const selected = AsyncResult.isSuccess(preferences) + ? (preferences.value.toolCallVisibility ?? "showAll") + : null; + + return ( + + + {TOOL_CALL_VISIBILITY_OPTIONS.map((option, index) => ( + 0} + disabled={selected === null} + onPress={() => savePreferences({ toolCallVisibility: option.value })} + /> + ))} + + + Hide the commands, edits, and searches agents run. Hide successful keeps failures. Zen shows + only final answers. Approvals, questions, and errors always show. + + + ); +} diff --git a/apps/mobile/src/features/settings/settings-search.ts b/apps/mobile/src/features/settings/settings-search.ts index b3a2e0b68ab97..dabf15f6a2055 100644 --- a/apps/mobile/src/features/settings/settings-search.ts +++ b/apps/mobile/src/features/settings/settings-search.ts @@ -26,6 +26,12 @@ const SETTINGS_SEARCH_TERMS = { "terminal", "word break", "math", + "tool calls in chat", + "hide tool calls", + "commands edits searches", + "work log", + "zen", + "final answers only", ], SettingsNavigation: ["tabs", "tab order", "reorder", "hide tabs"], SettingsKeyboard: ["return key", "send message", "new line", "hardware keyboard"], @@ -106,12 +112,6 @@ const SETTINGS_SEARCH_TERMS = { "link machine", "fast worktree checkout", "clone cache", - "tool calls in chat", - "hide tool calls", - "commands edits searches", - "work log", - "zen", - "final answers only", ], SettingsEnvironmentMaintenance: ["updates", "provider updates", "restart", "manage environments"], SettingsAbout: [ diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index d4b54a7e5a848..6e410a7e1bd4c 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -15,11 +15,7 @@ import type { SetStateAction } from "react"; import { applyClaudePromptEffortPrefix, resolvePromptInjectedEffort } from "@t3tools/shared/model"; import { getProviderModelCapabilities } from "../providerModels"; import { sanitizeThreadErrorMessage } from "@t3tools/client-runtime/errors"; -import type { - EnvironmentThread, - ThreadStructure, - EnvironmentThreadShell, -} from "@t3tools/client-runtime/state/shell"; +import type { EnvironmentThread, ThreadStructure } from "@t3tools/client-runtime/state/shell"; import { ANTIGRAVITY_DEFAULT_MODEL, PROVIDER_SEND_TURN_MAX_ATTACHMENTS, @@ -873,18 +869,6 @@ function threadHasStarted(thread: Thread | null | undefined): boolean { return Boolean(thread && (thread.latestRun !== null || thread.itemCount > 0 || thread.runtime)); } -export function threadShellHasStarted( - shell: - | Pick - | null - | undefined, -): boolean { - return Boolean( - shell && - (shell.latestRun !== null || shell.latestUserMessageAt !== null || shell.runtime !== null), - ); -} - // Imported history lacks a session until its first prompt; resolve its instance before locking the driver. export function deriveLockedProvider(input: { thread: Thread | null | undefined; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index ff4a642b5c31d..fc0ab9265e4fd 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -318,7 +318,7 @@ import { ComposerBannerStack, type ComposerBannerStackItem } from "./chat/Compos import { modelFallbackBannerItem } from "./chat/ComposerUsageLimits"; import { ComposerSurface } from "./chat/ComposerSurface"; import { resolveThreadSyncPhase } from "../threadSync"; -import { hasAvailableCompactionProvider } from "./chat/ContextWindowMeter.logic"; +import { hasAvailableCompactionProvider } from "./chat/compaction.logic"; import { deriveLatestContextWindowSnapshot, hasDismissedResumeCompaction, @@ -2481,15 +2481,6 @@ export default function ChatView(props: ChatViewProps) { pendingUserInputs.length > 0 || showPlanFollowUpPrompt; const compactDisabled = compactThreadUnavailable; - const compactDisabledReason = compactDisabled - ? !canOperateThread - ? "This connection cannot change threads." - : !activeProject - ? "Choose a project before compacting" - : !manualCompactionProviderAvailable - ? "Compaction is unavailable for this provider" - : "Compacting is unavailable right now" - : null; const resumeCompactionTokens = activeContextWindow && resolveIdleCompaction( @@ -3634,7 +3625,6 @@ export default function ChatView(props: ChatViewProps) { draftId={draftId} activeThreadId={activeThreadId} activeThread={activeThread} - activeThreadShell={activeThreadShell} promptHistoryEntries={timelineEntries} forceExpandedOnMobile={ forceExpandedMobileComposer && isDraftHeroState @@ -3703,9 +3693,6 @@ export default function ChatView(props: ChatViewProps) { respondingRequestIds={respondingRequestIds} showPlanFollowUpPrompt={showPlanFollowUpPrompt} activeProposedPlan={activeProposedPlan} - threadSyncPhase={ - activeEnvironmentUnavailable ? null : threadSyncPhase - } runtimeMode={runtimeMode} defaultRuntimeMode={defaultRuntimeMode} interactionMode={interactionMode} @@ -3718,12 +3705,8 @@ export default function ChatView(props: ChatViewProps) { activeThreadModelSelection={activeThread?.modelSelection} activeProjectId={activeProject?.id ?? null} projectName={activeProject?.title} - contextWindowEntries={serverVisibleTurnItems} - contextWindowLiveUsage={activeThreadLiveTokenUsage} - contextWindowProviderThread={activeProviderThread} compactThreadUnavailable={compactThreadUnavailable} compactDisabled={compactDisabled} - compactDisabledReason={compactDisabledReason} resolvedTheme={resolvedTheme} settings={settings} keybindings={keybindings} diff --git a/apps/web/src/components/Sidebar.browser.test.tsx b/apps/web/src/components/Sidebar.browser.test.tsx index 875a2225d70a1..656012ae8a8e5 100644 --- a/apps/web/src/components/Sidebar.browser.test.tsx +++ b/apps/web/src/components/Sidebar.browser.test.tsx @@ -42,7 +42,6 @@ describe("sidebar header new thread", () => { { mode: "boring", count: 2, shift: false, picker: true }, { mode: "boring", count: 2, shift: true, picker: false }, { mode: "boring", count: 1, shift: false, picker: false }, - { mode: "legacy", count: 2, shift: false, picker: false }, ] as const)("$mode sidebar, $count projects, shift=$shift: picker=$picker", async (scenario) => { using stack = new DisposableStack(); const previousSettings = getClientSettings(); @@ -50,7 +49,6 @@ describe("sidebar header new thread", () => { __setClientSettingsForTests({ ...DEFAULT_CLIENT_SETTINGS, boringSidebarEnabled: scenario.mode === "boring", - legacySidebarEnabled: scenario.mode === "legacy", }); const projects = [project("active"), project("idle")].slice(0, scenario.count); const currentProjectRef = { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 905b28ded8618..d863e285a80e7 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -133,7 +133,7 @@ import { useTerminalFocus } from "../hooks/useTerminalFocus"; import { isCommandPaletteOpen, openCommandPalette } from "../commandPaletteBus"; import { registerSettleAndAdvance } from "../settleAndAdvance"; import { startNewThreadFromContext } from "../lib/chatThreadActions"; -import { useBoringSidebarEnabled, useClientSettings } from "../hooks/useSettings"; +import { useClientSettings } from "../hooks/useSettings"; import { useCopyToClipboard } from "../hooks/useCopyToClipboard"; import { useLocalStorage } from "../hooks/useLocalStorage"; import { useNowMinute } from "../hooks/useNowMinute"; @@ -989,8 +989,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve ); }, }); - const boringSidebarEnabled = useBoringSidebarEnabled(); - const legacySidebarEnabled = useClientSettings((settings) => settings.legacySidebarEnabled); + const boringSidebarEnabled = useClientSettings((settings) => settings.boringSidebarEnabled); const folderRowsMultiline = useClientSettings((settings) => settings.boringSidebarMultilineRows); const newThreadContext = useHandleNewThread(); // Row callbacks capture only the handler; the rest of the context changes with the route. @@ -2954,7 +2953,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve const handleNewThreadClick = (event?: ReactMouseEvent) => { if ( - legacySidebarEnabled || shouldCreateNewThreadInCurrentProject( event?.shiftKey ?? false, scopeGroups.length, @@ -3201,7 +3199,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve newThreadShortcutLabel={newThreadShortcutLabel} newThreadInProjectShortcutLabel={newThreadInProjectShortcutLabel} showNewThreadInProjectHint={ - !legacySidebarEnabled && !shouldCreateNewThreadInCurrentProject( false, scopeGroups.length, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index be106a251592e..89439a62760e6 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -86,7 +86,6 @@ import { deriveComposerSendState, getAntigravitySendBlockReason, resolveComposerInteractionMode, - threadShellHasStarted, } from "../ChatView.logic"; import { dataTransferHasComposerMention, @@ -118,7 +117,6 @@ import { reconcileAttachmentContextReferences, useRetainedContextPayloads, } from "./composerContextUndo"; -import type { ThreadSyncPhase } from "../../threadSync"; import { ComposerBanner } from "./ComposerBanner"; import { ComposerSurface } from "./ComposerSurface"; import { ComposerBannerStack, type ComposerBannerStackItem } from "./ComposerBannerStack"; @@ -215,11 +213,6 @@ import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; import { buildPullRequestReferenceContext } from "../pullRequest/pullRequestDetail.logic"; import { getComposerPromptInjectionState, getComposerReasoningStep } from "./composerProviderState"; import { readPromptUltrathink } from "./TraitsPicker"; -import { - providerSupportsManualCompaction, - resolveContextWindowModelDisplayName, - shouldReserveContextWindowMeter, -} from "./ContextWindowMeter.logic"; import { buildAttachmentVideoPreview, buildExpandedImagePreview, @@ -245,7 +238,7 @@ import { proposedPlanTitle } from "@t3tools/shared/proposedPlanText"; import { hasProviderSetup } from "./ProviderStatusBanner"; import { type AppModelOption, getAppModelOptionsForInstance } from "../../modelSelection"; import type { UnifiedSettings } from "@t3tools/contracts/settings"; -import { isVideoAttachment, type SessionPhase, type Thread, type ThreadShell } from "../../types"; +import { isVideoAttachment, type SessionPhase, type Thread } from "../../types"; import { type ComposerPromptHistoryPosition } from "./composerPromptHistory"; import type { PendingUserInputDraftAnswer } from "@t3tools/client-runtime/state/pending-user-input"; import type { @@ -258,7 +251,6 @@ import { resolveComposerDispatchMode, type ComposerDispatchMode, } from "@t3tools/client-runtime/state/composer-dispatch"; -import { deriveLatestContextWindowSnapshot } from "@t3tools/client-runtime/state/context-window"; import { resolveProviderSkillsForCwd, resolveProviderSlashCommandsForCwd, @@ -369,7 +361,6 @@ export interface ChatComposerProps { activeThreadId: ThreadId | null; activeThread: Thread | undefined; - activeThreadShell: ThreadShell | null; promptHistoryEntries: ReadonlyArray; forceExpandedOnMobile: boolean; projectSelectionRequired: boolean; @@ -420,7 +411,6 @@ export interface ChatComposerProps { showPlanFollowUpPrompt: boolean; activeProposedPlan: LatestProposedPlanState | null; - threadSyncPhase: ThreadSyncPhase | null; runtimeMode: RuntimeMode; defaultRuntimeMode?: RuntimeMode; @@ -435,12 +425,8 @@ export interface ChatComposerProps { activeProjectId: ProjectId | null; projectName?: string | undefined; - contextWindowEntries: Parameters[0]; - contextWindowLiveUsage: Parameters[1]; - contextWindowProviderThread: Parameters[2]; compactThreadUnavailable: boolean; compactDisabled: boolean; - compactDisabledReason: string | null; resolvedTheme: "light" | "dark"; settings: UnifiedSettings; @@ -562,12 +548,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) reportedModelSelection, activeProjectId, projectName, - contextWindowEntries, - contextWindowLiveUsage, - contextWindowProviderThread, compactThreadUnavailable, compactDisabled, - compactDisabledReason, resolvedTheme, settings, keybindings, @@ -609,11 +591,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) editingQueuedAttachments, onRemoveEditingQueuedAttachment, } = props; - const activeContextWindow = deriveLatestContextWindowSnapshot( - contextWindowEntries, - contextWindowLiveUsage, - contextWindowProviderThread, - ); const primaryEnvironmentId = usePrimaryEnvironmentId(); const composerDraftTargetKey = composerTargetKey(composerDraftTarget); const { @@ -920,8 +897,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ? providerInstanceEntries.find((entry) => hasProviderSetup(entry.snapshot))?.instanceId : undefined)) : undefined; - const resolvedCompactDisabledReason = - compactDisabledReason ?? (noProviderAvailable ? "Compacting is unavailable right now" : null); const { modes: compatibleRuntimeModes, mode: compatibleRuntimeMode } = resolveCompatibleRuntimeMode( runtimeMode, @@ -954,7 +929,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) (multipleModelSelections === null ? providerSendBlockReason : null))); const isSendDisabled = sendDisabledReason !== null; const selectedProviderStatus = selectedProviderEntry?.snapshot ?? null; - const compactCommandAvailable = providerSupportsManualCompaction(selectedProviderEntry); const selectedProviderSkills = selectedProviderStatus ? resolveProviderSkillsForCwd(selectedProviderStatus, gitCwd) : NO_PROVIDER_SKILLS; @@ -1000,19 +974,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) : (normalizeModelSlug(selectedModelForPicker, selectedProvider) ?? selectedModelForPicker); })(); - const activeThreadModelDisplayName = resolveContextWindowModelDisplayName( - activeThreadModelSelection, - modelOptionsByInstance, - ); - const reserveContextWindowMeter = shouldReserveContextWindowMeter({ - meterEnabled: settings.contextWindowMeterEnabled, - detailLoading: props.threadSyncPhase === "loading", - threadStarted: threadShellHasStarted(props.activeThreadShell), - providerReportsContextWindow: selectedProviderStatus - ? selectedProviderStatus.reportsContextWindow === true - : null, - }); - const [composerCursor, setComposerCursor] = useState(() => { const value = isLiteralPendingAnswer ? (activePendingProgress?.customAnswer ?? prompt) : prompt; return collapseExpandedComposerCursor(value, value.length, isLiteralPendingAnswer); @@ -3623,11 +3584,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) {speechInput.available ? ( <> diff --git a/apps/web/src/components/chat/ComposerFooterControls.tsx b/apps/web/src/components/chat/ComposerFooterControls.tsx index 03f11e8bbbf9f..c82278f455e25 100644 --- a/apps/web/src/components/chat/ComposerFooterControls.tsx +++ b/apps/web/src/components/chat/ComposerFooterControls.tsx @@ -8,14 +8,12 @@ import { createModelSelection } from "@t3tools/shared/model"; import { BotIcon, PencilRulerIcon } from "lucide-react"; import { type ComponentProps, memo, useId, useRef, useState } from "react"; -import type { ContextWindowSnapshot } from "@t3tools/client-runtime/state/context-window"; import { Menu as MenuPrimitive } from "@base-ui/react/menu"; import { Menu, MenuPopup, MenuTrigger } from "../ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { ComposerControl, ComposerControlIcon } from "./ComposerControl"; import { focusRequestedPermission, useComposerMenuProps } from "./composerEventScope"; import { ComposerPrimaryActions } from "./ComposerPrimaryActions"; -import { ContextWindowMeter, ContextWindowMeterPlaceholder } from "./ContextWindowMeter"; import { resolveModelPickerSelectedModel } from "./ModelPickerContent"; import { ProviderModelPicker } from "./ProviderModelPicker"; import { runtimeModeConfig } from "./runtimeModeConfig"; @@ -249,9 +247,6 @@ export const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryA children?: React.ReactNode; compact: boolean; canOperateThread: boolean; - activeContextWindow: ContextWindowSnapshot | null; - reserveContextWindowMeter: boolean; - activeThreadModelDisplayName: string | null; isPreparingWorktree: boolean; pendingAction: { questionIndex: number; @@ -280,26 +275,12 @@ export const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryA onPreviousPendingQuestion: () => void; onInterrupt: () => void; onImplementPlanInNewThread: () => void; - onCompactContext?: (() => void) | undefined; - compactDisabled: boolean; - compactDisabledReason: string | null; compactBeforeSendTokens: number | null; keepFullHistory: boolean; onToggleKeepFullHistory: () => void; }) { return ( <> - {props.activeContextWindow ? ( - - ) : props.reserveContextWindowMeter ? ( - - ) : null} command.name === "compact") ?? false; -} - -export function hasAvailableCompactionProvider(input: { - readonly providers: ReadonlyArray; - readonly driverKind: ProviderDriverKind; - readonly instanceId: ProviderInstanceId | null; - readonly lockedInstanceId: ProviderInstanceId | null; -}): boolean { - const driverProviders = input.providers.filter( - (provider) => provider.driverKind === input.driverKind, - ); - const lockedContinuationGroupKey = input.lockedInstanceId - ? driverProviders.find((provider) => provider.instanceId === input.lockedInstanceId) - ?.continuationGroupKey - : undefined; - const compatibleProviders = lockedContinuationGroupKey - ? driverProviders.filter( - (provider) => provider.continuationGroupKey === lockedContinuationGroupKey, - ) - : driverProviders; - - return providerSupportsManualCompaction( - resolveSelectableProviderInstanceEntry(compatibleProviders, input.instanceId ?? undefined), - ); -} - -export function resolveContextWindowModelDisplayName( - selection: ModelSelection | null | undefined, - modelOptionsByInstance: ReadonlyMap>, -): string | null { - if (!selection) { - return null; - } - - const selectedModel = modelOptionsByInstance - .get(selection.instanceId) - ?.find((model) => model.slug === selection.model); - - return selectedModel ? getDisplayModelName(selectedModel) : selection.model; -} - -export function formatContextWindowCompactionMessage( - modelDisplayName: string | null | undefined, - autoCompactThreshold?: number | null, -): string { - if (typeof autoCompactThreshold === "number" && autoCompactThreshold > 0) { - return `Compacts automatically at about ${autoCompactThreshold.toLocaleString("en-US")} tokens.`; - } - return modelDisplayName - ? `Context for ${modelDisplayName} compacts automatically when needed.` - : "Context compacts automatically when needed."; -} - -export function shouldReserveContextWindowMeter(input: { - readonly meterEnabled: boolean; - readonly detailLoading: boolean; - readonly threadStarted: boolean; - readonly providerReportsContextWindow: boolean | null; -}): boolean { - return ( - input.meterEnabled && - input.detailLoading && - input.threadStarted && - input.providerReportsContextWindow !== false - ); -} - -export function formatContextWindowCost(cost: { - readonly amount: number; - readonly currency: string; -}): string { - const fractionDigits = Math.abs(cost.amount) > 0 && Math.abs(cost.amount) < 0.01 ? 4 : 2; - return `${cost.currency} ${cost.amount.toFixed(fractionDigits)}`; -} diff --git a/apps/web/src/components/chat/ContextWindowMeter.tsx b/apps/web/src/components/chat/ContextWindowMeter.tsx deleted file mode 100644 index 9f2c2dbc91863..0000000000000 --- a/apps/web/src/components/chat/ContextWindowMeter.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { Button } from "../ui/button"; -import type { ContextWindowSnapshot } from "@t3tools/client-runtime/state/context-window"; -import { formatContextWindowTokens } from "~/lib/contextWindow"; -import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; -import { - formatContextWindowCompactionMessage, - formatContextWindowCost, -} from "./ContextWindowMeter.logic"; -import { Minimize2Icon } from "lucide-react"; -import { composerFloatingLayerProps } from "./composerEventScope"; - -function formatPercentage(value: number | null): string | null { - if (value === null || !Number.isFinite(value)) { - return null; - } - if (value < 10) { - return `${value.toFixed(1).replace(/\.0$/, "")}%`; - } - return `${Math.round(value)}%`; -} - -export function ContextWindowMeter(props: { - usage: ContextWindowSnapshot; - modelDisplayName?: string | null; - onCompact?: (() => void) | undefined; - compactDisabled?: boolean | undefined; - compactDisabledReason?: string | null | undefined; -}) { - const { usage, modelDisplayName, onCompact, compactDisabled, compactDisabledReason } = props; - const usedPercentage = formatPercentage(usage.usedPercentage); - const normalizedPercentage = Math.max(0, Math.min(100, usage.usedPercentage ?? 0)); - const radius = 9.75; - const circumference = 2 * Math.PI * radius; - const dashOffset = circumference * (1 - normalizedPercentage / 100); - const totalProcessedTokens = usage.totalProcessedTokens ?? null; - const showTotalProcessed = totalProcessedTokens !== null && totalProcessedTokens > 0; - const isOverloaded = normalizedPercentage > 90; - const usageColor = isOverloaded - ? "var(--color-error)" - : "color-mix(in oklab, var(--color-muted-foreground) 72%, transparent)"; - - return ( - - - - - - - } - /> - -
-
-
Context Window
- {usage.maxTokens !== null && usedPercentage ? ( -
- {usedPercentage} - · - - {formatContextWindowTokens(usage.usedTokens)}/ - {formatContextWindowTokens(usage.maxTokens ?? null)} - -
- ) : ( -
- {formatContextWindowTokens(usage.usedTokens)} -
- )} -
- {usage.maxTokens !== null ? ( -
-
-
- ) : null} - {showTotalProcessed ? ( -
- Total processed - - {formatContextWindowTokens(totalProcessedTokens)} - -
- ) : null} - {usage.cost != null ? ( -
- Cost - - {formatContextWindowCost(usage.cost)} - -
- ) : null} - {usage.compactsAutomatically ? ( -
- {formatContextWindowCompactionMessage(modelDisplayName, usage.autoCompactThreshold)} -
- ) : null} - {onCompact ? ( - <> - - {compactDisabled && compactDisabledReason ? ( -
- {compactDisabledReason} -
- ) : null} - - ) : null} -
- - - ); -} - -export function ContextWindowMeterPlaceholder() { - return
- } - /> - ); -} - /** Signed out of T3 Connect, the row offers to sign in instead of its switch. */ function GuestLinksRowWithSignIn() { const { isSignedIn } = useAuth(); diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index c53ca8a2dc71a..ffdd4784c0eae 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -22,6 +22,7 @@ import { type ResponseStreamingMode, type QuitConfirmationMode, SidebarProjectSortOrder, + ToolCallVisibility, } from "@t3tools/contracts/settings"; import { desktopUpdateChannelOf } from "@t3tools/shared/cliRelease"; import { createModelSelection } from "@t3tools/shared/model"; @@ -528,8 +529,8 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.followUpBehavior !== DEFAULT_UNIFIED_SETTINGS.followUpBehavior ? ["Follow-up behavior"] : []), - ...(settings.contextWindowMeterEnabled !== DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled - ? ["Context window indicator"] + ...(settings.toolCallVisibility !== DEFAULT_UNIFIED_SETTINGS.toolCallVisibility + ? ["Tool calls in chat"] : []), ...(settings.responseStreamingMode !== DEFAULT_UNIFIED_SETTINGS.responseStreamingMode ? ["Response streaming"] @@ -645,7 +646,7 @@ export function useSettingsRestore(onRestored?: () => void) { composerRichTextEnabled: DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled, sendShortcut: DEFAULT_UNIFIED_SETTINGS.sendShortcut, followUpBehavior: DEFAULT_UNIFIED_SETTINGS.followUpBehavior, - contextWindowMeterEnabled: DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled, + toolCallVisibility: DEFAULT_UNIFIED_SETTINGS.toolCallVisibility, environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode, glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, panelAnimationDurationMs: DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs, @@ -746,42 +747,6 @@ function AutoSettleDaysInput({ ); } -export function LegacyFeaturesSection() { - const settings = useScopedSettings(); - const updateSettings = useUpdateScopedSettings(); - - return ( - - { - updateSettings({ planModeEnabled: Boolean(checked) }); - }} - aria-label="Plan mode (legacy)" - /> - } - /> - - updateSettings({ contextWindowMeterEnabled: Boolean(checked) }) - } - aria-label="Context window indicator (legacy)" - /> - } - /> - - ); -} - export function GeneralSettingsPanel() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); @@ -1233,10 +1198,63 @@ export function ChatSettingsPanel() { } /> + + + + ); } +const TOOL_CALL_VISIBILITY_LABELS: Record = { + showAll: "Show all", + hideSuccessful: "Hide successful", + hideAll: "Hide all", + zen: "Zen", +}; + +const isToolCallVisibility = Schema.is(ToolCallVisibility); + +function ToolCallVisibilityRow() { + const settings = useScopedSettings(); + const updateSettings = useUpdateScopedSettings(); + return ( + + updateSettings({ toolCallVisibility: DEFAULT_UNIFIED_SETTINGS.toolCallVisibility }) + } + /> + ) : null + } + control={ + + } + /> + ); +} + function DiffColorsSettingsRow() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); diff --git a/apps/web/src/components/settings/settingsSearch.test.ts b/apps/web/src/components/settings/settingsSearch.test.ts index 77857947790da..6f62a0fa78693 100644 --- a/apps/web/src/components/settings/settingsSearch.test.ts +++ b/apps/web/src/components/settings/settingsSearch.test.ts @@ -235,7 +235,7 @@ describe("settings search targets", () => { for (const scope of ["all", "environment", "project"] as const) { expect(isSettingsSearchScopeAvailable("project-defaults", scope)).toBe(true); } - for (const id of ["legacy-plan-mode", "legacy-context-window-indicator", "boring-sidebar"]) { + for (const id of ["tool-call-visibility", "boring-sidebar"]) { expect(getSettingsSearchTargetScope(id)!.scope).toBeNull(); } }); diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index c2e099d54c667..8ff910afd7814 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -460,7 +460,7 @@ export const SETTINGS_SEARCH_ITEMS = [ id: "boring-sidebar", title: "Boring sidebar (group by project)", to: "/settings/labs", - searchTerms: ["beta folders project tree legacy old classic sidebar"], + searchTerms: ["beta folders project tree classic sidebar"], }, { id: "boring-sidebar-multiline", @@ -471,9 +471,9 @@ export const SETTINGS_SEARCH_ITEMS = [ { id: "tool-call-visibility", title: "Tool calls in chat", - to: "/settings/labs", + to: "/settings/chat", searchTerms: [ - "beta hide tool calls commands edits searches work log activity quiet chat zen final answers only", + "hide tool calls commands edits searches work log activity quiet chat zen final answers only", ], }, { @@ -869,18 +869,6 @@ export const SETTINGS_SEARCH_ITEMS = [ title: "Open source licenses", to: "/settings/about", }, - { - id: "legacy-plan-mode", - title: "Plan mode (legacy)", - to: "/settings/labs", - searchTerms: ["build plan composer old"], - }, - { - id: "legacy-context-window-indicator", - title: "Context window indicator (legacy)", - to: "/settings/labs", - searchTerms: ["composer meter usage tokens circle old"], - }, { id: "keybindings", title: "Keybindings", @@ -1348,7 +1336,6 @@ const SPLIT_SETTINGS_PATHS: ReadonlySet = new Set([ // Section anchors that moved but are not search results themselves. const MOVED_SECTION_ANCHORS: Readonly> = { "authorized-clients": "/settings/access", - "legacy-features": "/settings/labs", "text-generation": "/settings/threads", about: "/settings/about", }; diff --git a/apps/web/src/hooks/useSettings.ts b/apps/web/src/hooks/useSettings.ts index 43dfe3e045bb7..05b8e66c2c3a0 100644 --- a/apps/web/src/hooks/useSettings.ts +++ b/apps/web/src/hooks/useSettings.ts @@ -415,13 +415,6 @@ export function useEnvironmentIdentificationMode(): EnvironmentIdentificationMod }); } -/** Legacy sidebar opt-ins still count, so those users land in the boring sidebar. */ -export function useBoringSidebarEnabled(): boolean { - return useClientSettings( - (settings) => settings.boringSidebarEnabled || settings.legacySidebarEnabled, - ); -} - export async function prepareModelPreferencesPatch( settings: ServerSettings, patch: ServerSettingsPatch, diff --git a/apps/web/src/lib/contextWindow.test.ts b/apps/web/src/lib/contextWindow.test.ts index 7d49d24a73fe8..f3540d3738596 100644 --- a/apps/web/src/lib/contextWindow.test.ts +++ b/apps/web/src/lib/contextWindow.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vite-plus/test"; import { OrchestrationV2ProviderTurnTokenUsage } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; -import { formatContextWindowCompactionMessage } from "../components/chat/ContextWindowMeter.logic"; +import { formatContextWindowCompactionMessage } from "../components/chat/compaction.logic"; import * as DateTime from "effect/DateTime"; import { ContextTransferId, @@ -67,9 +67,9 @@ describe("live provider-turn usage (#8144)", () => { expect(snapshot?.remainingTokens).toBe(158_000); expect(snapshot?.usedPercentage).toBe(21); expect(snapshot?.autoCompactThreshold).toBe(150_000); - expect( - formatContextWindowCompactionMessage("Claude Sonnet 4.6", snapshot?.autoCompactThreshold), - ).toBe("Compacts automatically at about 150,000 tokens."); + expect(formatContextWindowCompactionMessage(snapshot?.autoCompactThreshold)).toBe( + "Compacts automatically at about 150,000 tokens.", + ); }); }); diff --git a/apps/web/src/shortcuts/chatRouteCommands.test.ts b/apps/web/src/shortcuts/chatRouteCommands.test.ts index 6c9ff18690ec4..7f11ea1135799 100644 --- a/apps/web/src/shortcuts/chatRouteCommands.test.ts +++ b/apps/web/src/shortcuts/chatRouteCommands.test.ts @@ -29,13 +29,11 @@ describe("new thread commands", () => { { mode: "normal", count: 2, command: "chat.new", picker: true }, { mode: "boring", count: 2, command: "chat.new", picker: true }, { mode: "boring", count: 1, command: "chat.new", picker: false }, - { mode: "legacy", count: 2, command: "chat.new", picker: false }, { mode: "boring", count: 2, command: "chat.newLocal", picker: false }, ] as const)("$mode sidebar, $count projects, $command: picker=$picker", (scenario) => { using _settings = vi.spyOn(settings, "getClientSettings").mockReturnValue({ ...DEFAULT_CLIENT_SETTINGS, boringSidebarEnabled: scenario.mode === "boring", - legacySidebarEnabled: scenario.mode === "legacy", }); using _projects = vi .spyOn(entities, "readProjects") diff --git a/apps/web/src/shortcuts/chatRouteCommands.ts b/apps/web/src/shortcuts/chatRouteCommands.ts index cb9474c9a9237..70848449b2efe 100644 --- a/apps/web/src/shortcuts/chatRouteCommands.ts +++ b/apps/web/src/shortcuts/chatRouteCommands.ts @@ -123,10 +123,8 @@ export function installChatRouteCommands(router: AnyRouter): () => void { void scratch.startScratchThread(environmentId); }, "chat.new": (event) => { - const { legacySidebarEnabled } = getClientSettings(); const groups = readProjectGroups(); if ( - !legacySidebarEnabled && !shouldCreateNewThreadInCurrentProject( false, groups.filter((group) => !group.archived).length, diff --git a/docs/user/activity-log.md b/docs/user/activity-log.md index 221e58e006c2f..d4e6e7e32c940 100644 --- a/docs/user/activity-log.md +++ b/docs/user/activity-log.md @@ -20,7 +20,8 @@ were affected. ## Hide tool calls -**Settings → Labs → Tool calls in chat** hides commands, file edits, searches, and MCP tool calls +**Tool calls in chat** in **Settings → Chat** (web and desktop) or **Settings → Appearance** +(mobile) hides commands, file edits, searches, and MCP tool calls from the conversation. **Hide successful** shows a call while it runs and keeps it only if it fails. **Hide all** hides every call and shows **Thinking** while the agent works. **Zen** also hides the agent's in-between messages, so each turn shows your message diff --git a/docs/user/composer.md b/docs/user/composer.md index 22b499ebe7b0c..6eb1e65ebac95 100644 --- a/docs/user/composer.md +++ b/docs/user/composer.md @@ -275,7 +275,7 @@ Provider commands must start the message to run. T3 Code commands such as `/model` and `/plan`, and skill mentions, work on any line. Send `/compact` in an existing conversation to reduce context usage when the -provider supports it, even with the context meter turned off. +provider supports it. Send `/clear` on its own, or pick it from the slash menu, to start a fresh session in the same thread. Your next message goes to a new provider session that doesn't see the @@ -283,10 +283,9 @@ earlier messages, and the thread marks where it began with **New session**. Earlier messages and diffs stay in the thread. Dismiss the notice above the composer to keep the current session instead. -On web and desktop, turn on **Settings → Labs → Legacy → Context -window indicator (legacy)** to see context usage. The meter is off by default -and appears once the provider reports usage. Open it for usage details and -**Compact context** when the provider supports compaction. +On web and desktop, turn on **Context window** in **Settings → Appearance → +Thread details** to see context usage in the thread details card. It appears +once the provider reports usage. Claude instances can also compact idle threads for you. See [compact long conversations](./providers-claude.md#compact-long-conversations). diff --git a/docs/user/providers-acp.md b/docs/user/providers-acp.md index ab660b746cae0..e2af205478a96 100644 --- a/docs/user/providers-acp.md +++ b/docs/user/providers-acp.md @@ -99,9 +99,9 @@ backed by that native session. Importing the same agent session again returns th including when another client performs the import. The thread keeps the native session's title and last-update value as provider metadata without -overwriting a title you set in T3 Code. Agents that report ACP context usage drive the composer's -context meter and cumulative cost metadata. The meter is off by default; see -[commands and skills](./composer.md#commands-and-skills) to enable it on web and desktop. +overwriting a title you set in T3 Code. Agents that report ACP context usage drive the thread details +context window and cumulative cost metadata. See +[commands and skills](./composer.md#commands-and-skills) to show it on web and desktop. Text resources and links render as assistant output; binary resources, images, and audio that T3 Code cannot render yet appear as explicit placeholders instead of disappearing. diff --git a/docs/user/providers-claude.md b/docs/user/providers-claude.md index 1eef74a864261..9cd403743fb2e 100644 --- a/docs/user/providers-claude.md +++ b/docs/user/providers-claude.md @@ -51,10 +51,9 @@ Set **Auto-compact after** in the Claude provider settings to an integer between 300,000 tokens. This changes when compaction happens, not the model's context window. Leave it empty for Claude Code's default. -You can also send `/compact` in an existing conversation. Web and desktop offer -**Compact context** from the context meter. The meter is off by default; see -[commands and skills](./composer.md#commands-and-skills) to enable it. Claude Code -compacts automatically whether the meter is on or off. +You can also send `/compact` in an existing conversation. Claude Code also +compacts automatically. To see context usage, see +[commands and skills](./composer.md#commands-and-skills). When you return to a large thread after Claude's prompt cache expires, a **Compact** chip with the thread's token count shows diff --git a/docs/user/providers-pi.md b/docs/user/providers-pi.md index 92f984b0b7477..18a02b3fc116b 100644 --- a/docs/user/providers-pi.md +++ b/docs/user/providers-pi.md @@ -41,7 +41,7 @@ overriding it. Other models keep their Pi defaults when no level is selected. Th use Pi's native session files for resume, rollback, and forks within the same Pi instance. Forks preserve the native conversation through the selected turn in the destination workspace. Switching providers uses portable conversation context. Extension -dialogs appear in the T3 Code composer, and the composer context meter follows Pi's own usage +dialogs appear in the T3 Code composer, and the thread details context window follows Pi's own usage reporting while a response streams and after it settles. Pi skills appear in the composer's `$` menu. This includes user skills and project skills that Pi diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index 422e4c49a2a03..17542a06df604 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -6,8 +6,7 @@ need a separate branch and working directory. ## Start a thread On web and desktop, **New thread** opens the project picker when you have multiple -projects in the default or project-grouped sidebar. The legacy sidebar always -starts in the current project. **Shift-click** to start in +projects in the default or project-grouped sidebar. **Shift-click** to start in the current project, or use a project's own new-thread action. A new thread carries your model and mode selections, unless the destination project has its own model default. Its branch and workspace mode come from your configured defaults. To continue in diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 61103efc51859..d9ea105a9c4dc 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -502,7 +502,6 @@ export const ClientSettingsSchema = Schema.Struct({ pullRequestLinksOpenInApp: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), - contextWindowMeterEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), composerRichTextEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), sendShortcut: Schema.Literals(["enter", "mod-enter-multiline", "mod-enter"]).pipe( Schema.withDecodingDefault(Effect.succeed("enter")), @@ -526,9 +525,6 @@ export const ClientSettingsSchema = Schema.Struct({ threadDetailsItems: Schema.Record(Schema.String, Schema.Boolean).pipe( Schema.withDecodingDefault(Effect.succeed({})), ), - // Opt-in for the removed legacy sidebar. Still read so those users land in - // the boring sidebar; the boring sidebar toggle clears it. - legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), sidebarWorkingShelfEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)), @@ -1926,7 +1922,6 @@ export const ClientSettingsPatch = Schema.Struct({ ), pullRequestLinksOpenInApp: Schema.optionalKey(Schema.Boolean), planModeEnabled: Schema.optionalKey(Schema.Boolean), - contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean), composerRichTextEnabled: Schema.optionalKey(Schema.Boolean), sendShortcut: Schema.optionalKey(Schema.Literals(["enter", "mod-enter-multiline", "mod-enter"])), followUpBehavior: Schema.optionalKey(Schema.Literals(["queue", "steer"])), @@ -1937,7 +1932,6 @@ export const ClientSettingsPatch = Schema.Struct({ toolCallVisibility: Schema.optionalKey(ToolCallVisibility), threadDetailsUsageEnabled: Schema.optionalKey(Schema.Boolean), threadDetailsItems: Schema.optionalKey(Schema.Record(Schema.String, Schema.Boolean)), - legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), sidebarWorkingShelfEnabled: Schema.optionalKey(Schema.Boolean), sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), sidebarProjectGroupingOverrides: Schema.optionalKey( From dc28f79bec3dc11a5d27d8c893237fda3cefd447 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sun, 8 Nov 2026 16:00:07 -0800 Subject: [PATCH 2/4] feat(clients): remove plan mode from web, desktop, and mobile The Plan mode (legacy) setting is gone. Clients behave as the setting's default "off" did: sends always use build mode, and the Build/Plan toggle, /plan, /default, Shift+Tab, and the plan follow-up and implement flows are removed. Agents can still start plan threads through the server; their plan cards still render, and the next user message moves the thread to build mode. Co-Authored-By: Claude Opus 5.5 --- .../settings/DesktopClientSettings.test.ts | 1 - .../settings/ThreadSettingsSections.tsx | 25 -- .../src/features/settings/settings-search.ts | 2 - .../features/threads/NewTaskDraftScreen.tsx | 4 +- .../src/features/threads/ThreadComposer.tsx | 6 - .../features/threads/ThreadDetailScreen.tsx | 3 - .../features/threads/ThreadRouteScreen.tsx | 1 - .../features/threads/ThreadSettingsSheet.tsx | 2 +- .../threads/new-task-flow-provider.test.tsx | 3 - .../threads/new-task-flow-provider.tsx | 43 +- .../threads/use-composer-command-menu.test.ts | 85 +--- .../threads/use-composer-command-menu.ts | 84 +--- .../threads/use-legacy-plan-mode-enabled.ts | 16 - .../src/persistence/mobile-preferences.ts | 5 - apps/mobile/src/state/legacy-plan-mode.ts | 34 -- apps/mobile/src/state/thread-outbox-model.ts | 12 +- .../src/state/thread-task-permissions.test.ts | 4 - .../src/state/use-thread-composer-state.ts | 49 +-- .../web/src/components/ChatView.logic.test.ts | 56 --- apps/web/src/components/ChatView.logic.ts | 94 +---- apps/web/src/components/ChatView.tsx | 63 +-- apps/web/src/components/chat/ChatComposer.tsx | 96 +---- .../chat/CompactComposerControlsMenu.tsx | 38 +- .../components/chat/ComposerControlsBar.tsx | 17 - .../chat/ComposerFooterControls.tsx | 68 +-- .../chat/ComposerModelEffort.browser.test.tsx | 1 - ...omposerPermissionsSection.browser.test.tsx | 26 +- .../chat/ComposerPlanFollowUpBanner.tsx | 20 - .../chat/ComposerPrimaryActions.tsx | 76 +--- apps/web/src/components/chat/TraitsPicker.tsx | 20 +- apps/web/src/components/chat/chatViewSend.ts | 13 +- .../src/components/chat/composerCommandKey.ts | 9 +- .../chat/composerControlsOwnership.test.ts | 28 +- .../chat/composerControlsOwnership.ts | 8 +- .../chat/composerProviderState.test.tsx | 12 +- .../components/chat/composerProviderState.tsx | 22 +- .../components/chat/useComposerMenuItems.ts | 20 - .../chat/useComposerProviderModel.ts | 1 - .../chat/useComposerResume.browser.test.tsx | 1 - .../src/components/chat/useComposerResume.ts | 7 +- .../components/chat/useComposerSelection.ts | 28 +- .../src/components/chat/useComposerSend.ts | 153 +------ .../components/chat/useMultipleModelSend.ts | 5 +- .../components/chat/useProposedPlanSend.ts | 397 ------------------ .../chat/useQueuedRunEdit.browser.test.tsx | 3 - .../components/chat/useThreadLifecycle.tsx | 6 +- .../pullRequestDetail.logic.test.ts | 22 +- .../settings/ProjectDefaultsSettings.tsx | 2 - .../components/settings/SettingsPanels.tsx | 4 +- .../settings/SourceControlWritingSettings.tsx | 8 +- .../settings/VoiceSettingsPanel.tsx | 1 - .../src/composer-list-continuation.test.ts | 2 +- apps/web/src/composer-logic.test.ts | 4 +- apps/web/src/composer-logic.ts | 14 +- apps/web/src/modelSelection.test.ts | 6 - apps/web/src/modelSelection.ts | 2 +- apps/web/src/proposedPlan.test.ts | 27 -- apps/web/src/proposedPlan.ts | 30 -- apps/web/src/providerModels.ts | 4 +- apps/web/src/session-logic.ts | 67 +-- docs/user/composer.md | 2 +- docs/user/providers-antigravity.md | 3 +- docs/user/providers-fx.md | 1 - docs/user/providers-muse.md | 3 +- docs/user/providers-opencode.md | 2 - packages/contracts/src/settings.ts | 2 - 66 files changed, 146 insertions(+), 1727 deletions(-) delete mode 100644 apps/mobile/src/features/threads/use-legacy-plan-mode-enabled.ts delete mode 100644 apps/mobile/src/state/legacy-plan-mode.ts delete mode 100644 apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx delete mode 100644 apps/web/src/components/chat/useProposedPlanSend.ts delete mode 100644 apps/web/src/proposedPlan.test.ts diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 7ff1d262857d5..1ab9b3159e1f9 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -51,7 +51,6 @@ const clientSettings: ClientSettings = { glassOpacity: 80, onboardingCompletedAt: null, panelAnimationDurationMs: 0, - planModeEnabled: false, proactivePanelsEnabled: true, showSkillsInSlashMenu: false, persistComposerContextStrip: true, diff --git a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx index 85dbee06143a1..41600619b3a47 100644 --- a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx +++ b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx @@ -57,7 +57,6 @@ export function SettingsLabsRouteScreen() { setting="fastWorktreeCheckout" description="On a Mac, clone new worktrees from a checkout cached in the T3 home folder instead of writing every file. Uses Git’s normal checkout when it can’t. Turning it off deletes the cache." /> - ); @@ -300,27 +299,3 @@ function MachineBetaSection({ ); } - -function LegacySettingsSection() { - const savePreferences = useAtomSet(updateMobilePreferencesAtom); - const preferences = useAtomValue(mobilePreferencesAtom); - const planModeEnabled = - AsyncResult.isSuccess(preferences) && preferences.value.planModeEnabled === true; - - return ( - - - savePreferences({ planModeEnabled: value })} - /> - - - Opt into retired interfaces kept for compatibility. Plan Mode restores the Build/Plan - control; otherwise every task runs in Build mode. - - - ); -} diff --git a/apps/mobile/src/features/settings/settings-search.ts b/apps/mobile/src/features/settings/settings-search.ts index dabf15f6a2055..4d02bd9789b1d 100644 --- a/apps/mobile/src/features/settings/settings-search.ts +++ b/apps/mobile/src/features/settings/settings-search.ts @@ -105,8 +105,6 @@ const SETTINGS_SEARCH_TERMS = { "start from remote", ], SettingsLabs: [ - "legacy", - "plan mode", "beta", "linked environments", "link machine", diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 8f02fe677cdd3..a3b02b8985d4d 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -33,6 +33,7 @@ import { KeyboardController } from "react-native-keyboard-controller"; import { AuthOrchestrationOperateScope, + DEFAULT_PROVIDER_INTERACTION_MODE, PROVIDER_SEND_TURN_MAX_ATTACHMENTS, resolveEnvironmentMachineKind, } from "@t3tools/contracts"; @@ -873,7 +874,7 @@ export function NewTaskDraftScreen(props: { threadId: ThreadId.make(creationMetadata.threadId), modelSelection: flow.selectedModel, runtimeMode: flow.runtimeMode, - interactionMode: flow.interactionMode, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, branch: flow.selectedBranchName, worktreePath: flow.selectedWorktreePath, createdAt: creationMetadata.createdAt, @@ -971,7 +972,6 @@ export function NewTaskDraftScreen(props: { offersSideChat={false} onReconnectEnvironment={() => onReconnectEnvironment(selectedProject.environmentId)} canSwitchThreadProvider - onUpdateThreadInteractionMode={flow.planModeEnabled ? flow.setInteractionMode : undefined} onRespondToApproval={async () => {}} onSelectUserInputOption={() => {}} onChangeUserInputCustomAnswer={() => {}} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 57bda9aa98c9b..8987d9fadc9f0 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -21,7 +21,6 @@ import { type EnvironmentId, type MessageId, type ModelSelection, - type ProviderInteractionMode, type ServerConfig as T3ServerConfig, type UsageLimitsReport, } from "@t3tools/contracts"; @@ -253,7 +252,6 @@ export interface ThreadComposerProps { ) => Promise; readonly onShowUsageLimits: (report: UsageLimitsReport | null) => void; readonly canSwitchProvider: boolean; - readonly onUpdateInteractionMode?: (interactionMode: ProviderInteractionMode) => void; readonly onHeightChange?: (height: number) => void; readonly onEditorFocusChange?: (focused: boolean) => void; } @@ -558,10 +556,6 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps hasThread: !props.isDraft, hasCompactableConversation: props.hasCompactableConversation, onChangeDraftMessage: props.onChangeDraftMessage, - onUpdateInteractionMode: - selectedProviderStatus?.showInteractionModeToggle === false - ? undefined - : props.onUpdateInteractionMode, offersUsageLimits: usageLimitsOffered, onUsageLimits: usageLimitsOffered && draftAttachments.length === 0 ? openUsageLimits : undefined, diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 27cce9c5795ba..f3c81cc9b1363 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -41,7 +41,6 @@ import { deriveBottomControlInsets, type NativeLayoutMetrics } from "../../lib/r import type { EnvironmentId, MessageId, - ProviderInteractionMode, RuntimeRequestId, ScopedThreadRef, ServerConfig as T3ServerConfig, @@ -301,7 +300,6 @@ export interface ThreadDetailScreenProps { ) => Promise; readonly onReconnectEnvironment: () => void; readonly canSwitchThreadProvider: boolean; - readonly onUpdateThreadInteractionMode?: (interactionMode: ProviderInteractionMode) => void; readonly onRespondToApproval: ApprovalResponder; readonly onSelectUserInputOption: ( requestId: RuntimeRequestId, @@ -1933,7 +1931,6 @@ function ThreadDetailBody( onSendMessage={handleSendMessage} onShowUsageLimits={showUsageLimits} canSwitchProvider={props.canSwitchThreadProvider} - onUpdateInteractionMode={props.onUpdateThreadInteractionMode} onHeightChange={handleComposerHeightChange} onEditorFocusChange={handleOwnedInputFocusChange} /> diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 80a3e8643a567..365d6d584c292 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1063,7 +1063,6 @@ function ThreadRouteContent( offersSideChat={composer.sideChatUnavailableReason === null} onReconnectEnvironment={handleReconnectEnvironment} canSwitchThreadProvider={composer.canSwitchThreadProvider} - onUpdateThreadInteractionMode={composer.onUpdateInteractionMode} onRespondToApproval={requests.onRespondToApproval} onSelectUserInputOption={requests.onSelectUserInputOption} onChangeUserInputCustomAnswer={requests.onChangeUserInputCustomAnswer} diff --git a/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx b/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx index a505e5b3f39f7..8403be8baaac8 100644 --- a/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx +++ b/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx @@ -1338,7 +1338,7 @@ export function ExistingThreadSettingsRouteScreen({ option.selection.model === modelSelection.model, ) ?? null; const updateModelSelection = (selection: ModelSelection) => - updateThreadDraftModelSelection(threadKey, selection, serverConfig?.providers ?? []); + updateThreadDraftModelSelection(threadKey, selection); return ( ({ useOlderSettledCount: () => 0 vi.mock("../../state/project-grouping", () => ({ useMobileProjectGroupingSettings: () => ({ sidebarProjectGroupingMode: "none" }), })); -vi.mock("./use-legacy-plan-mode-enabled", () => ({ - useLegacyPlanModeState: () => ({ enabled: false, loaded: true }), -})); vi.mock("../../components/ui/Dialog", () => ({ alert: vi.fn() })); vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() })); vi.mock("../../state/use-remote-environment-registry", () => ({ diff --git a/apps/mobile/src/features/threads/new-task-flow-provider.tsx b/apps/mobile/src/features/threads/new-task-flow-provider.tsx index 3876fd2f83dbb..5b42c28adc518 100644 --- a/apps/mobile/src/features/threads/new-task-flow-provider.tsx +++ b/apps/mobile/src/features/threads/new-task-flow-provider.tsx @@ -7,7 +7,6 @@ import React, { useEffect, useRef, useState } from "react"; import type { EnvironmentId, ModelSelection, - ProviderInteractionMode, ProviderOptionSelection, RuntimeMode, ServerProvider, @@ -83,11 +82,6 @@ import { type HomeProjectScope, } from "../home/homeThreadList"; import { useMobileProjectGroupingSettings } from "../../state/project-grouping"; -import { - resolvePendingTaskInteractionMode, - resolveProviderInteractionMode, -} from "../../state/legacy-plan-mode"; -import { useLegacyPlanModeState } from "./use-legacy-plan-mode-enabled"; import { resolveNewTaskDraftWorkspace, resolveNewTaskBranchWorktreePath, @@ -156,8 +150,6 @@ type NewTaskFlowContextValue = { readonly availableBranches: ReadonlyArray; readonly currentCheckoutBranchName: string | null; readonly runtimeMode: RuntimeMode; - readonly interactionMode: ProviderInteractionMode; - readonly planModeEnabled: boolean; readonly expandedProvider: string | null; readonly environments: ReadonlyArray<{ readonly environmentId: EnvironmentId; @@ -209,7 +201,6 @@ type NewTaskFlowContextValue = { readonly setSubmitting: (value: boolean) => void; readonly loadBranches: () => void; readonly setRuntimeMode: (value: RuntimeMode) => void; - readonly setInteractionMode: (value: ProviderInteractionMode) => void; readonly setSelectedModelOptions: ( value: ReadonlyArray | undefined, ) => void; @@ -230,7 +221,6 @@ export function NewTaskFlowProvider( const threads = useThreadShells(); const { savedConnectionsById } = useSavedRemoteConnections(); const groupingSettings = useMobileProjectGroupingSettings(); - const { enabled: legacyPlanModeEnabled } = useLegacyPlanModeState(); const projectScopes = sortHomeProjectScopes({ scopes: buildHomeProjectScopes({ @@ -482,11 +472,6 @@ export function NewTaskFlowProvider( selectedEnvironmentServerConfig?.providers.find( (provider) => provider.instanceId === selectedModel?.instanceId, ) ?? null; - const planModeEnabled = - legacyPlanModeEnabled && selectedProviderStatus?.showInteractionModeToggle !== false; - const interactionMode = planModeEnabled - ? (selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE) - : DEFAULT_PROVIDER_INTERACTION_MODE; const setSelectedModelKey = ( key: string | null, options?: ReadonlyArray, @@ -501,15 +486,7 @@ export function NewTaskFlowProvider( const selection = withRememberedModelOptions( options ? { ...option.selection, options } : option.selection, ); - const provider = selectedEnvironmentServerConfig?.providers.find( - (candidate) => candidate.instanceId === selection.instanceId, - ); - updateComposerDraftSettings(selectedProjectDraftKey, { - modelSelection: selection, - ...(provider?.showInteractionModeToggle === false - ? { interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE } - : {}), - }); + updateComposerDraftSettings(selectedProjectDraftKey, { modelSelection: selection }); setStickyComposerModelSelection(selection); }; const setSelectedModelOptions = (options: ReadonlyArray | undefined) => { @@ -709,13 +686,6 @@ export function NewTaskFlowProvider( updateComposerDraftSettings(selectedProjectDraftKey, { runtimeMode: value }); } }; - const setInteractionMode = (value: ProviderInteractionMode) => { - if (selectedProjectDraftKey) { - updateComposerDraftSettings(selectedProjectDraftKey, { - interactionMode: resolveProviderInteractionMode(selectedProviderStatus, value), - }); - } - }; const buildPendingTaskMessage = ( metadata: TurnCommandMetadata, @@ -761,13 +731,7 @@ export function NewTaskFlowProvider( defaultRuntimeMode, managedPolicy, ), - interactionMode: resolvePendingTaskInteractionMode({ - planModeEnabled: legacyPlanModeEnabled, - draftInteractionMode: draft.interactionMode, - provider: selectedEnvironmentServerConfig?.providers.find( - (candidate) => candidate.instanceId === draftModelSelection.instanceId, - ), - }), + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, creation: { projectId: selectedProject.id, projectTitle: selectedProject.title, @@ -875,8 +839,6 @@ export function NewTaskFlowProvider( availableBranches, currentCheckoutBranchName, runtimeMode, - interactionMode, - planModeEnabled, expandedProvider, environments, selectedProject, @@ -907,7 +869,6 @@ export function NewTaskFlowProvider( setSubmitting: (value: boolean) => updateFlow({ submitting: value }), loadBranches, setRuntimeMode, - setInteractionMode, setSelectedModelOptions, setExpandedProvider: (value: string | null) => updateFlow({ expandedProvider: value }), } satisfies NewTaskFlowContextValue; diff --git a/apps/mobile/src/features/threads/use-composer-command-menu.test.ts b/apps/mobile/src/features/threads/use-composer-command-menu.test.ts index 253c8d9e806ee..74bf278b0a438 100644 --- a/apps/mobile/src/features/threads/use-composer-command-menu.test.ts +++ b/apps/mobile/src/features/threads/use-composer-command-menu.test.ts @@ -62,37 +62,29 @@ describe("resolveComposerMenuSelection", () => { }); describe("mobile slash commands", () => { - const antigravity = { - driver: ProviderDriverKind.make("antigravity"), - showInteractionModeToggle: false, - slashCommands: [{ name: "plan", description: "Plan with Antigravity" }], - }; - - it.each([false, true])( - "keeps native /plan with legacy mode enabled=%s", - (allowInteractionMode) => { - const items = buildComposerSlashCommandItems({ - query: "pl", - atMessageStart: true, - hasThread: true, - allowInteractionMode, - selectedProviderStatus: antigravity, - }); + it("keeps a provider's native /plan", () => { + const items = buildComposerSlashCommandItems({ + query: "pl", + atMessageStart: true, + hasThread: true, + selectedProviderStatus: { + driver: ProviderDriverKind.make("antigravity"), + slashCommands: [{ name: "plan", description: "Plan with Antigravity" }], + }, + }); - expect(items).toHaveLength(1); - expect(items[0]?.type).toBe("provider-slash-command"); - const item = items[0]; - if (!item) throw new Error("Expected the native plan command"); - expect( - resolveComposerCommandSelection({ - draftMessage: "/pl", - trigger: { rangeStart: 0, rangeEnd: 3 }, - item, - allowInteractionMode, - }), - ).toEqual({ text: "/plan ", cursor: 6, interactionMode: null }); - }, - ); + expect(items).toHaveLength(1); + expect(items[0]?.type).toBe("provider-slash-command"); + const item = items[0]; + if (!item) throw new Error("Expected the native plan command"); + expect( + resolveComposerCommandSelection({ + draftMessage: "/pl", + trigger: { rangeStart: 0, rangeEnd: 3 }, + item, + }), + ).toEqual({ text: "/plan ", cursor: 6 }); + }); it("offers T3's /clear in place of the provider's, only at the message start", () => { const claude = { @@ -104,7 +96,6 @@ describe("mobile slash commands", () => { query: "cle", atMessageStart, hasThread: true, - allowInteractionMode: true, offersFreshSession, selectedProviderStatus: claude, }).map((item) => item.id); @@ -112,36 +103,4 @@ describe("mobile slash commands", () => { expect(build(false)).toEqual(["pcmd:clear"]); expect(build(true, false)).toEqual([]); }); - - it("still applies the T3 plan command for supported providers", () => { - const items = buildComposerSlashCommandItems({ - query: "plan", - atMessageStart: true, - hasThread: true, - allowInteractionMode: true, - selectedProviderStatus: { - driver: ProviderDriverKind.make("codex"), - slashCommands: [], - }, - }); - const item = items[0]; - if (!item) throw new Error("Expected the T3 plan command"); - expect( - resolveComposerCommandSelection({ - draftMessage: "/plan", - trigger: { rangeStart: 0, rangeEnd: 5 }, - item, - allowInteractionMode: true, - }), - ).toEqual({ text: "", cursor: 0, interactionMode: "plan" }); - - expect( - resolveComposerCommandSelection({ - draftMessage: "/plan", - trigger: { rangeStart: 0, rangeEnd: 5 }, - item, - allowInteractionMode: false, - }), - ).toEqual({ text: "/plan ", cursor: 6, interactionMode: null }); - }); }); diff --git a/apps/mobile/src/features/threads/use-composer-command-menu.ts b/apps/mobile/src/features/threads/use-composer-command-menu.ts index 1d2fe31a2d420..7ef0885cb6ffd 100644 --- a/apps/mobile/src/features/threads/use-composer-command-menu.ts +++ b/apps/mobile/src/features/threads/use-composer-command-menu.ts @@ -2,7 +2,6 @@ import * as Dialog from "../../components/ui/Dialog"; import type { EnvironmentId, ProjectId, - ProviderInteractionMode, ServerProvider, ThreadId, ThreadPullRequestLink, @@ -107,41 +106,20 @@ export function buildComposerSlashCommandItems(input: { /** T3's `/clear` replaces the provider's, which would clear a session T3 still treats as live. */ readonly offersFreshSession?: boolean; readonly offersSideChat?: boolean; - readonly allowInteractionMode: boolean; - readonly selectedProviderStatus: Pick< - ServerProvider, - "driver" | "slashCommands" | "showInteractionModeToggle" - > | null; + readonly selectedProviderStatus: Pick | null; }): ComposerCommandItem[] { const query = input.query.toLowerCase(); - const allowInteractionMode = - input.allowInteractionMode && input.selectedProviderStatus?.showInteractionModeToggle !== false; - const builtIn = [ - { - id: "cmd:model", - type: "slash-command", - command: "model", - label: "/model", - description: "Switch model", - }, - { - id: "cmd:plan", - type: "slash-command", - command: "plan", - label: "/plan", - description: "Switch to plan mode", - }, - { - id: "cmd:default", - type: "slash-command", - command: "default", - label: "/default", - description: "Switch to default mode", - }, - ] satisfies ComposerCommandItem[]; - const items: ComposerCommandItem[] = builtIn.filter( - (item) => item.command.includes(query) && (item.command === "model" || allowInteractionMode), - ); + const items: ComposerCommandItem[] = "model".includes(query) + ? [ + { + id: "cmd:model", + type: "slash-command", + command: "model", + label: "/model", + description: "Switch model", + }, + ] + : []; // Providers expand commands only at the start of a message. T3 commands // change local state and do not have this restriction, except `/clear`. @@ -199,24 +177,8 @@ export function resolveComposerCommandSelection(input: { readonly draftMessage: string; readonly trigger: Pick; readonly item: ComposerCommandItem; - readonly allowInteractionMode: boolean; -}): { - readonly text: string; - readonly cursor: number; - readonly interactionMode: ProviderInteractionMode | null; -} { +}): { readonly text: string; readonly cursor: number } { const { draftMessage, trigger, item } = input; - if ( - input.allowInteractionMode && - item.type === "slash-command" && - (item.command === "plan" || item.command === "default") - ) { - return { - ...replaceTextRange(draftMessage, trigger.rangeStart, trigger.rangeEnd, ""), - interactionMode: item.command, - }; - } - let replacement = ""; if (item.type === "path") { replacement = `${serializeComposerFileLink(item.path)} `; @@ -227,10 +189,7 @@ export function resolveComposerCommandSelection(input: { } else if (item.type === "provider-slash-command") { replacement = `/${item.command.name} `; } - return { - ...replaceTextRange(draftMessage, trigger.rangeStart, trigger.rangeEnd, replacement), - interactionMode: null, - }; + return replaceTextRange(draftMessage, trigger.rangeStart, trigger.rangeEnd, replacement); } export function useComposerCommandMenu({ @@ -251,7 +210,6 @@ export function useComposerCommandMenu({ offersUsageLimits = false, enabled = true, onChangeDraftMessage, - onUpdateInteractionMode, onUsageLimits, onFreshSession, offersSideChat = false, @@ -274,7 +232,6 @@ export function useComposerCommandMenu({ readonly offersUsageLimits?: boolean; readonly enabled?: boolean; readonly onChangeDraftMessage: (value: string) => void; - readonly onUpdateInteractionMode?: (mode: ProviderInteractionMode) => void; readonly onUsageLimits?: () => void; /** Arms a fresh session for the next message; absent where `/clear` is unavailable. */ readonly onFreshSession?: () => void; @@ -384,7 +341,6 @@ export function useComposerCommandMenu({ offersUsageLimits, offersFreshSession: onFreshSession !== undefined, offersSideChat, - allowInteractionMode: onUpdateInteractionMode !== undefined, selectedProviderStatus: selectedProviderStatus ? { ...selectedProviderStatus, @@ -636,19 +592,9 @@ export function useComposerCommandMenu({ return; } - const result = resolveComposerCommandSelection({ - draftMessage, - trigger, - item, - allowInteractionMode: - onUpdateInteractionMode !== undefined && - selectedProviderStatus?.showInteractionModeToggle !== false, - }); + const result = resolveComposerCommandSelection({ draftMessage, trigger, item }); setSelection({ start: result.cursor, end: result.cursor }); onChangeDraftMessage(result.text); - if (result.interactionMode !== null) { - onUpdateInteractionMode?.(result.interactionMode); - } }; return { diff --git a/apps/mobile/src/features/threads/use-legacy-plan-mode-enabled.ts b/apps/mobile/src/features/threads/use-legacy-plan-mode-enabled.ts deleted file mode 100644 index 694a34e632e3f..0000000000000 --- a/apps/mobile/src/features/threads/use-legacy-plan-mode-enabled.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useAtomValue } from "@effect/atom-react"; -import { AsyncResult } from "effect/reactivity"; - -import { mobilePreferencesAtom } from "../../state/preferences"; -import { resolveLegacyPlanModeEnabled } from "../../state/legacy-plan-mode"; - -export function useLegacyPlanModeState(): { readonly enabled: boolean } { - const preferences = useAtomValue(mobilePreferencesAtom); - const loaded = AsyncResult.isSuccess(preferences); - return { - enabled: resolveLegacyPlanModeEnabled({ - loaded, - preference: loaded ? preferences.value.planModeEnabled : undefined, - }), - }; -} diff --git a/apps/mobile/src/persistence/mobile-preferences.ts b/apps/mobile/src/persistence/mobile-preferences.ts index 436f3b172cdae..6dcfe21548a2c 100644 --- a/apps/mobile/src/persistence/mobile-preferences.ts +++ b/apps/mobile/src/persistence/mobile-preferences.ts @@ -116,7 +116,6 @@ export interface Preferences { readonly microphones?: ReadonlyArray; readonly projectGroupingEnabled?: boolean; readonly projectGroupingMode?: SidebarProjectGroupingMode; - readonly planModeEnabled?: boolean; readonly toolCallVisibility?: ToolCallVisibility; readonly workingShelfEnabled?: boolean; readonly useLastProjectForNewThreads?: boolean; @@ -201,7 +200,6 @@ function sanitizePreferences(parsed: Preferences): Preferences { microphones?: ReadonlyArray; projectGroupingEnabled?: boolean; projectGroupingMode?: SidebarProjectGroupingMode; - planModeEnabled?: boolean; toolCallVisibility?: ToolCallVisibility; workingShelfEnabled?: boolean; useLastProjectForNewThreads?: boolean; @@ -326,9 +324,6 @@ function sanitizePreferences(parsed: Preferences): Preferences { ) { preferences.projectGroupingMode = parsed.projectGroupingMode; } - if (typeof parsed.planModeEnabled === "boolean") { - preferences.planModeEnabled = parsed.planModeEnabled; - } if (isToolCallVisibility(parsed.toolCallVisibility)) { preferences.toolCallVisibility = parsed.toolCallVisibility; } diff --git a/apps/mobile/src/state/legacy-plan-mode.ts b/apps/mobile/src/state/legacy-plan-mode.ts deleted file mode 100644 index f4ab2f8c681c2..0000000000000 --- a/apps/mobile/src/state/legacy-plan-mode.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - DEFAULT_PROVIDER_INTERACTION_MODE, - type ProviderInteractionMode, - type ServerProvider, -} from "@t3tools/contracts"; - -type InteractionModeProvider = Pick; - -export function resolveProviderInteractionMode( - provider: InteractionModeProvider | null | undefined, - interactionMode: ProviderInteractionMode | null | undefined, -): ProviderInteractionMode { - return provider?.showInteractionModeToggle === false - ? DEFAULT_PROVIDER_INTERACTION_MODE - : (interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE); -} - -export function resolveLegacyPlanModeEnabled(input: { - readonly loaded: boolean; - readonly preference: boolean | undefined; -}): boolean { - return input.loaded && input.preference === true; -} - -export function resolvePendingTaskInteractionMode(input: { - readonly planModeEnabled: boolean; - readonly draftInteractionMode: ProviderInteractionMode | undefined; - readonly provider?: InteractionModeProvider | null; -}): ProviderInteractionMode { - if (input.provider?.showInteractionModeToggle === false || !input.planModeEnabled) { - return DEFAULT_PROVIDER_INTERACTION_MODE; - } - return input.draftInteractionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE; -} diff --git a/apps/mobile/src/state/thread-outbox-model.ts b/apps/mobile/src/state/thread-outbox-model.ts index 4796ec19f27ec..32f4a52237726 100644 --- a/apps/mobile/src/state/thread-outbox-model.ts +++ b/apps/mobile/src/state/thread-outbox-model.ts @@ -7,6 +7,7 @@ import type { EnvironmentShellStatus } from "@t3tools/client-runtime/state/shell import type { ThreadFamilyRead } from "@t3tools/client-runtime/state/threads"; import { CommandId, + DEFAULT_PROVIDER_INTERACTION_MODE, EnvironmentId, IsoDateTime, MessageId, @@ -28,7 +29,6 @@ import { DraftComposerAttachmentSchema } from "../lib/composer-image-schema"; import type { ComposerDispatchMode } from "@t3tools/client-runtime/state/composer-dispatch"; import type { DraftComposerAttachment } from "../lib/composerImages"; import { scopedThreadKey } from "../lib/scopedEntities"; -import { resolveProviderInteractionMode } from "./legacy-plan-mode"; // Keep current writes until a compatible native baseline includes the v4 reader. const THREAD_OUTBOX_SCHEMA_VERSION = 3; @@ -105,6 +105,16 @@ export interface ThreadSettingsSnapshot { readonly interactionMode: ProviderInteractionModeType; } +// Providers without a Build/Plan toggle always run in build mode. +function resolveProviderInteractionMode( + provider: Pick | undefined, + interactionMode: ProviderInteractionModeType, +): ProviderInteractionModeType { + return provider?.showInteractionModeToggle === false + ? DEFAULT_PROVIDER_INTERACTION_MODE + : interactionMode; +} + export function resolveQueuedThreadSettings( message: QueuedThreadMessage, thread: ThreadSettingsSnapshot, diff --git a/apps/mobile/src/state/thread-task-permissions.test.ts b/apps/mobile/src/state/thread-task-permissions.test.ts index 1bebfb373e327..5a31c1955b928 100644 --- a/apps/mobile/src/state/thread-task-permissions.test.ts +++ b/apps/mobile/src/state/thread-task-permissions.test.ts @@ -271,11 +271,7 @@ describe("mobile task permissions", () => { expect(state.enqueue).not.toHaveBeenCalled(); expect(state.clearDraft).not.toHaveBeenCalled(); composer.onChangeDraftMessage("Edited locally"); - composer.onUpdateInteractionMode("plan"); expect(state.draft.text).toBe("Edited locally"); - expect(state.updateSettings).toHaveBeenCalledWith("secondary:thread", { - interactionMode: "plan", - }); }); it("keeps an armed fresh session for the message after an approval redirect", async () => { diff --git a/apps/mobile/src/state/use-thread-composer-state.ts b/apps/mobile/src/state/use-thread-composer-state.ts index e19009fcc16ba..465a8a425d292 100644 --- a/apps/mobile/src/state/use-thread-composer-state.ts +++ b/apps/mobile/src/state/use-thread-composer-state.ts @@ -26,10 +26,8 @@ import { orchestrationV2WorktreePending, type EnvironmentId, type ModelSelection, - type ProviderInteractionMode, type RuntimeMode, type ScopedThreadRef, - type ServerProvider, type ThreadId, } from "@t3tools/contracts"; @@ -49,7 +47,6 @@ import { describeUnavailableModelSelection, isModelSelectionUnavailable, } from "../lib/modelOptions"; -import { resolveProviderInteractionMode } from "./legacy-plan-mode"; import type { DraftComposerImageAttachment } from "../lib/composerImages"; import { scopedThreadKey } from "../lib/scopedEntities"; @@ -181,19 +178,8 @@ export function useThreadDraftForThread(input: { }; } -/** A provider without the interaction-mode toggle runs in the default mode. */ -export function updateThreadDraftModelSelection( - threadKey: string, - value: ModelSelection, - providers: ReadonlyArray, -): void { - const provider = providers.find((candidate) => candidate.instanceId === value.instanceId); - updateComposerDraftSettings(threadKey, { - modelSelection: value, - ...(provider?.showInteractionModeToggle === false - ? { interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE } - : {}), - }); +export function updateThreadDraftModelSelection(threadKey: string, value: ModelSelection): void { + updateComposerDraftSettings(threadKey, { modelSelection: value }); } export function useThreadComposerState(input: { @@ -328,15 +314,8 @@ export function useThreadComposerState(input: { serverConfig?.providers ?? [], ); const runtimeMode = selectedDraft.runtimeMode ?? selectedThread?.runtimeMode ?? null; - const selectedProvider = selectedEnvironmentRuntime?.serverConfig?.providers.find( - (provider) => provider.instanceId === modelSelection?.instanceId, - ); - const interactionMode = selectedThread - ? resolveProviderInteractionMode( - selectedProvider, - selectedDraft.interactionMode ?? selectedThread.interactionMode, - ) - : null; + // Plan mode is not offered to users; every send runs in build mode. + const interactionMode = selectedThread ? DEFAULT_PROVIDER_INTERACTION_MODE : null; const canSwitchThreadProvider = threadAllowsProviderSwitch({ thread: selectedThreadShell, projection: selectedThreadProjection?.projection, @@ -781,10 +760,7 @@ export function useThreadComposerState(input: { context: draft.context, modelSelection, runtimeMode: draft.runtimeMode ?? thread.runtimeMode, - interactionMode: resolveProviderInteractionMode( - provider, - draft.interactionMode ?? thread.interactionMode, - ), + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, ...(compactBeforeSend ? { compactBeforeSend, dispatchMode: "queue" as const } : followUpDispatchMode === null @@ -865,20 +841,6 @@ export function useThreadComposerState(input: { removeComposerDraftAttachment(threadKey, imageId); }; - const onUpdateInteractionMode = (value: ProviderInteractionMode) => { - if (!selectedThreadKey) { - return; - } - const modelSelection = - getComposerDraftSnapshot(selectedThreadKey).modelSelection ?? selectedThread?.modelSelection; - const provider = selectedEnvironmentRuntime?.serverConfig?.providers.find( - (candidate) => candidate.instanceId === modelSelection?.instanceId, - ); - updateComposerDraftSettings(selectedThreadKey, { - interactionMode: resolveProviderInteractionMode(provider, value), - }); - }; - return { feedbackSubmissions, dismissFeedback, @@ -917,7 +879,6 @@ export function useThreadComposerState(input: { onNativePasteText, onRemoveDraftImage, onSendMessage, - onUpdateInteractionMode, sideChatUnavailableReason, }; } diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 2e772cc0998c0..7de02cc3941ad 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -3,7 +3,6 @@ import { buildExpiredTerminalContextToastCopy, mergeDirectPreviewAnnotation, planComposerTurn, - restorePlanFollowUpComposer, snapshotComposerSend, withMessageContext, deriveAvailableRevertTurnCounts, @@ -1143,61 +1142,6 @@ describe("planComposerTurn", () => { }); }); -describe("restorePlanFollowUpComposer", () => { - // The draft is the failed follow-up's thread; the refs and cursor are whatever thread is on - // screen. - it.each([ - { - name: "puts the follow-up back while its thread is open", - before: { draft: "", refs: "", cursor: null }, - routeIsCurrent: true, - after: { draft: "Tighten step 2", refs: "Tighten step 2", cursor: "Tighten step 2" }, - }, - { - name: "keeps a draft typed while the follow-up was in flight", - before: { draft: "Next idea", refs: "Next idea", cursor: null }, - routeIsCurrent: true, - after: { draft: "Next idea", refs: "Next idea", cursor: null }, - }, - { - name: "leaves another thread's composer alone after navigating away", - before: { draft: "", refs: "Other thread", cursor: null }, - routeIsCurrent: false, - after: { draft: "Tighten step 2", refs: "Other thread", cursor: null }, - }, - ])("$name", ({ before, routeIsCurrent, after }) => { - const composer: { draft: string; refs: string; cursor: string | null } = { ...before }; - restorePlanFollowUpComposer({ - snapshot: { - prompt: "Tighten step 2", - environmentContexts: [], - terminalContexts: [], - reviewComments: [], - previewAnnotations: [], - threadContexts: [], - }, - draftHasUserContent: composer.draft.trim().length > 0, - routeIsCurrent, - writeComposerRefs: (snapshot) => { - composer.refs = snapshot.prompt; - }, - writePrompt: (prompt) => { - composer.draft = prompt; - }, - writeTerminalContexts: () => undefined, - writeReviewComments: () => undefined, - writePreviewAnnotations: () => undefined, - writeThreadContexts: () => undefined, - writeEnvironmentContexts: () => undefined, - resetCursor: ({ prompt }) => { - composer.cursor = prompt; - }, - }); - - expect(composer).toEqual(after); - }); -}); - describe("withMessageContext", () => { const context = { records: [] } as unknown as NonNullable< Parameters[0]["context"] diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 6e410a7e1bd4c..2b8424f9efe0a 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -30,7 +30,6 @@ import { type ModelSelection, type OrchestrationMessageContext, type PreviewAnnotationPayload, - type ProviderInteractionMode, ProviderDriverKind, type ProviderInstanceId, type ServerProvider, @@ -72,8 +71,7 @@ import { } from "../composerDraftStore"; import * as Schema from "effect/Schema"; import { appAtomRegistry } from "../rpc/atomRegistry"; -import { environmentThreadShells, environmentThreadDetails } from "../state/threads"; -import { waitForAtomValue } from "../state/waitForAtomValue"; +import { environmentThreadDetails } from "../state/threads"; import { filterTerminalContextsWithText, formatTerminalContextLabel, @@ -90,7 +88,7 @@ import { } from "../lib/composerContextRecords"; import type { ComposerDispatchMode } from "@t3tools/client-runtime/state/composer-dispatch"; import type { DraftThreadEnvMode } from "../composerDraftStore"; -import { collapseExpandedComposerCursor, type ComposerSubmissionIntent } from "../composer-logic"; +import { type ComposerSubmissionIntent } from "../composer-logic"; import type { ReviewCommentContext } from "../reviewCommentContext"; import { derivePhase, type TimelineEntry } from "../session-logic"; import type { PreviewMiniPlayerSource } from "../previewMiniPlayerStore"; @@ -535,21 +533,6 @@ export function resolveComposerProviderSelection(input: { }; } -export function resolveComposerInteractionMode(input: { - planModeEnabled: boolean; - provider: Pick | null | undefined; - interactionMode: ProviderInteractionMode; -}): { enabled: boolean; interactionMode: ProviderInteractionMode } { - const enabled = - input.planModeEnabled && - input.provider != null && - input.provider.showInteractionModeToggle !== false; - return { - enabled, - interactionMode: enabled ? input.interactionMode : "default", - }; -} - export function getAntigravitySendBlockReason( provider: | Pick @@ -815,22 +798,6 @@ export function shouldShowBranchMismatchBanner(input: { return input.composerHasContent || input.wasShownForCurrentMismatch; } -export function shouldShowPlanFollowUpPrompt(input: { - pendingUserInputCount: number; - interactionMode: ProviderInteractionMode; - latestTurnSettled: boolean; - hasActionableProposedPlan: boolean; - hasComposerAttachments: boolean; -}): boolean { - return ( - input.pendingUserInputCount === 0 && - input.interactionMode === "plan" && - input.latestTurnSettled && - input.hasActionableProposedPlan && - !input.hasComposerAttachments - ); -} - const sessionDismissedBranchMismatchKeys = new Set(); export function dismissBranchMismatchForSession(key: string): void { @@ -923,19 +890,6 @@ export function getStartedThreadModelChangeBlockReason(input: { }; } -export async function waitForStartedServerThread( - threadRef: ScopedThreadRef, - timeoutMs = 1_000, -): Promise { - const threadAtom = environmentThreadShells.threadShellAtom(threadRef); - return waitForAtomValue({ - registry: appAtomRegistry, - atom: threadAtom, - predicate: threadHasStarted, - timeoutMs, - }); -} - export async function waitForRevertedMessage( threadRef: ScopedThreadRef, turnCount: number, @@ -1152,50 +1106,6 @@ export function shouldRefocusComposerOnWindowFocus( ); } -export interface PlanFollowUpComposerSnapshot { - readonly prompt: string; - readonly terminalContexts: ReadonlyArray; - readonly reviewComments: ReadonlyArray; - readonly previewAnnotations: ReadonlyArray; - readonly threadContexts: ReadonlyArray; - readonly environmentContexts: ReadonlyArray; -} - -// A draft typed while the send was in flight is newer and stays. Composer refs and the cursor -// belong to the thread on screen, so they follow only while the follow-up's route is current. -export function restorePlanFollowUpComposer(input: { - readonly snapshot: PlanFollowUpComposerSnapshot; - readonly draftHasUserContent: boolean; - readonly routeIsCurrent: boolean; - readonly writeComposerRefs: (snapshot: PlanFollowUpComposerSnapshot) => void; - readonly writePrompt: (prompt: string) => void; - readonly writeTerminalContexts: (contexts: ReadonlyArray) => void; - readonly writeReviewComments: (comments: ReadonlyArray) => void; - readonly writePreviewAnnotations: (annotations: ReadonlyArray) => void; - readonly writeThreadContexts: (records: ReadonlyArray) => void; - readonly writeEnvironmentContexts: (records: ReadonlyArray) => void; - readonly resetCursor: (options: { - cursor: number; - prompt: string; - detectTrigger: boolean; - }) => void; -}): void { - if (input.draftHasUserContent) return; - if (input.routeIsCurrent) input.writeComposerRefs(input.snapshot); - input.writePrompt(input.snapshot.prompt); - input.writeTerminalContexts(input.snapshot.terminalContexts); - input.writeReviewComments(input.snapshot.reviewComments); - input.writePreviewAnnotations(input.snapshot.previewAnnotations); - input.writeThreadContexts(input.snapshot.threadContexts); - input.writeEnvironmentContexts(input.snapshot.environmentContexts); - if (!input.routeIsCurrent) return; - input.resetCursor({ - cursor: collapseExpandedComposerCursor(input.snapshot.prompt, input.snapshot.prompt.length), - prompt: input.snapshot.prompt, - detectTrigger: true, - }); -} - /** Includes the annotation screenshot only within the attachment cap; exceeding it rejects the whole turn. */ export function mergeDirectPreviewAnnotation(input: { readonly images: ReadonlyArray; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index fc0ab9265e4fd..92149651480db 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -28,7 +28,6 @@ import { useRightPanelActions } from "./chat/useRightPanelActions"; import { type TerminalLaunchContext, useChatTerminalActions } from "./chat/useChatTerminalActions"; import { useComposerSend } from "./chat/useComposerSend"; import { useComposerResume } from "./chat/useComposerResume"; -import { useImplementPlanInNewThread } from "./chat/useProposedPlanSend"; import { ThreadFind, ThreadFindCanvas } from "./chat/ThreadFindProvider"; import { closeThreadFind, openThreadFind, useThreadFindOpen } from "./chat/threadFindStore"; import { THREAD_FIND_BAR_RESERVED_HEIGHT } from "./chat/ThreadFindBar"; @@ -138,9 +137,7 @@ import { derivePhase, deriveActiveWorkStartedAt, deriveCanInterruptRunningThread, - findLatestProposedPlan, findRevertCheckpoint, - hasActionableProposedPlan, isLatestRunSettled, } from "../session-logic"; import { type LegendListRef } from "@legendapp/list/react"; @@ -333,11 +330,9 @@ import { buildLocalDraftThread, deriveCommittedServerUserMessageIds, dismissBranchMismatchForSession, - shouldShowPlanFollowUpPrompt, shouldRenderPreviewMiniPlayer, deriveLockedProvider, resolveFileAttachmentUrl, - resolveComposerInteractionMode, resolveComposerProviderSelection, resolveThreadMetadataUpdateForNextTurn, resolveSendEnvMode, @@ -583,9 +578,6 @@ export default function ChatView(props: ChatViewProps) { const composerRuntimeMode = useComposerDraftStore( (store) => store.getComposerDraft(composerDraftTarget)?.runtimeMode ?? null, ); - const composerInteractionMode = useComposerDraftStore( - (store) => store.getComposerDraft(composerDraftTarget)?.interactionMode ?? null, - ); const composerActiveProvider = useComposerDraftStore( (store) => store.getComposerDraft(composerDraftTarget)?.activeProvider ?? null, ); @@ -1312,12 +1304,6 @@ export default function ChatView(props: ChatViewProps) { const selectedProvider = selectedProviderEntry?.driverKind ?? requestedDriverKind; const activeProviderInstanceId = selectedProviderEntry?.instanceId ?? null; const activeProviderStatus = selectedProviderEntry?.snapshot ?? null; - const { enabled: interactionModeEnabled, interactionMode } = resolveComposerInteractionMode({ - planModeEnabled: settings.planModeEnabled, - provider: activeProviderStatus, - interactionMode: - composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE, - }); const conversationProviderStatus = providerStatuses.find((status) => status.instanceId === activeRuntime?.providerInstanceId) ?? activeProviderStatus; @@ -1374,19 +1360,6 @@ export default function ChatView(props: ChatViewProps) { const onHeldRequestIdsChange = useStableCallback((requestIds: ReadonlySet) => { setHeldApprovalRequestIds((current) => (shallow(current, requestIds) ? current : requestIds)); }); - const activeProposedPlan = (() => { - if (!latestRunSettled) { - return null; - } - return findLatestProposedPlan(serverProjection, activeLatestRun?.runId ?? null); - })(); - const showPlanFollowUpPrompt = shouldShowPlanFollowUpPrompt({ - pendingUserInputCount: pendingUserInputs.length, - interactionMode, - latestTurnSettled: latestRunSettled, - hasActionableProposedPlan: hasActionableProposedPlan(activeProposedPlan), - hasComposerAttachments: composerHasAttachments, - }); // Answering an approval or question on any client resumes quota spending, so include it in the key. const usageLimitsKey = activeProviderInstanceId === null || (isServerThread && activeThread === undefined) @@ -1875,7 +1848,7 @@ export default function ChatView(props: ChatViewProps) { threadId: activeThreadId, message: { messageId: newMessageId(), role: "user", text, attachments: [] }, runtimeMode, - interactionMode, + interactionMode: DEFAULT_INTERACTION_MODE, dispatchMode: "queue", }, }); @@ -2478,8 +2451,7 @@ export default function ChatView(props: ChatViewProps) { activeEnvironmentUnavailable || feedbackUploading || pendingApprovals.length > 0 || - pendingUserInputs.length > 0 || - showPlanFollowUpPrompt; + pendingUserInputs.length > 0; const compactDisabled = compactThreadUnavailable; const resumeCompactionTokens = activeContextWindow && @@ -2846,14 +2818,11 @@ export default function ChatView(props: ChatViewProps) { const { getModelDisabledReason, - handleInteractionModeChange, - handleInteractionModeChangeStable, handleRuntimeModeChangeStable, onEnvModeChangeStable, onProviderModelSelectStable, onStartFromOriginChange, openProviderSetupStable, - toggleInteractionModeStable, } = useComposerSelection({ environmentId, composerDraftTarget, @@ -2867,8 +2836,6 @@ export default function ChatView(props: ChatViewProps) { supportsProviderSwitchingViaHandoff, multipleModelSelections, runtimeMode, - interactionMode, - interactionModeEnabled, isLocalDraftThread, canOverrideServerThreadEnvMode, setPendingServerThreadEnvModeEntry, @@ -2887,7 +2854,6 @@ export default function ChatView(props: ChatViewProps) { threadDetailLoading, activeEnvironmentUnavailable, runtimeMode, - interactionMode, localCheckoutBranchMismatch, composerRef, persistThreadSettingsForNextTurn, @@ -2895,21 +2861,6 @@ export default function ChatView(props: ChatViewProps) { scrollToEnd, setThreadError, }); - const onImplementPlanInNewThread = useImplementPlanInNewThread({ - environmentId, - routeThreadKey, - activeThread, - activeThreadBranch, - activeProposedPlan, - isServerThread, - isSendBusy, - composerRef, - beginLocalDispatch, - resetLocalDispatch, - activeProject, - activeEnvironmentUnavailable, - defaultRuntimeMode, - }); const { onSend } = useComposerSend({ environmentId, routeThreadKey, @@ -2921,7 +2872,6 @@ export default function ChatView(props: ChatViewProps) { activeThreadBranch, activeProject, activeProjectDefaultModelSelection, - activeProposedPlan, activeMessageCount, isServerThread, isLocalDraftThread, @@ -2952,7 +2902,6 @@ export default function ChatView(props: ChatViewProps) { keepFullHistory, freshSessionAvailable: freshSessionCommandAvailable, resumeCompactionTokens, - showPlanFollowUpPrompt, usageLimitsKey, usageLimitsOffered, openUsageLimits, @@ -2981,13 +2930,11 @@ export default function ChatView(props: ChatViewProps) { setKeepFullHistory, anchorNewTurn, acknowledgeActiveThreadWoke, - handleInteractionModeChange, onSideChat: sideChat.available ? sendSideChatStable : undefined, sideChatDisabledReason, handleNewThreadInActiveProject, persistThreadSettingsForNextTurn, }); - const onImplementPlanInNewThreadStable = useStableCallback(onImplementPlanInNewThread); const onWorktreeSetupWorkLocally = () => { if (!worktreeSetup || worktreeSetup.phase !== "running" || !draftId) { @@ -3691,11 +3638,8 @@ export default function ChatView(props: ChatViewProps) { activePendingDraftAnswers={activePendingDraftAnswers} activePendingQuestionIndex={activePendingQuestionIndex} respondingRequestIds={respondingRequestIds} - showPlanFollowUpPrompt={showPlanFollowUpPrompt} - activeProposedPlan={activeProposedPlan} runtimeMode={runtimeMode} defaultRuntimeMode={defaultRuntimeMode} - interactionMode={interactionMode} lockedProvider={modelPickerLockedProvider} providerStatuses={providerStatuses as ServerProvider[]} providerCatalogKnown={serverConfig !== null} @@ -3736,7 +3680,6 @@ export default function ChatView(props: ChatViewProps) { onSend={onComposerSendStable} onResume={onComposerResumeStable} onInterrupt={onInterruptStable} - onImplementPlanInNewThread={onImplementPlanInNewThreadStable} onSelectActivePendingUserInputOption={ onSelectActivePendingUserInputOptionStable } @@ -3753,9 +3696,7 @@ export default function ChatView(props: ChatViewProps) { onProviderModelSelect={onProviderModelSelectStable} onOpenProviderSetup={openProviderSetupStable} getModelDisabledReason={getModelDisabledReason} - toggleInteractionMode={toggleInteractionModeStable} handleRuntimeModeChange={handleRuntimeModeChangeStable} - handleInteractionModeChange={handleInteractionModeChangeStable} focusComposer={focusComposerStable} scheduleComposerFocus={scheduleComposerFocusStable} setThreadError={setThreadError} diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 89439a62760e6..e9040012d684a 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -28,7 +28,6 @@ import type { PreviewAnnotationPayload, ThreadContextRecord, EnvironmentContextRecord, - ProviderInteractionMode, ResolvedKeybindingsConfig, RuntimeMode, RuntimeRequestId, @@ -82,11 +81,7 @@ import { replaceTextRange, } from "../../composer-logic"; import { DEFAULT_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder"; -import { - deriveComposerSendState, - getAntigravitySendBlockReason, - resolveComposerInteractionMode, -} from "../ChatView.logic"; +import { deriveComposerSendState, getAntigravitySendBlockReason } from "../ChatView.logic"; import { dataTransferHasComposerMention, makeComposerMentionDragHandlers, @@ -208,7 +203,6 @@ import { import { ComposerPrimaryActions } from "./ComposerPrimaryActions"; import { ComposerFooterPrimaryActions } from "./ComposerFooterControls"; import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; -import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner"; import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; import { buildPullRequestReferenceContext } from "../pullRequest/pullRequestDetail.logic"; import { getComposerPromptInjectionState, getComposerReasoningStep } from "./composerProviderState"; @@ -234,19 +228,13 @@ import { toastManager } from "../ui/toast"; import { BuildingIcon, PlayIcon } from "lucide-react"; import { Button } from "../ui/button"; import { useManagedPolicy } from "../../state/managedPolicy"; -import { proposedPlanTitle } from "@t3tools/shared/proposedPlanText"; import { hasProviderSetup } from "./ProviderStatusBanner"; import { type AppModelOption, getAppModelOptionsForInstance } from "../../modelSelection"; import type { UnifiedSettings } from "@t3tools/contracts/settings"; import { isVideoAttachment, type SessionPhase, type Thread } from "../../types"; import { type ComposerPromptHistoryPosition } from "./composerPromptHistory"; import type { PendingUserInputDraftAnswer } from "@t3tools/client-runtime/state/pending-user-input"; -import type { - LatestProposedPlanState, - PendingApproval, - PendingUserInput, - TimelineEntry, -} from "../../session-logic"; +import type { PendingApproval, PendingUserInput, TimelineEntry } from "../../session-logic"; import { resolveComposerDispatchMode, type ComposerDispatchMode, @@ -334,8 +322,6 @@ export interface ChatComposerHandle { selectedProvider: ProviderDriverKind; selectedModel: string; selectedProviderModels: ReadonlyArray; - interactionMode: ProviderInteractionMode; - interactionModeEnabled: boolean; shownPendingApprovalIds: ReadonlyArray; }; validateProviderInput: (providerInput: string) => boolean; @@ -409,12 +395,8 @@ export interface ChatComposerProps { activePendingQuestionIndex: number; respondingRequestIds: RuntimeRequestId[]; - showPlanFollowUpPrompt: boolean; - activeProposedPlan: LatestProposedPlanState | null; - runtimeMode: RuntimeMode; defaultRuntimeMode?: RuntimeMode; - interactionMode: ProviderInteractionMode; lockedProvider: ProviderDriverKind | null; providerStatuses: ServerProvider[]; @@ -463,7 +445,6 @@ export interface ChatComposerProps { ) => void; onResume: () => void; onInterrupt: () => void; - onImplementPlanInNewThread: () => void; onSelectActivePendingUserInputOption: (questionId: string, optionValue: string) => void; onAdvanceActivePendingUserInput: () => void; onDismissActivePendingUserInput: (requestId: RuntimeRequestId) => void; @@ -483,9 +464,7 @@ export interface ChatComposerProps { ) => void; onOpenProviderSetup: (instanceId: ProviderInstanceId) => void; getModelDisabledReason: (instanceId: ProviderInstanceId, model: string) => string | null; - toggleInteractionMode: () => void; handleRuntimeModeChange: (mode: RuntimeMode) => void; - handleInteractionModeChange: (mode: ProviderInteractionMode) => void; focusComposer: () => void; scheduleComposerFocus: () => void; @@ -536,10 +515,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) activePendingDraftAnswers, activePendingQuestionIndex, respondingRequestIds, - showPlanFollowUpPrompt, - activeProposedPlan, runtimeMode, - interactionMode: requestedInteractionMode, lockedProvider, providerStatuses, providerCatalogKnown, @@ -571,7 +547,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) onSend, onResume, onInterrupt, - onImplementPlanInNewThread, onSelectActivePendingUserInputOption, onAdvanceActivePendingUserInput, onDismissActivePendingUserInput, @@ -580,9 +555,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) onProviderModelSelect, onOpenProviderSetup, getModelDisabledReason, - toggleInteractionMode, handleRuntimeModeChange, - handleInteractionModeChange, focusComposer, scheduleComposerFocus, setThreadError, @@ -944,11 +917,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const selectedPromptEffort = composerProviderState.promptEffort; const selectedModelOptionsForDispatch = composerProviderState.modelOptionsForDispatch; - const { enabled: planModeUiEnabled, interactionMode } = resolveComposerInteractionMode({ - planModeEnabled: settings.planModeEnabled, - provider: selectedProviderStatus, - interactionMode: requestedInteractionMode, - }); const selectedModelSelection: ModelSelection = createModelSelection( selectedInstanceId, selectedModel, @@ -1120,7 +1088,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) pullRequestProjectId, pullRequestRepository, threadPullRequests, - planModeUiEnabled, sideChatAvailable: props.sideChatAvailable === true, sideChatDisabledReason: props.sideChatDisabledReason ?? null, freshSessionAvailable: props.onFreshSession !== undefined, @@ -1166,16 +1133,14 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const activePendingUserInput = pendingUserInputs[0] ?? null; const isChoiceOnlyPendingQuestion = activePendingProgress?.activeQuestion?.allowCustomAnswer === false; - const showComposerTopDrawer = - pendingUserInputs.length > 0 || - (!isComposerCollapsedMobile && showPlanFollowUpPrompt && activeProposedPlan !== null); + const showComposerTopDrawer = pendingUserInputs.length > 0; const showCollapsedMobilePromptRow = isComposerCollapsedMobile; const showComposerAttachAction = fileStagingLimit !== null && (!activePendingProgress || (supportsQuestionAttachments && activePendingProgress.activeQuestion?.allowCustomAnswer !== false)); - const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null; + const composerFooterHasWideActions = activePendingProgress !== null; const editPromptFromTraits = (edit: (prompt: string) => string) => { const currentPrompt = getComposerDraft(composerDraftTarget)?.prompt ?? ""; @@ -1922,14 +1887,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) if (applied) setComposerHighlightedItemId(null); return; } - if (!planModeUiEnabled) return; - void handleInteractionModeChange(item.command === "plan" ? "plan" : "default"); - const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { - expectedText: snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd), - }); - if (applied) { - setComposerHighlightedItemId(null); - } return; } if (item.type === "provider-slash-command") { @@ -2091,7 +2048,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) if (activePendingProgress) { return activePendingProgress.isLastQuestion && Boolean(activePendingResolvedAnswers); } - return showPlanFollowUpPrompt || composerSendState.hasSendableContent; + return composerSendState.hasSendableContent; }; const submitComposerRef = useRef< @@ -2278,7 +2235,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) isCompacting, followUpBehavior, isSoftwareKeyboardOpen, - planModeUiEnabled, isLiteralPendingAnswer, pendingUserInputs, promptHistoryEntries, @@ -2299,7 +2255,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) setComposerTrigger, setComposerHighlightedItemId, dismissComposerTrigger, - toggleInteractionMode, nudgeComposerMenuHighlight, onSelectComposerItem, resolveActiveComposerTrigger, @@ -2396,13 +2351,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) reportedModelSelection={reportedModelSelection} promptUltrathink={promptUltrathink} onPromptEdit={editPromptFromTraitsStable} - planModeEnabled={settings.planModeEnabled} - planModeUiEnabled={planModeUiEnabled} - interactionMode={interactionMode} compatibleRuntimeMode={compatibleRuntimeMode} defaultRuntimeMode={props.defaultRuntimeMode} compatibleRuntimeModeOptions={compatibleRuntimeModeOptions} - toggleInteractionMode={toggleInteractionMode} handleRuntimeModeChange={handleRuntimeModeChange} composerControlsCollapsed={composerControlsCollapsed} effortDisplay={settings.effortDisplay} @@ -2822,12 +2773,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) void onInterrupt(); }; const handleInterruptPrimaryActionStable = useStableCallback(handleInterruptPrimaryAction); - const handleImplementPlanInNewThreadPrimaryAction = () => { - void onImplementPlanInNewThread(); - }; - const handleImplementPlanInNewThreadPrimaryActionStable = useStableCallback( - handleImplementPlanInNewThreadPrimaryAction, - ); const scheduleComposerCollapseCheck = () => { if (!isMobileViewport || mobileComposerExpandInFlightRef.current) { return; @@ -2947,7 +2892,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) reportedModelSelection, prompt: promptRef.current, cursor: readComposerSnapshotStable().expandedCursor, - planModeEnabled: settings.planModeEnabled, direction, }); if (!change) return; @@ -3085,8 +3029,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) selectedProvider, selectedModel, selectedProviderModels, - interactionMode, - interactionModeEnabled: planModeUiEnabled, }), setMultipleModelSelections, validateProviderInput: (providerInput: string) => { @@ -3140,7 +3082,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) selectedModel, selectedInstanceId, reportedModelSelection, - settings.planModeEnabled, selectedModelSelection, multipleModelSelections, setMultipleModelSelections, @@ -3150,8 +3091,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) selectedPromptEffort, selectedProvider, selectedProviderModels, - interactionMode, - planModeUiEnabled, compactThreadContextStable, ], ); @@ -3281,11 +3220,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) onAdvance={onAdvanceActivePendingUserInput} onDismiss={onDismissActivePendingUserInput} /> - ) : !isComposerCollapsedMobile && showPlanFollowUpPrompt && activeProposedPlan ? ( - ) : null} @@ -3491,13 +3425,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ? isChoiceOnlyPendingQuestion ? "Choose an option above" : "Type your own answer, or leave this blank to use the selected option" - : showPlanFollowUpPrompt && activeProposedPlan - ? "Add feedback to refine the plan, or leave this blank to implement it" - : projectSelectionRequired - ? "Choose a project above to start a thread" - : showProviderUnavailable - ? "Enable a provider in Settings to send a message" - : DEFAULT_COMPOSER_PLACEHOLDER + : projectSelectionRequired + ? "Choose a project above to start a thread" + : showProviderUnavailable + ? "Enable a provider in Settings to send a message" + : DEFAULT_COMPOSER_PLACEHOLDER } disabled={ projectSelectionRequired || @@ -3518,8 +3450,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) pendingAction={pendingPrimaryAction} isRunning={false} canInterrupt={false} - showPlanFollowUpPrompt={false} - promptHasText={false} isSendBusy={isSendBusy} sendDisabledReason={voiceSendDisabledReason} isEnvironmentUnavailable={ @@ -3532,7 +3462,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) preserveComposerFocusOnPointerDown onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion} onInterrupt={handleInterruptPrimaryActionStable} - onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryActionStable} /> ) : null} @@ -3604,10 +3533,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }, }, )} - showPlanFollowUpPrompt={ - pendingUserInputs.length === 0 && showPlanFollowUpPrompt - } - promptHasText={prompt.trim().length > 0 || voiceInputActive} voiceInputActive={voiceInputActive} isSendBusy={isSendBusy} sendDisabledReason={voiceSendDisabledReason} @@ -3625,7 +3550,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) onResume={onResume} onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion} onInterrupt={handleInterruptPrimaryActionStable} - onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryActionStable} compactBeforeSendTokens={props.resumeCompactionTokens} keepFullHistory={props.keepFullHistory} onToggleKeepFullHistory={props.onToggleKeepFullHistory} diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx index e5fbddf56c769..1472f55bf60b8 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx @@ -1,29 +1,19 @@ -import { ProviderInteractionMode, RuntimeMode } from "@t3tools/contracts"; +import type { RuntimeMode } from "@t3tools/contracts"; import { memo, useRef } from "react"; import { EllipsisIcon } from "lucide-react"; -import { - Menu, - MenuPopup, - MenuRadioGroup, - MenuRadioItem, - MenuSeparator as MenuDivider, - MenuTrigger, -} from "../ui/menu"; +import { Menu, MenuPopup, MenuTrigger } from "../ui/menu"; import { ComposerControl, ComposerControlIcon } from "./ComposerControl"; import { focusRequestedPermission, useComposerMenuProps } from "./composerEventScope"; import { useComposerMenuState } from "./useComposerMenuState"; import { ComposerPermissionsSection, type RuntimeModeOption } from "./ComposerFooterControls"; export const CompactComposerControlsMenu = memo(function CompactComposerControlsMenu(props: { - interactionMode: ProviderInteractionMode; runtimeMode: RuntimeMode; runtimeModeOptions: ReadonlyArray; - showInteractionModeToggle: boolean; - showPermissions?: boolean; + showPermissions: boolean; size?: "sm" | "xs"; /** Close the portaled popup when its trigger hides. */ hidden?: boolean; - onToggleInteractionMode: () => void; onRuntimeModeChange: (mode: RuntimeMode) => void; }) { const composerFloatingLayerProps = useComposerMenuProps(); @@ -44,36 +34,20 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls size={size} className="shrink-0" aria-label="More composer controls" - data-composer-shortcut={props.showPermissions !== false ? "composer.mode" : undefined} + data-composer-shortcut={props.showPermissions ? "composer.mode" : undefined} /> } > - {props.showInteractionModeToggle ? ( - <> -
Mode
- { - if (!value || value === props.interactionMode) return; - props.onToggleInteractionMode(); - }} - > - Chat - Plan - - {props.showPermissions !== false ? : null} - - ) : null} - {props.showPermissions === false ? null : ( + {props.showPermissions ? ( - )} + ) : null}
); diff --git a/apps/web/src/components/chat/ComposerControlsBar.tsx b/apps/web/src/components/chat/ComposerControlsBar.tsx index 0002beeaf9fef..7c4e6f04b77a0 100644 --- a/apps/web/src/components/chat/ComposerControlsBar.tsx +++ b/apps/web/src/components/chat/ComposerControlsBar.tsx @@ -15,7 +15,6 @@ import { type ModelSelection, type ProviderDriverKind, type ProviderInstanceId, - type ProviderInteractionMode, type RuntimeMode, type ServerProvider, } from "@t3tools/contracts"; @@ -51,16 +50,12 @@ export function ComposerControlsBar(props: { reportedModelSelection: ChatComposerProps["reportedModelSelection"]; promptUltrathink: PromptUltrathink; onPromptEdit: (edit: (prompt: string) => string) => void; - planModeEnabled: boolean; - planModeUiEnabled: boolean; effortDisplay: EffortDisplay; /** The strip under a new thread names the permissions, so the row does not repeat them. */ permissionsShownInStrip: boolean; - interactionMode: ProviderInteractionMode; compatibleRuntimeMode: RuntimeMode; defaultRuntimeMode: RuntimeMode | undefined; compatibleRuntimeModeOptions: ReadonlyArray; - toggleInteractionMode: () => void; handleRuntimeModeChange: (mode: RuntimeMode) => void; composerControlsCollapsed: boolean; composerControlsHidden: boolean; @@ -102,13 +97,9 @@ export function ComposerControlsBar(props: { reportedModelSelection, promptUltrathink, onPromptEdit, - planModeEnabled, - planModeUiEnabled, - interactionMode, compatibleRuntimeMode, defaultRuntimeMode, compatibleRuntimeModeOptions, - toggleInteractionMode, handleRuntimeModeChange, composerControlsCollapsed, composerControlsHidden, @@ -147,7 +138,6 @@ export function ComposerControlsBar(props: { pickerOwnsModeShortcut, } = resolveComposerControlsOwnership({ matchesDefault: accessMatchesDefault || props.permissionsShownInStrip, - planModeUiEnabled, collapsed: composerControlsCollapsed, measuredHiddenBlockCount, }); @@ -163,7 +153,6 @@ export function ComposerControlsBar(props: { reportedModelSelection, promptUltrathink, onPromptEdit, - planModeEnabled, }); const effort = describeComposerEffort(traitsInput, props.effortDisplay, controlSize); const restingBlockDefs = showModeBlock @@ -172,15 +161,12 @@ export function ComposerControlsBar(props: { id: "mode", content: (