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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ jobs:
env:
OPENCODE_CHANNEL: ${{ inputs.channel || 'dev' }}
PAWWORK_FEEDBACK_FORM_URL: ${{ vars.PAWWORK_FEEDBACK_FORM_URL || '' }}
PAWWORK_BUILD_SHA: ${{ github.sha }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Check desktop runtime imports
if: ${{ inputs.phase != 'finalize' }}
Expand Down
12 changes: 7 additions & 5 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { PromptProvider } from "@/context/prompt"
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
import { SettingsProvider } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal"
import { AboutModal, type AboutInfo } from "@/components/about-modal"
import DirectoryLayout from "@/pages/directory-layout"
import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error"
Expand Down Expand Up @@ -78,8 +79,9 @@ declare global {
wsl?: boolean
}
api?: {
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
setDesktopContext?: (context: DesktopContext) => Promise<void>
getAboutInfo?: () => Promise<AboutInfo>
onAboutOpen?: (handler: () => void) => () => void
}
}
}
Expand Down Expand Up @@ -228,17 +230,17 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
cssLight: "pawwork-theme-css-light",
cssDark: "pawwork-theme-css-dark",
}}
onThemeApplied={(_, mode) => {
void window.api?.setTitlebar?.({ mode })
}}
>
<LanguageProvider locale={props.locale}>
<UiI18nBridge>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<QueryProvider>
<DialogProvider>
<MarkedProvider>
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
<FileComponentProvider component={File}>
<AboutModal />
{props.children}
</FileComponentProvider>
</MarkedProvider>
</DialogProvider>
</QueryProvider>
Expand Down
62 changes: 62 additions & 0 deletions packages/app/src/components/about-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { onCleanup, onMount } from "solid-js"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"

import { useLanguage } from "@/context/language"

export type AboutInfo = {
version: string
electronVersion: string
chromeVersion: string
buildSha: string
}

function AboutDialogBody(props: { info: AboutInfo }) {
const language = useLanguage()
return (
<Dialog title={language.t("about.title")} class="w-full max-w-[400px] mx-auto">
<dl class="text-sm space-y-1 p-6 pt-0">
<div>
<dt class="inline">{language.t("about.version")}: </dt>
<dd class="inline">{props.info.version}</dd>
</div>
<div>
<dt class="inline">{language.t("about.build")}: </dt>
<dd class="inline">{props.info.buildSha}</dd>
</div>
<div>
<dt class="inline">{language.t("about.electron")}: </dt>
<dd class="inline">{props.info.electronVersion}</dd>
</div>
<div>
<dt class="inline">{language.t("about.chromium")}: </dt>
<dd class="inline">{props.info.chromeVersion}</dd>
</div>
</dl>
</Dialog>
)
}

export function AboutModal() {
const dialog = useDialog()
let unsubscribe: (() => void) | undefined

onMount(() => {
unsubscribe = window.api?.onAboutOpen?.(async () => {
let info: AboutInfo | undefined
try {
info = await window.api?.getAboutInfo?.()
} catch (error) {
console.warn("[about] failed to fetch info", error)
return
}
if (!info) return
const data = info
dialog.show(() => <AboutDialogBody info={data} />)
})
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

onCleanup(() => unsubscribe?.())

return null
}
5 changes: 1 addition & 4 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function Titlebar() {
class="shrink-0 bg-background-base relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
classList={{ "h-11": platform.platform === "desktop" && !mac() }}
style={{ height: currentTitlebarHeight(), "min-height": currentTitlebarHeight() }}
data-shell-drag-region
data-shell-drag-region={!windows() || undefined}
>
<div
classList={{
Expand Down Expand Up @@ -197,9 +197,6 @@ export function Titlebar() {
data-shell-slot="right-portal"
class="flex items-center gap-1 shrink-0 justify-end"
/>
<Show when={windows()}>
<div class="w-36 shrink-0" />
</Show>
</div>
</header>
)
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1030,4 +1030,9 @@ export const dict = {
"workspace.chip.empty": "No workspaces available",
"workspace.chip.popover.title": "Workspaces",
"workspace.chip.add": "Add workspace",
"about.title": "About PawWork",
"about.version": "Version",
"about.build": "Build",
"about.electron": "Electron",
"about.chromium": "Chromium",
}
5 changes: 5 additions & 0 deletions packages/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,4 +1001,9 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败",
"error.childStore.storeCreateFailed": "创建存储失败",
"terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}",
"about.title": "关于爪印",
"about.version": "版本",
"about.build": "构建",
"about.electron": "Electron",
"about.chromium": "Chromium",
} satisfies Partial<Record<Keys, string>>
2 changes: 1 addition & 1 deletion packages/app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
}

@media (min-width: 1280px) {
[data-component="desktop-shell-frame"][data-platform="desktop"]:not([data-os="macos"]) {
[data-component="desktop-shell-frame"][data-platform="desktop"][data-os="linux"] {
margin: var(--shell-frame-margin);
border: 1px solid var(--shell-frame-border);
border-radius: var(--shell-frame-radius);
Expand Down
8 changes: 7 additions & 1 deletion packages/app/src/shell-frame-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ test("desktop shell shares titlebar height across titlebar and narrow sidebar ge
const sessionHeader = read("./components/session/session-header.tsx")
const wideDesktopQuery = css.indexOf("@media (min-width: 1280px)")
const macMainSeamRule = css.indexOf('[data-component="desktop-shell-main"][data-platform="desktop"][data-os="macos"] {')
const wideFrameRule = css.indexOf('[data-component="desktop-shell-frame"][data-platform="desktop"]:not([data-os="macos"]) {')
const wideFrameRule = css.indexOf('[data-component="desktop-shell-frame"][data-platform="desktop"][data-os="linux"] {')

expect(css).toContain('[data-component="desktop-shell"][data-platform="desktop"] {')
expect(css).toContain("--shell-titlebar-height: 44px;")
Expand Down Expand Up @@ -52,3 +52,9 @@ test("session header uses a view title on home and breadcrumb title in sessions"
expect(sessionHeader).not.toContain('language.t("session.header.searchFiles")')
expect(sessionHeader).not.toContain('language.t("session.header.search.placeholder"')
})

test("titlebar drops Windows-only 138px placeholder and conditional drag region", () => {
const titlebar = read("./components/titlebar.tsx")
expect(titlebar).not.toContain('class="w-36 shrink-0"')
expect(titlebar).toContain('data-shell-drag-region={!windows() || undefined}')
})
2 changes: 2 additions & 0 deletions packages/desktop-electron/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const channel = (() => {
return "dev"
})()
const feedbackFormUrl = process.env.PAWWORK_FEEDBACK_FORM_URL ?? ""
const buildSha = process.env.PAWWORK_BUILD_SHA ?? ""

const OPENCODE_ROOT = path.resolve(process.cwd(), "../opencode")
const { runtimeDir: OPENCODE_SERVER_DIST, runtimeEntry: OPENCODE_SERVER_ENTRY } = embeddedServerArtifacts(OPENCODE_ROOT)
Expand All @@ -33,6 +34,7 @@ export default defineConfig({
define: {
"import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel),
"import.meta.env.PAWWORK_FEEDBACK_FORM_URL": JSON.stringify(feedbackFormUrl),
"import.meta.env.PAWWORK_BUILD_SHA": JSON.stringify(buildSha),
},
build: {
rollupOptions: {
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop-electron/src/main/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
interface ImportMetaEnv {
readonly OPENCODE_CHANNEL: string
readonly PAWWORK_FEEDBACK_FORM_URL?: string
readonly PAWWORK_BUILD_SHA?: string
}

interface ImportMeta {
Expand Down
4 changes: 4 additions & 0 deletions packages/desktop-electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { normalizeDesktopContextPayload, syncWindowTitleForDesktopContext } from
import { createDesktopContextStore } from "./desktop-context-store"
import { createFeedbackHandler, feedbackDialogLabels } from "./feedback"
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
import { registerAboutIpc, triggerAbout } from "./ipc/about"
import { filePath, initLogging, tail } from "./logging"
import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu"
Expand Down Expand Up @@ -476,6 +477,7 @@ function wireMenu() {
reportProblem: () => {
void reportProblem()
},
triggerAbout: (win) => triggerAbout(win),
}, focusedMenuLocale())
}

Expand Down Expand Up @@ -532,6 +534,8 @@ registerIpcHandlers({
},
})

registerAboutIpc()
Comment thread
Astro-Han marked this conversation as resolved.

function killSidecar() {
if (!server) return
server.stop()
Expand Down
7 changes: 0 additions & 7 deletions packages/desktop-electron/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ import type {
ReportProblemResult,
ServerReadyData,
SqliteMigrationProgress,
TitlebarTheme,
UpdateInfo,
WindowConfig,
WslConfig,
} from "../preload/types"
import { attachmentPathMime } from "./attachment-mime"
import { getStore } from "./store"
import { setTitlebar } from "./windows"

const pickerFilters = (ext?: string[]) => {
if (!ext || ext.length === 0) return undefined
Expand Down Expand Up @@ -302,11 +300,6 @@ export function registerIpcHandlers(deps: Deps) {

ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor))
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (!win) return
setTitlebar(win, theme)
})
}

export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) {
Expand Down
33 changes: 33 additions & 0 deletions packages/desktop-electron/src/main/ipc/about.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { app, BrowserWindow, ipcMain } from "electron"

export type AboutInfo = {
version: string
electronVersion: string
chromeVersion: string
buildSha: string
}

function readBuildSha(): string {
const sha = import.meta.env.PAWWORK_BUILD_SHA
return sha && sha.length > 0 ? sha : "unknown"
}

export function registerAboutIpc() {
ipcMain.handle("about:get-info", (): AboutInfo => ({
version: app.getVersion(),
electronVersion: process.versions.electron ?? "unknown",
chromeVersion: process.versions.chrome ?? "unknown",
buildSha: readBuildSha(),
}))
}

function isAppShellWindow(win: BrowserWindow): boolean {
// Loading window loads `loading.html`; the About bridge is only mounted in the main app renderer.
return !win.webContents.getURL().endsWith("loading.html")
}

export function triggerAbout(browserWindow?: BrowserWindow) {
const candidate = browserWindow && isAppShellWindow(browserWindow) ? browserWindow : undefined
const target = candidate ?? BrowserWindow.getAllWindows().find(isAppShellWindow)
target?.webContents.send("about:open")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
68 changes: 68 additions & 0 deletions packages/desktop-electron/src/main/menu-template.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { expect, test } from "bun:test"
import { buildMacosMenuTemplate, buildWindowsMenuTemplate, type MenuTemplateDeps } from "./menu-template"

const stubDeps: MenuTemplateDeps = {
trigger: () => {},
checkForUpdates: () => {},
reload: () => {},
relaunch: () => {},
reportProblem: () => {},
openExternal: () => {},
newWindow: () => {},
triggerAbout: () => {},
}

const baseOptions = {
deps: stubDeps,
appName: "PawWork",
locale: "en" as const,
feedbackEnabled: true,
}

test("Windows template has 6 top-level menus: File / Edit / View / Go / Window / Help", () => {
const tpl = buildWindowsMenuTemplate(baseOptions)
expect(tpl).toHaveLength(6)
const labels = tpl.map((m) => m.label)
expect(labels).toEqual(["File", "Edit", "View", "Go", "Window", "Help"])
})

test("Windows Help submenu contains 'Check for Updates' and 'About PawWork'", () => {
const tpl = buildWindowsMenuTemplate(baseOptions)
const help = tpl.find((m) => m.label === "Help")
expect(help).toBeDefined()
const labels = (help?.submenu ?? []).map((s) => s.label)
expect(labels).toContain("Check for Updates...")
expect(labels).toContain("About PawWork")
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("Windows New Session accelerator matches macOS (CmdOrCtrl+Shift+S)", () => {
const tpl = buildWindowsMenuTemplate(baseOptions)
const file = tpl.find((m) => m.label === "File")
const newSession = (file?.submenu ?? []).find((s) => s.label === "New Session")
expect(newSession?.accelerator).toBe("CmdOrCtrl+Shift+S")
})

test("Windows accelerators use CmdOrCtrl + Alt (no bare Cmd or Option)", () => {
const tpl = buildWindowsMenuTemplate(baseOptions)
const collect = (items: ReturnType<typeof buildWindowsMenuTemplate>): string[] =>
items.flatMap((i) => [i.accelerator ?? "", ...collect(i.submenu ?? [])])
const accels = collect(tpl).filter(Boolean)
for (const a of accels) {
expect(a).not.toMatch(/(^|\+)Cmd(\+|$)/)
expect(a).not.toMatch(/(^|\+)Option(\+|$)/)
}
expect(accels.some((a) => a === "CmdOrCtrl+Shift+S")).toBe(true)
})

test("macOS template still has 7 top-level menus including PawWork app menu", () => {
const tpl = buildMacosMenuTemplate(baseOptions)
expect(tpl).toHaveLength(7)
expect(tpl[0].label).toBe("PawWork")
})

test("macOS About menu item still uses role:about (system About panel)", () => {
const tpl = buildMacosMenuTemplate(baseOptions)
const appMenu = tpl[0]
const aboutItem = (appMenu.submenu ?? []).find((s) => s.role === "about")
expect(aboutItem).toBeTruthy()
})
Loading
Loading