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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 90 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ on:
required: false
type: string

# Release metadata is a read-modify-write upload to the same latest*.yml asset.
# Queue same target/phase runs across arch to avoid release asset clobbering.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.phase || 'submit' }}-${{ inputs.channel || 'dev' }}-${{ inputs.target || 'macos' }}-${{ inputs.arch || 'arm64' }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ inputs.source_ref || github.ref_name }}-${{ inputs.phase || 'submit' }}-${{ inputs.channel || 'dev' }}-${{ inputs.target || 'macos' }}
cancel-in-progress: false

permissions:
actions: read
Expand Down Expand Up @@ -297,19 +299,63 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Read desktop package version
id: package_version
shell: bash
run: |
set -euo pipefail
version=$(node -p "require('./packages/desktop-electron/package.json').version")
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Build Electron app
if: ${{ inputs.phase != 'finalize' }}
run: bun run build
working-directory: packages/desktop-electron
env:
OPENCODE_CHANNEL: ${{ inputs.channel || 'dev' }}
PAWWORK_FEEDBACK_FORM_URL: ${{ vars.PAWWORK_FEEDBACK_FORM_URL || '' }}

- name: Setup Apple API Key
if: runner.os == 'macOS'
run: printenv APPLE_API_KEY_CONTENT > $RUNNER_TEMP/apple-api-key.p8
env:
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }}

- name: Download existing updater metadata
# macOS finalize can run after notarization as a separate phase; Windows finalizes during full packaging.
if: ${{ (runner.os == 'macOS' && (inputs.phase == 'finalize' || inputs.phase == 'full') && inputs.arch == matrix.arch_label) || (runner.os == 'Windows' && inputs.phase == 'full') }}
shell: bash
run: |
set -euo pipefail

existing_dir="$RUNNER_TEMP/existing-latest-yml"
Comment thread
Astro-Han marked this conversation as resolved.
mkdir -p "$existing_dir"
tag="v${{ steps.package_version.outputs.version }}"

