mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-09 19:45:17 +02:00
commit
e2178c1488
2 changed files with 206 additions and 72 deletions
42
.github/workflows/electron-build.yml
vendored
42
.github/workflows/electron-build.yml
vendored
|
|
@ -16,9 +16,9 @@ jobs:
|
|||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
|
@ -39,17 +39,17 @@ jobs:
|
|||
node -e "
|
||||
const fs = require('fs');
|
||||
const version = '${{ steps.version.outputs.version }}';
|
||||
|
||||
|
||||
// Update apps/x/package.json
|
||||
const rootPackage = JSON.parse(fs.readFileSync('apps/x/package.json', 'utf8'));
|
||||
rootPackage.version = version;
|
||||
fs.writeFileSync('apps/x/package.json', JSON.stringify(rootPackage, null, 2) + '\n');
|
||||
|
||||
|
||||
// Update apps/x/apps/main/package.json
|
||||
const mainPackage = JSON.parse(fs.readFileSync('apps/x/apps/main/package.json', 'utf8'));
|
||||
mainPackage.version = version;
|
||||
fs.writeFileSync('apps/x/apps/main/package.json', JSON.stringify(mainPackage, null, 2) + '\n');
|
||||
|
||||
|
||||
console.log('Updated version to:', version);
|
||||
"
|
||||
|
||||
|
|
@ -61,25 +61,25 @@ jobs:
|
|||
# Create a temporary keychain
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||
|
||||
|
||||
# Create keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
|
||||
# Decode and import certificate
|
||||
echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
|
||||
security import $RUNNER_TEMP/certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
|
||||
|
||||
|
||||
# Allow codesign to access the keychain
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
|
||||
# Add keychain to search list
|
||||
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain
|
||||
|
||||
|
||||
# Verify certificate was imported
|
||||
security find-identity -v "$KEYCHAIN_PATH"
|
||||
|
||||
|
||||
# Clean up certificate file
|
||||
rm -f $RUNNER_TEMP/certificate.p12
|
||||
|
||||
|
|
@ -122,9 +122,9 @@ jobs:
|
|||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
|
@ -145,17 +145,17 @@ jobs:
|
|||
node -e "
|
||||
const fs = require('fs');
|
||||
const version = '${{ steps.version.outputs.version }}';
|
||||
|
||||
|
||||
// Update apps/x/package.json
|
||||
const rootPackage = JSON.parse(fs.readFileSync('apps/x/package.json', 'utf8'));
|
||||
rootPackage.version = version;
|
||||
fs.writeFileSync('apps/x/package.json', JSON.stringify(rootPackage, null, 2) + '\n');
|
||||
|
||||
|
||||
// Update apps/x/apps/main/package.json
|
||||
const mainPackage = JSON.parse(fs.readFileSync('apps/x/apps/main/package.json', 'utf8'));
|
||||
mainPackage.version = version;
|
||||
fs.writeFileSync('apps/x/apps/main/package.json', JSON.stringify(mainPackage, null, 2) + '\n');
|
||||
|
||||
|
||||
console.log('Updated version to:', version);
|
||||
"
|
||||
|
||||
|
|
@ -187,9 +187,9 @@ jobs:
|
|||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
|
@ -212,17 +212,17 @@ jobs:
|
|||
node -e "
|
||||
const fs = require('fs');
|
||||
const version = '${{ steps.version.outputs.version }}';
|
||||
|
||||
|
||||
// Update apps/x/package.json
|
||||
const rootPackage = JSON.parse(fs.readFileSync('apps/x/package.json', 'utf8'));
|
||||
rootPackage.version = version;
|
||||
fs.writeFileSync('apps/x/package.json', JSON.stringify(rootPackage, null, 2) + '\n');
|
||||
|
||||
|
||||
// Update apps/x/apps/main/package.json
|
||||
const mainPackage = JSON.parse(fs.readFileSync('apps/x/apps/main/package.json', 'utf8'));
|
||||
mainPackage.version = version;
|
||||
fs.writeFileSync('apps/x/apps/main/package.json', JSON.stringify(mainPackage, null, 2) + '\n');
|
||||
|
||||
|
||||
console.log('Updated version to:', version);
|
||||
"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import {
|
||||
ArrowUp,
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
ImagePlus,
|
||||
LoaderIcon,
|
||||
Mic,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
ShieldCheck,
|
||||
Square,
|
||||
|
|
@ -29,6 +30,7 @@ import {
|
|||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
|
|
@ -283,6 +285,51 @@ function ChatInputInner({
|
|||
const [permissionMode, setPermissionMode] = useState<PermissionMode>('auto')
|
||||
const [recentWorkDirs, setRecentWorkDirs] = useState<RecentWorkDir[]>([])
|
||||
|
||||
// Responsive toolbar: measure real overflow and progressively collapse items
|
||||
// right→left until everything fits. Stages:
|
||||
// 1 code→icon · 2 perm→icon · 3 search label hidden · 4 workDir→icon
|
||||
// 5 code→menu · 6 perm→menu · 7 search→menu · 8 workDir→menu
|
||||
// Once items move into the "⋯" overflow menu (≥5) no icon is ever hidden.
|
||||
// overflow-hidden on the left group is the hard guarantee against any overlap.
|
||||
const toolbarRef = useRef<HTMLDivElement>(null)
|
||||
const leftGroupRef = useRef<HTMLDivElement>(null)
|
||||
const lastWidthRef = useRef(0)
|
||||
const [collapseLevel, setCollapseLevel] = useState(0)
|
||||
|
||||
// Re-evaluate from scratch (level 0) whenever the available width changes…
|
||||
useEffect(() => {
|
||||
const outer = toolbarRef.current
|
||||
if (!outer) return
|
||||
const ro = new ResizeObserver(() => {
|
||||
const w = outer.clientWidth
|
||||
if (w !== lastWidthRef.current) {
|
||||
lastWidthRef.current = w
|
||||
setCollapseLevel(0)
|
||||
}
|
||||
})
|
||||
ro.observe(outer)
|
||||
return () => ro.disconnect()
|
||||
}, [])
|
||||
|
||||
// …or when the *set* of items changes (an item appears/disappears, or the model
|
||||
// name width changes). Deliberately excludes the in-place toggles (searchEnabled,
|
||||
// permissionMode, codeModeEnabled, codingAgent): those fire from the overflow menu
|
||||
// for items already inside it, so resetting here would unmount the open menu. The
|
||||
// no-dep effect below still re-collapses if any toggle happens to widen the row.
|
||||
useLayoutEffect(() => {
|
||||
setCollapseLevel(0)
|
||||
}, [workDir, searchAvailable, codeModeFeatureEnabled, lockedModel, activeModelKey])
|
||||
|
||||
// After each render, if the left group still overflows, collapse one more step.
|
||||
// Runs before paint, so the intermediate (overflowing) state is never visible.
|
||||
useLayoutEffect(() => {
|
||||
const el = leftGroupRef.current
|
||||
if (!el) return
|
||||
if (el.scrollWidth > el.clientWidth + 1 && collapseLevel < 8) {
|
||||
setCollapseLevel((l) => Math.min(8, l + 1))
|
||||
}
|
||||
})
|
||||
|
||||
// When a run exists, freeze the dropdown to the run's resolved model+provider.
|
||||
useEffect(() => {
|
||||
if (!runId) {
|
||||
|
|
@ -757,7 +804,8 @@ function ChatInputInner({
|
|||
className="min-h-6 rounded-none border-0 py-0 shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-4 pb-3">
|
||||
<div ref={toolbarRef} className="flex items-center gap-2 px-4 pb-3">
|
||||
<div ref={leftGroupRef} className="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
|
@ -862,26 +910,32 @@ function ChatInputInner({
|
|||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{workDir && (
|
||||
{workDir && collapseLevel < 8 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="group flex h-7 max-w-[180px] shrink-0 items-center rounded-full border border-border bg-muted/40 pl-2.5 pr-2 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
|
||||
{/* Level 4: collapse to a square icon */}
|
||||
<div className={cn(
|
||||
"group flex h-7 shrink-0 items-center rounded-full border border-border bg-muted/40 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
||||
collapseLevel >= 4 ? "w-7 justify-center" : "max-w-[180px] pl-2.5 pr-2"
|
||||
)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSetWorkDir}
|
||||
className="flex min-w-0 items-center gap-1.5"
|
||||
>
|
||||
<FolderCog className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">{basename(workDir) || workDir}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearWorkDir}
|
||||
aria-label="Remove work directory"
|
||||
className="flex h-3.5 w-0 shrink-0 items-center justify-center overflow-hidden opacity-0 transition-all duration-150 ease-out hover:text-red-500 group-hover:ml-1 group-hover:w-3.5 group-hover:opacity-100"
|
||||
>
|
||||
<X className="h-3.5 w-3.5 shrink-0" />
|
||||
{collapseLevel < 4 && <span className="truncate">{basename(workDir) || workDir}</span>}
|
||||
</button>
|
||||
{collapseLevel < 4 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearWorkDir}
|
||||
aria-label="Remove work directory"
|
||||
className="flex h-3.5 w-0 shrink-0 items-center justify-center overflow-hidden opacity-0 transition-all duration-150 ease-out hover:text-red-500 group-hover:ml-1 group-hover:w-3.5 group-hover:opacity-100"
|
||||
>
|
||||
<X className="h-3.5 w-3.5 shrink-0" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
|
|
@ -889,7 +943,7 @@ function ChatInputInner({
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{searchAvailable && (
|
||||
{searchAvailable && collapseLevel < 7 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchEnabled((v) => !v)}
|
||||
|
|
@ -903,16 +957,14 @@ function ChatInputInner({
|
|||
)}
|
||||
>
|
||||
<Globe className="h-4 w-4 shrink-0" />
|
||||
<span
|
||||
className={cn(
|
||||
'overflow-hidden whitespace-nowrap text-xs font-medium transition-all duration-150 ease-out',
|
||||
searchEnabled ? 'ml-1.5 max-w-[60px] opacity-100' : 'max-w-0 opacity-0'
|
||||
)}
|
||||
>
|
||||
Search
|
||||
</span>
|
||||
{searchEnabled && collapseLevel < 3 && (
|
||||
<span className="ml-1.5 whitespace-nowrap text-xs font-medium">
|
||||
Search
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{collapseLevel < 6 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
|
|
@ -923,7 +975,8 @@ function ChatInputInner({
|
|||
}}
|
||||
disabled={Boolean(runId)}
|
||||
className={cn(
|
||||
"flex h-7 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition-colors",
|
||||
"flex h-7 shrink-0 items-center gap-1.5 rounded-full text-xs font-medium transition-colors",
|
||||
collapseLevel >= 2 ? "w-7 justify-center" : "px-2.5",
|
||||
permissionMode === 'auto'
|
||||
? "bg-secondary text-foreground hover:bg-secondary/70"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
|
|
@ -931,8 +984,8 @@ function ChatInputInner({
|
|||
)}
|
||||
aria-label="Permission mode"
|
||||
>
|
||||
<ShieldCheck className="h-3.5 w-3.5" />
|
||||
<span>{permissionMode === 'auto' ? 'Auto' : 'Manual'}</span>
|
||||
<ShieldCheck className="h-3.5 w-3.5 shrink-0" />
|
||||
{collapseLevel < 2 && <span>{permissionMode === 'auto' ? 'Auto' : 'Manual'}</span>}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
|
|
@ -943,37 +996,54 @@ function ChatInputInner({
|
|||
: 'Manual approval prompts — click for auto-permission'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{codeModeFeatureEnabled && (codeModeEnabled ? (
|
||||
<div className="flex h-7 shrink-0 items-center rounded-full bg-secondary text-xs font-medium text-foreground">
|
||||
)}
|
||||
{codeModeFeatureEnabled && collapseLevel < 5 && (codeModeEnabled ? (
|
||||
collapseLevel >= 1 ? (
|
||||
/* Level 1: collapse the pill to a single icon */
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeModeEnabled(false)}
|
||||
className="flex h-full items-center gap-1.5 rounded-l-full pl-2.5 pr-2 transition-colors hover:bg-secondary/70"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-secondary text-foreground transition-colors hover:bg-secondary/70"
|
||||
>
|
||||
<Terminal className="h-3.5 w-3.5" />
|
||||
<span>Code</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Code mode on — click to disable</TooltipContent>
|
||||
<TooltipContent side="top">Code mode on ({codingAgent === 'claude' ? 'Claude Code' : 'Codex'}) — click to disable</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="text-foreground/30">·</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleCodingAgent}
|
||||
className="flex h-full items-center rounded-r-full pl-2 pr-2.5 transition-colors hover:bg-secondary/70"
|
||||
>
|
||||
<span>{codingAgent === 'claude' ? 'Claude' : 'Codex'}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Coding agent: {codingAgent === 'claude' ? 'Claude Code' : 'Codex'} — click to swap
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-7 shrink-0 items-center rounded-full bg-secondary text-xs font-medium text-foreground">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeModeEnabled(false)}
|
||||
className="flex h-full items-center gap-1.5 rounded-l-full pl-2.5 pr-2 transition-colors hover:bg-secondary/70"
|
||||
>
|
||||
<Terminal className="h-3.5 w-3.5" />
|
||||
<span>Code</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Code mode on — click to disable</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="text-foreground/30">·</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleCodingAgent}
|
||||
className="flex h-full items-center rounded-r-full pl-2 pr-2.5 transition-colors hover:bg-secondary/70"
|
||||
>
|
||||
<span>{codingAgent === 'claude' ? 'Claude' : 'Codex'}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Coding agent: {codingAgent === 'claude' ? 'Claude Code' : 'Codex'} — click to swap
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
|
@ -989,25 +1059,89 @@ function ChatInputInner({
|
|||
<TooltipContent side="top">Use a coding agent (Claude Code or Codex)</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
{collapseLevel >= 5 && (
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="More options"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">More options</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="start" side="top" className="min-w-52">
|
||||
{workDir && collapseLevel >= 8 && (
|
||||
<DropdownMenuItem onSelect={() => { void handleSetWorkDir() }}>
|
||||
<FolderCog className="size-4" />
|
||||
<span className="min-w-0 flex-1 truncate">{basename(workDir) || workDir}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{searchAvailable && collapseLevel >= 7 && (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={searchEnabled}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onCheckedChange={(c) => setSearchEnabled(Boolean(c))}
|
||||
>
|
||||
Web search
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
{collapseLevel >= 6 && (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={permissionMode === 'auto'}
|
||||
disabled={Boolean(runId)}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onCheckedChange={(c) => setPermissionMode(c ? 'auto' : 'manual')}
|
||||
>
|
||||
Auto-approve actions
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
{codeModeFeatureEnabled && collapseLevel >= 5 && (
|
||||
<>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={codeModeEnabled}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onCheckedChange={(c) => setCodeModeEnabled(Boolean(c))}
|
||||
>
|
||||
Code mode
|
||||
</DropdownMenuCheckboxItem>
|
||||
{codeModeEnabled && (
|
||||
<DropdownMenuItem onSelect={(e) => { e.preventDefault(); handleToggleCodingAgent() }}>
|
||||
<Terminal className="size-4" />
|
||||
<span className="min-w-0 flex-1">Coding agent</span>
|
||||
<span className="text-xs text-muted-foreground">{codingAgent === 'claude' ? 'Claude' : 'Codex'}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
{lockedModel ? (
|
||||
<span
|
||||
className="flex h-7 shrink-0 items-center gap-1 rounded-full px-2 text-xs text-muted-foreground"
|
||||
className="flex h-7 min-w-0 items-center gap-1 rounded-full px-2 text-xs text-muted-foreground"
|
||||
title={`${providerDisplayNames[lockedModel.provider] || lockedModel.provider} — fixed for this chat`}
|
||||
>
|
||||
<span className="max-w-[150px] truncate">{getSelectedModelDisplayName(lockedModel.model)}</span>
|
||||
<span className="min-w-0 truncate">{getSelectedModelDisplayName(lockedModel.model)}</span>
|
||||
</span>
|
||||
) : configuredModels.length > 0 ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-7 shrink-0 items-center gap-1 rounded-full px-2 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
className="flex h-7 min-w-0 items-center gap-1 rounded-full px-2 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<span className="max-w-[150px] truncate">
|
||||
<span className="min-w-0 truncate">
|
||||
{getSelectedModelDisplayName(configuredModels.find((m) => `${m.provider}/${m.model}` === activeModelKey)?.model || configuredModels[0]?.model || 'Model')}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
<ChevronDown className="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue