Skip to content

Commit 90f1e16

Browse files
committed
Merge branch 'shuvcode-dev' into integration
Resolved conflict in bun.lock by regenerating lockfile. Closes #308
2 parents aa340fe + e0faaa0 commit 90f1e16

File tree

118 files changed

+10008
-7094
lines changed

Some content is hidden

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

118 files changed

+10008
-7094
lines changed

.github/last-synced-tag

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v1.1.23
1+
v1.1.25

.github/workflows/nix-desktop.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ jobs:
2525
matrix:
2626
os:
2727
- blacksmith-4vcpu-ubuntu-2404
28+
- blacksmith-4vcpu-ubuntu-2404-arm
29+
- macos-15
2830
- macos-latest
2931
runs-on: ${{ matrix.os }}
3032
timeout-minutes: 60
@@ -33,7 +35,7 @@ jobs:
3335
uses: actions/checkout@v6
3436

3537
- name: Setup Nix
36-
uses: DeterminateSystems/nix-installer-action@v21
38+
uses: nixbuild/nix-quick-install-action@v34
3739

3840
- name: Build desktop via flake
3941
run: |

STATS.md

Lines changed: 202 additions & 201 deletions
Large diffs are not rendered by default.

bun.lock

Lines changed: 92 additions & 48 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infra/console.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const ZEN_MODELS = [
119119
new sst.Secret("ZEN_MODELS5"),
120120
new sst.Secret("ZEN_MODELS6"),
121121
new sst.Secret("ZEN_MODELS7"),
122+
new sst.Secret("ZEN_MODELS8"),
122123
]
123124
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
124125
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")

