summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages
diff options
context:
space:
mode:
authorFilip <[email protected]>2025-09-24 18:05:15 +0200
committerGitHub <[email protected]>2025-09-24 11:05:15 -0500
commitd3b6545e7c6069c9db031634b7890e6b8eb4de2a (patch)
tree5457b28378cf453e2c138337fc1abee3b3423ed2 /packages/app/src/pages
parent3f911b22b011af74426699dac079b33badf862b1 (diff)
downloadopencode-d3b6545e7c6069c9db031634b7890e6b8eb4de2a.tar.gz
opencode-d3b6545e7c6069c9db031634b7890e6b8eb4de2a.zip
feat(app): added command palette (#2630)
Co-authored-by: Adam <[email protected]>
Diffstat (limited to 'packages/app/src/pages')
-rw-r--r--packages/app/src/pages/index.tsx92
1 files changed, 72 insertions, 20 deletions
diff --git a/packages/app/src/pages/index.tsx b/packages/app/src/pages/index.tsx
index 50c729789..9133f40d8 100644
--- a/packages/app/src/pages/index.tsx
+++ b/packages/app/src/pages/index.tsx
@@ -1,8 +1,9 @@
-import { FileIcon, Icon, IconButton, Logo, Tooltip } from "@/ui"
+import { Button, FileIcon, Icon, IconButton, Logo, Tooltip } from "@/ui"
import { Tabs } from "@/ui/tabs"
import { Select } from "@/components/select"
import FileTree from "@/components/file-tree"
import { For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
+import { SelectDialog } from "@/components/select-dialog"
import { useLocal, useSDK } from "@/context"
import { Code } from "@/components/code"
import {
@@ -28,6 +29,7 @@ export default function Page() {
activeItem: undefined as string | undefined,
prompt: "",
dragging: undefined as "left" | "right" | undefined,
+ modelSelectOpen: false,
})
let inputRef: HTMLInputElement | undefined = undefined
@@ -43,6 +45,17 @@ export default function Page() {
})
const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.getModifierState(MOD) && e.shiftKey && e.key.toLowerCase() === "p") {
+ e.preventDefault()
+ setStore("modelSelectOpen", true)
+ return
+ }
+ if (e.getModifierState(MOD) && e.key.toLowerCase() === "p") {
+ e.preventDefault()
+ setStore("modelSelectOpen", true)
+ return
+ }
+
const inputFocused = document.activeElement === inputRef
if (inputFocused) {
if (e.key === "Escape") {
@@ -190,7 +203,7 @@ export default function Page() {
path: { id: session!.id },
body: {
agent: local.agent.current()!.name,
- model: local.model.current(),
+ model: { modelID: local.model.current()!.id, providerID: local.model.current()!.provider.id },
parts: [
{
type: "text",
@@ -265,7 +278,7 @@ export default function Page() {
class="fixed top-0 right-0 h-full border-l border-border-subtle/30 flex flex-col overflow-hidden"
style={`width: ${local.layout.rightWidth()}px`}
>
- <div class="relative flex-1 min-h-0 overflow-y-auto no-scrollbar">
+ <div class="relative flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
<Show when={local.session.active()} fallback={<SessionList />}>
{(activeSession) => (
<div class="relative">
@@ -470,7 +483,7 @@ export default function Page() {
type="text"
value={store.prompt}
onInput={(e) => setStore("prompt", e.currentTarget.value)}
- placeholder="It all starts with a prompt..."
+ placeholder="Placeholder text..."
class="w-full p-1 pb-4 text-text font-light placeholder-text-muted/70 text-sm focus:outline-none"
/>
<div class="flex justify-between items-center text-xs text-text-muted">
@@ -479,24 +492,13 @@ export default function Page() {
options={local.agent.list().map((a) => a.name)}
current={local.agent.current().name}
onSelect={local.agent.set}
- size="sm"
class="uppercase"
/>
- <Select
- options={local.model.list()}
- current={local.model.current()}
- onSelect={local.model.set}
- label={(x) => x.modelID}
- value={(x) => `${x.providerID}.${x.modelID}`}
- filter={{
- keys: ["providerID", "modelID"],
- placeholder: "Filter models",
- }}
- groupBy={(x) => x.providerID}
- size="sm"
- class="uppercase"
- />
- <span class="text-text-muted/70">{local.model.parsed().provider}</span>
+ <Button onClick={() => setStore("modelSelectOpen", true)}>
+ {local.model.current()?.name ?? "Select model"}
+ <Icon name="chevron-down" size={24} class="text-text-muted" />
+ </Button>
+ <span class="text-text-muted/70 whitespace-nowrap">{local.model.current()?.provider.name}</span>
</div>
<div class="flex gap-1 items-center">
<IconButton class="text-text-muted" size="xs" variant="ghost">
@@ -510,6 +512,56 @@ export default function Page() {
</div>
</form>
</div>
+ <Show when={store.modelSelectOpen}>
+ <SelectDialog
+ key={(x) => `${x.provider.id}:${x.id}`}
+ items={local.model.list()}
+ current={local.model.current()}
+ render={(i) => (
+ <div class="w-full flex items-center justify-between">
+ <div class="flex items-center gap-x-2 text-text-muted grow min-w-0">
+ <img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-4 invert opacity-40" />
+ <span class="text-xs text-text whitespace-nowrap">{i.name}</span>
+ <span class="text-xs text-text-muted/80 whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
+ {i.id}
+ </span>
+ </div>
+ <div class="flex items-center gap-x-1 text-text-muted/40 shrink-0">
+ <Tooltip forceMount={false} value="Reasoning">
+ <Icon name="brain" size={16} classList={{ "text-accent": i.reasoning }} />
+ </Tooltip>
+ <Tooltip forceMount={false} value="Tools">
+ <Icon name="hammer" size={16} classList={{ "text-secondary": i.tool_call }} />
+ </Tooltip>
+ <Tooltip forceMount={false} value="Attachments">
+ <Icon name="photo" size={16} classList={{ "text-success": i.attachment }} />
+ </Tooltip>
+ <div class="rounded-full bg-text-muted/20 text-text-muted/80 w-9 h-4 flex items-center justify-center text-[10px]">
+ {new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ compactDisplay: "short",
+ }).format(i.limit.context)}
+ </div>
+ <Tooltip forceMount={false} value={`$${i.cost?.input}/1M input, $${i.cost?.output}/1M output`}>
+ <div class="rounded-full bg-success/20 text-success/80 w-9 h-4 flex items-center justify-center text-[10px]">
+ <Switch fallback="FREE">
+ <Match when={i.cost?.input > 10}>$$$</Match>
+ <Match when={i.cost?.input > 1}>$$</Match>
+ <Match when={i.cost?.input > 0.1}>$</Match>
+ </Switch>
+ </div>
+ </Tooltip>
+ </div>
+ </div>
+ )}
+ filter={{
+ keys: ["provider.name", "name", "id"],
+ }}
+ groupBy={(x) => x.provider.name}
+ onClose={() => setStore("modelSelectOpen", false)}
+ onSelect={(x) => local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined)}
+ />
+ </Show>
</div>
)
}