Merge branch 'dev' into go-page
128
.github/workflows/publish.yml
vendored
|
|
@ -99,7 +99,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ needs.version.outputs.version }}
|
version: ${{ needs.version.outputs.version }}
|
||||||
|
|
||||||
|
|
@ -240,11 +239,131 @@ jobs:
|
||||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||||
|
|
||||||
|
build-electron:
|
||||||
|
needs:
|
||||||
|
- build-cli
|
||||||
|
- version
|
||||||
|
continue-on-error: false
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
settings:
|
||||||
|
- host: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
platform_flag: --mac --x64
|
||||||
|
- host: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
platform_flag: --mac --arm64
|
||||||
|
- host: "blacksmith-4vcpu-windows-2025"
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
platform_flag: --win
|
||||||
|
- host: "blacksmith-4vcpu-ubuntu-2404"
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
platform_flag: --linux
|
||||||
|
- host: "blacksmith-4vcpu-ubuntu-2404"
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
platform_flag: --linux
|
||||||
|
runs-on: ${{ matrix.settings.host }}
|
||||||
|
# if: github.ref_name == 'beta'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: apple-actions/import-codesign-certs@v2
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
with:
|
||||||
|
keychain: build
|
||||||
|
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Setup Apple API Key
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
|
||||||
|
- name: Cache apt packages
|
||||||
|
if: contains(matrix.settings.host, 'ubuntu')
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/apt-cache
|
||||||
|
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron-${{ hashFiles('.github/workflows/publish.yml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.settings.target }}-apt-electron-
|
||||||
|
|
||||||
|
- name: Install dependencies (ubuntu only)
|
||||||
|
if: contains(matrix.settings.host, 'ubuntu')
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" rpm
|
||||||
|
sudo chmod -R a+rw ~/apt-cache
|
||||||
|
|
||||||
|
- name: Setup git committer
|
||||||
|
id: committer
|
||||||
|
uses: ./.github/actions/setup-git-committer
|
||||||
|
with:
|
||||||
|
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||||
|
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: bun ./scripts/prepare.ts
|
||||||
|
working-directory: packages/desktop-electron
|
||||||
|
env:
|
||||||
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
RUST_TARGET: ${{ matrix.settings.target }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: bun run build
|
||||||
|
working-directory: packages/desktop-electron
|
||||||
|
env:
|
||||||
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
|
||||||
|
- name: Package and publish
|
||||||
|
if: needs.version.outputs.release
|
||||||
|
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish always --config electron-builder.config.ts
|
||||||
|
working-directory: packages/desktop-electron
|
||||||
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
- name: Package (no publish)
|
||||||
|
if: ${{ !needs.version.outputs.release }}
|
||||||
|
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
|
||||||
|
working-directory: packages/desktop-electron
|
||||||
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-electron-${{ matrix.settings.target }}
|
||||||
|
path: packages/desktop-electron/dist/*
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: needs.version.outputs.release
|
||||||
|
with:
|
||||||
|
name: latest-yml-${{ matrix.settings.target }}
|
||||||
|
path: packages/desktop-electron/dist/latest*.yml
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs:
|
needs:
|
||||||
- version
|
- version
|
||||||
- build-cli
|
- build-cli
|
||||||
- build-tauri
|
- build-tauri
|
||||||
|
- build-electron
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
@ -281,6 +400,12 @@ jobs:
|
||||||
name: opencode-cli
|
name: opencode-cli
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
if: needs.version.outputs.release
|
||||||
|
with:
|
||||||
|
pattern: latest-yml-*
|
||||||
|
path: /tmp/latest-yml
|
||||||
|
|
||||||
- name: Cache apt packages (AUR)
|
- name: Cache apt packages (AUR)
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -308,3 +433,4 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
GH_REPO: ${{ needs.version.outputs.repo }}
|
GH_REPO: ${{ needs.version.outputs.repo }}
|
||||||
NPM_CONFIG_PROVENANCE: false
|
NPM_CONFIG_PROVENANCE: false
|
||||||
|
LATEST_YML_DIR: /tmp/latest-yml
|
||||||
|
|
|
||||||
11
AGENTS.md
|
|
@ -20,6 +20,17 @@
|
||||||
|
|
||||||
Prefer single word names for variables and functions. Only use multiple words if necessary.
|
Prefer single word names for variables and functions. Only use multiple words if necessary.
|
||||||
|
|
||||||
|
### Naming Enforcement (Read This)
|
||||||
|
|
||||||
|
THIS RULE IS MANDATORY FOR AGENT WRITTEN CODE.
|
||||||
|
|
||||||
|
- Use single word names by default for new locals, params, and helper functions.
|
||||||
|
- Multi-word names are allowed only when a single word would be unclear or ambiguous.
|
||||||
|
- Do not introduce new camelCase compounds when a short single-word alternative is clear.
|
||||||
|
- Before finishing edits, review touched lines and shorten newly introduced identifiers where possible.
|
||||||
|
- Good short names to prefer: `pid`, `cfg`, `err`, `opts`, `dir`, `root`, `child`, `state`, `timeout`.
|
||||||
|
- Examples to avoid unless truly required: `inputPID`, `existingClient`, `connectTimeout`, `workerPath`.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// Good
|
// Good
|
||||||
const foo = 1
|
const foo = 1
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-8jEwsY7X7N/vKKbVZ0L8Djj2SfH9HCY+2jKSlaCrm9o=",
|
"x86_64-linux": "sha256-jtBYpfiE9g0otqZEtOksW1Nbg+O8CJP9OEOEhsa7sa8=",
|
||||||
"aarch64-linux": "sha256-L0G7mSzzR+sZW0uACosJGsE2y/Uh3Vi4piXL4UJOmCw=",
|
"aarch64-linux": "sha256-m+YNZIB7I7EMPyfqkKsvDvmBX9R1szmEKxXpxTNFLH8=",
|
||||||
"aarch64-darwin": "sha256-1S/g/51MSHjDfsL+U8wlt9Rl50hFf7I3fHgbhSqBIP4=",
|
"aarch64-darwin": "sha256-1gVmtkC1/I8sdHZcaeSFJheySVlpCyKCjf9zbVsVqAQ=",
|
||||||
"x86_64-darwin": "sha256-cveFpKVwcrUOzomU4J3wgYEKRwmJQF0KQiRqKgLJqWs="
|
"x86_64-darwin": "sha256-Tvk5YL6Z0xRul4jopbGme/997iHBylXC0Cq3RnjQb+I="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,8 @@
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
"web-tree-sitter"
|
"web-tree-sitter",
|
||||||
|
"electron"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.2.15",
|
"version": "1.2.17",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||||
import { Font } from "@opencode-ai/ui/font"
|
import { Font } from "@opencode-ai/ui/font"
|
||||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import { Navigate, Route, Router } from "@solidjs/router"
|
import { BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
||||||
import { ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
|
import { Component, ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
|
||||||
import { CommandProvider } from "@/context/command"
|
import { CommandProvider } from "@/context/command"
|
||||||
import { CommentsProvider } from "@/context/comments"
|
import { CommentsProvider } from "@/context/comments"
|
||||||
import { FileProvider } from "@/context/file"
|
import { FileProvider } from "@/context/file"
|
||||||
|
|
@ -28,6 +28,7 @@ import { TerminalProvider } from "@/context/terminal"
|
||||||
import DirectoryLayout from "@/pages/directory-layout"
|
import DirectoryLayout from "@/pages/directory-layout"
|
||||||
import Layout from "@/pages/layout"
|
import Layout from "@/pages/layout"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
|
import { Dynamic } from "solid-js/web"
|
||||||
|
|
||||||
const Home = lazy(() => import("@/pages/home"))
|
const Home = lazy(() => import("@/pages/home"))
|
||||||
const Session = lazy(() => import("@/pages/session"))
|
const Session = lazy(() => import("@/pages/session"))
|
||||||
|
|
@ -144,13 +145,15 @@ export function AppInterface(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
defaultServer: ServerConnection.Key
|
defaultServer: ServerConnection.Key
|
||||||
servers?: Array<ServerConnection.Any>
|
servers?: Array<ServerConnection.Any>
|
||||||
|
router?: Component<BaseRouterProps>
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
||||||
<ServerKey>
|
<ServerKey>
|
||||||
<GlobalSDKProvider>
|
<GlobalSDKProvider>
|
||||||
<GlobalSyncProvider>
|
<GlobalSyncProvider>
|
||||||
<Router
|
<Dynamic
|
||||||
|
component={props.router ?? Router}
|
||||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||||
>
|
>
|
||||||
<Route path="/" component={HomeRoute} />
|
<Route path="/" component={HomeRoute} />
|
||||||
|
|
@ -158,7 +161,7 @@ export function AppInterface(props: {
|
||||||
<Route path="/" component={SessionIndexRoute} />
|
<Route path="/" component={SessionIndexRoute} />
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
</Route>
|
</Route>
|
||||||
</Router>
|
</Dynamic>
|
||||||
</GlobalSyncProvider>
|
</GlobalSyncProvider>
|
||||||
</GlobalSDKProvider>
|
</GlobalSDKProvider>
|
||||||
</ServerKey>
|
</ServerKey>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
|
||||||
export interface TerminalProps extends ComponentProps<"div"> {
|
export interface TerminalProps extends ComponentProps<"div"> {
|
||||||
pty: LocalPTY
|
pty: LocalPTY
|
||||||
onSubmit?: () => void
|
onSubmit?: () => void
|
||||||
onCleanup?: (pty: LocalPTY) => void
|
onCleanup?: (pty: Partial<LocalPTY> & { id: string }) => void
|
||||||
onConnect?: () => void
|
onConnect?: () => void
|
||||||
onConnectError?: (error: unknown) => void
|
onConnectError?: (error: unknown) => void
|
||||||
}
|
}
|
||||||
|
|
@ -126,8 +126,8 @@ const persistTerminal = (input: {
|
||||||
term: Term | undefined
|
term: Term | undefined
|
||||||
addon: SerializeAddon | undefined
|
addon: SerializeAddon | undefined
|
||||||
cursor: number
|
cursor: number
|
||||||
pty: LocalPTY
|
id: string
|
||||||
onCleanup?: (pty: LocalPTY) => void
|
onCleanup?: (pty: Partial<LocalPTY> & { id: string }) => void
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.addon || !input.onCleanup || !input.term) return
|
if (!input.addon || !input.onCleanup || !input.term) return
|
||||||
const buffer = (() => {
|
const buffer = (() => {
|
||||||
|
|
@ -140,7 +140,7 @@ const persistTerminal = (input: {
|
||||||
})()
|
})()
|
||||||
|
|
||||||
input.onCleanup({
|
input.onCleanup({
|
||||||
...input.pty,
|
id: input.id,
|
||||||
buffer,
|
buffer,
|
||||||
cursor: input.cursor,
|
cursor: input.cursor,
|
||||||
rows: input.term.rows,
|
rows: input.term.rows,
|
||||||
|
|
@ -158,6 +158,19 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
|
||||||
|
const id = local.pty.id
|
||||||
|
const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
|
||||||
|
const restoreSize =
|
||||||
|
restore &&
|
||||||
|
typeof local.pty.cols === "number" &&
|
||||||
|
Number.isSafeInteger(local.pty.cols) &&
|
||||||
|
local.pty.cols > 0 &&
|
||||||
|
typeof local.pty.rows === "number" &&
|
||||||
|
Number.isSafeInteger(local.pty.rows) &&
|
||||||
|
local.pty.rows > 0
|
||||||
|
? { cols: local.pty.cols, rows: local.pty.rows }
|
||||||
|
: undefined
|
||||||
|
const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined
|
||||||
let ws: WebSocket | undefined
|
let ws: WebSocket | undefined
|
||||||
let term: Term | undefined
|
let term: Term | undefined
|
||||||
let ghostty: Ghostty
|
let ghostty: Ghostty
|
||||||
|
|
@ -190,7 +203,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
const pushSize = (cols: number, rows: number) => {
|
const pushSize = (cols: number, rows: number) => {
|
||||||
return sdk.client.pty
|
return sdk.client.pty
|
||||||
.update({
|
.update({
|
||||||
ptyID: local.pty.id,
|
ptyID: id,
|
||||||
size: { cols, rows },
|
size: { cols, rows },
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
@ -319,18 +332,6 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
const mod = loaded.mod
|
const mod = loaded.mod
|
||||||
const g = loaded.ghostty
|
const g = loaded.ghostty
|
||||||
|
|
||||||
const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
|
|
||||||
const restoreSize =
|
|
||||||
restore &&
|
|
||||||
typeof local.pty.cols === "number" &&
|
|
||||||
Number.isSafeInteger(local.pty.cols) &&
|
|
||||||
local.pty.cols > 0 &&
|
|
||||||
typeof local.pty.rows === "number" &&
|
|
||||||
Number.isSafeInteger(local.pty.rows) &&
|
|
||||||
local.pty.rows > 0
|
|
||||||
? { cols: local.pty.cols, rows: local.pty.rows }
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const t = new mod.Terminal({
|
const t = new mod.Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: "bar",
|
cursorStyle: "bar",
|
||||||
|
|
@ -427,14 +428,14 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
await write(restore)
|
await write(restore)
|
||||||
fit.fit()
|
fit.fit()
|
||||||
scheduleSize(t.cols, t.rows)
|
scheduleSize(t.cols, t.rows)
|
||||||
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
|
if (scrollY !== undefined) t.scrollToLine(scrollY)
|
||||||
startResize()
|
startResize()
|
||||||
} else {
|
} else {
|
||||||
fit.fit()
|
fit.fit()
|
||||||
scheduleSize(t.cols, t.rows)
|
scheduleSize(t.cols, t.rows)
|
||||||
if (restore) {
|
if (restore) {
|
||||||
await write(restore)
|
await write(restore)
|
||||||
if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY)
|
if (scrollY !== undefined) t.scrollToLine(scrollY)
|
||||||
}
|
}
|
||||||
startResize()
|
startResize()
|
||||||
}
|
}
|
||||||
|
|
@ -446,9 +447,9 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
const once = { value: false }
|
const once = { value: false }
|
||||||
let closing = false
|
let closing = false
|
||||||
|
|
||||||
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`)
|
const url = new URL(sdk.url + `/pty/${id}/connect`)
|
||||||
url.searchParams.set("directory", sdk.directory)
|
url.searchParams.set("directory", sdk.directory)
|
||||||
url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0))
|
url.searchParams.set("cursor", String(start !== undefined ? start : restore ? -1 : 0))
|
||||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
||||||
url.username = server.current?.http.username ?? ""
|
url.username = server.current?.http.username ?? ""
|
||||||
url.password = server.current?.http.password ?? ""
|
url.password = server.current?.http.password ?? ""
|
||||||
|
|
@ -542,7 +543,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000)
|
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000)
|
||||||
|
|
||||||
const finalize = () => {
|
const finalize = () => {
|
||||||
persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
|
persistTerminal({ term, addon: serializeAddon, cursor, id, onCleanup: props.onCleanup })
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,7 @@ export function Titlebar() {
|
||||||
<header
|
<header
|
||||||
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
|
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
|
||||||
style={{ "min-height": minHeight() }}
|
style={{ "min-height": minHeight() }}
|
||||||
|
data-tauri-drag-region
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
onDblClick={maximize}
|
onDblClick={maximize}
|
||||||
>
|
>
|
||||||
|
|
@ -276,6 +277,7 @@ export function Titlebar() {
|
||||||
"flex items-center min-w-0 justify-end": true,
|
"flex items-center min-w-0 justify-end": true,
|
||||||
"pr-2": !windows(),
|
"pr-2": !windows(),
|
||||||
}}
|
}}
|
||||||
|
data-tauri-drag-region
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
>
|
>
|
||||||
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { retry } from "@opencode-ai/util/retry"
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { playSound, soundSrc } from "@/utils/sound"
|
import { playSound, soundSrc } from "@/utils/sound"
|
||||||
import { createAim } from "@/utils/aim"
|
import { createAim } from "@/utils/aim"
|
||||||
|
import { setNavigate } from "@/utils/notification-click"
|
||||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||||
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
|
@ -107,6 +108,7 @@ export default function Layout(props: ParentProps) {
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
setNavigate(navigate)
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { useNotification } from "@/context/notification"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
|
||||||
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
|
@ -137,13 +136,6 @@ const SessionRow = (props: {
|
||||||
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||||
{props.session.title}
|
{props.session.title}
|
||||||
</span>
|
</span>
|
||||||
<Show when={props.session.summary}>
|
|
||||||
{(summary) => (
|
|
||||||
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
|
||||||
<DiffChanges changes={summary()} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</A>
|
</A>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1254,6 +1254,7 @@ export default function Page() {
|
||||||
|
|
||||||
<SessionComposerRegion
|
<SessionComposerRegion
|
||||||
state={composer}
|
state={composer}
|
||||||
|
ready={!store.deferRender && messagesReady()}
|
||||||
centered={centered()}
|
centered={centered()}
|
||||||
inputRef={(el) => {
|
inputRef={(el) => {
|
||||||
inputRef = el
|
inputRef = el
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||||
import { PromptInput } from "@/components/prompt-input"
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
|
|
@ -12,6 +13,7 @@ import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
||||||
|
|
||||||
export function SessionComposerRegion(props: {
|
export function SessionComposerRegion(props: {
|
||||||
state: SessionComposerState
|
state: SessionComposerState
|
||||||
|
ready: boolean
|
||||||
centered: boolean
|
centered: boolean
|
||||||
inputRef: (el: HTMLDivElement) => void
|
inputRef: (el: HTMLDivElement) => void
|
||||||
newSessionWorktree: string
|
newSessionWorktree: string
|
||||||
|
|
@ -61,7 +63,44 @@ export function SessionComposerRegion(props: {
|
||||||
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
||||||
})
|
})
|
||||||
|
|
||||||
const open = createMemo(() => props.state.dock() && !props.state.closing())
|
const [gate, setGate] = createStore({
|
||||||
|
ready: false,
|
||||||
|
})
|
||||||
|
let timer: number | undefined
|
||||||
|
let frame: number | undefined
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
if (timer !== undefined) {
|
||||||
|
window.clearTimeout(timer)
|
||||||
|
timer = undefined
|
||||||
|
}
|
||||||
|
if (frame !== undefined) {
|
||||||
|
cancelAnimationFrame(frame)
|
||||||
|
frame = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
sessionKey()
|
||||||
|
const ready = props.ready
|
||||||
|
const delay = 140
|
||||||
|
|
||||||
|
clear()
|
||||||
|
setGate("ready", false)
|
||||||
|
if (!ready) return
|
||||||
|
|
||||||
|
frame = requestAnimationFrame(() => {
|
||||||
|
frame = undefined
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
setGate("ready", true)
|
||||||
|
timer = undefined
|
||||||
|
}, delay)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(clear)
|
||||||
|
|
||||||
|
const open = createMemo(() => gate.ready && props.state.dock() && !props.state.closing())
|
||||||
const config = createMemo(() =>
|
const config = createMemo(() =>
|
||||||
open()
|
open()
|
||||||
? {
|
? {
|
||||||
|
|
@ -76,7 +115,7 @@ export function SessionComposerRegion(props: {
|
||||||
const progress = useSpring(() => (open() ? 1 : 0), config)
|
const progress = useSpring(() => (open() ? 1 : 0), config)
|
||||||
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
|
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
|
||||||
const [height, setHeight] = createSignal(320)
|
const [height, setHeight] = createSignal(320)
|
||||||
const dock = createMemo(() => props.state.dock() || value() > 0.001)
|
const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001)
|
||||||
const full = createMemo(() => Math.max(78, height()))
|
const full = createMemo(() => Math.max(78, height()))
|
||||||
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
|
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -281,10 +281,8 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||||
style={{
|
style={{
|
||||||
"--checkbox-align": "flex-start",
|
"--checkbox-align": "flex-start",
|
||||||
"--checkbox-offset": "1px",
|
"--checkbox-offset": "1px",
|
||||||
transition:
|
transition: "opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
|
||||||
"opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), filter 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
|
|
||||||
opacity: todo().status === "pending" ? "0.94" : "1",
|
opacity: todo().status === "pending" ? "0.94" : "1",
|
||||||
filter: todo().status === "pending" ? "blur(0.3px)" : "blur(0px)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextStrikethrough
|
<TextStrikethrough
|
||||||
|
|
@ -294,13 +292,12 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||||
style={{
|
style={{
|
||||||
"line-height": "var(--line-height-normal)",
|
"line-height": "var(--line-height-normal)",
|
||||||
transition:
|
transition:
|
||||||
"color 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), filter 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
|
"color 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
|
||||||
color:
|
color:
|
||||||
todo().status === "completed" || todo().status === "cancelled"
|
todo().status === "completed" || todo().status === "cancelled"
|
||||||
? "var(--text-weak)"
|
? "var(--text-weak)"
|
||||||
: "var(--text-strong)",
|
: "var(--text-strong)",
|
||||||
opacity: todo().status === "pending" ? "0.92" : "1",
|
opacity: todo().status === "pending" ? "0.92" : "1",
|
||||||
filter: todo().status === "pending" ? "blur(0.3px)" : "blur(0px)",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|
|
||||||
|
|
@ -550,227 +550,228 @@ export function MessageTimeline(props: {
|
||||||
"--sticky-accordion-top": showHeader() ? "48px" : "0px",
|
"--sticky-accordion-top": showHeader() ? "48px" : "0px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={showHeader()}>
|
<div ref={props.setContentRef} class="min-w-0 w-full">
|
||||||
<div
|
<Show when={showHeader()}>
|
||||||
data-session-title
|
<div
|
||||||
classList={{
|
data-session-title
|
||||||
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
classList={{
|
||||||
"w-full": true,
|
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
||||||
"pb-4": true,
|
"w-full": true,
|
||||||
"pl-2 pr-3 md:pl-4 md:pr-3": true,
|
"pb-4": true,
|
||||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
"pl-2 pr-3 md:pl-4 md:pr-3": true,
|
||||||
}}
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||||
>
|
}}
|
||||||
<div class="h-12 w-full flex items-center justify-between gap-2">
|
>
|
||||||
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
<div class="h-12 w-full flex items-center justify-between gap-2">
|
||||||
<Show when={parentID()}>
|
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
||||||
<IconButton
|
<Show when={parentID()}>
|
||||||
tabIndex={-1}
|
<IconButton
|
||||||
icon="arrow-left"
|
tabIndex={-1}
|
||||||
variant="ghost"
|
icon="arrow-left"
|
||||||
onClick={navigateParent}
|
variant="ghost"
|
||||||
aria-label={language.t("common.goBack")}
|
onClick={navigateParent}
|
||||||
/>
|
aria-label={language.t("common.goBack")}
|
||||||
</Show>
|
|
||||||
<Show when={titleValue() || title.editing}>
|
|
||||||
<Show
|
|
||||||
when={title.editing}
|
|
||||||
fallback={
|
|
||||||
<h1
|
|
||||||
class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
|
|
||||||
onDblClick={openTitleEditor}
|
|
||||||
>
|
|
||||||
{titleValue()}
|
|
||||||
</h1>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<InlineInput
|
|
||||||
ref={(el) => {
|
|
||||||
titleRef = el
|
|
||||||
}}
|
|
||||||
value={title.draft}
|
|
||||||
disabled={title.saving}
|
|
||||||
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
|
|
||||||
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
|
||||||
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.preventDefault()
|
|
||||||
void saveTitleEditor()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
event.preventDefault()
|
|
||||||
closeTitleEditor()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={closeTitleEditor}
|
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={titleValue() || title.editing}>
|
||||||
|
<Show
|
||||||
|
when={title.editing}
|
||||||
|
fallback={
|
||||||
|
<h1
|
||||||
|
class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
|
||||||
|
onDblClick={openTitleEditor}
|
||||||
|
>
|
||||||
|
{titleValue()}
|
||||||
|
</h1>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InlineInput
|
||||||
|
ref={(el) => {
|
||||||
|
titleRef = el
|
||||||
|
}}
|
||||||
|
value={title.draft}
|
||||||
|
disabled={title.saving}
|
||||||
|
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
|
||||||
|
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||||
|
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
void saveTitleEditor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault()
|
||||||
|
closeTitleEditor()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={closeTitleEditor}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={sessionID()}>
|
||||||
|
{(id) => (
|
||||||
|
<div class="shrink-0 flex items-center gap-3">
|
||||||
|
<SessionContextUsage placement="bottom" />
|
||||||
|
<DropdownMenu
|
||||||
|
gutter={4}
|
||||||
|
placement="bottom-end"
|
||||||
|
open={title.menuOpen}
|
||||||
|
onOpenChange={(open) => setTitle("menuOpen", open)}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
as={IconButton}
|
||||||
|
icon="dot-grid"
|
||||||
|
variant="ghost"
|
||||||
|
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||||
|
aria-label={language.t("common.moreOptions")}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
style={{ "min-width": "104px" }}
|
||||||
|
onCloseAutoFocus={(event) => {
|
||||||
|
if (!title.pendingRename) return
|
||||||
|
event.preventDefault()
|
||||||
|
setTitle("pendingRename", false)
|
||||||
|
openTitleEditor()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
setTitle("pendingRename", true)
|
||||||
|
setTitle("menuOpen", false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||||
|
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={sessionID()}>
|
|
||||||
{(id) => (
|
|
||||||
<div class="shrink-0 flex items-center gap-3">
|
|
||||||
<SessionContextUsage placement="bottom" />
|
|
||||||
<DropdownMenu
|
|
||||||
gutter={4}
|
|
||||||
placement="bottom-end"
|
|
||||||
open={title.menuOpen}
|
|
||||||
onOpenChange={(open) => setTitle("menuOpen", open)}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Trigger
|
|
||||||
as={IconButton}
|
|
||||||
icon="dot-grid"
|
|
||||||
variant="ghost"
|
|
||||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
|
||||||
aria-label={language.t("common.moreOptions")}
|
|
||||||
/>
|
|
||||||
<DropdownMenu.Portal>
|
|
||||||
<DropdownMenu.Content
|
|
||||||
style={{ "min-width": "104px" }}
|
|
||||||
onCloseAutoFocus={(event) => {
|
|
||||||
if (!title.pendingRename) return
|
|
||||||
event.preventDefault()
|
|
||||||
setTitle("pendingRename", false)
|
|
||||||
openTitleEditor()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
setTitle("pendingRename", true)
|
|
||||||
setTitle("menuOpen", false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref={props.setContentRef}
|
|
||||||
role="log"
|
|
||||||
class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
|
|
||||||
classList={{
|
|
||||||
"w-full": true,
|
|
||||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
|
||||||
"mt-0.5": props.centered,
|
|
||||||
"mt-0": !props.centered,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={props.turnStart > 0 || props.historyMore}>
|
|
||||||
<div class="w-full flex justify-center">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="large"
|
|
||||||
class="text-12-medium opacity-50"
|
|
||||||
disabled={props.historyLoading}
|
|
||||||
onClick={props.onLoadEarlier}
|
|
||||||
>
|
|
||||||
{props.historyLoading
|
|
||||||
? language.t("session.messages.loadingEarlier")
|
|
||||||
: language.t("session.messages.loadEarlier")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<For each={rendered()}>
|
|
||||||
{(messageID) => {
|
<div
|
||||||
const active = createMemo(() => activeMessageID() === messageID)
|
role="log"
|
||||||
const queued = createMemo(() => {
|
class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
|
||||||
if (active()) return false
|
classList={{
|
||||||
const activeID = activeMessageID()
|
"w-full": true,
|
||||||
if (activeID) return messageID > activeID
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||||
return false
|
"mt-0.5": props.centered,
|
||||||
})
|
"mt-0": !props.centered,
|
||||||
const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], {
|
}}
|
||||||
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
>
|
||||||
})
|
<Show when={props.turnStart > 0 || props.historyMore}>
|
||||||
const commentCount = createMemo(() => comments().length)
|
<div class="w-full flex justify-center">
|
||||||
return (
|
<Button
|
||||||
<div
|
variant="ghost"
|
||||||
id={props.anchor(messageID)}
|
size="large"
|
||||||
data-message-id={messageID}
|
class="text-12-medium opacity-50"
|
||||||
ref={(el) => {
|
disabled={props.historyLoading}
|
||||||
props.onRegisterMessage(el, messageID)
|
onClick={props.onLoadEarlier}
|
||||||
onCleanup(() => props.onUnregisterMessage(messageID))
|
|
||||||
}}
|
|
||||||
classList={{
|
|
||||||
"min-w-0 w-full max-w-full": true,
|
|
||||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Show when={commentCount() > 0}>
|
{props.historyLoading
|
||||||
<div class="w-full px-4 md:px-5 pb-2">
|
? language.t("session.messages.loadingEarlier")
|
||||||
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
|
: language.t("session.messages.loadEarlier")}
|
||||||
<div class="flex w-max min-w-full justify-end gap-2">
|
</Button>
|
||||||
<Index each={comments()}>
|
</div>
|
||||||
{(commentAccessor: () => MessageComment) => {
|
</Show>
|
||||||
const comment = createMemo(() => commentAccessor())
|
<For each={rendered()}>
|
||||||
return (
|
{(messageID) => {
|
||||||
<div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
|
const active = createMemo(() => activeMessageID() === messageID)
|
||||||
<div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
|
const queued = createMemo(() => {
|
||||||
<FileIcon
|
if (active()) return false
|
||||||
node={{ path: comment().path, type: "file" }}
|
const activeID = activeMessageID()
|
||||||
class="size-3.5 shrink-0"
|
if (activeID) return messageID > activeID
|
||||||
/>
|
return false
|
||||||
<span class="truncate">{getFilename(comment().path)}</span>
|
})
|
||||||
<Show when={comment().selection}>
|
const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], {
|
||||||
{(selection) => (
|
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
||||||
<span class="shrink-0 text-text-weak">
|
})
|
||||||
{selection().startLine === selection().endLine
|
const commentCount = createMemo(() => comments().length)
|
||||||
? `:${selection().startLine}`
|
return (
|
||||||
: `:${selection().startLine}-${selection().endLine}`}
|
<div
|
||||||
</span>
|
id={props.anchor(messageID)}
|
||||||
)}
|
data-message-id={messageID}
|
||||||
</Show>
|
ref={(el) => {
|
||||||
|
props.onRegisterMessage(el, messageID)
|
||||||
|
onCleanup(() => props.onUnregisterMessage(messageID))
|
||||||
|
}}
|
||||||
|
classList={{
|
||||||
|
"min-w-0 w-full max-w-full": true,
|
||||||
|
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={commentCount() > 0}>
|
||||||
|
<div class="w-full px-4 md:px-5 pb-2">
|
||||||
|
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
|
||||||
|
<div class="flex w-max min-w-full justify-end gap-2">
|
||||||
|
<Index each={comments()}>
|
||||||
|
{(commentAccessor: () => MessageComment) => {
|
||||||
|
const comment = createMemo(() => commentAccessor())
|
||||||
|
return (
|
||||||
|
<div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
|
||||||
|
<div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
|
||||||
|
<FileIcon
|
||||||
|
node={{ path: comment().path, type: "file" }}
|
||||||
|
class="size-3.5 shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="truncate">{getFilename(comment().path)}</span>
|
||||||
|
<Show when={comment().selection}>
|
||||||
|
{(selection) => (
|
||||||
|
<span class="shrink-0 text-text-weak">
|
||||||
|
{selection().startLine === selection().endLine
|
||||||
|
? `:${selection().startLine}`
|
||||||
|
: `:${selection().startLine}-${selection().endLine}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words">
|
||||||
|
{comment().comment}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words">
|
)
|
||||||
{comment().comment}
|
}}
|
||||||
</div>
|
</Index>
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Index>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
</Show>
|
<SessionTurn
|
||||||
<SessionTurn
|
sessionID={sessionID() ?? ""}
|
||||||
sessionID={sessionID() ?? ""}
|
messageID={messageID}
|
||||||
messageID={messageID}
|
active={active()}
|
||||||
active={active()}
|
queued={queued()}
|
||||||
queued={queued()}
|
status={active() ? sessionStatus() : undefined}
|
||||||
status={active() ? sessionStatus() : undefined}
|
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
||||||
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
|
||||||
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
|
editToolDefaultOpen={settings.general.editToolPartsExpanded()}
|
||||||
editToolDefaultOpen={settings.general.editToolPartsExpanded()}
|
classes={{
|
||||||
classes={{
|
root: "min-w-0 w-full relative",
|
||||||
root: "min-w-0 w-full relative",
|
content: "flex flex-col justify-between !overflow-visible",
|
||||||
content: "flex flex-col justify-between !overflow-visible",
|
container: "w-full px-4 md:px-5",
|
||||||
container: "w-full px-4 md:px-5",
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
}}
|
||||||
}}
|
</For>
|
||||||
</For>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export function TerminalPanel() {
|
||||||
|
|
||||||
const all = createMemo(() => terminal.all())
|
const all = createMemo(() => terminal.all())
|
||||||
const ids = createMemo(() => all().map((pty) => pty.id))
|
const ids = createMemo(() => all().map((pty) => pty.id))
|
||||||
const byId = createMemo(() => new Map(all().map((pty) => [pty.id, pty])))
|
const byId = createMemo(() => new Map(all().map((pty) => [pty.id, { ...pty }])))
|
||||||
|
|
||||||
const handleTerminalDragStart = (event: unknown) => {
|
const handleTerminalDragStart = (event: unknown) => {
|
||||||
const id = getDraggableId(event)
|
const id = getDraggableId(event)
|
||||||
|
|
@ -189,7 +189,13 @@ export function TerminalPanel() {
|
||||||
>
|
>
|
||||||
<Tabs.List class="h-10">
|
<Tabs.List class="h-10">
|
||||||
<SortableProvider ids={ids()}>
|
<SortableProvider ids={ids()}>
|
||||||
<For each={all()}>{(pty) => <SortableTerminalTab terminal={pty} onClose={close} />}</For>
|
<For each={ids()}>
|
||||||
|
{(id) => (
|
||||||
|
<Show when={byId().get(id)}>
|
||||||
|
{(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />}
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</SortableProvider>
|
</SortableProvider>
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,27 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { afterEach, describe, expect, test } from "bun:test"
|
||||||
import { handleNotificationClick } from "./notification-click"
|
import { handleNotificationClick, setNavigate } from "./notification-click"
|
||||||
|
|
||||||
describe("notification click", () => {
|
describe("notification click", () => {
|
||||||
test("focuses and navigates when href exists", () => {
|
afterEach(() => {
|
||||||
const calls: string[] = []
|
setNavigate(undefined as any)
|
||||||
handleNotificationClick("/abc/session/123", {
|
|
||||||
focus: () => calls.push("focus"),
|
|
||||||
location: {
|
|
||||||
assign: (href) => calls.push(href),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(calls).toEqual(["focus", "/abc/session/123"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("only focuses when href is missing", () => {
|
test("navigates via registered navigate function", () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
handleNotificationClick(undefined, {
|
setNavigate((href) => calls.push(href))
|
||||||
focus: () => calls.push("focus"),
|
handleNotificationClick("/abc/session/123")
|
||||||
location: {
|
expect(calls).toEqual(["/abc/session/123"])
|
||||||
assign: (href) => calls.push(href),
|
})
|
||||||
},
|
|
||||||
})
|
test("does not navigate when href is missing", () => {
|
||||||
expect(calls).toEqual(["focus"])
|
const calls: string[] = []
|
||||||
|
setNavigate((href) => calls.push(href))
|
||||||
|
handleNotificationClick(undefined)
|
||||||
|
expect(calls).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to location.assign without registered navigate", () => {
|
||||||
|
handleNotificationClick("/abc/session/123")
|
||||||
|
// falls back to window.location.assign — no error thrown
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
type WindowTarget = {
|
let nav: ((href: string) => void) | undefined
|
||||||
focus: () => void
|
|
||||||
location: {
|
export const setNavigate = (fn: (href: string) => void) => {
|
||||||
assign: (href: string) => void
|
nav = fn
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleNotificationClick = (href?: string, target: WindowTarget = window) => {
|
export const handleNotificationClick = (href?: string) => {
|
||||||
target.focus()
|
window.focus()
|
||||||
if (!href) return
|
if (!href) return
|
||||||
target.location.assign(href)
|
if (nav) nav(href)
|
||||||
|
else window.location.assign(href)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.2.15",
|
"version": "1.2.17",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
||||||
...(isBedrock
|
...(isBedrock
|
||||||
? {
|
? {
|
||||||
anthropic_version: "bedrock-2023-05-31",
|
anthropic_version: "bedrock-2023-05-31",
|
||||||
anthropic_beta: supports1m ? "context-1m-2025-08-07" : undefined,
|
anthropic_beta: supports1m ? ["context-1m-2025-08-07"] : undefined,
|
||||||
model: undefined,
|
model: undefined,
|
||||||
stream: undefined,
|
stream: undefined,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.2.15",
|
"version": "1.2.17",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.2.15",
|
"version": "1.2.17",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.2.15",
|
"version": "1.2.17",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|
|
||||||
28
packages/desktop-electron/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
out/
|
||||||
|
|
||||||
|
resources/opencode-cli*
|
||||||
|
resources/icons
|
||||||
4
packages/desktop-electron/AGENTS.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Desktop package notes
|
||||||
|
|
||||||
|
- Renderer process should only call `window.api` from `src/preload`.
|
||||||
|
- Main process should register IPC handlers in `src/main/ipc.ts`.
|
||||||
32
packages/desktop-electron/README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# OpenCode Desktop
|
||||||
|
|
||||||
|
Native OpenCode desktop app, built with Tauri v2.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
From the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
bun run --cwd packages/desktop tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the Vite dev server on http://localhost:1420 and opens the native window.
|
||||||
|
|
||||||
|
If you only want the web dev server (no native shell):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd packages/desktop dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
To create a production `dist/` and build the native app bundle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd packages/desktop tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions.
|
||||||
97
packages/desktop-electron/electron-builder.config.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import type { Configuration } from "electron-builder"
|
||||||
|
|
||||||
|
const channel = (() => {
|
||||||
|
const raw = process.env.OPENCODE_CHANNEL
|
||||||
|
if (raw === "dev" || raw === "beta" || raw === "prod") return raw
|
||||||
|
return "dev"
|
||||||
|
})()
|
||||||
|
|
||||||
|
const getBase = (): Configuration => ({
|
||||||
|
artifactName: "opencode-electron-${os}-${arch}.${ext}",
|
||||||
|
directories: {
|
||||||
|
output: "dist",
|
||||||
|
buildResources: "resources",
|
||||||
|
},
|
||||||
|
files: ["out/**/*", "resources/**/*"],
|
||||||
|
extraResources: [
|
||||||
|
{
|
||||||
|
from: "resources/",
|
||||||
|
to: "",
|
||||||
|
filter: ["opencode-cli*"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: "native/",
|
||||||
|
to: "native/",
|
||||||
|
filter: ["index.js", "index.d.ts", "build/Release/mac_window.node", "swift-build/**"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mac: {
|
||||||
|
category: "public.app-category.developer-tools",
|
||||||
|
icon: `resources/icons/icon.icns`,
|
||||||
|
hardenedRuntime: true,
|
||||||
|
gatekeeperAssess: false,
|
||||||
|
entitlements: "resources/entitlements.plist",
|
||||||
|
entitlementsInherit: "resources/entitlements.plist",
|
||||||
|
notarize: true,
|
||||||
|
target: ["dmg", "zip"],
|
||||||
|
},
|
||||||
|
dmg: {
|
||||||
|
sign: true,
|
||||||
|
},
|
||||||
|
protocols: {
|
||||||
|
name: "OpenCode",
|
||||||
|
schemes: ["opencode"],
|
||||||
|
},
|
||||||
|
win: {
|
||||||
|
icon: `resources/icons/icon.ico`,
|
||||||
|
target: ["nsis"],
|
||||||
|
},
|
||||||
|
nsis: {
|
||||||
|
oneClick: false,
|
||||||
|
allowToChangeInstallationDirectory: true,
|
||||||
|
installerIcon: `resources/icons/icon.ico`,
|
||||||
|
installerHeaderIcon: `resources/icons/icon.ico`,
|
||||||
|
},
|
||||||
|
linux: {
|
||||||
|
icon: `resources/icons`,
|
||||||
|
category: "Development",
|
||||||
|
target: ["AppImage", "deb", "rpm"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
const base = getBase()
|
||||||
|
|
||||||
|
switch (channel) {
|
||||||
|
case "dev": {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
appId: "ai.opencode.desktop.dev",
|
||||||
|
productName: "OpenCode Dev",
|
||||||
|
rpm: { packageName: "opencode-dev" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "beta": {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
appId: "ai.opencode.desktop.beta",
|
||||||
|
productName: "OpenCode Beta",
|
||||||
|
protocols: { name: "OpenCode Beta", schemes: ["opencode"] },
|
||||||
|
publish: { provider: "github", owner: "anomalyco", repo: "opencode-beta", channel: "latest" },
|
||||||
|
rpm: { packageName: "opencode-beta" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "prod": {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
appId: "ai.opencode.desktop",
|
||||||
|
productName: "OpenCode",
|
||||||
|
protocols: { name: "OpenCode", schemes: ["opencode"] },
|
||||||
|
publish: { provider: "github", owner: "anomalyco", repo: "opencode", channel: "latest" },
|
||||||
|
rpm: { packageName: "opencode" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getConfig()
|
||||||
41
packages/desktop-electron/electron.vite.config.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { defineConfig } from "electron-vite"
|
||||||
|
import appPlugin from "@opencode-ai/app/vite"
|
||||||
|
|
||||||
|
const channel = (() => {
|
||||||
|
const raw = process.env.OPENCODE_CHANNEL
|
||||||
|
if (raw === "dev" || raw === "beta" || raw === "prod") return raw
|
||||||
|
return "dev"
|
||||||
|
})()
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: {
|
||||||
|
define: {
|
||||||
|
"import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel),
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: { index: "src/main/index.ts" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: { index: "src/preload/index.ts" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
plugins: [appPlugin],
|
||||||
|
publicDir: "../app/public",
|
||||||
|
root: "src/renderer",
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: "src/renderer/index.html",
|
||||||
|
loading: "src/renderer/loading.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
11
packages/desktop-electron/icons/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Tauri Icons
|
||||||
|
|
||||||
|
Here's the process I've been using to create icons:
|
||||||
|
|
||||||
|
- Save source image as `app-icon.png` in `packages/desktop`
|
||||||
|
- `cd` to `packages/desktop`
|
||||||
|
- Run `bun tauri icon -o src-tauri/icons/{environment}`
|
||||||
|
- Use [Image2Icon](https://img2icnsapp.com/)'s 'Big Sur Icon' preset to generate an `icon.icns` file and place it in the appropriate icons folder
|
||||||
|
|
||||||
|
The Image2Icon step is necessary as the `icon.icns` generated by `app-icon.png` does not apply the shadow/padding expected by macOS,
|
||||||
|
so app icons appear larger than expected.
|
||||||
BIN
packages/desktop-electron/icons/beta/128x128.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
packages/desktop-electron/icons/beta/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/desktop-electron/icons/beta/32x32.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
packages/desktop-electron/icons/beta/64x64.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
packages/desktop-electron/icons/beta/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
packages/desktop-electron/icons/beta/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
packages/desktop-electron/icons/beta/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
packages/desktop-electron/icons/beta/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
packages/desktop-electron/icons/beta/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
packages/desktop-electron/icons/beta/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
packages/desktop-electron/icons/beta/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
packages/desktop-electron/icons/beta/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
packages/desktop-electron/icons/beta/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
packages/desktop-electron/icons/beta/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
BIN
packages/desktop-electron/icons/beta/icon.icns
Normal file
BIN
packages/desktop-electron/icons/beta/icon.ico
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
packages/desktop-electron/icons/beta/icon.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 687 B |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 582 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
packages/desktop-electron/icons/beta/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
packages/desktop-electron/icons/dev/128x128.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
packages/desktop-electron/icons/dev/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
packages/desktop-electron/icons/dev/32x32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
packages/desktop-electron/icons/dev/64x64.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
packages/desktop-electron/icons/dev/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
packages/desktop-electron/icons/dev/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
packages/desktop-electron/icons/dev/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
packages/desktop-electron/icons/dev/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
packages/desktop-electron/icons/dev/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/desktop-electron/icons/dev/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
packages/desktop-electron/icons/dev/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
packages/desktop-electron/icons/dev/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
packages/desktop-electron/icons/dev/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
packages/desktop-electron/icons/dev/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 3 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |