Skip to content

Commit 0646893

Browse files
Apply PR #11168: feat(app, ui): Transitions, spacing, scroll fade, prompt area update
1 parent 6984cdb commit 0646893

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1299
-383
lines changed

packages/app/src/components/dialog-select-model.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ const ModelList: Component<{
9090

9191
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
9292
provider?: string
93-
children?: JSX.Element
93+
children?: JSX.Element | ((open: boolean) => JSX.Element)
9494
triggerAs?: T
9595
triggerProps?: ComponentProps<T>
9696
}) {
@@ -186,8 +186,9 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
186186
</Kobalte.Trigger>
187187
<Kobalte.Portal>
188188
<Kobalte.Content
189+
class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
190+
data-component="model-popover-content"
189191
ref={(el) => setStore("content", el)}
190-
class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
191192
onEscapeKeyDown={(event) => {
192193
setStore("dismiss", "escape")
193194
setStore("open", false)

packages/app/src/components/prompt-input.tsx

Lines changed: 74 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import { useNavigate, useParams } from "@solidjs/router"
3232
import { useSync } from "@/context/sync"
3333
import { useComments } from "@/context/comments"
3434
import { FileIcon } from "@opencode-ai/ui/file-icon"
35+
import { MorphChevron } from "@opencode-ai/ui/morph-chevron"
3536
import { Button } from "@opencode-ai/ui/button"
37+
import { CycleLabel } from "@opencode-ai/ui/cycle-label"
3638
import { Icon } from "@opencode-ai/ui/icon"
3739
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
3840
import type { IconName } from "@opencode-ai/ui/icons/provider"
@@ -42,6 +44,7 @@ import { Select } from "@opencode-ai/ui/select"
4244
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/util/path"
4345
import { useDialog } from "@opencode-ai/ui/context/dialog"
4446
import { ImagePreview } from "@opencode-ai/ui/image-preview"
47+
import { ReasoningIcon } from "@opencode-ai/ui/reasoning-icon"
4548
import { ModelSelectorPopover } from "@/components/dialog-select-model"
4649
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
4750
import { useProviders } from "@/hooks/use-providers"
@@ -922,7 +925,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
922925
.abort({
923926
sessionID,
924927
})
925-
.catch(() => {})
928+
.catch(() => { })
926929
}
927930

928931
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
@@ -1348,18 +1351,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
13481351

13491352
const contextParts: Array<
13501353
| {
1351-
id: string
1352-
type: "text"
1353-
text: string
1354-
synthetic?: boolean
1355-
}
1354+
id: string
1355+
type: "text"
1356+
text: string
1357+
synthetic?: boolean
1358+
}
13561359
| {
1357-
id: string
1358-
type: "file"
1359-
mime: string
1360-
url: string
1361-
filename?: string
1362-
}
1360+
id: string
1361+
type: "file"
1362+
mime: string
1363+
url: string
1364+
filename?: string
1365+
}
13631366
> = []
13641367

