Skip to content

Commit e6a0675

Browse files
committed
Merge branch 'dev' into feat/markdown-frontmatter-interpolation
2 parents 729a402 + d93c8c7 commit e6a0675

File tree

84 files changed

+438
-152
lines changed

Some content is hidden

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

84 files changed

+438
-152
lines changed

.github/workflows/opencode.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ jobs:
2929
uses: sst/opencode/github@latest
3030
env:
3131
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
32+
OPENCODE_PERMISSION: '{"bash": "deny"}'
3233
with:
3334
model: opencode/claude-haiku-4-5

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</picture>
88
</a>
99
</p>
10-
<p align="center">The AI coding agent built for the terminal.</p>
10+
<p align="center">The open source AI coding agent.</p>
1111
<p align="center">
1212
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
1313
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>

packages/console/app/src/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function App() {
1313
root={(props) => (
1414
<MetaProvider>
1515
<Title>opencode</Title>
16-
<Meta name="description" content="OpenCode - The AI coding agent built for the terminal." />
16+
<Meta name="description" content="OpenCode - The open source coding agent." />
1717
<Favicon />
1818
<Suspense>{props.children}</Suspense>
1919
</MetaProvider>

packages/console/app/src/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export const config = {
99
github: {
1010
repoUrl: "https://github.com/sst/opencode",
1111
starsFormatted: {
12-
compact: "35K",
13-
full: "35,000",
12+
compact: "38K",
13+
full: "38,000",
1414
},
1515
},
1616

@@ -22,8 +22,8 @@ export const config = {
2222

2323
// Static stats (used on landing page)
2424
stats: {
25-
contributors: "350",
26-
commits: "5,000",
25+
contributors: "375",
26+
commits: "5,250",
2727
monthlyUsers: "400,000",
2828
},
2929
} as const

packages/console/app/src/routes/index.tsx

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,9 @@ export default function Home() {
157157
<section data-component="what">
158158
<div data-slot="section-title">
159159
<h3>What is OpenCode?</h3>
160-
<p>OpenCode is an open source agent that helps you write and run code directly from the terminal.</p>
160+
<p>OpenCode is an open source agent that helps you write code in your terminal, IDE, or desktop.</p>
161161
</div>
162162
<ul>
163-
<li>
164-
<span>[*]</span>
165-
<div>
166-
<strong>Native TUI</strong> A responsive, native, themeable terminal UI
167-
</div>
168-
</li>
169163
<li>
170164
<span>[*]</span>
171165
<div>
@@ -199,7 +193,7 @@ export default function Home() {
199193
<li>
200194
<span>[*]</span>
201195
<div>
202-
<strong>Any editor</strong> OpenCode runs in your terminal, pair it with any IDE
196+
<strong>Any editor</strong> Available as a terminal interface, desktop app, and IDE extension
203197
</div>
204198
</li>
205199
</ul>
@@ -651,9 +645,8 @@ export default function Home() {
651645
<ul>
652646
<li>
653647
<Faq question="What is OpenCode?">
654-
OpenCode is an open source agent that helps you write and run code directly from the terminal. You can
655-
pair OpenCode with any AI model, and because it’s terminal-based you can pair it with your preferred
656-
code editor.
648+
OpenCode is an open source agent that helps you write and run code with any AI model. It's available
649+
as a terminal-based interface, desktop app, or IDE extension.
657650
</Faq>
658651
</li>
659652
<li>
@@ -674,7 +667,7 @@ export default function Home() {
674667
</li>
675668
<li>
676669
<Faq question="Can I only use OpenCode in the terminal?">
677-
Yes, for now. We are actively working on a desktop app. Join the waitlist for early access.
670+
Not anymore! OpenCode is now available as an app for your desktop.
678671
</Faq>
679672
</li>
680673
<li>

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

Lines changed: 174 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ import { Select } from "@opencode-ai/ui/select"
1717
import { Tag } from "@opencode-ai/ui/tag"
1818
import { getDirectory, getFilename } from "@opencode-ai/util/path"
1919
import { useLayout } from "@/context/layout"
20+
import { popularProviders, useProviders } from "@/hooks/use-providers"
21+
import { Dialog } from "@opencode-ai/ui/dialog"
22+
import { List, ListRef } from "@opencode-ai/ui/list"
23+
import { iife } from "@opencode-ai/util/iife"
24+
import { Input } from "@opencode-ai/ui/input"
25+
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
26+
import { IconName } from "@opencode-ai/ui/icons/provider"
2027

2128
interface PromptInputProps {
2229
class?: string
@@ -58,6 +65,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
5865
const local = useLocal()
5966
const session = useSession()
6067
const layout = useLayout()
68+
const providers = useProviders()
6169
let editorRef!: HTMLDivElement
6270

6371
const [store, setStore] = createStore<{
@@ -461,60 +469,173 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
461469
<Icon name="chevron-down" size="small" />
462470
</Button>
463471
<Show when={layout.dialog.opened() === "model"}>
464-
<SelectDialog
465-
defaultOpen
466-
onOpenChange={(open) => {
467-
if (open) {
468-
layout.dialog.open("model")
469-
} else {
470-
layout.dialog.close("model")
471-
}
472-
}}
473-
title="Select model"
474-
placeholder="Search models"
475-
emptyMessage="No model results"
476-
key={(x) => `${x.provider.id}:${x.id}`}
477-
items={local.model.list()}
478-
current={local.model.current()}
479-
filterKeys={["provider.name", "name", "id"]}
480-
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
481-
groupBy={(x) => x.provider.name}
482-
sortGroupsBy={(a, b) => {
483-
const order = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
484-
if (a.category === "Recent" && b.category !== "Recent") return -1
485-
if (b.category === "Recent" && a.category !== "Recent") return 1
486-
const aProvider = a.items[0].provider.id
487-
const bProvider = b.items[0].provider.id
488-
if (order.includes(aProvider) && !order.includes(bProvider)) return -1
489-
if (!order.includes(aProvider) && order.includes(bProvider)) return 1
490-
return order.indexOf(aProvider) - order.indexOf(bProvider)
491-
}}
492-
onSelect={(x) =>
493-
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
494-
}
495-
actions={
496-
<Button
497-
class="h-7 -my-1 text-14-medium"
498-
icon="plus-small"
499-
tabIndex={-1}
500-
onClick={() => layout.dialog.open("provider")}
472+
<Switch>
473+
<Match when={providers().connected().length > 0}>
474+
<SelectDialog
475+
defaultOpen
476+
onOpenChange={(open) => {
477+
if (open) {
478+
layout.dialog.open("model")
479+
} else {
480+
layout.dialog.close("model")
481+
}
482+
}}
483+
title="Select model"
484+
placeholder="Search models"
485+
emptyMessage="No model results"
486+
key={(x) => `${x.provider.id}:${x.id}`}
487+
items={local.model.list()}
488+
current={local.model.current()}
489+
filterKeys={["provider.name", "name", "id"]}
490+
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
491+
groupBy={(x) => x.provider.name}
492+
sortGroupsBy={(a, b) => {
493+
if (a.category === "Recent" && b.category !== "Recent") return -1
494+
if (b.category === "Recent" && a.category !== "Recent") return 1
495+
const aProvider = a.items[0].provider.id
496+
const bProvider = b.items[0].provider.id
497+
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
498+
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
499+
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
500+
}}
501+
onSelect={(x) =>
502+
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
503+
}
504+
actions={
505+
<Button
506+
class="h-7 -my-1 text-14-medium"
507+
icon="plus-small"
508+
tabIndex={-1}
509+
onClick={() => layout.dialog.open("provider")}
510+
>
511+
Connect provider
512+
</Button>
513+
}
501514
>
502-
Connect provider
503-
</Button>
504-
}
505-
>
506-
{(i) => (
507-
<div class="w-full flex items-center gap-x-2.5">
508-
<span>{i.name}</span>
509-
<Show when={!i.cost || i.cost?.input === 0}>
510-
<Tag>Free</Tag>
511-
</Show>
512-
<Show when={i.latest}>
513-
<Tag>Latest</Tag>
514-
</Show>
515-
</div>
516-
)}
517-
</SelectDialog>
515+
{(i) => (
516+
<div class="w-full flex items-center gap-x-2.5">
517+
<span>{i.name}</span>
518+
<Show when={!i.cost || i.cost?.input === 0}>
519+
<Tag>Free</Tag>
520+
</Show>
521+
<Show when={i.latest}>
522+
<Tag>Latest</Tag>
523+
</Show>
524+
</div>
525+
)}
526+
</SelectDialog>
527+
</Match>
528+
<Match when={true}>
529+
{iife(() => {
530+
let listRef: ListRef | undefined
531+
const handleKey = (e: KeyboardEvent) => {
532+
if (e.key === "Escape") return
533+
listRef?.onKeyDown(e)
534+
}
535+
return (
536+
<Dialog
537+
modal
538+
defaultOpen
539+
onOpenChange={(open) => {
540+
if (open) {
541+
layout.dialog.open("model")
542+
} else {
543+
layout.dialog.close("model")
544+
}
545+
}}
546+
>
547+
<Dialog.Header>
548+
<Dialog.Title>Select model</Dialog.Title>
549+
<Dialog.CloseButton tabIndex={-1} />
550+
</Dialog.Header>
551+
<Dialog.Body>
552+
<Input hidden type="text" class="opacity-0 size-0" autofocus onKeyDown={handleKey} />
553+
<div class="flex flex-col gap-3 px-2.5">
554+
<div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
555+
<List
556+
ref={(ref) => (listRef = ref)}
557+
items={local.model.list()}
558+
current={local.model.current()}
559+
key={(x) => `${x.provider.id}:${x.id}`}
560+
onSelect={(x) => {
561+
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
562+
recent: true,
563+
})
564+
layout.dialog.close("model")
565+
}}
566+
>
567+
{(i) => (
568+
<div class="w-full flex items-center gap-x-2.5">
569+
<span>{i.name}</span>
570+
<Tag>Free</Tag>
571+
<Show when={i.latest}>
572+
<Tag>Latest</Tag>
573+
</Show>
574+
</div>
575+
)}
576+
</List>
577+
<div />
578+
<div />
579+
</div>
580+
<div class="px-1.5 pb-1.5">
581+
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
582+
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-6">
583+
<div class="px-2 text-14-medium text-text-base">
584+
Add more models from popular providers
585+
</div>
586+
<List
587+
class="w-full"
588+
key={(x) => x?.id}
589+
items={providers().popular()}
590+
activeIcon="plus-small"
591+
sortBy={(a, b) => {
592+
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
593+
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
594+
return a.name.localeCompare(b.name)
595+
}}
596+
onSelect={(x) => {
597+
layout.dialog.close("model")
598+
}}
599+
>
600+
{(i) => (
601+
<div class="w-full flex items-center gap-x-4">
602+
<ProviderIcon
603+
data-slot="list-item-extra-icon"
604+
id={i.id as IconName}
605+
// TODO: clean this up after we update icon in models.dev
606+
classList={{
607+
"text-icon-weak-base": true,
608+
"size-4 mx-0.5": i.id === "opencode",
609+
"size-5": i.id !== "opencode",
610+
}}
611+
/>
612+
<span>{i.name}</span>
613+
<Show when={i.id === "opencode"}>
614+
<Tag>Recommended</Tag>
615+
</Show>
616+
<Show when={i.id === "anthropic"}>
617+
<div class="text-14-regular text-text-weak">
618+
Connect with Claude Pro/Max or API key
619+
</div>
620+
</Show>
621+
</div>
622+
)}
623+
</List>
624+
<Button variant="ghost" class="w-full justify-start">
625+
<div class="flex items-center gap-2">
626+
<Icon name="plus-small" />
627+
<div class="text-text-strong">View all providers</div>
628+
</div>
629+
</Button>
630+
</div>
631+
</div>
632+
</div>
633+
</Dialog.Body>
634+
</Dialog>
635+
)
636+
})}
637+
</Match>
638+
</Switch>
518639
</Show>
519640
</div>
520641
<Tooltip

packages/desktop/src/context/layout.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
4848
open: undefined as undefined | "provider" | "model",
4949
},
5050
})
51+
const usedColors = new Set<string>()
5152

5253
function pickAvailableColor() {
53-
const available = PASTEL_COLORS.filter((c) => !colors().has(c))
54+
const available = PASTEL_COLORS.filter((c) => !usedColors.has(c))
5455
if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
5556
return available[Math.floor(Math.random() * available.length)]
5657
}
@@ -69,21 +70,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
6970
function colorize(project: Project & { expanded: boolean }) {
7071
if (project.icon?.color) return project
7172
const color = pickAvailableColor()
73+
usedColors.add(color)
7274
project.icon = { ...project.icon, color }
7375
globalSdk.client.project.update({ projectID: project.id, icon: { color } })
7476
return project
7577
}
7678

7779
const enriched = createMemo(() => store.projects.flatMap(enrich))
7880
const list = createMemo(() => enriched().flatMap(colorize))
79-
const colors = createMemo(
80-
() =>
81-
new Set(
82-
list()
83-
.map((p) => p.icon?.color)
84-
.filter(Boolean),
85-
),
86-
)
8781

8882
async function loadProjectSessions(directory: string) {
8983
const [, setStore] = globalSync.child(directory)

0 commit comments

Comments
 (0)