nix/hashes.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"nodeModules": {
3-
"x86_64-linux": "sha256-4ndHIlS9t1ynRdFszJ1nvcu3YhunhuOc7jcuHI1FbnM=",
4-
"aarch64-linux": "sha256-H9eUk/yVrQqVrAYONlb6As7mjkPXtOauBVfMBeVAmRo=",
5-
"aarch64-darwin": "sha256-C0E9KAEj3GI83HwirIL2zlXYIe92T+7Iv6F51BB6slY=",
6-
"x86_64-darwin": "sha256-wj5fZnyfu6Sf1HcqvsQM3M7dl5BKRAHmoqm1Ai1cL2M="
3+
"x86_64-linux": "sha256-07XxcHLuToM4QfWVyaPLACxjPZ93ZM7gtpX2o08Lp18=",
4+
"aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=",
5+
"aarch64-darwin": "sha256-U2UvE70nM0OI0VhIku8qnX+ptPbA+Q/y1BGXbFMcyt4=",
6+
"x86_64-darwin": "sha256-CpZFHBMPJSib2Vqs6oC8HQjQtviPUMa/qezHAe22N/A="
77
}
88
}

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opencode-ai/app",
3-
"version": "1.1.23",
3+
"version": "1.1.25",
44
"description": "",
55
"type": "module",
66
"exports": {

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

Lines changed: 153 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,175 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
44
import { List } from "@opencode-ai/ui/list"
55
import { getDirectory, getFilename } from "@opencode-ai/util/path"
66
import { useParams } from "@solidjs/router"
7-
import { createMemo } from "solid-js"
7+
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
8+
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
89
import { useLayout } from "@/context/layout"
910
import { useFile } from "@/context/file"
1011

12+
type EntryType = "command" | "file"
13+
14+
type Entry = {
15+
id: string
16+
type: EntryType
17+
title: string
18+
description?: string
19+
keybind?: string
20+
category: "Commands" | "Files"
21+
option?: CommandOption
22+
path?: string
23+
}
24+
1125
export function DialogSelectFile() {
26+
const command = useCommand()
1227
const layout = useLayout()
1328
const file = useFile()
1429
const dialog = useDialog()
1530
const params = useParams()
1631
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
1732
const tabs = createMemo(() => layout.tabs(sessionKey()))
1833
const view = createMemo(() => layout.view(sessionKey()))
34+
const state = { cleanup: undefined as (() => void) | void, committed: false }
35+
const [grouped, setGrouped] = createSignal(false)
36+
const common = ["session.new", "session.previous", "session.next", "terminal.toggle", "review.toggle"]
37+
const limit = 5
38+
39+
const allowed = createMemo(() =>
40+
command.options.filter(
41+
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
42+
),
43+
)
44+
45+
const commandItem = (option: CommandOption): Entry => ({
46+
id: "command:" + option.id,
47+
type: "command",
48+
title: option.title,
49+
description: option.description,
50+
keybind: option.keybind,
51+
category: "Commands",
52+
option,
53+
})
54+
55+
const fileItem = (path: string): Entry => ({
56+
id: "file:" + path,
57+
type: "file",
58+
title: path,
59+
category: "Files",
60+
path,
61+
})
62+
63+
const list = createMemo(() => allowed().map(commandItem))
64+
65+
const picks = createMemo(() => {
66+
const all = allowed()
67+
const order = new Map(common.map((id, index) => [id, index]))
68+
const picked = all.filter((option) => order.has(option.id))
69+
const base = picked.length ? picked : all.slice(0, limit)
70+
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
71+
return sorted.map(commandItem)
72+
})
73+
74+
const recent = createMemo(() => {
75+
const all = tabs().all()
76+
const active = tabs().active()
77+
const order = active ? [active, ...all.filter((item) => item !== active)] : all
78+
const seen = new Set<string>()
79+
const items: Entry[] = []
80+
81+
for (const item of order) {
82+
const path = file.pathFromTab(item)
83+
if (!path) continue
84+
if (seen.has(path)) continue
85+
seen.add(path)
86+
items.push(fileItem(path))
87+
}
88+
89+
return items.slice(0, limit)
90+
})
91+
92+
const items = async (filter: string) => {
93+
const query = filter.trim()
94+
setGrouped(query.length > 0)
95+
if (!query) return [...picks(), ...recent()]
96+
const files = await file.searchFiles(query)
97+
const entries = files.map(fileItem)
98+
return [...list(), ...entries]
99+
}
100+
101+
const handleMove = (item: Entry | undefined) => {
102+
state.cleanup?.()
103+
if (!item) return
104+
if (item.type !== "command") return
105+
state.cleanup = item.option?.onHighlight?.()
106+
}
107+
108+
const open = (path: string) => {
109+
const value = file.tab(path)
110+
tabs().open(value)
111+
file.load(path)
112+
view().reviewPanel.open()
113+
}
114+
115+
const handleSelect = (item: Entry | undefined) => {
116+
if (!item) return
117+
state.committed = true
118+
state.cleanup = undefined
119+
dialog.close()
120+
121+
if (item.type === "command") {
122+
item.option?.onSelect?.("palette")
123+
return
124+
}
125+
126+
if (!item.path) return
127+
open(item.path)
128+
}
129+
130+
onCleanup(() => {
131+
if (state.committed) return
132+
state.cleanup?.()
133+
})
134+
19135
return (
20-
<Dialog title="Select file">
136+
<Dialog title="Search">
21137
<List
22-
search={{ placeholder: "Search files", autofocus: true }}
23-
emptyMessage="No files found"
24-
items={file.searchFiles}
25-
key={(x) => x}
26-
onSelect={(path) => {
27-
if (path) {
28-
const value = file.tab(path)
29-
tabs().open(value)
30-
file.load(path)
31-
view().reviewPanel.open()
32-
}
33-
dialog.close()
34-
}}
138+
search={{ placeholder: "Search files and commands", autofocus: true }}
139+
emptyMessage="No results found"
140+
items={items}
141+
key={(item) => item.id}
142+
filterKeys={["title", "description", "category"]}
143+
groupBy={(item) => (grouped() ? item.category : "")}
144+
onMove={handleMove}
145+
onSelect={handleSelect}
35146
>
36-
{(i) => (
37-
<div class="w-full flex items-center justify-between rounded-md">
38-
<div class="flex items-center gap-x-3 grow min-w-0">
39-
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
40-
<div class="flex items-center text-14-regular">
41-
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
42-
{getDirectory(i)}
43-
</span>
44-
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
147+
{(item) => (
148+
<Show
149+
when={item.type === "command"}
150+
fallback={
151+
<div class="w-full flex items-center justify-between rounded-md">
152+
<div class="flex items-center gap-x-3 grow min-w-0">
153+
<FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
154+
<div class="flex items-center text-14-regular">
155+
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
156+
{getDirectory(item.path ?? "")}
157+
</span>
158+
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
159+
</div>
160+
</div>
161+
</div>
162+
}
163+
>
164+
<div class="w-full flex items-center justify-between gap-4">
165+
<div class="flex items-center gap-2 min-w-0">
166+
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
167+
<Show when={item.description}>
168+
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
169+
</Show>
45170
</div>
171+
<Show when={item.keybind}>
172+
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(item.keybind ?? "")}</span>
173+
</Show>
46174
</div>
47-
</div>
175+
</Show>
48176
)}
49177
</List>
50178
</Dialog>

packages/app/src/components/titlebar.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ export function Titlebar() {
8181
>
8282
<Show when={mac()}>
8383
<div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
84+
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
85+
<IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
86+
</div>
87+
</Show>
88+
<Show when={!mac()}>
89+
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
90+
<IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
91+
</div>
8492
</Show>
85-
<IconButton
86-
icon="menu"
87-
variant="ghost"
88-
class="xl:hidden size-8 rounded-md"
89-
onClick={layout.mobileSidebar.toggle}
90-
/>
9193
<TooltipKeybind
9294
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
9395
placement="bottom"

packages/app/src/context/command.tsx

Lines changed: 12 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
1+
import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
22
import { createSimpleContext } from "@opencode-ai/ui/context"
3-
import { useDialog } from "@opencode-ai/ui/context/dialog"
4-
import { Dialog } from "@opencode-ai/ui/dialog"
5-
import { List } from "@opencode-ai/ui/list"
63

74
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
85

@@ -114,67 +111,11 @@ export function formatKeybind(config: string): string {
114111
return IS_MAC ? parts.join("") : parts.join("+")
115112
}
116113

117-
function DialogCommand(props: { options: CommandOption[] }) {
118-
const dialog = useDialog()
119-
let cleanup: (() => void) | void
120-
let committed = false
121-
122-
const handleMove = (option: CommandOption | undefined) => {
123-
cleanup?.()
124-
cleanup = option?.onHighlight?.()
125-
}
126-
127-
const handleSelect = (option: CommandOption | undefined) => {
128-
if (option) {
129-
committed = true
130-
cleanup = undefined
131-
dialog.close()
132-
option.onSelect?.("palette")
133-
}
134-
}
135-
136-
onCleanup(() => {
137-
if (!committed) {
138-
cleanup?.()
139-
}
140-
})
141-
142-
return (
143-
<Dialog title="Commands">
144-
<List
145-
search={{ placeholder: "Search commands", autofocus: true }}
146-
emptyMessage="No commands found"
147-
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
148-
key={(x) => x?.id}
149-
filterKeys={["title", "description", "category"]}
150-
groupBy={(x) => x.category ?? ""}
151-
onMove={handleMove}
152-
onSelect={handleSelect}
153-
>
154-
{(option) => (
155-
<div class="w-full flex items-center justify-between gap-4">
156-
<div class="flex items-center gap-2 min-w-0">
157-
<span class="text-14-regular text-text-strong whitespace-nowrap">{option.title}</span>
158-
<Show when={option.description}>
159-
<span class="text-14-regular text-text-weak truncate">{option.description}</span>
160-
</Show>
161-
</div>
162-
<Show when={option.keybind}>
163-
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(option.keybind!)}</span>
164-
</Show>
165-
</div>
166-
)}
167-
</List>
168-
</Dialog>
169-
)
170-
}
171-
172114
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
173115
name: "Command",
174116
init: () => {
175117
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
176118
const [suspendCount, setSuspendCount] = createSignal(0)
177-
const dialog = useDialog()
178119

179120
const options = createMemo(() => {
180121
const seen = new Set<string>()
@@ -202,12 +143,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
202143

203144
const suspended = () => suspendCount() > 0
204145

205-
const showPalette = () => {
206-
if (!dialog.active) {
207-
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
146+
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
147+
for (const option of options()) {
148+
if (option.id === id || option.id === "suggested." + id) {
149+
option.onSelect?.(source)
150+
return
151+
}
208152
}
209153
}
210154

155+
const showPalette = () => {
156+
run("file.open", "palette")
157+
}
158+
211159
const handleKeyDown = (event: KeyboardEvent) => {
212160
if (suspended()) return
213161

@@ -248,12 +196,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
248196
})
249197
},
250198
trigger(id: string, source?: "palette" | "keybind" | "slash") {
251-
for (const option of options()) {
252-
if (option.id === id || option.id === "suggested." + id) {
253-
option.onSelect?.(source)
254-
return
255-
}
256-
}
199+
run(id, source)
257200
},
258201
keybind(id: string) {
259202
const option = options().find((x) => x.id === id || x.id === "suggested." + id)

0 commit comments

Comments
 (0)