13651368
const commentNote = (path: string, selection: FileSelection | undefined, comment: string) => {
@@ -1431,13 +1434,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14311434

14321435
const optimisticParts = requestParts.map((part) => ({
14331436
...part,
1434-
sessionID: session.id,
1437+
sessionID: session?.id || '',
14351438
messageID,
14361439
})) as unknown as Part[]
14371440

14381441
const optimisticMessage: Message = {
14391442
id: messageID,
1440-
sessionID: session.id,
1443+
sessionID: session?.id || '',
14411444
role: "user",
14421445
time: { created: Date.now() },
14431446
agent,
@@ -1448,9 +1451,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14481451
if (sessionDirectory === projectDirectory) {
14491452
sync.set(
14501453
produce((draft) => {
1451-
const messages = draft.message[session.id]
1454+
const messages = draft.message[session?.id || '']
14521455
if (!messages) {
1453-
draft.message[session.id] = [optimisticMessage]
1456+
draft.message[session?.id || ''] = [optimisticMessage]
14541457
} else {
14551458
const result = Binary.search(messages, messageID, (m) => m.id)
14561459
messages.splice(result.index, 0, optimisticMessage)
@@ -1466,9 +1469,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14661469

14671470
globalSync.child(sessionDirectory)[1](
14681471
produce((draft) => {
1469-
const messages = draft.message[session.id]
1472+
const messages = draft.message[session?.id || '']
14701473
if (!messages) {
1471-
draft.message[session.id] = [optimisticMessage]
1474+
draft.message[session?.id || ''] = [optimisticMessage]
14721475
} else {
14731476
const result = Binary.search(messages, messageID, (m) => m.id)
14741477
messages.splice(result.index, 0, optimisticMessage)
@@ -1485,7 +1488,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14851488
if (sessionDirectory === projectDirectory) {
14861489
sync.set(
14871490
produce((draft) => {
1488-
const messages = draft.message[session.id]
1491+
const messages = draft.message[session?.id || '']
14891492
if (messages) {
14901493
const result = Binary.search(messages, messageID, (m) => m.id)
14911494
if (result.found) messages.splice(result.index, 1)
@@ -1498,7 +1501,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14981501

14991502
globalSync.child(sessionDirectory)[1](
15001503
produce((draft) => {
1501-
const messages = draft.message[session.id]
1504+
const messages = draft.message[session?.id || '']
15021505
if (messages) {
15031506
const result = Binary.search(messages, messageID, (m) => m.id)
15041507
if (result.found) messages.splice(result.index, 1)
@@ -1519,15 +1522,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
15191522
const worktree = WorktreeState.get(sessionDirectory)
15201523
if (!worktree || worktree.status !== "pending") return true
15211524

1522-
if (sessionDirectory === projectDirectory) {
1523-
sync.set("session_status", session.id, { type: "busy" })
1525+
if (sessionDirectory === projectDirectory && session?.id) {
1526+
sync.set("session_status", session?.id, { type: "busy" })
15241527
}
15251528

15261529
const controller = new AbortController()
15271530

15281531
const cleanup = () => {
1529-
if (sessionDirectory === projectDirectory) {
1530-
sync.set("session_status", session.id, { type: "idle" })
1532+
if (sessionDirectory === projectDirectory && session?.id) {
1533+
sync.set("session_status", session?.id, { type: "idle" })
15311534
}
15321535
removeOptimisticMessage()
15331536
for (const item of commentItems) {
@@ -1544,7 +1547,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
15441547
restoreInput()
15451548
}
15461549

1547-
pending.set(session.id, { abort: controller, cleanup })
1550+
pending.set(session?.id || '', { abort: controller, cleanup })
15481551

15491552
const abort = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
15501553
if (controller.signal.aborted) {
@@ -1572,7 +1575,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
15721575
if (timer.id === undefined) return
15731576
clearTimeout(timer.id)
15741577
})
1575-
pending.delete(session.id)
1578+
pending.delete(session?.id || '')
15761579
if (controller.signal.aborted) return false
15771580
if (result.status === "failed") throw new Error(result.message)
15781581
return true
@@ -1582,7 +1585,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
15821585
const ok = await waitForWorktree()
15831586
if (!ok) return
15841587
await client.session.prompt({
1585-
sessionID: session.id,
1588+
sessionID: session?.id || '',
15861589
agent,
15871590
model,
15881591
messageID,
@@ -1592,9 +1595,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
15921595
}
15931596

15941597
void send().catch((err) => {
1595-
pending.delete(session.id)
1596-
if (sessionDirectory === projectDirectory) {
1597-
sync.set("session_status", session.id, { type: "idle" })
1598+
pending.delete(session?.id || '')
1599+
if (sessionDirectory === projectDirectory && session?.id) {
1600+
sync.set("session_status", session?.id, { type: "idle" })
15981601
}
15991602
showToast({
16001603
title: language.t("prompt.toast.promptSendFailed.title"),
@@ -1616,6 +1619,24 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
16161619
})
16171620
}
16181621

1622+
const currrentModelVariant = createMemo(() => {
1623+
const modelVariant = local.model.variant.current() ?? ''
1624+
return modelVariant === 'xhigh' ? 'xHigh' : (modelVariant.length > 0 ? modelVariant[0].toUpperCase() + modelVariant.slice(1) : 'Default')
1625+
})
1626+
1627+
const reasoningPercentage = createMemo(() => {
1628+
const variants = local.model.variant.list()
1629+
const current = local.model.variant.current()
1630+
const totalEntries = variants.length + 1
1631+
1632+
if (totalEntries <= 2 || current === 'Default') {
1633+
return 0
1634+
}
1635+
1636+
const currentIndex = current ? variants.indexOf(current) + 1 : 0
1637+
return ((currentIndex + 1) / totalEntries) * 100
1638+
}, [local.model.variant])
1639+
16191640
return (
16201641
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
16211642
<Show when={store.popover}>
@@ -1668,7 +1689,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
16681689
</>
16691690
}
16701691
>
1671-
<Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
1692+
<Icon name="brain" size="normal" class="text-icon-info-active shrink-0" />
16721693
<span class="text-14-regular text-text-strong whitespace-nowrap">
16731694
@{(item as { type: "agent"; name: string }).name}
16741695
</span>
@@ -1891,7 +1912,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
18911912
</Show>
18921913
</div>
18931914
<div class="relative p-3 flex items-center justify-between">
1894-
<div class="flex items-center justify-start gap-0.5">
1915+
<div class="flex items-center justify-start gap-1">
18951916
<Switch>
18961917
<Match when={store.mode === "shell"}>
18971918
<div class="flex items-center gap-2 px-2 h-6">
@@ -1922,12 +1943,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
19221943
title={language.t("command.model.choose")}
19231944
keybind={command.keybind("model.choose")}
19241945
>
1925-
<Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
1946+
<Button as="div" variant="ghost" onClick={() => dialog.render(<DialogSelectModelUnpaid />, "select-model")}>
19261947
<Show when={local.model.current()?.provider?.id}>
19271948
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
19281949
</Show>
19291950
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
1930-
<Icon name="chevron-down" size="small" />
1951+
<MorphChevron expanded={dialog.isActive("select-model")} />
19311952
</Button>
19321953
</TooltipKeybind>
19331954
}
@@ -1938,11 +1959,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
19381959
keybind={command.keybind("model.choose")}
19391960
>
19401961
<ModelSelectorPopover triggerAs={Button} triggerProps={{ variant: "ghost" }}>
1941-
<Show when={local.model.current()?.provider?.id}>
1942-
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
1943-
</Show>
1944-
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
1945-
<Icon name="chevron-down" size="small" />
1962+
{(open) => (
1963+
<>
1964+
<Show when={local.model.current()?.provider?.id}>
1965+
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
1966+
</Show>
1967+
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
1968+
<MorphChevron expanded={open} class="text-text-weak" />
1969+
</>
1970+
)}
19461971
</ModelSelectorPopover>
19471972
</TooltipKeybind>
19481973
</Show>
@@ -1954,10 +1979,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
19541979
>
19551980
<Button
19561981
variant="ghost"
1957-
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
1982+
class="text-text-strong text-12-regular"
19581983
onClick={() => local.model.variant.cycle()}
19591984
>
1960-
{local.model.variant.current() ?? language.t("common.default")}
1985+
<Show when={local.model.variant.list().length > 1}>
1986+
<ReasoningIcon percentage={reasoningPercentage()} size={16} strokeWidth={1.25} />
1987+
</Show>
1988+
<CycleLabel value={currrentModelVariant()} />
19611989
</Button>
19621990
</TooltipKeybind>
19631991
</Show>
@@ -1971,7 +1999,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
19711999
variant="ghost"
19722000
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
19732001
classList={{
1974-
"_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
2002+
"_hidden group-hover/prompt-input:flex items-center justify-center": true,
19752003
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
19762004
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
19772005
}}
@@ -1993,7 +2021,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
19932021
</Match>
19942022
</Switch>
19952023
</div>
1996-
<div class="flex items-center gap-3 absolute right-3 bottom-3">
2024+
<div class="flex items-center gap-2 absolute right-3 bottom-3">
19972025
<input
19982026
ref={fileInputRef}
19992027
type="file"
@@ -2016,7 +2044,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
20162044
onClick={() => fileInputRef.click()}
20172045
aria-label={language.t("prompt.action.attachFile")}
20182046
>
2019-
<Icon name="photo" class="size-4.5" />
2047+
<Icon name="photo" class="size-4.5 text-icon-base" />
20202048
</Button>
20212049
</Tooltip>
20222050
</Show>
@@ -2035,7 +2063,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
20352063
<Match when={true}>
20362064
<div class="flex items-center gap-2">
20372065
<span>{language.t("prompt.action.send")}</span>
2038-
<Icon name="enter" size="small" class="text-icon-base" />
2066+
<Icon name="enter" size="normal" class="text-icon-base" />
20392067
</div>
20402068
</Match>
20412069
</Switch>
@@ -2046,7 +2074,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
20462074
disabled={!prompt.dirty() && !working()}
20472075
icon={working() ? "stop" : "arrow-up"}
20482076
variant="primary"
2049-
class="h-6 w-4.5"
2077+
class="h-6 w-5"
20502078
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
20512079
/>
20522080
</Tooltip>

packages/app/src/components/session-context-usage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
6464
}
6565

6666
const circle = () => (
67-
<div class="p-1">
68-
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
67+
<div class="p-1 text-icon-base">
68+
<ProgressCircle size={16} strokeWidth={1} percentage={context()?.percentage ?? 0} />
6969
</div>
7070
)
7171

@@ -101,7 +101,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
101101
<Button
102102
type="button"
103103
variant="ghost"
104-
class="size-6"
104+
class="size-6 text-icon-base"
105105
onClick={openContext}
106106
aria-label={language.t("context.usage.view")}
107107
>

0 commit comments

Comments
 (0)