diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1c41a66faa..15f865b742 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -339,7 +339,8 @@ jobs: env: OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} GH_TOKEN: ${{ steps.committer.outputs.token }} - CSC_KEYCHAIN: build.keychain + CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} + CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} diff --git a/bun.lock b/bun.lock index e905ada8c5..9f6ef7b628 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -96,7 +96,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.11", + "version": "1.18.9", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -194,7 +194,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +221,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -243,7 +243,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +267,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,7 +287,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.11", + "version": "1.18.9", "bin": { "opencode": "./bin/opencode", }, @@ -381,16 +381,16 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@zip.js/zip.js": "2.7.62", - "drizzle-orm": "catalog:", "effect": "catalog:", "electron-context-menu": "4.1.2", "electron-log": "^5", "electron-store": "11.0.2", "electron-updater": "6.8.9", "electron-window-state": "^5.0.3", + "marked": "^15", }, "devDependencies": { "@actions/artifact": "4.0.0", @@ -435,7 +435,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +449,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "effect": "catalog:", }, @@ -461,7 +461,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +493,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,7 +509,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -540,7 +540,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -559,7 +559,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.11", + "version": "1.18.9", "bin": { "opencode": "./bin/opencode", }, @@ -690,7 +690,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -766,7 +766,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "cross-spawn": "catalog:", }, @@ -781,7 +781,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -796,7 +796,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", @@ -811,6 +811,7 @@ "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", + "@solidjs/router": "catalog:", "diff": "catalog:", "dompurify": "3.3.1", "fuzzysort": "catalog:", @@ -840,7 +841,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -853,7 +854,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -887,7 +888,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -906,7 +907,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -948,7 +949,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -975,7 +976,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1027,7 +1028,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.11", + "version": "1.18.9", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -6044,6 +6045,8 @@ "@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], + "@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], "@opencode-ai/llm/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], diff --git a/nix/hashes.json b/nix/hashes.json index 25f0e76812..a7b441535c 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-a7NyYa9vRUEqDfZNDPXXmFO58RDEgioyuGSl5CPBvxo=", - "aarch64-linux": "sha256-l4OJtSEllHvRhktjcaJYwkXBSaJvsIoyoLusbZfYMcM=", - "aarch64-darwin": "sha256-IIl0BQGs1/HLFh0auiQjiwfSQ2nfHcK2G2BAphYW59c=", - "x86_64-darwin": "sha256-vVeuPyd4ZIRYrHouphTuEb4rkRZLKKTAHe840jNh9rU=" + "x86_64-linux": "sha256-fK6zAHJC3ut/KUdfqLPxVMH7Z1yv65YZ7qcHF45kPas=", + "aarch64-linux": "sha256-CSWZY/vVUCKzlwfisQQHqHjawUwL0BRc8Y9o2y/QZYA=", + "aarch64-darwin": "sha256-IyFm5NbnU63BaOO/F4/v1exz3VbvkY96yjg9iun+O9Q=", + "x86_64-darwin": "sha256-XnXFoUxqXmEx7AKx5YhaJuLY2NYMtI0ICafaenU5L6k=" } } diff --git a/packages/app/e2e/regression/project-picker-recent-search.spec.ts b/packages/app/e2e/regression/project-picker-recent-search.spec.ts deleted file mode 100644 index 2cdb0b4a03..0000000000 --- a/packages/app/e2e/regression/project-picker-recent-search.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expect, test } from "@playwright/test" -import type { Page } from "@playwright/test" -import { fixture, pageMessages } from "../smoke/session-timeline.fixture" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectAppVisible } from "../utils/waits" - -const NAMES = ["alpha-service", "bravo-web", "charlie-api", "delta-tools", "echo-infra", "foxtrot-docs"] -const worktrees = NAMES.map((name) => `/opencode-demo/${name}`) - -// The sixth project sits outside the five-item recent cap, so it is only reachable if the -// dialog hands every recent project to the list filter instead of a pre-truncated slice. -const OUTSIDE_CAP = "foxtrot-docs" - -// Dialog rows carry data-directory-path; the sidebar project list does not, so this -// scopes assertions to the picker instead of matching the sidebar entry of the same name. -const rows = (page: Page) => page.locator("[data-directory-path]") -const row = (page: Page, name: string) => page.locator(`[data-directory-path*="${name}"]`) - -async function openProjectDialog(page: Page) { - await mockOpenCodeServer(page, { - sessions: fixture.sessions, - provider: fixture.provider, - directory: fixture.directory, - project: fixture.project, - pageMessages, - fileList: () => [], - findFiles: () => [], - }) - await page.addInitScript((dirs) => { - localStorage.setItem( - "opencode.global.dat:server", - JSON.stringify({ - projects: { local: dirs.map((worktree: string) => ({ worktree, expanded: false })) }, - lastProject: {}, - }), - ) - }, worktrees) - await page.goto("/") - const add = page.getByRole("button", { name: "Add project" }).first() - await expectAppVisible(add) - await add.click() - await expect(rows(page)).toHaveCount(5) - return page.getByRole("textbox").last() -} - -test("searches every recent project, not just the five most recent", async ({ page }) => { - const search = await openProjectDialog(page) - await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) - - await search.fill("foxtrot") - - await expect(row(page, OUTSIDE_CAP)).toHaveCount(1) -}) - -test("still caps the idle recent list at five projects", async ({ page }) => { - await openProjectDialog(page) - - await expect(row(page, NAMES[4])).toHaveCount(1) - await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) -}) diff --git a/packages/app/package.json b/packages/app/package.json index 2dd4a05d86..08eb9a059f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.11", + "version": "1.18.9", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx index adadeda303..67c9a89929 100644 --- a/packages/app/src/components/debug-bar.tsx +++ b/packages/app/src/components/debug-bar.tsx @@ -66,7 +66,7 @@ function Cell(props: {
-
- {props.label} -
-
- {props.value} -
+ {props.label} +
+
+ {props.value}
) @@ -124,25 +116,15 @@ function FocusCell(props: { active: boolean; inline?: boolean; onClick: () => vo aria-pressed={props.active} classList={{ "flex min-w-0 items-center font-mono uppercase hover:bg-surface-raised-base focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-border-focus": true, - "min-h-[20px] w-fit justify-start rounded px-1.5 py-0.5 text-left": !!props.inline, + "min-h-[20px] w-fit flex-row justify-start gap-1.5 rounded px-1.5 py-0.5 text-left": !!props.inline, "min-h-[42px] w-full flex-col justify-center rounded-[8px] px-0.5 py-1 text-center": !props.inline, "bg-surface-raised-base text-text-strong": props.active, }} onClick={props.onClick} > - - FOCUS - - {props.active ? "ON" : "OFF"} - + FOCUS + + {props.active ? "ON" : "OFF"} ) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 0267b4b4ef..8291bfc947 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -29,7 +29,7 @@ import { } from "solid-js" import { createStore, produce } from "solid-js/store" import { useParams } from "@solidjs/router" -import { ExternalLink } from "@/components/external-link" +import { Link } from "@/components/link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" @@ -836,12 +836,12 @@ function ProviderConnection(props: {
{language.t("provider.connect.opencodeZen.line2")}
{language.t("provider.connect.opencodeZen.visit.prefix")} - {language.t("provider.connect.opencodeZen.visit.link")} - + {language.t("provider.connect.opencodeZen.visit.suffix")}
@@ -886,9 +886,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.opencodeZen.line2")}
{language.t("provider.connect.opencodeZen.visit.prefix")} - + {language.t("provider.connect.opencodeZen.visit.link")} - + {language.t("provider.connect.opencodeZen.visit.suffix")}
@@ -967,9 +967,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.oauth.code.visit.prefix")} - + {language.t("provider.connect.oauth.code.visit.link")} - + {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
@@ -1006,9 +1006,7 @@ function ProviderConnection(props: {
{language.t("provider.connect.oauth.code.visit.prefix")} - - {language.t("provider.connect.oauth.code.visit.link")} - + {language.t("provider.connect.oauth.code.visit.link")} {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
@@ -1079,9 +1077,7 @@ function ProviderConnection(props: {
{language.t("provider.connect.oauth.auto.visit.prefix")} - - {language.t("provider.connect.oauth.auto.visit.link")} - + {language.t("provider.connect.oauth.auto.visit.link")} {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}

{language.t("provider.custom.description.prefix")} - + {language.t("provider.custom.description.link")} - + {language.t("provider.custom.description.suffix")}

diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index fc248d821e..2a6c877df1 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -18,8 +18,6 @@ interface DialogSelectDirectoryProps { server: ServerConnection.Any } -const RECENT_PROJECT_LIMIT = 5 - type Row = { absolute: string search: string @@ -59,9 +57,9 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const [filter, setFilter] = createSignal("") let list: ListRef | undefined - const missingHome = createMemo(() => !sync.data.path.home) + const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) const [fallbackPath] = createResource( - () => (missingHome() ? true : undefined), + () => (missingBase() ? true : undefined), async (): Promise => { if ((await sdk.protocol) !== "v1") return return sdk.client.path @@ -104,6 +102,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { return projects .map((project, index) => ({ project, at: byProject.get(project.worktree) ?? 0, index })) .sort((a, b) => b.at - a.at || a.index - b.index) + .slice(0, 5) .map(({ project }) => { const row = toRow(project.worktree, home(), "recent") const name = project.name || getFilename(project.worktree) @@ -117,10 +116,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const items = async (value: string) => { const results = await directories(value) const directoryRows = results.map((absolute) => toRow(absolute, home(), "folders")) - // Cap the idle list only. Once a query narrows the results, every project stays searchable. - const recent = recentProjects() - const visible = value ? recent : recent.slice(0, RECENT_PROJECT_LIMIT) - return uniqueRows([...visible, ...directoryRows]) + return uniqueRows([...recentProjects(), ...directoryRows]) } function resolve(absolute: string) { diff --git a/packages/app/src/components/dialog-usage-exceeded.tsx b/packages/app/src/components/dialog-usage-exceeded.tsx index bf5da751e2..e428d4c2bb 100644 --- a/packages/app/src/components/dialog-usage-exceeded.tsx +++ b/packages/app/src/components/dialog-usage-exceeded.tsx @@ -17,7 +17,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) { const platform = usePlatform() const runAction = () => { - if (props.link) platform.openExternal(props.link) + if (props.link) platform.openLink(props.link) props.onClose?.() dialog.close() } diff --git a/packages/app/src/components/external-link.tsx b/packages/app/src/components/external-link.tsx deleted file mode 100644 index 133e752eab..0000000000 --- a/packages/app/src/components/external-link.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ComponentProps, splitProps } from "solid-js" - -export interface ExternalLinkProps extends Omit, "href"> { - href: string -} - -export function ExternalLink(props: ExternalLinkProps) { - const [local, rest] = splitProps(props, ["href", "children", "class", "target", "rel"]) - - return ( - - {local.children} - - ) -} diff --git a/packages/app/src/components/link.tsx b/packages/app/src/components/link.tsx new file mode 100644 index 0000000000..85f7efc539 --- /dev/null +++ b/packages/app/src/components/link.tsx @@ -0,0 +1,26 @@ +import { ComponentProps, splitProps } from "solid-js" +import { usePlatform } from "@/context/platform" + +export interface LinkProps extends Omit, "href"> { + href: string +} + +export function Link(props: LinkProps) { + const platform = usePlatform() + const [local, rest] = splitProps(props, ["href", "children", "class"]) + + return ( + { + if (!local.href) return + event.preventDefault() + platform.openLink(local.href) + }} + {...rest} + > + {local.children} + + ) +} diff --git a/packages/app/src/components/prompt-input-v2.tsx b/packages/app/src/components/prompt-input-v2.tsx index 96b99aaf4a..2d6a5e98d6 100644 --- a/packages/app/src/components/prompt-input-v2.tsx +++ b/packages/app/src/components/prompt-input-v2.tsx @@ -344,7 +344,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): if (item?.commentID) comments.remove(item.path, item.commentID) }, openAttachment: (attachment) => - dialog.show(() => ), + dialog.show(() => ), openContext(key) { const item = controller.contextItem(key) if (item) openComment(item, props, sync, layout, files, comments) @@ -377,7 +377,6 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): }), readClipboardImage: platform.readClipboardImage, getPathForFile: platform.getPathForFile, - store: platform.draftStore?.putBlob, }, view: { placeholder: designPlaceholder, diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 08c1ee3489..7923e18ee2 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1489,7 +1489,7 @@ export const PromptInput: Component = (props) => { - dialog.show(() => ) + dialog.show(() => ) } onRemove={removeAttachment} removeLabel={language.t("prompt.attachment.remove")} diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index 6f3ef57c66..9568b17d19 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -3,13 +3,28 @@ import { makeEventListener } from "@solid-primitives/event-listener" import { showToast } from "@/utils/toast" import { type ContentPart, type ImageAttachmentPart, type usePrompt } from "@/context/prompt" import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" import { uuid } from "@/utils/uuid" import { getCursorPosition } from "./editor-dom" -import { createBlobReference, type DraftStore } from "@/utils/draft-store" import { attachmentMime } from "./files" import { normalizePaste, pasteMode } from "./paste" +function dataUrl(file: File, mime: string) { + return new Promise((resolve) => { + const reader = new FileReader() + reader.addEventListener("error", () => resolve("")) + reader.addEventListener("load", () => { + const value = typeof reader.result === "string" ? reader.result : "" + const idx = value.indexOf(",") + if (idx === -1) { + resolve(value) + return + } + resolve(`data:${mime};base64,${value.slice(idx + 1)}`) + }) + reader.readAsDataURL(file) + }) +} + type PromptTarget = Pick["capture"]>, "current" | "cursor" | "set"> type AttachmentTarget = { prompt: PromptTarget; cursor: number | undefined } @@ -21,7 +36,6 @@ type PromptAttachmentsCoreInput = { warn?: () => void readClipboardImage?: () => Promise getPathForFile?: (file: File) => string - draftStore?: DraftStore } export type PromptAttachmentsInput = { @@ -51,13 +65,16 @@ export function createPromptAttachmentsCore(input: PromptAttachmentsCoreInput) { return false } + const url = await dataUrl(file, mime) + if (!url) return false + const attachment: ImageAttachmentPart = { type: "image", id: uuid(), filename: file.name, sourcePath: input.getPathForFile?.(file) || undefined, mime, - blob: input.draftStore ? await input.draftStore.putBlob(file) : await createBlobReference(file), + dataUrl: url, } target.prompt.set([...target.prompt.current(), attachment], target.cursor) return true @@ -149,10 +166,8 @@ export function createPromptAttachmentsCore(input: PromptAttachmentsCoreInput) { export function createPromptAttachments(input: PromptAttachmentsInput) { const language = useLanguage() - const platform = usePlatform() const attachments = createPromptAttachmentsCore({ ...input, - draftStore: platform.draftStore, capture: input.prompt.capture, warn: () => { showToast({ diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts index a1e3448b31..7f0fea2bfe 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -22,7 +22,7 @@ type ContextFile = { type BuildRequestPartsInput = { prompt: Prompt context: ContextFile[] - images: (Omit & { dataUrl: string })[] + images: ImageAttachmentPart[] text: string messageID: string sessionID: string diff --git a/packages/app/src/components/prompt-input/history-store.ts b/packages/app/src/components/prompt-input/history-store.ts index 3858b8b9bc..b64636f3c0 100644 --- a/packages/app/src/components/prompt-input/history-store.ts +++ b/packages/app/src/components/prompt-input/history-store.ts @@ -1,13 +1,7 @@ import { createStore, type SetStoreFunction, type Store } from "solid-js/store" import type { Prompt } from "@/context/prompt" import { Persist, persisted } from "@/utils/persist" -import { - clonePromptHistoryComments, - clonePromptParts, - prependHistoryEntry, - type PromptHistoryComment, - type PromptHistoryStoredEntry, -} from "./history" +import { prependHistoryEntry, type PromptHistoryComment, type PromptHistoryStoredEntry } from "./history" export type PromptInputHistory = { entries: (mode: "normal" | "shell") => PromptHistoryStoredEntry[] @@ -41,23 +35,13 @@ export function createPromptInputHistory(): PromptInputHistory { } export function createPersistedPromptInputHistory() { - const [normal, setNormal, normalInit] = persisted( - Persist.prompt(Persist.global("prompt-history", ["prompt-history.v1"])), + const [normal, setNormal] = persisted( + Persist.global("prompt-history", ["prompt-history.v1"]), createStore({ entries: [] }), ) - const [shell, setShell, shellInit] = persisted( - Persist.prompt(Persist.global("prompt-history-shell", ["prompt-history-shell.v1"])), + const [shell, setShell] = persisted( + Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]), createStore({ entries: [] }), ) - const history = createPromptInputHistoryStore(normal, setNormal, shell, setShell) - return { - ...history, - add(prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) { - const ready = mode === "shell" ? shellInit : normalInit - if (!(ready instanceof Promise)) return history.add(prompt, mode, comments) - const saved = clonePromptParts(prompt) - const metadata = clonePromptHistoryComments(comments) - void ready.then(() => history.add(saved, mode, metadata)) - }, - } + return createPromptInputHistoryStore(normal, setNormal, shell, setShell) } diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts index 94f24ce935..5e9c2c66ea 100644 --- a/packages/app/src/components/prompt-input/history.test.ts +++ b/packages/app/src/components/prompt-input/history.test.ts @@ -112,7 +112,7 @@ describe("prompt-input history", () => { end: 12, selection: { startLine: 1, startChar: 1, endLine: 2, endChar: 1 }, }, - { type: "image", id: "1", filename: "img.png", mime: "image/png", blob: { id: "blob", url: "blob:test" } }, + { type: "image", id: "1", filename: "img.png", mime: "image/png", dataUrl: "data:image/png;base64,abc" }, ] const copy = clonePromptParts(original) expect(copy).not.toBe(original) diff --git a/packages/app/src/components/prompt-input/image-attachments.tsx b/packages/app/src/components/prompt-input/image-attachments.tsx index ecc38db888..3a0cc3e4dd 100644 --- a/packages/app/src/components/prompt-input/image-attachments.tsx +++ b/packages/app/src/components/prompt-input/image-attachments.tsx @@ -100,7 +100,7 @@ export const PromptImageAttachments: Component = (p } > {attachment.filename} props.onOpen(attachment)} diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index c82c3a4399..051bf4d06c 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -22,7 +22,6 @@ import { ScopedKey } from "@/utils/server-scope" import { createPromptSubmissionState } from "./submission-state" import { normalizeSessionInfo } from "@/utils/session" import { Event } from "@opencode-ai/schema/event" -import { blobDataUrl } from "@/utils/draft-store" type PendingPrompt = { abort: AbortController @@ -96,12 +95,10 @@ export async function sendFollowupDraft(input: FollowupSendInput) { providerID: input.draft.model.providerID, variant: input.draft.variant, }, - files: await Promise.all( - images.map(async (attachment) => ({ - uri: await blobDataUrl(attachment.blob, attachment.mime), - name: attachment.filename, - })), - ), + files: images.map((attachment) => ({ + uri: attachment.dataUrl, + name: attachment.filename, + })), }) return true } catch (err) { @@ -111,16 +108,10 @@ export async function sendFollowupDraft(input: FollowupSendInput) { } const messageID = input.messageID ?? Identifier.ascending("message") - const encodedImages = await Promise.all( - images.map(async (attachment) => ({ - ...attachment, - dataUrl: await blobDataUrl(attachment.blob, attachment.mime), - })), - ) const { requestParts, optimisticParts } = buildRequestParts({ prompt: input.draft.prompt, context: input.draft.context, - images: encodedImages, + images, text, sessionID: input.draft.sessionID, messageID, @@ -525,12 +516,10 @@ export function createPromptSubmit(input: PromptSubmitInput) { arguments: args.join(" "), agent, model: { id: model.modelID, providerID: model.providerID, variant }, - files: await Promise.all( - images.map(async (attachment) => ({ - uri: await blobDataUrl(attachment.blob, attachment.mime), - name: attachment.filename, - })), - ), + files: images.map((attachment) => ({ + uri: attachment.dataUrl, + name: attachment.filename, + })), }) .catch((err) => { serverSync().session.set("session_status", session.id, { type: "idle" }) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index dc6cdaa6f3..da1770cd13 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -289,9 +289,9 @@ export function SessionHeader() { return ( <> - + {(mount) => ( - +
- + {(keybind) => ( - {keybind} + {keybind()} )} @@ -319,9 +319,9 @@ export function SessionHeader() { )} - + {(mount) => ( - + { description={ <> {language.t("settings.general.row.theme.description")}{" "} - {language.t("common.learnMore")} + {language.t("common.learnMore")} } > diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx index b41da107ed..e50bd316c9 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -9,7 +9,7 @@ import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useUpdaterAction } from "../updater-action" import { useSettings } from "@/context/settings" -import { ExternalLink } from "../external-link" +import { Link } from "../link" import { SettingsListV2 } from "./parts/list" import { SettingsRowV2 } from "./parts/row" import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition" @@ -150,9 +150,9 @@ const AppearanceSection: Component<{ controller: AppearanceSettingsController }> description={ <> {language.t("settings.general.row.theme.description")}{" "} - + {language.t("common.learnMore")} - + } > diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 7d25e7c649..6503503476 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -382,11 +382,7 @@ export const Terminal = (props: TerminalProps) => { event.preventDefault() event.stopImmediatePropagation() - if (URL.canParse(text) && new URL(text).protocol === "file:" && platform.openLocalFile) { - platform.openLocalFile(text) - return - } - platform.openExternal(text) + platform.openLink(text) } onMount(() => { diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index 5003d0ed30..3823cf10b8 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -224,7 +224,6 @@ export function TabNavItem(props: {
- -