mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
Fix Retina blur, race conditions, and menu-label duplication in tray Set
- Rebuild the tray attention icon's @2x representation since toBitmap only read 1x pixels, blurring the glyph on Retina displays - Fix premultiplied-alpha math so light-glyph tinting uses per-pixel alpha instead of a flat 0xff, keeping antialiased edges valid - Always push ui:openSettings and leave a longer-lived pending flag, since there was no reliable signal that a renderer's listener was attached - Preserve tray attention state across macOS menu-bar hide/show toggles instead of resetting it on tray destroy - Route macOS tray creation through syncMacMenuBarIcon so startup and the live toggle share one visibility policy - Reuse app-menu translation keys for tray Settings/Check for Updates and drop the now-duplicate tray-scoped locale strings
This commit is contained in:
+33
-40
@@ -5,7 +5,7 @@
|
||||
import { existsSync, statSync } from 'node:fs'
|
||||
import { isAbsolute, join } from 'node:path'
|
||||
import os from 'node:os'
|
||||
import { app, BrowserWindow, dialog, ipcMain, nativeTheme } from 'electron'
|
||||
import { app, BrowserWindow, dialog, ipcMain, nativeTheme, type Tray } from 'electron'
|
||||
import { electronApp, is } from '@electron-toolkit/utils'
|
||||
import * as QRCode from 'qrcode'
|
||||
import {
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
rebuildAppMenu
|
||||
} from './menu/register-app-menu'
|
||||
import { checkForUpdatesFromMenu, isQuittingForUpdate } from './updater'
|
||||
import type { UpdateCheckOptions } from '../shared/types'
|
||||
import { recordUpdaterLifecycle } from './updater-lifecycle-diagnostics'
|
||||
import {
|
||||
configureElectronNetworkCompatibility,
|
||||
@@ -246,9 +247,8 @@ let keybindings: KeybindingService | null = null
|
||||
// sweep spare live sessions across that one reload (#5787).
|
||||
const expectedRendererReload = createWebContentsTimedFlag()
|
||||
const recoveryReloadInFlight = createWebContentsTimedFlag()
|
||||
// Why: the tray/menu-bar "Settings…" item can fire before a freshly created
|
||||
// renderer has attached its ui:openSettings listener, so leave a one-shot
|
||||
// intent the renderer pulls on mount instead of racing a time-based push.
|
||||
// Why: a tray/menu-bar "Settings…" click can precede the renderer attaching
|
||||
// its ui:openSettings listener; the renderer pulls this one-shot on mount.
|
||||
const pendingOpenSettings = createWebContentsTimedFlag()
|
||||
let firstWindowStartupServicesReady: Promise<void> = Promise.resolve()
|
||||
let managedWslCliReconciliationReady: Promise<void> = Promise.resolve()
|
||||
@@ -806,7 +806,6 @@ function showMainWindowFromTray(): void {
|
||||
}
|
||||
|
||||
function openSettingsFromSystemMenu(): void {
|
||||
const existingWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : null
|
||||
showMainWindowFromTray()
|
||||
const targetWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : null
|
||||
if (!targetWindow) {
|
||||
@@ -814,16 +813,13 @@ function openSettingsFromSystemMenu(): void {
|
||||
}
|
||||
recordCrashBreadcrumb('settings_opened')
|
||||
|
||||
// Why: an already-mounted renderer has its ui:openSettings listener attached,
|
||||
// so a direct push lands. A freshly recreated or still-loading renderer has
|
||||
// not subscribed yet — a time-based push would race its useEffect and could
|
||||
// be dropped (with no fallback if the load fails), so leave a one-shot intent
|
||||
// it consumes on mount instead.
|
||||
if (existingWindow === targetWindow && !targetWindow.webContents.isLoadingMainFrame()) {
|
||||
targetWindow.webContents.send('ui:openSettings')
|
||||
return
|
||||
}
|
||||
pendingOpenSettings.mark(targetWindow.webContents.id)
|
||||
// Why: no main-side signal proves the renderer listener is attached, so push
|
||||
// and leave a one-shot intent — a mounted renderer acts on the push, an
|
||||
// unmounted one pulls the intent at mount; only one fires per renderer life.
|
||||
targetWindow.webContents.send('ui:openSettings')
|
||||
// Why: 60s outlives the default flag TTL, which a cold renderer start can
|
||||
// exceed — an expired flag silently drops the Settings click.
|
||||
pendingOpenSettings.mark(targetWindow.webContents.id, 60_000)
|
||||
}
|
||||
|
||||
function quitFromSystemTray(): void {
|
||||
@@ -838,6 +834,13 @@ function quitFromSystemTray(): void {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
// Why: a manual check must run against a configured updater; the menu and
|
||||
// tray are both clickable before anything else configures it.
|
||||
function runUserInitiatedUpdateCheck(options?: UpdateCheckOptions): void {
|
||||
ensureAutoUpdaterConfigured()
|
||||
checkForUpdatesFromMenu(options)
|
||||
}
|
||||
|
||||
function getSystemTrayOptions(): SystemTrayOptions | null {
|
||||
if (!store) {
|
||||
return null
|
||||
@@ -847,21 +850,21 @@ function getSystemTrayOptions(): SystemTrayOptions | null {
|
||||
onOpen: showMainWindowFromTray,
|
||||
onOpenSettings: openSettingsFromSystemMenu,
|
||||
onCheckForUpdates: () => {
|
||||
ensureAutoUpdaterConfigured()
|
||||
checkForUpdatesFromMenu()
|
||||
// Why: updater status renders in the main window; with every window
|
||||
// closed a bare check would complete invisibly.
|
||||
showMainWindowFromTray()
|
||||
runUserInitiatedUpdateCheck()
|
||||
},
|
||||
onQuit: quitFromSystemTray
|
||||
}
|
||||
}
|
||||
|
||||
function syncMacMenuBarIcon(showMenuBarIcon: boolean): void {
|
||||
function syncMacMenuBarIcon(showMenuBarIcon: boolean): Tray | null {
|
||||
if (process.platform !== 'darwin' || isServeMode) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
const options = getSystemTrayOptions()
|
||||
if (options) {
|
||||
setMacMenuBarIconVisible(showMenuBarIcon, options)
|
||||
}
|
||||
return options ? setMacMenuBarIconVisible(showMenuBarIcon, options) : null
|
||||
}
|
||||
|
||||
function openMainWindow(): BrowserWindow {
|
||||
@@ -976,26 +979,21 @@ function openMainWindow(): BrowserWindow {
|
||||
recordCrashBreadcrumb('main_window_created')
|
||||
logStartupMilestone('window-created')
|
||||
// Why: Windows Tray construction can synchronously block on Shell_NotifyIcon,
|
||||
// so both desktop status-item platforms share post-paint creation. The timer
|
||||
// fallback also covers windows revealed without ready-to-show ever firing;
|
||||
// Windows still needs its close target and macOS still needs its entry point.
|
||||
// so both desktop status-item platforms defer creation to after first paint.
|
||||
let trayCreated = false
|
||||
const createSystemTrayDeferred = (): void => {
|
||||
if (trayCreated || window.isDestroyed() || isQuitting || !store) {
|
||||
return
|
||||
}
|
||||
trayCreated = true
|
||||
// Why: keep the two macOS visibility paths in agreement — syncMacMenuBarIcon
|
||||
// refuses serve mode, so the deferred creator must too, or an activated
|
||||
// headless host would show an icon the live toggle can't manage.
|
||||
if (
|
||||
process.platform === 'darwin' &&
|
||||
(isServeMode || store.getSettings().showMenuBarIcon === false)
|
||||
) {
|
||||
if (process.platform === 'darwin') {
|
||||
// Why: route through syncMacMenuBarIcon so startup and the live toggle
|
||||
// share one serve-mode/visibility policy.
|
||||
if (syncMacMenuBarIcon(store.getSettings().showMenuBarIcon !== false)) {
|
||||
logStartupMilestone('tray-created')
|
||||
}
|
||||
return
|
||||
}
|
||||
// Why: Windows notification-area creation can block before first paint;
|
||||
// macOS shares the deferred path to keep startup ordering identical.
|
||||
const options = getSystemTrayOptions()
|
||||
if (options && createSystemTray(options)) {
|
||||
logStartupMilestone('tray-created')
|
||||
@@ -2077,12 +2075,7 @@ app.whenReady().then(async () => {
|
||||
logStartupMilestone('i18n-ready')
|
||||
|
||||
registerAppMenu({
|
||||
onCheckForUpdates: (options) => {
|
||||
// Why: the menu is clickable before first paint; a manual check must
|
||||
// run against a configured updater (see attach-main-window-services).
|
||||
ensureAutoUpdaterConfigured()
|
||||
return checkForUpdatesFromMenu(options)
|
||||
},
|
||||
onCheckForUpdates: (options) => runUserInitiatedUpdateCheck(options),
|
||||
onBeforeReload: ({ ignoreCache, webContentsId }) => {
|
||||
if (mainWindow?.webContents.id === webContentsId) {
|
||||
markExpectedRendererReload(webContentsId)
|
||||
|
||||
@@ -19,6 +19,7 @@ const {
|
||||
const makeImage = (name: string) => ({
|
||||
name,
|
||||
addRepresentation: vi.fn(),
|
||||
getScaleFactors: vi.fn(() => [1, 2]),
|
||||
getSize: vi.fn(() => ({ width: 16, height: 16 })),
|
||||
setTemplateImage: vi.fn(),
|
||||
toBitmap: vi.fn(() => Buffer.alloc(16 * 16 * 4)),
|
||||
@@ -135,6 +136,7 @@ beforeEach(() => {
|
||||
)
|
||||
for (const image of [baseMacImage, retinaMacImage, tintedMacImage, attentionImage]) {
|
||||
image.addRepresentation.mockClear()
|
||||
image.getScaleFactors.mockReset().mockReturnValue([1, 2])
|
||||
image.getSize.mockReset().mockReturnValue({ width: 16, height: 16 })
|
||||
image.setTemplateImage.mockClear()
|
||||
image.toBitmap.mockClear()
|
||||
@@ -190,7 +192,7 @@ describe('createSystemTray', () => {
|
||||
expect(builtMenuItems().map((item) => item.label)).toEqual([
|
||||
'Open Orca',
|
||||
undefined,
|
||||
'Settings...',
|
||||
'Settings',
|
||||
'Check for Updates...',
|
||||
undefined,
|
||||
'Quit'
|
||||
@@ -200,7 +202,7 @@ describe('createSystemTray', () => {
|
||||
|
||||
for (const [label, callback] of [
|
||||
['Open Orca', options.onOpen],
|
||||
['Settings...', options.onOpenSettings],
|
||||
['Settings', options.onOpenSettings],
|
||||
['Check for Updates...', options.onCheckForUpdates],
|
||||
['Quit', options.onQuit]
|
||||
] as const) {
|
||||
@@ -278,6 +280,13 @@ describe('setTrayAttention', () => {
|
||||
setTrayAttention(true)
|
||||
expect(tintTemplateMock).toHaveBeenCalledWith(baseMacImage, false)
|
||||
expect(composeAttentionMock).toHaveBeenCalledWith(tintedMacImage)
|
||||
// Why: the attention image is rebuilt from 1x pixels, so the @2x
|
||||
// representation must be re-added or the glyph blurs on Retina.
|
||||
expect(tintTemplateMock).toHaveBeenCalledWith(baseMacImage, false, 2)
|
||||
expect(attentionImage.addRepresentation).toHaveBeenCalledWith({
|
||||
scaleFactor: 2,
|
||||
dataURL: 'data:image/png;base64,attention'
|
||||
})
|
||||
expect(attentionImage.setTemplateImage).toHaveBeenCalledWith(false)
|
||||
expect(created.setImage).toHaveBeenCalledWith(attentionImage)
|
||||
expect(created.setToolTip).toHaveBeenCalledWith('Orca - activity waiting')
|
||||
@@ -309,6 +318,33 @@ describe('setTrayAttention', () => {
|
||||
|
||||
expect(trayInstances[0].setImage).toHaveBeenCalledWith(attentionImage)
|
||||
})
|
||||
|
||||
it('ignores repeated same-state attention calls', async () => {
|
||||
setPlatform('win32')
|
||||
const { createSystemTray, setTrayAttention } = await loadModule()
|
||||
createSystemTray(createOptions())
|
||||
const created = trayInstances[0]
|
||||
created.setImage.mockClear()
|
||||
|
||||
setTrayAttention(true)
|
||||
setTrayAttention(true)
|
||||
|
||||
expect(created.setImage).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps a pending attention dot across a macOS hide/show toggle', async () => {
|
||||
setPlatform('darwin')
|
||||
const { setMacMenuBarIconVisible, setTrayAttention } = await loadModule()
|
||||
const options = createOptions()
|
||||
setMacMenuBarIconVisible(true, options)
|
||||
setTrayAttention(true)
|
||||
|
||||
setMacMenuBarIconVisible(false, options)
|
||||
setMacMenuBarIconVisible(true, options)
|
||||
|
||||
expect(trayInstances).toHaveLength(2)
|
||||
expect(trayInstances[1].setImage).toHaveBeenCalledWith(attentionImage)
|
||||
})
|
||||
})
|
||||
|
||||
describe('destroySystemTray', () => {
|
||||
@@ -372,7 +408,7 @@ describe('macOS hardening', () => {
|
||||
})
|
||||
createSystemTray(options)
|
||||
|
||||
const settingsItem = builtMenuItems().find((item) => item.label === 'Settings...')
|
||||
const settingsItem = builtMenuItems().find((item) => item.label === 'Settings')
|
||||
expect(() => settingsItem?.click?.()).not.toThrow()
|
||||
expect(error).toHaveBeenCalledWith('[system-tray] menu action failed', expect.any(Error))
|
||||
})
|
||||
|
||||
@@ -49,11 +49,21 @@ function applyTrayImage(): void {
|
||||
try {
|
||||
// Why: disabling template tinting makes the amber dot possible, but the
|
||||
// glyph then needs literal pixels chosen for the current menu-bar theme.
|
||||
const tintedGlyph = tintTrayTemplateForAttention(
|
||||
baseTrayImage,
|
||||
nativeTheme.shouldUseDarkColors
|
||||
const useLightGlyph = nativeTheme.shouldUseDarkColors
|
||||
const attentionImage = composeTrayAttentionIcon(
|
||||
tintTrayTemplateForAttention(baseTrayImage, useLightGlyph)
|
||||
)
|
||||
const attentionImage = composeTrayAttentionIcon(tintedGlyph)
|
||||
if (baseTrayImage.getScaleFactors().includes(2)) {
|
||||
// Why: toBitmap reads only the 1x pixels, so rebuild the @2x
|
||||
// representation or the glyph blurs on Retina during attention.
|
||||
const retinaAttentionImage = composeTrayAttentionIcon(
|
||||
tintTrayTemplateForAttention(baseTrayImage, useLightGlyph, 2)
|
||||
)
|
||||
attentionImage.addRepresentation({
|
||||
scaleFactor: 2,
|
||||
dataURL: retinaAttentionImage.toDataURL()
|
||||
})
|
||||
}
|
||||
attentionImage.setTemplateImage(false)
|
||||
tray.setImage(attentionImage)
|
||||
tray.setToolTip(translateMain('tray.activityWaiting', 'Orca - activity waiting'))
|
||||
@@ -168,34 +178,28 @@ export function createSystemTray(opts: SystemTrayOptions): Tray | null {
|
||||
// Why: reflect any attention event that fired before the tray existed.
|
||||
applyTrayImage()
|
||||
|
||||
const menu = Menu.buildFromTemplate(
|
||||
process.platform === 'darwin'
|
||||
? [
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: translateMain('tray.openOrca', 'Open Orca'),
|
||||
click: safeMenuAction(() => opts.onOpen())
|
||||
},
|
||||
{ type: 'separator' },
|
||||
// Why: reuse the app menu's keys so the two entry points never drift.
|
||||
...(process.platform === 'darwin'
|
||||
? ([
|
||||
{
|
||||
label: translateMain('tray.openOrca', 'Open Orca'),
|
||||
click: safeMenuAction(() => opts.onOpen())
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: translateMain('tray.settings', 'Settings...'),
|
||||
label: translateMain('menu.settings', 'Settings'),
|
||||
click: safeMenuAction(() => opts.onOpenSettings())
|
||||
},
|
||||
{
|
||||
label: translateMain('tray.checkForUpdates', 'Check for Updates...'),
|
||||
label: translateMain('menu.checkForUpdates', 'Check for Updates...'),
|
||||
click: safeMenuAction(() => opts.onCheckForUpdates())
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: translateMain('tray.quit', 'Quit'), click: safeMenuAction(() => opts.onQuit()) }
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: translateMain('tray.openOrca', 'Open Orca'),
|
||||
click: safeMenuAction(() => opts.onOpen())
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: translateMain('tray.quit', 'Quit'), click: safeMenuAction(() => opts.onQuit()) }
|
||||
]
|
||||
)
|
||||
{ type: 'separator' }
|
||||
] as Electron.MenuItemConstructorOptions[])
|
||||
: []),
|
||||
{ label: translateMain('tray.quit', 'Quit'), click: safeMenuAction(() => opts.onQuit()) }
|
||||
])
|
||||
tray.setContextMenu(menu)
|
||||
if (process.platform === 'win32') {
|
||||
tray.setToolTip('Orca')
|
||||
@@ -245,5 +249,6 @@ export function destroySystemTray(): void {
|
||||
}
|
||||
tray = null
|
||||
baseTrayImage = null
|
||||
attentionActive = false
|
||||
// Why: attention is owned by the notification/visibility flow, and must
|
||||
// survive the macOS hide/show toggle so a re-shown icon keeps its dot.
|
||||
}
|
||||
|
||||
@@ -114,4 +114,31 @@ describe('tintTrayTemplateForAttention', () => {
|
||||
|
||||
expect(createFromBitmapMock.mock.calls[0][0]).toEqual(Buffer.from([0xff, 0xff, 0xff, 0xff]))
|
||||
})
|
||||
|
||||
it('keeps white edge pixels premultiplied-valid at partial alpha', () => {
|
||||
createFromBitmapMock.mockClear()
|
||||
const base = {
|
||||
getSize: () => ({ width: 1, height: 1 }),
|
||||
toBitmap: () => Buffer.from([0x00, 0x00, 0x00, 0x80])
|
||||
}
|
||||
|
||||
tintTrayTemplateForAttention(base as never, true)
|
||||
|
||||
// Why: the bitmap is premultiplied — white at 50% coverage is 0x80, not 0xff.
|
||||
expect(createFromBitmapMock.mock.calls[0][0]).toEqual(Buffer.from([0x80, 0x80, 0x80, 0x80]))
|
||||
})
|
||||
|
||||
it('reads and sizes the requested scale factor', () => {
|
||||
createFromBitmapMock.mockClear()
|
||||
const toBitmap = vi.fn(() => Buffer.from([0x00, 0x00, 0x00, 0xff]))
|
||||
const base = {
|
||||
getSize: () => ({ width: 1, height: 1 }),
|
||||
toBitmap
|
||||
}
|
||||
|
||||
tintTrayTemplateForAttention(base as never, true, 2)
|
||||
|
||||
expect(toBitmap).toHaveBeenCalledWith({ scaleFactor: 2 })
|
||||
expect(createFromBitmapMock.mock.calls[0][1]).toEqual({ width: 2, height: 2 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,22 +13,28 @@ const RING_RGB = { r: 0xff, g: 0xff, b: 0xff }
|
||||
*/
|
||||
export function tintTrayTemplateForAttention(
|
||||
base: NativeImage,
|
||||
useLightGlyph: boolean
|
||||
useLightGlyph: boolean,
|
||||
scaleFactor = 1
|
||||
): NativeImage {
|
||||
const { width, height } = base.getSize()
|
||||
if (width <= 0 || height <= 0) {
|
||||
return base
|
||||
}
|
||||
|
||||
const bitmap = Buffer.from(base.toBitmap())
|
||||
const channel = useLightGlyph ? 0xff : 0x00
|
||||
const bitmap = Buffer.from(base.toBitmap({ scaleFactor }))
|
||||
for (let offset = 0; offset < bitmap.length; offset += 4) {
|
||||
// Why: the bitmap is premultiplied, so a light glyph must write the pixel's
|
||||
// alpha (not 0xff) or antialiased edges become invalid over-bright pixels.
|
||||
const channel = useLightGlyph ? bitmap[offset + 3] : 0x00
|
||||
bitmap[offset] = channel
|
||||
bitmap[offset + 1] = channel
|
||||
bitmap[offset + 2] = channel
|
||||
}
|
||||
|
||||
return nativeImage.createFromBitmap(bitmap, { width, height })
|
||||
return nativeImage.createFromBitmap(bitmap, {
|
||||
width: width * scaleFactor,
|
||||
height: height * scaleFactor
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1214,14 +1214,17 @@ export function useIpcEvents(): void {
|
||||
})
|
||||
)
|
||||
|
||||
// Why: the tray/menu-bar "Settings…" item can fire before this listener
|
||||
// attaches on a freshly created window; pull any one-shot intent left for
|
||||
// us so the request is not lost to that race.
|
||||
void window.api.ui.consumePendingOpenSettings().then((open) => {
|
||||
if (open) {
|
||||
useAppStore.getState().openSettingsPage()
|
||||
}
|
||||
})
|
||||
// Why: a tray/menu-bar "Settings…" click can fire before this listener
|
||||
// attaches on a fresh window; consume any intent queued for us. Guarded
|
||||
// with `?.` so a stale preload bundle doesn't crash the listener set.
|
||||
void window.api.ui
|
||||
.consumePendingOpenSettings?.()
|
||||
.then((open) => {
|
||||
if (open) {
|
||||
useAppStore.getState().openSettingsPage()
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
unsubs.push(
|
||||
window.api.ui.onOpenSetupGuide?.(() => {
|
||||
|
||||
@@ -72,9 +72,7 @@
|
||||
"minimizeNotice": {
|
||||
"body": "Orca is still running in the system tray"
|
||||
},
|
||||
"activityWaiting": "Orca - activity waiting",
|
||||
"settings": "Settings...",
|
||||
"checkForUpdates": "Check for Updates..."
|
||||
"activityWaiting": "Orca - activity waiting"
|
||||
},
|
||||
"worktreeJumpPalette": {
|
||||
"matchLabel": {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"menuBarIcon": {
|
||||
"title": "Mostrar icono en la barra de menús",
|
||||
"description": "Manten un acceso directo de Orca e indicador de actividad en la barra de menús de macOS."
|
||||
"description": "Mantén un acceso directo de Orca e indicador de actividad en la barra de menús de macOS."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -72,9 +72,7 @@
|
||||
"minimizeNotice": {
|
||||
"body": "Orca sigue ejecutándose en la bandeja del sistema"
|
||||
},
|
||||
"activityWaiting": "Orca: actividad en espera",
|
||||
"settings": "Ajustes...",
|
||||
"checkForUpdates": "Buscar actualizaciones..."
|
||||
"activityWaiting": "Orca: actividad en espera"
|
||||
},
|
||||
"worktreeJumpPalette": {
|
||||
"matchLabel": {
|
||||
|
||||
@@ -72,9 +72,7 @@
|
||||
"minimizeNotice": {
|
||||
"body": "Orcaはシステムトレイで実行中です"
|
||||
},
|
||||
"activityWaiting": "Orca - アクティビティ待機中",
|
||||
"settings": "設定...",
|
||||
"checkForUpdates": "アップデートを確認..."
|
||||
"activityWaiting": "Orca - アクティビティ待機中"
|
||||
},
|
||||
"worktreeJumpPalette": {
|
||||
"matchLabel": {
|
||||
|
||||
@@ -72,9 +72,7 @@
|
||||
"minimizeNotice": {
|
||||
"body": "Orca가 시스템 트레이에서 계속 실행 중입니다"
|
||||
},
|
||||
"activityWaiting": "Orca - 활동 대기 중",
|
||||
"settings": "설정...",
|
||||
"checkForUpdates": "업데이트 확인..."
|
||||
"activityWaiting": "Orca - 활동 대기 중"
|
||||
},
|
||||
"worktreeJumpPalette": {
|
||||
"matchLabel": {
|
||||
|
||||
@@ -72,9 +72,7 @@
|
||||
"minimizeNotice": {
|
||||
"body": "Orca 仍在系统托盘中运行"
|
||||
},
|
||||
"activityWaiting": "Orca - 活动等待中",
|
||||
"settings": "设置...",
|
||||
"checkForUpdates": "检查更新..."
|
||||
"activityWaiting": "Orca - 活动等待中"
|
||||
},
|
||||
"worktreeJumpPalette": {
|
||||
"matchLabel": {
|
||||
|
||||
Reference in New Issue
Block a user