diff --git a/packages/desktop/src/components/chat/ChatInput.tsx b/packages/desktop/src/components/chat/ChatInput.tsx index 5e7d794..ae7c377 100644 --- a/packages/desktop/src/components/chat/ChatInput.tsx +++ b/packages/desktop/src/components/chat/ChatInput.tsx @@ -2,6 +2,7 @@ import type { AskUserQuestion } from '@anton/protocol' import { Brain, Plus, Send, Square } from 'lucide-react' import type React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' +import { anyProviderReady } from '../../lib/providers.js' import type { Skill } from '../../lib/skills.js' import type { ChatImageAttachment } from '../../lib/store.js' import { useIsCurrentSessionWorking, useStore } from '../../lib/store.js' @@ -175,6 +176,11 @@ export function ChatInput({ const handle = richInputRef.current if (!handle) return + // Gate Enter-key / programmatic sends on provider readiness to match the + // disabled send button — avoids silently emitting turns that will fail. + const { providers, harnessStatuses } = sessionStore.getState() + if (!anyProviderReady(providers, harnessStatuses)) return + const blocks = handle.getContentBlocks() const hasContent = blocks.some((b) => (b.type === 'text' ? b.text.trim().length > 0 : true)) if (!hasContent) return @@ -261,6 +267,8 @@ export function ChatInput({ } const hasContent = input.trim().length > 0 || imageCount > 0 + const anyReady = sessionStore((s) => anyProviderReady(s.providers, s.harnessStatuses)) + const canSend = hasContent && anyReady const rootClass = `composer composer--${variant}` @@ -376,10 +384,10 @@ export function ChatInput({ diff --git a/packages/desktop/src/components/chat/HarnessProviderSwitch.tsx b/packages/desktop/src/components/chat/HarnessProviderSwitch.tsx index 5bf502f..25935e8 100644 --- a/packages/desktop/src/components/chat/HarnessProviderSwitch.tsx +++ b/packages/desktop/src/components/chat/HarnessProviderSwitch.tsx @@ -16,6 +16,7 @@ import { ChevronDown } from 'lucide-react' import { useEffect, useRef, useState } from 'react' +import { isProviderReady } from '../../lib/providers.js' import type { ProviderInfo } from '../../lib/store.js' import { sessionStore } from '../../lib/store/sessionStore.js' import { ProviderIcon } from './ModelSelector.js' @@ -35,6 +36,7 @@ export function HarnessProviderSwitch() { const currentProvider = sessionStore((s) => s.currentProvider) const currentModel = sessionStore((s) => s.currentModel) const providers = sessionStore((s) => s.providers) + const harnessStatuses = sessionStore((s) => s.harnessStatuses) const switchSessionProvider = sessionStore((s) => s.switchSessionProvider) const [open, setOpen] = useState(false) @@ -102,6 +104,8 @@ export function HarnessProviderSwitch() { {harnessProviders.map((p) => { const defaultModel = p.defaultModels?.[0] ?? p.models[0] ?? currentModel ?? '' const isCurrent = p.name === currentProvider + const status = harnessStatuses[p.name] + const ready = isProviderReady(p, harnessStatuses) return ( + {anyReady ? ( + <> +
+ +
+
+
{formatModelName(currentModel) || currentModel}
+
{currentProvider}
+
+ + + ) : ( + <> +
+
No model selected
+
Connect a CLI or API key below to enable chat.
+
+ + + )} diff --git a/packages/desktop/src/index.css b/packages/desktop/src/index.css index cdd25fc..941a0c2 100644 --- a/packages/desktop/src/index.css +++ b/packages/desktop/src/index.css @@ -1670,6 +1670,16 @@ button { white-space: nowrap; } +/* Empty state ("Select a model"): no avatar is rendered, so the pill loses the + 18px avatar that normally sets its height and the 3px left-padding leaves the + label jammed against the border. Match the normal pill's vertical size (avatar + 18px + 3px*2 padding + 1px*2 border = 26px, box-sizing is border-box) and + balance horizontal padding so it reads as the same control. */ +.composer__model--empty { + padding: 3px 10px; + min-height: 26px; +} + .composer__model:hover { background: var(--bg-elev-3); border-color: var(--border-strong); diff --git a/packages/desktop/src/lib/providers.ts b/packages/desktop/src/lib/providers.ts new file mode 100644 index 0000000..1a854ba --- /dev/null +++ b/packages/desktop/src/lib/providers.ts @@ -0,0 +1,24 @@ +import type { ProviderInfo } from './store.js' + +type HarnessStatusMap = Record< + string, + { installed: boolean; auth?: { loggedIn: boolean } } | undefined +> + +// A harness provider is "ready" only when the CLI is installed AND logged in. +// `hasApiKey` is always true for harness providers on the backend (they don't +// need keys) so it cannot be used as a readiness signal. +export function isProviderReady(p: ProviderInfo, harnessStatuses: HarnessStatusMap): boolean { + if (p.type === 'harness') { + const s = harnessStatuses[p.name] + return !!(s?.installed && s?.auth?.loggedIn) + } + return p.hasApiKey +} + +export function anyProviderReady( + providers: ProviderInfo[], + harnessStatuses: HarnessStatusMap, +): boolean { + return providers.some((p) => isProviderReady(p, harnessStatuses)) +}