download_or_warn() {
local pattern="$1"
local err="$RUNNER_TEMP/${pattern}.err"
if gh release download "$tag" --pattern "$pattern" --dir "$existing_dir" --repo "$GITHUB_REPOSITORY" 2>"$err"; then
return 0
fi
if grep -qiE 'no assets to download|no matches found|no assets match|could not find any assets' "$err"; then
echo "No existing $pattern found; expected for first release."
return 0
Comment thread
Astro-Han marked this conversation as resolved.
fi
cat "$err" >&2
return 1
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if [[ "$RUNNER_OS" == "macOS" ]]; then
download_or_warn latest-mac.yml
fi
if [[ "$RUNNER_OS" == "Windows" ]]; then
download_or_warn latest.yml
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Package signed app
if: ${{ runner.os == 'macOS' && (inputs.phase == 'submit' || inputs.phase == 'full') }}
run: npx electron-builder --mac dir --${{ matrix.arch_label }} --publish never --config electron-builder.config.ts
Expand Down Expand Up @@ -625,6 +671,7 @@ jobs:
timeout-minutes: 30
env:
OPENCODE_CHANNEL: ${{ inputs.channel || 'dev' }}
PAWWORK_FEEDBACK_FORM_URL: ${{ vars.PAWWORK_FEEDBACK_FORM_URL || '' }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Verify notarized artifacts
Expand Down Expand Up @@ -731,6 +778,7 @@ jobs:
timeout-minutes: 60
env:
OPENCODE_CHANNEL: ${{ inputs.channel || 'dev' }}
PAWWORK_FEEDBACK_FORM_URL: ${{ vars.PAWWORK_FEEDBACK_FORM_URL || '' }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload packaged app artifact
Expand All @@ -744,6 +792,46 @@ jobs:
packages/desktop-electron/dist/*.exe.blockmap
packages/desktop-electron/dist/latest*.yml

- name: Collect updater metadata
if: ${{ (runner.os == 'macOS' && (inputs.phase == 'finalize' || inputs.phase == 'full') && inputs.arch == matrix.arch_label) || (runner.os == 'Windows' && inputs.phase == 'full') }}
shell: bash
run: |
set -euo pipefail

case "${{ matrix.target }}-${{ matrix.arch_label }}" in
macos-arm64)
metadata_dir="$RUNNER_TEMP/latest-yml/latest-yml-aarch64-apple-darwin"
metadata_file="latest-mac.yml"
;;
macos-x64)
metadata_dir="$RUNNER_TEMP/latest-yml/latest-yml-x86_64-apple-darwin"
metadata_file="latest-mac.yml"
;;
windows-x64)
metadata_dir="$RUNNER_TEMP/latest-yml/latest-yml-x86_64-pc-windows-msvc"
metadata_file="latest.yml"
;;
*)
echo "Unsupported updater metadata target: ${{ matrix.target }}-${{ matrix.arch_label }}"
exit 1
;;
esac

mkdir -p "$metadata_dir"
cp "dist/$metadata_file" "$metadata_dir/$metadata_file"
working-directory: packages/desktop-electron

- name: Finalize updater metadata
if: ${{ (runner.os == 'macOS' && (inputs.phase == 'finalize' || inputs.phase == 'full') && inputs.arch == matrix.arch_label) || (runner.os == 'Windows' && inputs.phase == 'full') }}
run: bun ./scripts/finalize-latest-yml.ts
working-directory: packages/desktop-electron
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
EXISTING_LATEST_YML_DIR: ${{ runner.temp }}/existing-latest-yml
LATEST_YML_DIR: ${{ runner.temp }}/latest-yml
OPENCODE_VERSION: ${{ steps.package_version.outputs.version }}

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: ${{ runner.os == 'macOS' && (inputs.phase == 'finalize' || inputs.phase == 'full') && inputs.arch == matrix.arch_label }}
with:
Expand Down
82 changes: 81 additions & 1 deletion packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo"
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
import { MetaProvider } from "@solidjs/meta"
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import { type BaseRouterProps, Navigate, Route, Router, useLocation } from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { type Duration, Effect } from "effect"
import {
type Component,
createEffect,
createMemo,
createResource,
createSignal,
Expand Down Expand Up @@ -44,6 +45,7 @@ import { TerminalProvider } from "@/context/terminal"
import DirectoryLayout from "@/pages/directory-layout"
import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error"
import { buildDesktopContext, type DesktopContext } from "./utils/desktop-context"
import { useCheckServerHealth } from "./utils/server-health"

const HomeRoute = lazy(() => import("@/pages/home"))
Expand Down Expand Up @@ -77,6 +79,7 @@ declare global {
}
api?: {
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
setDesktopContext?: (context: DesktopContext) => Promise<void>
}
}
}
Expand Down Expand Up @@ -122,13 +125,90 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
return (
<AppShellProviders>
<Suspense fallback={<Loading />}>
<DesktopContextRouteBridge />
{props.appChildren}
{props.children}
</Suspense>
</AppShellProviders>
)
}

function DesktopContextRouteBridge() {
const language = useLanguage()
const location = useLocation()
const desktopContextMaxRetries = 5
let lastDesktopContext = ""
let pendingDesktopContext = ""
let desktopContextRetryTimer: number | undefined
let desktopContextRetryCount = 0
let disposed = false

const clearDesktopContextRetry = () => {
if (desktopContextRetryTimer !== undefined) window.clearTimeout(desktopContextRetryTimer)
desktopContextRetryTimer = undefined
}

const syncDesktopContext = (context: DesktopContext, serialized: string) => {
if (disposed || isSessionRoute(location.pathname)) return
const setDesktopContext = window.api?.setDesktopContext
if (!setDesktopContext) return
void setDesktopContext(context)
.then(() => {
if (disposed || pendingDesktopContext !== serialized) return
lastDesktopContext = serialized
pendingDesktopContext = ""
desktopContextRetryCount = 0
clearDesktopContextRetry()
})
.catch(() => {
if (disposed || pendingDesktopContext !== serialized || lastDesktopContext === serialized) return
if (desktopContextRetryCount >= desktopContextMaxRetries) {
pendingDesktopContext = ""
desktopContextRetryCount = 0
return
}
clearDesktopContextRetry()
desktopContextRetryCount += 1
const retryDelay = Math.min(4000, 250 * 2 ** (desktopContextRetryCount - 1))
desktopContextRetryTimer = window.setTimeout(() => {
desktopContextRetryTimer = undefined
if (disposed || pendingDesktopContext !== serialized || lastDesktopContext === serialized) return
syncDesktopContext(context, serialized)
}, retryDelay)
})
}

createEffect(() => {
if (!window.api?.setDesktopContext) return
if (isSessionRoute(location.pathname)) {
pendingDesktopContext = ""
desktopContextRetryCount = 0
clearDesktopContextRetry()
return
}
const context = buildDesktopContext({
route: `${location.pathname}${location.search}${location.hash}`,
locale: language.locale(),
})
const serialized = JSON.stringify(context)
if (serialized === lastDesktopContext || serialized === pendingDesktopContext) return
pendingDesktopContext = serialized
desktopContextRetryCount = 0
syncDesktopContext(context, serialized)
})

onCleanup(() => {
disposed = true
clearDesktopContextRetry()
})

return null
}

function isSessionRoute(pathname: string) {
Comment thread
Astro-Han marked this conversation as resolved.
return /^\/[^/]+\/session(?:\/[^/]+)?\/?$/.test(pathname)
}

export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
return (
<MetaProvider>
Expand Down
24 changes: 24 additions & 0 deletions packages/app/src/components/settings-general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,30 @@ export const SettingsGeneral: Component = () => {
void platform
.checkUpdate()
.then((result) => {
if (result.status === "busy") {
showToast({
title: language.t("settings.updates.toast.busy.title"),
description: language.t("settings.updates.toast.busy.description"),
})
return
}

if (result.status === "disabled") {
showToast({
title: language.t("settings.updates.toast.disabled.title"),
description: language.t("settings.updates.toast.disabled.description"),
})
return
}

if (result.status === "failed") {
showToast({
title: language.t("common.requestFailed"),
description: result.message || language.t("settings.updates.toast.failed.description"),
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return
}

if (!result.updateAvailable) {
showToast({
variant: "success",
Expand Down
6 changes: 5 additions & 1 deletion packages/app/src/context/platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ type PickerPaths = string | string[] | null
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
type UpdateInfo = { updateAvailable: boolean; version?: string }
type UpdateFailureReason = "check" | "download" | "metadata"
export type UpdateInfo =
| { updateAvailable: false; status: "disabled" | "none" | "busy"; version?: undefined }
| { updateAvailable: true; status: "ready"; version: string }
| { updateAvailable: false; status: "failed"; reason: UpdateFailureReason; message: string; version?: undefined }
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export type Platform = {
/** Platform discriminator */
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,10 @@ export const dict = {
"error.page.action.checking": "Checking...",
"error.page.action.checkUpdates": "Check for updates",
"error.page.action.updateTo": "Update to {{version}}",
"error.page.action.upToDate": "PawWork is up to date.",
"error.page.action.busy": "PawWork is already checking for updates.",
"error.page.action.checkFailed": "Failed to check for updates.",
"error.page.action.disabled": "Updates are not available in this build.",
"error.page.circular": "[Circular]",
"error.page.report.prefix": "Please report this error to the PawWork team",
"error.page.report.github": "on GitHub",
Expand Down Expand Up @@ -805,6 +809,11 @@ export const dict = {
"settings.updates.row.check.description": "Manually check for updates and install if available",
"settings.updates.action.checkNow": "Check now",
"settings.updates.action.checking": "Checking...",
"settings.updates.toast.busy.title": "Update check in progress",
"settings.updates.toast.busy.description": "PawWork is already checking for updates.",
"settings.updates.toast.disabled.title": "Updates unavailable",
"settings.updates.toast.disabled.description": "Updates are not available in this build.",
"settings.updates.toast.failed.description": "Failed to check for updates.",
"settings.updates.toast.latest.title": "You're up to date",
"settings.updates.toast.latest.description": "You're running the latest version of PawWork.",
"sound.option.none": "None",
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,10 @@ export const dict = {
"error.page.action.checking": "检查中...",
"error.page.action.checkUpdates": "检查更新",
"error.page.action.updateTo": "更新到 {{version}}",
"error.page.action.upToDate": "PawWork 已是最新版本。",
"error.page.action.busy": "PawWork 正在检查更新。",
"error.page.action.checkFailed": "检查更新失败。",
"error.page.action.disabled": "此构建不支持更新。",
"error.page.report.prefix": "请将此错误报告给 PawWork 团队",
"error.page.report.github": "在 GitHub 上",
"error.page.version": "版本:{{version}}",
Expand Down Expand Up @@ -699,6 +703,11 @@ export const dict = {
"settings.updates.row.check.description": "手动检查更新并在有更新时安装",
"settings.updates.action.checkNow": "立即检查",
"settings.updates.action.checking": "正在检查...",
"settings.updates.toast.busy.title": "正在检查更新",
"settings.updates.toast.busy.description": "PawWork 已经在检查更新。",
"settings.updates.toast.disabled.title": "更新不可用",
"settings.updates.toast.disabled.description": "此构建不支持更新。",
"settings.updates.toast.failed.description": "检查更新失败。",
"settings.updates.toast.latest.title": "已是最新版本",
"settings.updates.toast.latest.description": "你正在使用最新版本的 PawWork。",

Expand Down
Loading
Loading