Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const Loading = () => <div class="size-full" />

declare global {
interface Window {
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; serverUrl?: string; port?: number }
}
}

Expand Down Expand Up @@ -69,6 +69,20 @@ export function AppInterface(props: { defaultUrl?: string }) {
const defaultServerUrl = () => {
if (props.defaultUrl) return props.defaultUrl
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (window.__OPENCODE__?.serverUrl) return window.__OPENCODE__.serverUrl
if (window.__OPENCODE__?.port) return `http://127.0.0.1:${window.__OPENCODE__.port}`

// For remote access (e.g., mobile via Tailscale) in dev mode, use same origin
// (Vite proxy handles forwarding to the actual server, avoiding CORS)
if (import.meta.env.DEV && location.hostname !== "localhost" && location.hostname !== "127.0.0.1") {
return window.location.origin
}

// For remote access in production, use same hostname on port 4096
if (location.hostname !== "localhost" && location.hostname !== "127.0.0.1") {
return `${location.protocol}//${location.hostname}:4096`
}

if (import.meta.env.DEV)
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`

Expand Down
72 changes: 33 additions & 39 deletions packages/app/src/components/dialog-select-directory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
import { List } from "@opencode-ai/ui/list"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createMemo } from "solid-js"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { usePlatform } from "@/context/platform"
import {
joinPath,
displayPath,
normalizeQuery,
projectsToRelative,
filterProjects,
combineResults,
} from "@/utils/directory-search"

interface DialogSelectDirectoryProps {
title?: string
Expand All @@ -15,67 +25,51 @@ interface DialogSelectDirectoryProps {

export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
const sync = useGlobalSync()
const sdk = useGlobalSDK()
const globalSdk = useGlobalSDK()
const platform = usePlatform()
const dialog = useDialog()

const home = createMemo(() => sync.data.path.home)
const root = createMemo(() => sync.data.path.home || sync.data.path.directory)

function join(base: string | undefined, rel: string) {
const b = (base ?? "").replace(/[\\/]+$/, "")
const r = rel.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")
if (!b) return r
if (!r) return b
return b + "/" + r
}
// Create SDK client with home directory for file search
const sdk = createMemo(() =>
createOpencodeClient({
baseUrl: globalSdk.url,
fetch: platform.fetch,
directory: root(),
throwOnError: true,
}),
)

function display(rel: string) {
const full = join(root(), rel)
const h = home()
if (!h) return full
if (full === h) return "~"
if (full.startsWith(h + "/") || full.startsWith(h + "\\")) {
return "~" + full.slice(h.length)
}
return full
return displayPath(joinPath(root(), rel), home())
}

function normalizeQuery(query: string) {
const h = home()

if (!query) return query
if (query.startsWith("~/")) return query.slice(2)

if (h) {
const lc = query.toLowerCase()
const hc = h.toLowerCase()
if (lc === hc || lc.startsWith(hc + "/") || lc.startsWith(hc + "\\")) {
return query.slice(h.length).replace(/^[\\/]+/, "")
}
}

return query
}
// Get known projects from the server
const knownProjects = createMemo(() => projectsToRelative(sync.data.project, home()))

async function fetchDirs(query: string) {
const directory = root()
if (!directory) return [] as string[]

const results = await sdk.client.find
.files({ directory, query, type: "directory", limit: 50 })
.then((x) => x.data ?? [])
const results = await sdk()
.find.files({ directory, query, type: "directory", limit: 50 })
.then((x) => (Array.isArray(x.data) ? x.data : []))
.catch(() => [])

return results.map((x) => x.replace(/[\\/]+$/, ""))
return results.map((x: string) => x.replace(/[\\/]+$/, ""))
}

const directories = async (filter: string) => {
const query = normalizeQuery(filter.trim())
return fetchDirs(query)
const query = normalizeQuery(filter.trim(), home()).toLowerCase()
const matchingProjects = filterProjects(knownProjects(), query)
const searchResults = await fetchDirs(query)
return combineResults(matchingProjects, searchResults, 50)
}

function resolve(rel: string) {
const absolute = join(root(), rel)
const absolute = joinPath(root(), rel)
props.onSelect(props.multiple ? [absolute] : absolute)
dialog.close()
}
Expand Down
14 changes: 8 additions & 6 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,11 +372,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {

type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string }

const agentList = createMemo(() =>
sync.data.agent
const agentList = createMemo(() => {
const agents = sync.data.agent
if (!Array.isArray(agents)) return []
return agents
.filter((agent) => !agent.hidden && agent.mode !== "primary")
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
)
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name }))
})

const handleAtSelect = (option: AtOption | undefined) => {
if (!option) return
Expand Down Expand Up @@ -1547,8 +1549,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
</Show>
</div>
<div class="relative p-3 flex items-center justify-between">
<div class="flex items-center justify-start gap-0.5">
<div class="relative p-2 md:p-3 flex items-center justify-between gap-1.5 md:gap-2">
<div class="flex items-center justify-start gap-0 min-w-0 overflow-hidden">
<Switch>
<Match when={store.mode === "shell"}>
<div class="flex items-center gap-2 px-2 h-6">
Expand Down
8 changes: 6 additions & 2 deletions packages/app/src/context/file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,13 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
selectedLines,
setSelectedLines,
searchFiles: (query: string) =>
sdk.client.find.files({ query, dirs: "false" }).then((x) => (x.data ?? []).map(normalize)),
sdk.client.find
.files({ query, dirs: "false" })
.then((x) => (Array.isArray(x.data) ? x.data : []).map(normalize)),
searchFilesAndDirectories: (query: string) =>
sdk.client.find.files({ query, dirs: "true" }).then((x) => (x.data ?? []).map(normalize)),
sdk.client.find
.files({ query, dirs: "true" })
.then((x) => (Array.isArray(x.data) ? x.data : []).map(normalize)),
}
},
})
32 changes: 20 additions & 12 deletions packages/app/src/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,25 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}

const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const list = createMemo(() => {
const agents = sync.data.agent
if (!agents || typeof agents.filter !== "function") return []
return agents.filter((x) => x.mode !== "subagent" && !x.hidden)
})
const [store, setStore] = createStore<{
current?: string
}>({
current: list()[0]?.name,
current: undefined,
})
return {
list,
current() {
const available = list()
const available = list() ?? []
if (available.length === 0) return undefined
return available.find((x) => x.name === store.current) ?? available[0]
},
set(name: string | undefined) {
const available = list()
const available = list() ?? []
if (available.length === 0) {
setStore("current", undefined)
return
Expand All @@ -89,7 +93,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setStore("current", available[0].name)
},
move(direction: 1 | -1) {
const available = list()
const available = list() ?? []
if (available.length === 0) {
setStore("current", undefined)
return
Expand Down Expand Up @@ -129,14 +133,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
model: {},
})

const available = createMemo(() =>
providers.connected().flatMap((p) =>
Object.values(p.models).map((m) => ({
const available = createMemo(() => {
const conn = providers.connected()
if (!Array.isArray(conn)) return []
return conn.flatMap((p) =>
Object.values(p.models ?? {}).map((m) => ({
...m,
provider: p,
})),
),
)
)
})

const latest = createMemo(() =>
pipe(
Expand Down Expand Up @@ -206,7 +212,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}

throw new Error("No default model found")
// Return undefined instead of throwing during initial load
return undefined
})

const current = createMemo(() => {
Expand Down Expand Up @@ -264,7 +271,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => {
const currentAgent = agent.current()
if (currentAgent) setEphemeral("model", currentAgent.name, model ?? fallbackModel())
const fallback = fallbackModel()
if (currentAgent && (model || fallback)) setEphemeral("model", currentAgent.name, model ?? fallback!)
if (model) updateVisibility(model, "show")
if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
Expand Down
Loading