- Plan and build anything
+
+
+
+
+ {(i) => (
+
+
+
+
+
+ {getDirectory(i)}/
+
+ {getFilename(i)}
+
+
+
+
+ )}
+
+
+
+
)
}
@@ -223,7 +367,7 @@ function isEqual(arrA: ContentPart[], arrB: ContentPart[]): boolean {
if (partA.type === "text" && partA.content !== (partB as TextPart).content) {
return false
}
- if (partA.type === "attachment" && partA.fileId !== (partB as AttachmentPart).fileId) {
+ if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) {
return false
}
}
@@ -241,24 +385,48 @@ function getCursorPosition(parent: HTMLElement): number {
}
function setCursorPosition(parent: HTMLElement, position: number) {
- let child = parent.firstChild
- let offset = position
- while (child) {
- if (offset > child.textContent!.length) {
- offset -= child.textContent!.length
- child = child.nextSibling
- } else {
- try {
- const range = document.createRange()
- const sel = window.getSelection()
- range.setStart(child, offset)
- range.collapse(true)
- sel?.removeAllRanges()
- sel?.addRange(range)
- } catch (e) {
- console.error("Failed to set cursor position.", e)
- }
+ let remaining = position
+ let node = parent.firstChild
+ while (node) {
+ const length = node.textContent ? node.textContent.length : 0
+ const isText = node.nodeType === Node.TEXT_NODE
+ const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file"
+
+ if (isText && remaining <= length) {
+ const range = document.createRange()
+ const selection = window.getSelection()
+ range.setStart(node, remaining)
+ range.collapse(true)
+ selection?.removeAllRanges()
+ selection?.addRange(range)
return
}
+
+ if (isFile && remaining <= length) {
+ const range = document.createRange()
+ const selection = window.getSelection()
+ range.setStartAfter(node)
+ range.collapse(true)
+ selection?.removeAllRanges()
+ selection?.addRange(range)
+ return
+ }
+
+ remaining -= length
+ node = node.nextSibling
}
+
+ const fallbackRange = document.createRange()
+ const fallbackSelection = window.getSelection()
+ const last = parent.lastChild
+ if (last && last.nodeType === Node.TEXT_NODE) {
+ const len = last.textContent ? last.textContent.length : 0
+ fallbackRange.setStart(last, len)
+ }
+ if (!last || last.nodeType !== Node.TEXT_NODE) {
+ fallbackRange.selectNodeContents(parent)
+ }
+ fallbackRange.collapse(false)
+ fallbackSelection?.removeAllRanges()
+ fallbackSelection?.addRange(fallbackRange)
}
diff --git a/packages/desktop/src/components/select-dialog.tsx b/packages/desktop/src/components/select-dialog.tsx
deleted file mode 100644
index bf9aa0dbd..000000000
--- a/packages/desktop/src/components/select-dialog.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import { createEffect, Show, For, createMemo, type JSX, createResource } from "solid-js"
-import { Dialog } from "@kobalte/core/dialog"
-import { Icon } from "@opencode-ai/ui"
-import { IconButton } from "@/ui"
-import { createStore } from "solid-js/store"
-import { entries, flatMap, groupBy, map, pipe } from "remeda"
-import { createList } from "solid-list"
-import fuzzysort from "fuzzysort"
-
-interface SelectDialogProps
{
- items: T[] | ((filter: string) => Promise)
- key: (item: T) => string
- render: (item: T) => JSX.Element
- filter?: string[]
- current?: T
- placeholder?: string
- groupBy?: (x: T) => string
- onSelect?: (value: T | undefined) => void
- onClose?: () => void
-}
-
-export function SelectDialog(props: SelectDialogProps) {
- let scrollRef: HTMLDivElement | undefined
- const [store, setStore] = createStore({
- filter: "",
- mouseActive: false,
- })
-
- const [grouped] = createResource(
- () => store.filter,
- async (filter) => {
- const needle = filter.toLowerCase()
- const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || []
- const result = pipe(
- all,
- (x) => {
- if (!needle) return x
- if (!props.filter && Array.isArray(x) && x.every((e) => typeof e === "string")) {
- return fuzzysort.go(needle, x).map((x) => x.target) as T[]
- }
- return fuzzysort.go(needle, x, { keys: props.filter! }).map((x) => x.obj)
- },
- groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
- // mapValues((x) => x.sort((a, b) => props.key(a).localeCompare(props.key(b)))),
- entries(),
- map(([k, v]) => ({ category: k, items: v })),
- )
- return result
- },
- )
- const flat = createMemo(() => {
- return pipe(
- grouped() || [],
- flatMap((x) => x.items),
- )
- })
- const list = createList({
- items: () => flat().map(props.key),
- initialActive: props.current ? props.key(props.current) : undefined,
- loop: true,
- })
- const resetSelection = () => {
- const all = flat()
- if (all.length === 0) return
- list.setActive(props.key(all[0]))
- }
-
- createEffect(() => {
- store.filter
- scrollRef?.scrollTo(0, 0)
- resetSelection()
- })
-
- createEffect(() => {
- const all = flat()
- if (store.mouseActive || all.length === 0) return
- if (list.active() === props.key(all[0])) {
- scrollRef?.scrollTo(0, 0)
- return
- }
- const element = scrollRef?.querySelector(`[data-key="${list.active()}"]`)
- element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
- })
-
- const handleInput = (value: string) => {
- setStore("filter", value)
- resetSelection()
- }
-
- const handleSelect = (item: T) => {
- props.onSelect?.(item)
- props.onClose?.()
- }
-
- const handleKey = (e: KeyboardEvent) => {
- setStore("mouseActive", false)
-
- if (e.key === "Enter") {
- e.preventDefault()
- const selected = flat().find((x) => props.key(x) === list.active())
- if (selected) handleSelect(selected)
- } else if (e.key === "Escape") {
- e.preventDefault()
- props.onClose?.()
- } else {
- list.onKeyDown(e)
- }
- }
-
- return (
-
- )
-}
diff --git a/packages/desktop/src/components/session-timeline.tsx b/packages/desktop/src/components/session-timeline.tsx
index 2474b3101..0d8a7cd3c 100644
--- a/packages/desktop/src/components/session-timeline.tsx
+++ b/packages/desktop/src/components/session-timeline.tsx
@@ -1,11 +1,10 @@
import { useLocal, useSync } from "@/context"
import { Icon, Tooltip } from "@opencode-ai/ui"
import { Collapsible } from "@/ui"
-import type { AssistantMessage, Part, ToolPart } from "@opencode-ai/sdk"
+import type { AssistantMessage, Message, Part, ToolPart } from "@opencode-ai/sdk"
import { DateTime } from "luxon"
import {
createSignal,
- onMount,
For,
Match,
splitProps,
@@ -67,7 +66,7 @@ function ReadToolPart(props: { part: ToolPart }) {
{(state) => {
const path = state().input["filePath"] as string
return (
- local.file.open(path)}>
+ local.file.open(path)}>
Read {getFilename(path)}
)
@@ -253,7 +252,7 @@ export default function SessionTimeline(props: { session: string; class?: string
case "patch":
return false
case "text":
- return !part.synthetic
+ return !part.synthetic && part.text.trim()
case "reasoning":
return part.text.trim()
case "tool":
@@ -270,8 +269,17 @@ export default function SessionTimeline(props: { session: string; class?: string
}
}
+ const hasValidParts = (message: Message) => {
+ return sync.data.part[message.id]?.filter(valid).length > 0
+ }
+
+ const hasTextPart = (message: Message) => {
+ return !!sync.data.part[message.id]?.filter(valid).find((p) => p.type === "text")
+ }
+
const session = createMemo(() => sync.session.get(props.session))
const messages = createMemo(() => sync.data.message[props.session] ?? [])
+ const messagesWithValidParts = createMemo(() => sync.data.message[props.session]?.filter(hasValidParts) ?? [])
const working = createMemo(() => {
const last = messages()[messages().length - 1]
if (!last) return false
@@ -386,7 +394,7 @@ export default function SessionTimeline(props: { session: string; class?: string
[props.class ?? ""]: !!props.class,
}}
>
-
+
@@ -397,11 +405,16 @@ export default function SessionTimeline(props: { session: string; class?: string
{cost()}
-
-
+
+
{(message) => (
-
-
+
+
{(part) => (
-
{part.type}
}>
@@ -449,9 +462,9 @@ export default function SessionTimeline(props: { session: string; class?: string
-
+
Raw Session Data
-
+
@@ -460,9 +473,9 @@ export default function SessionTimeline(props: { session: string; class?: string
-
+
session
-
+
@@ -477,9 +490,9 @@ export default function SessionTimeline(props: { session: string; class?: string
-
+
{message.role === "user" ? "user" : "assistant"}
-
+
@@ -493,9 +506,9 @@ export default function SessionTimeline(props: { session: string; class?: string
-
+
{part.type}
-
+
diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx
index 80473d84a..58d479111 100644
--- a/packages/desktop/src/pages/index.tsx
+++ b/packages/desktop/src/pages/index.tsx
@@ -1,16 +1,14 @@
-import { Button, Icon, List, Tooltip } from "@opencode-ai/ui"
-import { FileIcon, IconButton } from "@/ui"
+import { Button, Icon, List, SelectDialog, Tooltip } from "@opencode-ai/ui"
+import { FileIcon } from "@/ui"
import FileTree from "@/components/file-tree"
import EditorPane from "@/components/editor-pane"
-import { For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
-import { SelectDialog } from "@/components/select-dialog"
+import { For, onCleanup, onMount, Show } from "solid-js"
import { useSync, useSDK, useLocal } from "@/context"
import type { LocalFile, TextSelection } from "@/context/local"
import SessionTimeline from "@/components/session-timeline"
-import { type PromptContentPart, type PromptSubmitValue } from "@/components/prompt-form"
import { createStore } from "solid-js/store"
import { getDirectory, getFilename } from "@/utils"
-import { PromptInput } from "@/components/prompt-input"
+import { ContentPart, PromptInput } from "@/components/prompt-input"
import { DateTime } from "luxon"
export default function Page() {
@@ -22,8 +20,7 @@ export default function Page() {
modelSelectOpen: false,
fileSelectOpen: false,
})
-
- let inputRef: HTMLTextAreaElement | undefined = undefined
+ let inputRef!: HTMLDivElement
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
@@ -50,7 +47,7 @@ export default function Page() {
const focused = document.activeElement === inputRef
if (focused) {
if (event.key === "Escape") {
- // inputRef?.blur()
+ inputRef?.blur()
}
return
}
@@ -77,7 +74,7 @@ export default function Page() {
}
if (event.key.length === 1 && event.key !== "Unidentified") {
- // inputRef?.focus()
+ inputRef?.focus()
}
}
@@ -104,9 +101,7 @@ export default function Page() {
}
}
- const handlePromptSubmit2 = () => {}
-
- const handlePromptSubmit = async (prompt: PromptSubmitValue) => {
+ const handlePromptSubmit = async (parts: ContentPart[]) => {
const existingSession = local.session.active()
let session = existingSession
if (!session) {
@@ -134,6 +129,7 @@ export default function Page() {
const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
+ const text = parts.map((part) => part.content).join("")
const attachments = new Map()
const registerAttachment = (path: string, selection: TextSelection | undefined, label?: string) => {
@@ -147,30 +143,27 @@ export default function Page() {
})
}
- const promptAttachments = prompt.parts.filter(
- (part): part is Extract => part.kind === "attachment",
- )
-
+ const promptAttachments = parts.filter((part) => part.type === "file")
for (const part of promptAttachments) {
- registerAttachment(part.path, part.selection, part.display)
+ registerAttachment(part.path, part.selection, part.content)
}
- const activeFile = local.context.active()
- if (activeFile) {
- registerAttachment(
- activeFile.path,
- activeFile.selection,
- activeFile.name ?? formatAttachmentLabel(activeFile.path, activeFile.selection),
- )
- }
+ // const activeFile = local.context.active()
+ // if (activeFile) {
+ // registerAttachment(
+ // activeFile.path,
+ // activeFile.selection,
+ // activeFile.name ?? formatAttachmentLabel(activeFile.path, activeFile.selection),
+ // )
+ // }
- for (const contextFile of local.context.all()) {
- registerAttachment(
- contextFile.path,
- contextFile.selection,
- formatAttachmentLabel(contextFile.path, contextFile.selection),
- )
- }
+ // for (const contextFile of local.context.all()) {
+ // registerAttachment(
+ // contextFile.path,
+ // contextFile.selection,
+ // formatAttachmentLabel(contextFile.path, contextFile.selection),
+ // )
+ // }
const attachmentParts = Array.from(attachments.values()).map((attachment) => {
const absolute = toAbsolutePath(attachment.path)
@@ -205,7 +198,7 @@ export default function Page() {
parts: [
{
type: "text",
- text: prompt.text,
+ text,
},
...attachmentParts,
],
@@ -213,16 +206,10 @@ export default function Page() {
})
}
- const plus = (
- setStore("fileSelectOpen", true)}
- >
-
-
- )
+ const handleNewSession = () => {
+ local.session.setActive(undefined)
+ inputRef?.focus()
+ }
return (
@@ -234,7 +221,8 @@ export default function Page() {
-
@@ -268,25 +256,30 @@ export default function Page() {
-
+
{(activeSession) => }
-
+
+
+
-
-
- {/*
setStore("modelSelectOpen", true)} */}
- {/* onInputRefChange={(element: HTMLTextAreaElement | undefined) => { */}
- {/* inputRef = element ?? undefined */}
- {/* }} */}
- {/* /> */}
+
+
{
+ inputRef = el
+ }}
+ onSubmit={handlePromptSubmit}
+ />
@@ -302,7 +295,7 @@ export default function Page() {
-
local.file.open(path, { view: "diff-unified", pinned: true })}
- class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 cursor-pointer hover:bg-background-element"
+ class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 hover:bg-background-element"
>
{getFilename(path)}
@@ -318,59 +311,16 @@ export default function Page() {
-
- `${x.provider.id}:${x.id}`}
- items={local.model.list()}
- current={local.model.current()}
- render={(i) => (
-
-
-

-
{i.name}
-
- {i.id}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {new Intl.NumberFormat("en-US", {
- notation: "compact",
- compactDisplay: "short",
- }).format(i.limit.context)}
-
-
-
-
- 10}>$$$
- 1}>$$
- 0.1}>$
-
-
-
-
-
- )}
- filter={["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)}
- />
-
x}
- render={(i) => (
+ onOpenChange={(open) => setStore("fileSelectOpen", open)}
+ onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
+ >
+ {(i) => (
@@ -382,9 +332,7 @@ export default function Page() {
)}
- onClose={() => setStore("fileSelectOpen", false)}
- onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
- />
+
)
diff --git a/packages/desktop/src/ui/collapsible.tsx b/packages/desktop/src/ui/collapsible.tsx
index d17b3e623..fbc6fcbfe 100644
--- a/packages/desktop/src/ui/collapsible.tsx
+++ b/packages/desktop/src/ui/collapsible.tsx
@@ -16,7 +16,7 @@ function CollapsibleTrigger(props: CollapsibleTriggerProps) {
return (
{
- variant?: "primary" | "secondary" | "outline" | "ghost"
- size?: "xs" | "sm" | "md" | "lg"
- children: JSX.Element
-}
-
-export function IconButton(props: IconButtonProps) {
- const [local, others] = splitProps(props, ["variant", "size", "class", "classList"])
- return (
-
- )
-}
diff --git a/packages/desktop/src/ui/index.ts b/packages/desktop/src/ui/index.ts
index a6ade6ff3..e273e8efe 100644
--- a/packages/desktop/src/ui/index.ts
+++ b/packages/desktop/src/ui/index.ts
@@ -5,4 +5,3 @@ export {
type CollapsibleContentProps,
} from "./collapsible"
export { FileIcon, type FileIconProps } from "./file-icon"
-export { IconButton, type IconButtonProps } from "./icon-button"
diff --git a/packages/opencode/src/cli/cmd/debug/file.ts b/packages/opencode/src/cli/cmd/debug/file.ts
index 7be304737..fabef32b8 100644
--- a/packages/opencode/src/cli/cmd/debug/file.ts
+++ b/packages/opencode/src/cli/cmd/debug/file.ts
@@ -1,3 +1,4 @@
+import { EOL } from "os"
import { File } from "../../../file"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
@@ -13,7 +14,7 @@ const FileSearchCommand = cmd({
async handler(args) {
await bootstrap(process.cwd(), async () => {
const results = await File.search({ query: args.query })
- console.log(results.join("\n"))
+ console.log(results.join(EOL))
})
},
})
diff --git a/packages/opencode/src/cli/cmd/debug/ripgrep.ts b/packages/opencode/src/cli/cmd/debug/ripgrep.ts
index 884b291b5..1c9f89973 100644
--- a/packages/opencode/src/cli/cmd/debug/ripgrep.ts
+++ b/packages/opencode/src/cli/cmd/debug/ripgrep.ts
@@ -1,3 +1,4 @@
+import { EOL } from "os"
import { Ripgrep } from "../../../file/ripgrep"
import { Instance } from "../../../project/instance"
import { bootstrap } from "../../bootstrap"
@@ -48,7 +49,7 @@ const FilesCommand = cmd({
files.push(file)
if (args.limit && files.length >= args.limit) break
}
- console.log(files.join("\n"))
+ console.log(files.join(EOL))
})
},
})
diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts
index c8825c83d..82952f1f6 100644
--- a/packages/opencode/src/cli/cmd/export.ts
+++ b/packages/opencode/src/cli/cmd/export.ts
@@ -4,6 +4,7 @@ import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap"
import { UI } from "../ui"
import * as prompts from "@clack/prompts"
+import { EOL } from "os"
export const ExportCommand = cmd({
command: "export [sessionID]",
@@ -67,7 +68,7 @@ export const ExportCommand = cmd({
}
process.stdout.write(JSON.stringify(exportData, null, 2))
- process.stdout.write("\n")
+ process.stdout.write(EOL)
} catch (error) {
UI.error(`Session not found: ${sessionID!}`)
process.exit(1)
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index 9a6445a22..60788c7d8 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -13,6 +13,7 @@ import { Identifier } from "../../id/id"
import { Agent } from "../../agent/agent"
import { Command } from "../../command"
import { SessionPrompt } from "../../session/prompt"
+import { EOL } from "os"
const TOOL: Record = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -194,13 +195,12 @@ export const RunCommand = cmd({
sessionID: session?.id,
...data,
}
- process.stdout.write(JSON.stringify(jsonEvent) + "\n")
+ process.stdout.write(JSON.stringify(jsonEvent) + EOL)
return true
}
return false
}
- let text = ""
const messageID = Identifier.ascending("message")
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
@@ -232,15 +232,14 @@ export const RunCommand = cmd({
}
if (part.type === "text") {
- text = part.text
+ const text = part.text
+ const isPiped = !process.stdout.isTTY
if (part.time?.end) {
if (outputJsonEvent("text", { part })) return
- UI.empty()
- UI.println(UI.markdown(text))
- UI.empty()
- text = ""
- return
+ if (!isPiped) UI.println()
+ process.stdout.write((isPiped ? text : UI.markdown(text)) + EOL)
+ if (!isPiped) UI.println()
}
}
})
@@ -254,13 +253,13 @@ export const RunCommand = cmd({
if ("data" in error && error.data && "message" in error.data) {
err = error.data.message
}
- errorMsg = errorMsg ? errorMsg + "\n" + err : err
+ errorMsg = errorMsg ? errorMsg + EOL + err : err
if (outputJsonEvent("error", { error })) return
UI.error(err)
})
- const result = await (async () => {
+ await (async () => {
if (args.command) {
return await SessionPrompt.command({
messageID,
@@ -289,15 +288,6 @@ export const RunCommand = cmd({
],
})
})()
-
- const isPiped = !process.stdout.isTTY
- if (isPiped) {
- const match = result.parts.findLast((x: any) => x.type === "text") as any
- if (outputJsonEvent("text", { text: match })) return
- if (match) process.stdout.write(UI.markdown(match.text))
- if (errorMsg) process.stdout.write(errorMsg)
- }
- UI.empty()
if (errorMsg) process.exit(1)
})
},
diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts
index 0437c4c69..fff271cd2 100644
--- a/packages/opencode/src/flag/flag.ts
+++ b/packages/opencode/src/flag/flag.ts
@@ -12,6 +12,7 @@ export namespace Flag {
// Experimental
export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER")
+ export const OPENCODE_EXPERIMENTAL_TURN_SUMMARY = truthy("OPENCODE_EXPERIMENTAL_TURN_SUMMARY")
export const OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP = truthy("OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP")
function truthy(key: string) {
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index df8fdd460..de29ca85f 100644
--- a/packages/opencode/src/index.ts
+++ b/packages/opencode/src/index.ts
@@ -21,6 +21,7 @@ import { AttachCommand } from "./cli/cmd/tui/attach"
import { TuiThreadCommand } from "./cli/cmd/tui/thread"
import { TuiSpawnCommand } from "./cli/cmd/tui/spawn"
import { AcpCommand } from "./cli/cmd/acp"
+import { EOL } from "os"
const cancel = new AbortController()
@@ -132,7 +133,7 @@ try {
const formatted = FormatError(e)
if (formatted) UI.error(formatted)
if (formatted === undefined) {
- UI.error("Unexpected error, check log file at " + Log.file() + " for more details\n")
+ UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL)
console.error(e)
}
process.exitCode = 1
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index c895c9316..7c40955dc 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -95,7 +95,14 @@ export namespace Provider {
switch (regionPrefix) {
case "us": {
- const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m))
+ const modelRequiresPrefix = [
+ "nova-micro",
+ "nova-lite",
+ "nova-pro",
+ "nova-premier",
+ "claude",
+ "deepseek"
+ ].some((m) => modelID.includes(m))
const isGovCloud = region.startsWith("us-gov")
if (modelRequiresPrefix && !isGovCloud) {
modelID = `${regionPrefix}.${modelID}`
@@ -121,9 +128,10 @@ export namespace Provider {
}
case "ap": {
const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
- if (isAustraliaRegion && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) =>
- modelID.includes(m),
- )) {
+ if (
+ isAustraliaRegion &&
+ ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m))
+ ) {
regionPrefix = "au"
modelID = `${regionPrefix}.${modelID}`
} else {
@@ -273,31 +281,31 @@ export namespace Provider {
cost:
!model.cost && !existing?.cost
? {
- input: 0,
- output: 0,
- cache_read: 0,
- cache_write: 0,
- }
+ input: 0,
+ output: 0,
+ cache_read: 0,
+ cache_write: 0,
+ }
: {
- cache_read: 0,
- cache_write: 0,
- ...existing?.cost,
- ...model.cost,
- },
+ cache_read: 0,
+ cache_write: 0,
+ ...existing?.cost,
+ ...model.cost,
+ },
options: {
...existing?.options,
...model.options,
},
limit: model.limit ??
existing?.limit ?? {
- context: 0,
- output: 0,
- },
+ context: 0,
+ output: 0,
+ },
modalities: model.modalities ??
existing?.modalities ?? {
- input: ["text"],
- output: ["text"],
- },
+ input: ["text"],
+ output: ["text"],
+ },
provider: model.provider ?? existing?.provider,
}
if (model.id && model.id !== modelID) {
@@ -509,7 +517,14 @@ export namespace Provider {
const provider = await state().then((state) => state.providers[providerID])
if (!provider) return
- const priority = ["3-5-haiku", "3.5-haiku", "gemini-2.5-flash", "gpt-5-nano"]
+ const priority = [
+ "claude-haiku-4-5",
+ "claude-haiku-4.5",
+ "3-5-haiku",
+ "3.5-haiku",
+ "gemini-2.5-flash",
+ "gpt-5-nano",
+ ]
for (const item of priority) {
for (const model of Object.keys(provider.info.models)) {
if (model.includes(item)) return getModel(providerID, model)
diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts
index 73735faf8..03722aa6a 100644
--- a/packages/opencode/src/session/compaction.ts
+++ b/packages/opencode/src/session/compaction.ts
@@ -98,7 +98,7 @@ export namespace SessionCompaction {
draft.time.compacting = undefined
})
})
- const toSummarize = await Session.messages(input.sessionID).then(MessageV2.filterSummarized)
+ const toSummarize = await Session.messages(input.sessionID).then(MessageV2.filterCompacted)
const model = await Provider.getModel(input.providerID, input.modelID)
const system = [
...SystemPrompt.summarize(model.providerID),
@@ -109,6 +109,7 @@ export namespace SessionCompaction {
const msg = (await Session.updateMessage({
id: Identifier.ascending("message"),
role: "assistant",
+ parentID: toSummarize.findLast((m) => m.info.role === "user")?.info.id!,
sessionID: input.sessionID,
system,
mode: "build",
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 313cb41ab..cf0faa802 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -5,6 +5,7 @@ import { Message } from "./message"
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
import { Identifier } from "../id/id"
import { LSP } from "../lsp"
+import { Snapshot } from "@/snapshot"
export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
@@ -243,6 +244,12 @@ export namespace MessageV2 {
time: z.object({
created: z.number(),
}),
+ summary: z
+ .object({
+ diffs: Snapshot.FileDiff.array(),
+ text: z.string(),
+ })
+ .optional(),
}).meta({
ref: "UserMessage",
})
@@ -281,6 +288,7 @@ export namespace MessageV2 {
.optional(),
system: z.string().array(),
finish: z.string().optional(),
+ parentID: z.string(),
modelID: z.string(),
providerID: z.string(),
mode: z.string(),
@@ -349,6 +357,7 @@ export namespace MessageV2 {
if (v1.role === "assistant") {
const info: Assistant = {
id: v1.id,
+ parentID: "",
sessionID: v1.metadata.sessionID,
role: "assistant",
time: {
@@ -600,7 +609,7 @@ export namespace MessageV2 {
return convertToModelMessages(result)
}
- export function filterSummarized(msgs: { info: MessageV2.Info; parts: MessageV2.Part[] }[]) {
+ export function filterCompacted(msgs: { info: MessageV2.Info; parts: MessageV2.Part[] }[]) {
const i = msgs.findLastIndex((m) => m.info.role === "assistant" && !!m.info.summary)
if (i === -1) return msgs.slice()
return msgs.slice(i)
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 6e57d1db3..c01ada1ad 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -50,6 +50,7 @@ import { spawn } from "child_process"
import { Command } from "../command"
import { $, fileURLToPath } from "bun"
import { ConfigMarkdown } from "../config/markdown"
+import { MessageSummary } from "./summary"
export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" })
@@ -235,7 +236,7 @@ export namespace SessionPrompt {
modelID: model.info.id,
})
step++
- await processor.next()
+ await processor.next(msgs.findLast((m) => m.info.role === "user")?.info.id!)
await using _ = defer(async () => {
await processor.end()
})
@@ -345,6 +346,11 @@ export namespace SessionPrompt {
}
state().queued.delete(input.sessionID)
SessionCompaction.prune(input)
+ MessageSummary.summarize({
+ sessionID: input.sessionID,
+ messageID: result.info.parentID,
+ providerID: model.providerID,
+ })
return result
}
}
@@ -355,7 +361,7 @@ export namespace SessionPrompt {
providerID: string
signal: AbortSignal
}) {
- let msgs = await Session.messages(input.sessionID).then(MessageV2.filterSummarized)
+ let msgs = await Session.messages(input.sessionID).then(MessageV2.filterCompacted)
const lastAssistant = msgs.findLast((msg) => msg.info.role === "assistant")
if (
lastAssistant?.info.role === "assistant" &&
@@ -900,9 +906,10 @@ export namespace SessionPrompt {
let snapshot: string | undefined
let blocked = false
- async function createMessage() {
+ async function createMessage(parentID: string) {
const msg: MessageV2.Info = {
id: Identifier.ascending("message"),
+ parentID,
role: "assistant",
system: input.system,
mode: input.agent,
@@ -938,11 +945,11 @@ export namespace SessionPrompt {
assistantMsg = undefined
}
},
- async next() {
+ async next(parentID: string) {
if (assistantMsg) {
throw new Error("end previous assistant message first")
}
- assistantMsg = await createMessage()
+ assistantMsg = await createMessage(parentID)
return assistantMsg
},
get message() {
@@ -1429,6 +1436,7 @@ export namespace SessionPrompt {
const msg: MessageV2.Assistant = {
id: Identifier.ascending("message"),
sessionID: input.sessionID,
+ parentID: userMsg.id,
system: [],
mode: input.agent,
cost: 0,
@@ -1701,6 +1709,7 @@ export namespace SessionPrompt {
const assistantMsg: MessageV2.Assistant = {
id: Identifier.ascending("message"),
sessionID: input.sessionID,
+ parentID: userMsg.id,
system: [],
mode: agentName,
cost: 0,
diff --git a/packages/opencode/src/session/prompt/summarize-turn.txt b/packages/opencode/src/session/prompt/summarize-turn.txt
new file mode 100644
index 000000000..718d0f637
--- /dev/null
+++ b/packages/opencode/src/session/prompt/summarize-turn.txt
@@ -0,0 +1,5 @@
+Your job is to generate a summary of what happened in this conversation and why.
+
+Keep the results to 2-3 sentences.
+
+Output the message summary now:
diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts
new file mode 100644
index 000000000..c5fd8c123
--- /dev/null
+++ b/packages/opencode/src/session/summary.ts
@@ -0,0 +1,46 @@
+import { Provider } from "@/provider/provider"
+import { fn } from "@/util/fn"
+import z from "zod"
+import { Session } from "."
+import { generateText } from "ai"
+import { MessageV2 } from "./message-v2"
+import SUMMARIZE_TURN from "./prompt/summarize-turn.txt"
+import { Flag } from "@/flag/flag"
+
+export namespace MessageSummary {
+ export const summarize = fn(
+ z.object({
+ sessionID: z.string(),
+ messageID: z.string(),
+ providerID: z.string(),
+ }),
+ async (input) => {
+ if (!Flag.OPENCODE_EXPERIMENTAL_TURN_SUMMARY) return
+ const messages = await Session.messages(input.sessionID).then((msgs) =>
+ msgs.filter(
+ (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
+ ),
+ )
+ const small = await Provider.getSmallModel(input.providerID)
+ if (!small) return
+
+ const result = await generateText({
+ model: small.language,
+ messages: [
+ {
+ role: "system",
+ content: SUMMARIZE_TURN,
+ },
+ ...MessageV2.toModelMessage(messages),
+ ],
+ })
+
+ const userMsg = messages.find((m) => m.info.id === input.messageID)!
+ userMsg.info.summary = {
+ text: result.text,
+ diffs: [],
+ }
+ await Session.updateMessage(userMsg.info)
+ },
+ )
+}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index da77b42f2..69fa7bdb8 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -1158,9 +1158,9 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
// status.Warn("Agent is working, please wait...")
return a, nil
}
- editor := os.Getenv("EDITOR")
+ editor := util.GetEditor()
if editor == "" {
- return a, toast.NewErrorToast("No EDITOR set, can't open editor")
+ return a, toast.NewErrorToast("No editor found. Set EDITOR environment variable (e.g., export EDITOR=vim)")
}
value := a.editor.Value()
@@ -1404,10 +1404,9 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
// Format to Markdown
markdownContent := formatConversationToMarkdown(messages)
- // Check if EDITOR is set
- editor := os.Getenv("EDITOR")
+ editor := util.GetEditor()
if editor == "" {
- return a, toast.NewErrorToast("No EDITOR set, can't open editor")
+ return a, toast.NewErrorToast("No editor found. Set EDITOR environment variable (e.g., export EDITOR=vim)")
}
// Create and write to temp file
diff --git a/packages/tui/internal/util/util.go b/packages/tui/internal/util/util.go
index fdefb2901..b49d2e292 100644
--- a/packages/tui/internal/util/util.go
+++ b/packages/tui/internal/util/util.go
@@ -3,6 +3,8 @@ package util
import (
"log/slog"
"os"
+ "os/exec"
+ "runtime"
"strings"
"time"
@@ -45,3 +47,25 @@ func Measure(tag string) func(...any) {
slog.Debug(tag, args...)
}
}
+
+func GetEditor() string {
+ if editor := os.Getenv("VISUAL"); editor != "" {
+ return editor
+ }
+ if editor := os.Getenv("EDITOR"); editor != "" {
+ return editor
+ }
+
+ commonEditors := []string{"vim", "nvim", "zed", "code", "cursor", "vi", "nano"}
+ if runtime.GOOS == "windows" {
+ commonEditors = []string{"vim", "nvim", "zed", "code.cmd", "cursor.cmd", "notepad.exe", "vi", "nano"}
+ }
+
+ for _, editor := range commonEditors {
+ if _, err := exec.LookPath(editor); err == nil {
+ return editor
+ }
+ }
+
+ return ""
+}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 4419b1f20..c32bfb7ee 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -5,6 +5,7 @@
"exports": {
".": "./src/components/index.ts",
"./*": "./src/components/*.tsx",
+ "./hooks": "./src/hooks/index.ts",
"./styles": "./src/styles/index.css",
"./styles/tailwind": "./src/styles/tailwind/index.css",
"./fonts/*": "./src/assets/fonts/*"
@@ -23,11 +24,13 @@
},
"dependencies": {
"@kobalte/core": "catalog:",
+ "@pierre/precision-diffs": "0.0.2-alpha.1-1",
"@solidjs/meta": "catalog:",
- "remeda": "catalog:",
+ "fuzzysort": "catalog:",
"luxon": "catalog:",
- "virtua": "catalog:",
+ "remeda": "catalog:",
"solid-js": "catalog:",
- "solid-list": "catalog:"
+ "solid-list": "catalog:",
+ "virtua": "catalog:"
}
}
diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css
index c9ccf4ecb..7bf096853 100644
--- a/packages/ui/src/components/button.css
+++ b/packages/ui/src/components/button.css
@@ -1,5 +1,4 @@
[data-component="button"] {
- cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -32,12 +31,7 @@
border-color: var(--border-weak-base);
background-color: var(--button-secondary-base);
color: var(--text-strong);
-
- /* shadow-xs */
- box-shadow:
- 0 1px 2px -1px rgba(19, 16, 16, 0.04),
- 0 1px 2px 0 rgba(19, 16, 16, 0.06),
- 0 1px 3px 0 rgba(19, 16, 16, 0.08);
+ box-shadow: var(--shadow-xs);
&:hover:not(:disabled) {
border-color: var(--border-hover);
@@ -84,12 +78,11 @@
padding: 0 8px 0 6px;
gap: 8px;
- /* text-12-medium */
font-family: var(--font-family-sans);
- font-size: var(--font-size-small);
+ font-size: var(--font-size-base);
font-style: normal;
font-weight: var(--font-weight-medium);
- line-height: var(--line-height-large); /* 166.667% */
+ line-height: var(--line-height-large); /* 171.429% */
letter-spacing: var(--letter-spacing-normal);
}
diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx
index db1da2fb9..cae658439 100644
--- a/packages/ui/src/components/button.tsx
+++ b/packages/ui/src/components/button.tsx
@@ -1,12 +1,14 @@
import { Button as Kobalte } from "@kobalte/core/button"
import { type ComponentProps, splitProps } from "solid-js"
-export interface ButtonProps {
+export interface ButtonProps
+ extends ComponentProps,
+ Pick, "class" | "classList" | "children"> {
size?: "normal" | "large"
variant?: "primary" | "secondary" | "ghost"
}
-export function Button(props: ComponentProps<"button"> & ButtonProps) {
+export function Button(props: ButtonProps) {
const [split, rest] = splitProps(props, ["variant", "size", "class", "classList"])
return (
["class"]
+ classList?: ComponentProps<"div">["classList"]
+}
+
+export function DialogRoot(props: DialogProps) {
+ let trigger!: HTMLElement
+ const [local, others] = splitProps(props, ["trigger", "class", "classList", "children"])
+
+ const resetTabIndex = () => {
+ trigger.tabIndex = 0
+ }
+
+ const handleTriggerFocus = (e: FocusEvent & { currentTarget: HTMLElement | null }) => {
+ const firstChild = e.currentTarget?.firstElementChild as HTMLElement
+ if (!firstChild) return
+
+ firstChild.focus()
+ trigger.tabIndex = -1
+
+ firstChild.addEventListener("focusout", resetTabIndex)
+ onCleanup(() => {
+ firstChild.removeEventListener("focusout", resetTabIndex)
+ })
+ }
+
+ return (
+
+
+
+ {props.trigger}
+
+
+
+
+
+
+
+ {local.children}
+
+
+
+
+
+ )
+}
+
+function DialogHeader(props: ComponentProps<"div">) {
+ return
+}
+
+function DialogBody(props: ComponentProps<"div">) {
+ return
+}
+
+function DialogTitle(props: DialogTitleProps & ComponentProps<"h2">) {
+ return
+}
+
+function DialogDescription(props: DialogDescriptionProps & ComponentProps<"p">) {
+ return
+}
+
+function DialogCloseButton(props: DialogCloseButtonProps & ComponentProps<"button">) {
+ return
+}
+
+export const Dialog = Object.assign(DialogRoot, {
+ Header: DialogHeader,
+ Title: DialogTitle,
+ Description: DialogDescription,
+ CloseButton: DialogCloseButton,
+ Body: DialogBody,
+})
diff --git a/packages/ui/src/components/icon-button.css b/packages/ui/src/components/icon-button.css
new file mode 100644
index 000000000..6fe95fccf
--- /dev/null
+++ b/packages/ui/src/components/icon-button.css
@@ -0,0 +1,117 @@
+[data-component="icon-button"] {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 100%;
+ text-decoration: none;
+ user-select: none;
+ aspect-ratio: 1;
+
+ &:disabled {
+ background-color: var(--icon-strong-disabled);
+ color: var(--icon-invert-base);
+ cursor: not-allowed;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &[data-variant="primary"] {
+ background-color: var(--icon-strong-base);
+
+ [data-slot="icon"] {
+ /* color: var(--icon-weak-base); */
+ color: var(--icon-invert-base);
+
+ /* &:hover:not(:disabled) { */
+ /* color: var(--icon-weak-hover); */
+ /* } */
+ /* &:active:not(:disabled) { */
+ /* color: var(--icon-string-active); */
+ /* } */
+ }
+
+ &:hover:not(:disabled) {
+ background-color: var(--icon-strong-hover);
+ }
+ &:active:not(:disabled) {
+ background-color: var(--icon-string-active);
+ }
+ &:focus:not(:disabled) {
+ background-color: var(--icon-strong-focus);
+ }
+ &:disabled {
+ background-color: var(--icon-strong-disabled);
+
+ [data-slot="icon"] {
+ color: var(--icon-invert-base);
+ }
+ }
+ }
+
+ &[data-variant="secondary"] {
+ background-color: var(--button-secondary-base);
+ color: var(--text-strong);
+
+ &:hover:not(:disabled) {
+ background-color: var(--surface-hover);
+ }
+ &:active:not(:disabled) {
+ background-color: var(--surface-active);
+ }
+ &:focus:not(:disabled) {
+ background-color: var(--surface-focus);
+ }
+ }
+
+ &[data-variant="ghost"] {
+ background-color: transparent;
+
+ [data-slot="icon"] {
+ color: var(--icon-weak-base);
+
+ &:hover:not(:disabled) {
+ color: var(--icon-weak-hover);
+ }
+ &:active:not(:disabled) {
+ color: var(--icon-string-active);
+ }
+ }
+
+ /* color: var(--text-strong); */
+ /**/
+ /* &:hover:not(:disabled) { */
+ /* background-color: var(--surface-hover); */
+ /* } */
+ /* &:active:not(:disabled) { */
+ /* background-color: var(--surface-active); */
+ /* } */
+ /* &:focus:not(:disabled) { */
+ /* background-color: var(--surface-focus); */
+ /* } */
+ }
+
+ &[data-size="normal"] {
+ width: 24px;
+ height: 24px;
+
+ font-size: var(--font-size-small);
+ line-height: var(--line-height-large);
+ gap: calc(var(--spacing) * 0.5);
+ }
+
+ &[data-size="large"] {
+ height: 32px;
+ padding: 0 8px 0 6px;
+ gap: 8px;
+
+ /* text-12-medium */
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-style: normal;
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large); /* 166.667% */
+ letter-spacing: var(--letter-spacing-normal);
+ }
+}
diff --git a/packages/ui/src/components/icon-button.tsx b/packages/ui/src/components/icon-button.tsx
new file mode 100644
index 000000000..f483f92a7
--- /dev/null
+++ b/packages/ui/src/components/icon-button.tsx
@@ -0,0 +1,27 @@
+import { Button as Kobalte } from "@kobalte/core/button"
+import { type ComponentProps, splitProps } from "solid-js"
+import { Icon, IconProps } from "./icon"
+
+export interface IconButtonProps {
+ icon: IconProps["name"]
+ size?: "normal" | "large"
+ variant?: "primary" | "secondary" | "ghost"
+}
+
+export function IconButton(props: ComponentProps<"button"> & IconButtonProps) {
+ const [split, rest] = splitProps(props, ["variant", "size", "class", "classList"])
+ return (
+
+
+
+ )
+}
diff --git a/packages/ui/src/components/icon.css b/packages/ui/src/components/icon.css
index abc193220..59c644b70 100644
--- a/packages/ui/src/components/icon.css
+++ b/packages/ui/src/components/icon.css
@@ -3,4 +3,27 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
+ /* resize: both; */
+ aspect-ratio: 1/1;
+ color: var(--icon-base);
+
+ &[data-size="small"] {
+ width: 16px;
+ height: 16px;
+ }
+
+ &[data-size="normal"] {
+ width: 20px;
+ height: 20px;
+ }
+
+ &[data-size="large"] {
+ width: 32px;
+ height: 32px;
+ }
+
+ [data-slot="svg"] {
+ width: 100%;
+ height: auto;
+ }
}
diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx
index 05dda6ea6..8d63bf0f8 100644
--- a/packages/ui/src/components/icon.tsx
+++ b/packages/ui/src/components/icon.tsx
@@ -128,28 +128,55 @@ const icons = {
mic: '',
} as const
+const newIcons = {
+ "circle-x": ``,
+ "magnifying-glass": ``,
+ "plus-small": ``,
+ "chevron-down": ``,
+ "arrow-up": ``,
+}
+
export interface IconProps extends ComponentProps<"svg"> {
- name: keyof typeof icons
- size?: number
+ name: keyof typeof icons | keyof typeof newIcons
+ size?: "small" | "normal" | "large"
}
export function Icon(props: IconProps) {
const [local, others] = splitProps(props, ["name", "size", "class", "classList"])
- const size = local.size ?? 24
+
+ if (local.name in newIcons) {
+ return (
+
+
+
+ )
+ }
+
return (
-
+
+
+
)
}
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index d6ddc3ec0..71cfd3a89 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -1,7 +1,11 @@
export * from "./button"
+export * from "./dialog"
export * from "./icon"
+export * from "./icon-button"
+export * from "./input"
export * from "./fonts"
export * from "./list"
export * from "./select"
+export * from "./select-dialog"
export * from "./tabs"
export * from "./tooltip"
diff --git a/packages/ui/src/components/input.css b/packages/ui/src/components/input.css
new file mode 100644
index 000000000..24cec19c5
--- /dev/null
+++ b/packages/ui/src/components/input.css
@@ -0,0 +1,23 @@
+[data-component="input"] {
+ /* [data-slot="label"] {} */
+
+ [data-slot="input"] {
+ color: var(--text-strong);
+
+ /* text-14-regular */
+ font-family: var(--font-family-sans);
+ font-size: 14px;
+ font-style: normal;
+ font-weight: var(--font-weight-regular);
+ line-height: var(--line-height-large); /* 142.857% */
+ letter-spacing: var(--letter-spacing-normal);
+
+ &:focus {
+ outline: none;
+ }
+
+ &::placeholder {
+ color: var(--text-weak);
+ }
+ }
+}
diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx
new file mode 100644
index 000000000..509e242c9
--- /dev/null
+++ b/packages/ui/src/components/input.tsx
@@ -0,0 +1,27 @@
+import { TextField as Kobalte } from "@kobalte/core/text-field"
+import { Show, splitProps } from "solid-js"
+import type { ComponentProps } from "solid-js"
+
+export interface InputProps extends ComponentProps {
+ label?: string
+ hideLabel?: boolean
+ description?: string
+}
+
+export function Input(props: InputProps) {
+ const [local, others] = splitProps(props, ["class", "label", "hideLabel", "description", "placeholder"])
+ return (
+
+
+
+ {local.label}
+
+
+
+
+ {local.description}
+
+
+
+ )
+}
diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css
index b98cae07c..d60b55aeb 100644
--- a/packages/ui/src/components/list.css
+++ b/packages/ui/src/components/list.css
@@ -12,7 +12,6 @@
scrollbar-width: none;
[data-slot="item"] {
- cursor: pointer;
width: 100%;
padding: 4px 12px;
text-align: left;
@@ -23,6 +22,9 @@
&[data-active="true"] {
background-color: var(--surface-raised-base-hover);
}
+ &:hover {
+ background-color: var(--surface-raised-base-hover);
+ }
&:focus {
outline: none;
}
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
index 8bfbbdc98..cb212d1a8 100644
--- a/packages/ui/src/components/list.tsx
+++ b/packages/ui/src/components/list.tsx
@@ -29,6 +29,7 @@ export function List(props: ListProps) {
// }
const handleSelect = (item: T) => {
props.onSelect?.(item)
+ list.setActive(props.key(item))
}
const handleKey = (e: KeyboardEvent) => {
@@ -64,10 +65,10 @@ export function List(props: ListProps) {
data-key={props.key(item)}
data-active={props.key(item) === list.active()}
onClick={() => handleSelect(item)}
- onMouseMove={(e) => {
- e.currentTarget.focus()
+ onMouseMove={() => {
+ // e.currentTarget.focus()
setStore("mouseActive", true)
- list.setActive(props.key(item))
+ // list.setActive(props.key(item))
}}
>
{props.children(item)}
diff --git a/packages/ui/src/components/select-dialog.css b/packages/ui/src/components/select-dialog.css
new file mode 100644
index 000000000..41d8f3921
--- /dev/null
+++ b/packages/ui/src/components/select-dialog.css
@@ -0,0 +1,109 @@
+[data-component="select-dialog-input"] {
+ display: flex;
+ height: 40px;
+ flex-shrink: 0;
+ padding: 4px 10px 4px 6px;
+ align-items: center;
+ gap: 12px;
+ align-self: stretch;
+
+ border-radius: 8px;
+ background: var(--surface-base);
+
+ [data-slot="input-container"] {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex: 1 0 0;
+
+ /* [data-slot="icon"] {} */
+
+ [data-slot="input"] {
+ width: 100%;
+ }
+ }
+
+ /* [data-slot="clear-button"] {} */
+}
+
+[data-component="select-dialog"] {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ [data-slot="empty-state"] {
+ display: flex;
+ padding: 32px 160px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ align-self: stretch;
+
+ [data-slot="message"] {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 2px;
+ color: var(--text-weak);
+ text-align: center;
+
+ /* text-14-regular */
+ font-family: var(--font-family-sans);
+ font-size: 14px;
+ font-style: normal;
+ font-weight: var(--font-weight-regular);
+ line-height: var(--line-height-large); /* 142.857% */
+ letter-spacing: var(--letter-spacing-normal);
+ }
+
+ [data-slot="filter"] {
+ color: var(--text-strong);
+ }
+ }
+
+ [data-slot="group"] {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
+ [data-slot="header"] {
+ display: flex;
+ padding: 4px 8px;
+ justify-content: space-between;
+ align-items: center;
+ align-self: stretch;
+
+ color: var(--text-weak);
+
+ /* text-12-medium */
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-style: normal;
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large); /* 166.667% */
+ letter-spacing: var(--letter-spacing-normal);
+ }
+
+ [data-slot="list"] {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ align-self: stretch;
+
+ [data-slot="item"] {
+ display: flex;
+ width: 100%;
+ height: 32px;
+ padding: 4px 8px 4px 4px;
+ align-items: center;
+
+ &[data-active="true"] {
+ border-radius: 8px;
+ background: var(--surface-raised-base-hover);
+ }
+ }
+ }
+ }
+}
diff --git a/packages/ui/src/components/select-dialog.tsx b/packages/ui/src/components/select-dialog.tsx
new file mode 100644
index 000000000..63fad13ec
--- /dev/null
+++ b/packages/ui/src/components/select-dialog.tsx
@@ -0,0 +1,156 @@
+import { createEffect, Show, For, type JSX, splitProps } from "solid-js"
+import { Dialog, DialogProps, Icon, IconButton, Input } from "@opencode-ai/ui"
+import { createStore } from "solid-js/store"
+import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
+
+interface SelectDialogProps
+ extends FilteredListProps,
+ Pick {
+ title: string
+ placeholder?: string
+ emptyMessage?: string
+ children: (item: T) => JSX.Element
+ onSelect?: (value: T | undefined) => void
+}
+
+export function SelectDialog(props: SelectDialogProps) {
+ const [dialog, others] = splitProps(props, ["trigger", "onOpenChange", "defaultOpen"])
+ let closeButton!: HTMLButtonElement
+ let scrollRef: HTMLDivElement | undefined
+ const [store, setStore] = createStore({
+ mouseActive: false,
+ })
+
+ const { filter, grouped, flat, reset, clear, active, setActive, onKeyDown, onInput } = useFilteredList({
+ items: others.items,
+ key: others.key,
+ filterKeys: others.filterKeys,
+ current: others.current,
+ groupBy: others.groupBy,
+ sortBy: others.sortBy,
+ sortGroupsBy: others.sortGroupsBy,
+ })
+
+ createEffect(() => {
+ filter()
+ scrollRef?.scrollTo(0, 0)
+ reset()
+ })
+
+ createEffect(() => {
+ const all = flat()
+ if (store.mouseActive || all.length === 0) return
+ if (active() === others.key(all[0])) {
+ scrollRef?.scrollTo(0, 0)
+ return
+ }
+ const element = scrollRef?.querySelector(`[data-key="${active()}"]`)
+ element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
+ })
+
+ const handleInput = (value: string) => {
+ onInput(value)
+ reset()
+ }
+
+ const handleSelect = (item: T | undefined) => {
+ others.onSelect?.(item)
+ closeButton.click()
+ }
+
+ const handleKey = (e: KeyboardEvent) => {
+ setStore("mouseActive", false)
+ if (e.key === "Escape") return
+
+ if (e.key === "Enter") {
+ e.preventDefault()
+ const selected = flat().find((x) => others.key(x) === active())
+ if (selected) handleSelect(selected)
+ } else {
+ onKeyDown(e)
+ }
+ }
+
+ const handleOpenChange = (open: boolean) => {
+ if (!open) clear()
+ props.onOpenChange?.(open)
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css
index b6b884a1f..0eb7cea15 100644
--- a/packages/ui/src/components/select.css
+++ b/packages/ui/src/components/select.css
@@ -1,6 +1,7 @@
[data-component="select"] {
[data-slot="trigger"] {
padding: 0 4px 0 8px;
+ box-shadow: none;
[data-slot="value"] {
overflow: hidden;
@@ -8,8 +9,8 @@
white-space: nowrap;
}
[data-slot="icon"] {
- width: fit-content;
- height: fit-content;
+ width: 16px;
+ height: 16px;
flex-shrink: 0;
color: var(--text-weak);
transition: transform 0.1s ease-in-out;
@@ -18,15 +19,15 @@
}
[data-component="select-content"] {
- min-width: 8rem;
+ min-width: 4rem;
overflow: hidden;
- border-radius: var(--radius-md);
+ border-radius: 8px;
border-width: 1px;
border-style: solid;
border-color: var(--border-weak-base);
- background-color: var(--surface-raised-base);
- padding: calc(var(--spacing) * 1);
- box-shadow: var(--shadow-md);
+ background-color: var(--surface-raised-stronger-non-alpha);
+ padding: 2px;
+ box-shadow: var(--shadow-xs);
z-index: 50;
&[data-closed] {
@@ -42,36 +43,35 @@
max-height: 12rem;
white-space: nowrap;
overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
&:focus {
outline: none;
}
}
- [data-slot="section"] {
- font-size: var(--text-xs);
- line-height: var(--text-xs--line-height);
- font-weight: var(--font-weight-light);
- text-transform: uppercase;
- color: var(--text-weak);
- opacity: 0.6;
- margin-top: calc(var(--spacing) * 3);
- margin-left: calc(var(--spacing) * 2);
- &:first-child {
- margin-top: 0;
- }
- }
+ /* [data-slot="section"] { */
+ /* } */
[data-slot="item"] {
position: relative;
display: flex;
align-items: center;
- padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2);
- border-radius: var(--radius-sm);
- font-size: var(--text-xs);
- line-height: var(--text-xs--line-height);
- color: var(--text-base);
- cursor: pointer;
+ padding: 0 6px 0 6px;
+ border-radius: 6px;
+
+ /* text-12-medium */
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-style: normal;
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large); /* 166.667% */
+ letter-spacing: var(--letter-spacing-normal);
+
+ color: var(--text-strong);
+
transition:
background-color 0.2s ease-in-out,
color 0.2s ease-in-out;
@@ -79,24 +79,20 @@
user-select: none;
&[data-highlighted] {
- background-color: var(--surface-base);
+ background: var(--surface-raised-base-hover);
}
-
&[data-disabled] {
- background-color: var(--surface-disabled);
+ background-color: var(--surface-raised-base);
pointer-events: none;
}
-
[data-slot="item-indicator"] {
margin-left: auto;
}
-
&:focus {
outline: none;
}
-
&:hover {
- background-color: var(--surface-hover);
+ background: var(--surface-raised-base-hover);
}
}
}
diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx
index ecf05d5e2..111608e28 100644
--- a/packages/ui/src/components/select.tsx
+++ b/packages/ui/src/components/select.tsx
@@ -52,7 +52,7 @@ export function Select(props: SelectProps & ButtonProps) {
{props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
-
+
)}
@@ -79,7 +79,7 @@ export function Select(props: SelectProps & ButtonProps) {
}}
-
+
diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css
index c6d09c656..70d7b03e1 100644
--- a/packages/ui/src/components/tabs.css
+++ b/packages/ui/src/components/tabs.css
@@ -10,7 +10,7 @@
background-color: var(--background-stronger);
overflow: clip;
- & [data-slot="list"] {
+ [data-slot="list"] {
width: 100%;
position: relative;
display: flex;
@@ -40,7 +40,7 @@
}
}
- & [data-slot="trigger"] {
+ [data-slot="trigger"] {
position: relative;
height: 36px;
padding: 8px 12px;
@@ -49,7 +49,7 @@
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
color: var(--text-weak);
- cursor: pointer;
+
white-space: nowrap;
flex-shrink: 0;
border-bottom: 1px solid var(--border-weak-base);
@@ -77,7 +77,7 @@
}
}
- & [data-slot="content"] {
+ [data-slot="content"] {
overflow-y: auto;
flex: 1;
diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/tooltip.tsx
index 14e433e21..ff13c8d61 100644
--- a/packages/ui/src/components/tooltip.tsx
+++ b/packages/ui/src/components/tooltip.tsx
@@ -36,7 +36,7 @@ export function Tooltip(props: TooltipProps) {
{typeof others.value === "function" ? others.value() : others.value}
- {/* */}
+ {/* */}
diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts
new file mode 100644
index 000000000..7eef78091
--- /dev/null
+++ b/packages/ui/src/hooks/index.ts
@@ -0,0 +1 @@
+export * from "./use-filtered-list"
diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx
new file mode 100644
index 000000000..b3ddf69ed
--- /dev/null
+++ b/packages/ui/src/hooks/use-filtered-list.tsx
@@ -0,0 +1,89 @@
+import fuzzysort from "fuzzysort"
+import { entries, flatMap, groupBy, map, pipe } from "remeda"
+import { createMemo, createResource } from "solid-js"
+import { createStore } from "solid-js/store"
+import { createList } from "solid-list"
+
+export interface FilteredListProps {
+ items: T[] | ((filter: string) => Promise)
+ key: (item: T) => string
+ filterKeys?: string[]
+ current?: T
+ groupBy?: (x: T) => string
+ sortBy?: (a: T, b: T) => number
+ sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
+ onSelect?: (value: T | undefined) => void
+}
+
+export function useFilteredList(props: FilteredListProps) {
+ const [store, setStore] = createStore<{ filter: string }>({ filter: "" })
+
+ const [grouped] = createResource(
+ () => store.filter,
+ async (filter) => {
+ const needle = filter?.toLowerCase()
+ const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || []
+ const result = pipe(
+ all,
+ (x) => {
+ if (!needle) return x
+ if (!props.filterKeys && Array.isArray(x) && x.every((e) => typeof e === "string")) {
+ return fuzzysort.go(needle, x).map((x) => x.target) as T[]
+ }
+ return fuzzysort.go(needle, x, { keys: props.filterKeys! }).map((x) => x.obj)
+ },
+ groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
+ entries(),
+ map(([k, v]) => ({ category: k, items: props.sortBy ? v.sort(props.sortBy) : v })),
+ (groups) => (props.sortGroupsBy ? groups.sort(props.sortGroupsBy) : groups),
+ )
+ return result
+ },
+ )
+
+ const flat = createMemo(() => {
+ return pipe(
+ grouped() || [],
+ flatMap((x) => x.items),
+ )
+ })
+
+ const list = createList({
+ items: () => flat().map(props.key),
+ initialActive: props.current ? props.key(props.current) : props.key(flat()[0]),
+ loop: true,
+ })
+
+ const reset = () => {
+ const all = flat()
+ if (all.length === 0) return
+ list.setActive(props.key(all[0]))
+ }
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Enter") {
+ event.preventDefault()
+ const selected = flat().find((x) => props.key(x) === list.active())
+ if (selected) props.onSelect?.(selected)
+ } else {
+ list.onKeyDown(event)
+ }
+ }
+
+ const onInput = (value: string) => {
+ setStore("filter", value)
+ reset()
+ }
+
+ return {
+ filter: () => store.filter,
+ grouped,
+ flat,
+ reset,
+ clear: () => setStore("filter", ""),
+ onKeyDown,
+ onInput,
+ active: list.active,
+ setActive: list.setActive,
+ }
+}
diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css
index 0a89a4a0d..dc5335c43 100644
--- a/packages/ui/src/styles/index.css
+++ b/packages/ui/src/styles/index.css
@@ -6,9 +6,13 @@
@import "./base.css" layer(base);
@import "../components/button.css" layer(components);
+@import "../components/dialog.css" layer(components);
@import "../components/icon.css" layer(components);
+@import "../components/icon-button.css" layer(components);
+@import "../components/input.css" layer(components);
@import "../components/list.css" layer(components);
@import "../components/select.css" layer(components);
+@import "../components/select-dialog.css" layer(components);
@import "../components/tabs.css" layer(components);
@import "../components/tooltip.css" layer(components);
diff --git a/packages/ui/src/styles/utilities.css b/packages/ui/src/styles/utilities.css
index f01a6b2ee..7d14b6539 100644
--- a/packages/ui/src/styles/utilities.css
+++ b/packages/ui/src/styles/utilities.css
@@ -5,11 +5,11 @@
pointer-events: none;
}
- ::selection {
- background-color: color-mix(in srgb, var(--color-primary) 33%, transparent);
- /* background-color: var(--color-primary); */
- /* color: var(--color-background); */
- }
+ /* ::selection { */
+ /* background-color: color-mix(in srgb, var(--color-primary) 33%, transparent); */
+ /* background-color: var(--color-primary); */
+ /* color: var(--color-background); */
+ /* } */
::-webkit-scrollbar-track {
background: var(--theme-background-panel);
@@ -36,6 +36,18 @@
}
}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
.text-12-regular {
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
diff --git a/packages/web/src/content/docs/sdk.mdx b/packages/web/src/content/docs/sdk.mdx
index 2b90bd9bc..6d66cebfd 100644
--- a/packages/web/src/content/docs/sdk.mdx
+++ b/packages/web/src/content/docs/sdk.mdx
@@ -69,7 +69,7 @@ opencode.server.close()
## Client only
-If you aready have a running instance of opencode, you can create a client instance to connect to it:
+If you already have a running instance of opencode, you can create a client instance to connect to it:
```javascript
import { createOpencodeClient } from "@opencode-ai/sdk"
diff --git a/sdks/vscode/bun.lock b/sdks/vscode/bun.lock
index a5d26f355..085f0661a 100644
--- a/sdks/vscode/bun.lock
+++ b/sdks/vscode/bun.lock
@@ -6,7 +6,7 @@
"devDependencies": {
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
- "@types/vscode": "^1.102.0",
+ "@types/vscode": "^1.94.0",
"@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1",
"@vscode/test-cli": "^0.0.11",
diff --git a/sdks/vscode/script/publish b/sdks/vscode/script/publish
index f8eb6d1f3..bc48f574c 100755
--- a/sdks/vscode/script/publish
+++ b/sdks/vscode/script/publish
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
+set -euo pipefail
# Get the latest Git tag
latest_tag=$(git tag --sort=committerdate | grep -E '^vscode-v[0-9]+\.[0-9]+\.[0-9]+$' | tail -1)
@@ -7,14 +8,14 @@ if [ -z "$latest_tag" ]; then
exit 1
fi
echo "Latest tag: $latest_tag"
-version=$(echo $latest_tag | sed 's/^vscode-v//')
+version=$(echo "$latest_tag" | sed 's/^vscode-v//')
echo "Latest version: $version"
# package-marketplace
-vsce package --no-git-tag-version --no-update-package-json --no-dependencies --skip-license -o dist/opencode.vsix $version
+vsce package --no-git-tag-version --no-update-package-json --no-dependencies --skip-license -o dist/opencode.vsix "$version"
# publish-marketplace
vsce publish --packagePath dist/opencode.vsix
# publish-openvsx
-npx ovsx publish dist/opencode.vsix -p $OPENVSX_TOKEN
\ No newline at end of file
+npx ovsx publish dist/opencode.vsix -p "$OPENVSX_TOKEN"
diff --git a/sdks/vscode/src/extension.ts b/sdks/vscode/src/extension.ts
index 4d302fa4d..77c70513f 100644
--- a/sdks/vscode/src/extension.ts
+++ b/sdks/vscode/src/extension.ts
@@ -1,46 +1,46 @@
// This method is called when your extension is deactivated
export function deactivate() {}
-import * as vscode from "vscode"
+import * as vscode from "vscode";
-const TERMINAL_NAME = "opencode"
+const TERMINAL_NAME = "opencode";
export function activate(context: vscode.ExtensionContext) {
let openNewTerminalDisposable = vscode.commands.registerCommand("opencode.openNewTerminal", async () => {
- await openTerminal()
- })
+ await openTerminal();
+ });
let openTerminalDisposable = vscode.commands.registerCommand("opencode.openTerminal", async () => {
// An opencode terminal already exists => focus it
- const existingTerminal = vscode.window.terminals.find((t) => t.name === TERMINAL_NAME)
+ const existingTerminal = vscode.window.terminals.find((t) => t.name === TERMINAL_NAME);
if (existingTerminal) {
- existingTerminal.show()
- return
+ existingTerminal.show();
+ return;
}
- await openTerminal()
- })
+ await openTerminal();
+ });
let addFilepathDisposable = vscode.commands.registerCommand("opencode.addFilepathToTerminal", async () => {
- const fileRef = getActiveFile()
- if (!fileRef) return
+ const fileRef = getActiveFile();
+ if (!fileRef) {return;}
- const terminal = vscode.window.activeTerminal
- if (!terminal) return
+ const terminal = vscode.window.activeTerminal;
+ if (!terminal) {return;}
if (terminal.name === TERMINAL_NAME) {
// @ts-ignore
- const port = terminal.creationOptions.env?.["_EXTENSION_OPENCODE_PORT"]
- port ? await appendPrompt(parseInt(port), fileRef) : terminal.sendText(fileRef)
- terminal.show()
+ const port = terminal.creationOptions.env?.["_EXTENSION_OPENCODE_PORT"];
+ port ? await appendPrompt(parseInt(port), fileRef) : terminal.sendText(fileRef);
+ terminal.show();
}
- })
+ });
- context.subscriptions.push(openTerminalDisposable, addFilepathDisposable)
+ context.subscriptions.push(openTerminalDisposable, addFilepathDisposable);
async function openTerminal() {
// Create a new terminal in split screen
- const port = Math.floor(Math.random() * (65535 - 16384 + 1)) + 16384
+ const port = Math.floor(Math.random() * (65535 - 16384 + 1)) + 16384;
const terminal = vscode.window.createTerminal({
name: TERMINAL_NAME,
iconPath: {
@@ -55,32 +55,32 @@ export function activate(context: vscode.ExtensionContext) {
_EXTENSION_OPENCODE_PORT: port.toString(),
OPENCODE_CALLER: "vscode",
},
- })
+ });
- terminal.show()
- terminal.sendText(`opencode --port ${port}`)
+ terminal.show();
+ terminal.sendText(`opencode --port ${port}`);
- const fileRef = getActiveFile()
- if (!fileRef) return
+ const fileRef = getActiveFile();
+ if (!fileRef) {return;}
// Wait for the terminal to be ready
- let tries = 10
- let connected = false
+ let tries = 10;
+ let connected = false;
do {
- await new Promise((resolve) => setTimeout(resolve, 200))
+ await new Promise((resolve) => setTimeout(resolve, 200));
try {
- await fetch(`http://localhost:${port}/app`)
- connected = true
- break
+ await fetch(`http://localhost:${port}/app`);
+ connected = true;
+ break;
} catch (e) {}
- tries--
- } while (tries > 0)
+ tries--;
+ } while (tries > 0);
// If connected, append the prompt to the terminal
if (connected) {
- await appendPrompt(port, `In ${fileRef}`)
- terminal.show()
+ await appendPrompt(port, `In ${fileRef}`);
+ terminal.show();
}
}
@@ -91,37 +91,37 @@ export function activate(context: vscode.ExtensionContext) {
"Content-Type": "application/json",
},
body: JSON.stringify({ text }),
- })
+ });
}
function getActiveFile() {
- const activeEditor = vscode.window.activeTextEditor
- if (!activeEditor) return
+ const activeEditor = vscode.window.activeTextEditor;
+ if (!activeEditor) {return;}
- const document = activeEditor.document
- const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
- if (!workspaceFolder) return
+ const document = activeEditor.document;
+ const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
+ if (!workspaceFolder) {return;}
// Get the relative path from workspace root
- const relativePath = vscode.workspace.asRelativePath(document.uri)
- let filepathWithAt = `@${relativePath}`
+ const relativePath = vscode.workspace.asRelativePath(document.uri);
+ let filepathWithAt = `@${relativePath}`;
// Check if there's a selection and add line numbers
- const selection = activeEditor.selection
+ const selection = activeEditor.selection;
if (!selection.isEmpty) {
// Convert to 1-based line numbers
- const startLine = selection.start.line + 1
- const endLine = selection.end.line + 1
+ const startLine = selection.start.line + 1;
+ const endLine = selection.end.line + 1;
if (startLine === endLine) {
// Single line selection
- filepathWithAt += `#L${startLine}`
+ filepathWithAt += `#L${startLine}`;
} else {
// Multi-line selection
- filepathWithAt += `#L${startLine}-${endLine}`
+ filepathWithAt += `#L${startLine}-${endLine}`;
}
}
- return filepathWithAt
+ return filepathWithAt;
}
}
diff --git a/sdks/vscode/tsconfig.json b/sdks/vscode/tsconfig.json
index 83733a8fa..710f9ede4 100644
--- a/sdks/vscode/tsconfig.json
+++ b/sdks/vscode/tsconfig.json
@@ -6,7 +6,8 @@
"sourceMap": true,
"rootDir": "src",
"typeRoots": ["./node_modules/@types"],
- "strict": true /* enable all strict type-checking options */
+ "strict": true /* enable all strict type-checking options */,
+ "skipLibCheck": true
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */