UX/copy tweaks (#5142)

* UX/copy tweaks

* UX/copy tweaks
This commit is contained in:
Neil 2026-06-10 19:53:03 -07:00 committed by GitHub
parent 05bf2bdd8e
commit 1a22e2d05c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 520 additions and 367 deletions

View File

@ -150,7 +150,7 @@ describe('StarNagService', () => {
emitAgentStarted(46)
expect(window.webContents.send).toHaveBeenCalledTimes(1)
expect(window.webContents.send).toHaveBeenCalledWith('star-nag:show')
expect(window.webContents.send).toHaveBeenCalledWith('star-nag:show', { mode: 'gh' })
expect(consoleInfoMock).toHaveBeenCalledTimes(1)
expect(consoleInfoMock).toHaveBeenCalledWith({
event: 'star_nag_shown',
@ -161,22 +161,39 @@ describe('StarNagService', () => {
})
})
it.each([null, true])(
'does not log a threshold exposure when checkOrcaStarred returns %s',
async (result) => {
const window = createWindow()
browserWindowMock.getAllWindows.mockReturnValue([window])
checkOrcaStarredMock.mockResolvedValue(result)
const { service, emitAgentStarted } = createHarness()
it('shows the browser fallback when checkOrcaStarred cannot determine star state', async () => {
const window = createWindow()
browserWindowMock.getAllWindows.mockReturnValue([window])
checkOrcaStarredMock.mockResolvedValue(null)
const { service, emitAgentStarted } = createHarness()
service.start()
emitAgentStarted(45)
await flushAsyncWork()
service.start()
emitAgentStarted(45)
await flushAsyncWork()
expect(window.webContents.send).not.toHaveBeenCalled()
expect(consoleInfoMock).not.toHaveBeenCalled()
}
)
expect(window.webContents.send).toHaveBeenCalledWith('star-nag:show', { mode: 'web' })
expect(consoleInfoMock).toHaveBeenCalledWith({
event: 'star_nag_shown',
app_version: '1.2.3',
threshold: STAR_NAG_INITIAL_THRESHOLD,
agents_since_baseline: 35,
source: 'threshold'
})
})
it('does not log a threshold exposure when checkOrcaStarred returns true', async () => {
const window = createWindow()
browserWindowMock.getAllWindows.mockReturnValue([window])
checkOrcaStarredMock.mockResolvedValue(true)
const { service, emitAgentStarted } = createHarness()
service.start()
emitAgentStarted(45)
await flushAsyncWork()
expect(window.webContents.send).not.toHaveBeenCalled()
expect(consoleInfoMock).not.toHaveBeenCalled()
})
it('does not block a later real prompt after crossing the threshold with no window', async () => {
const { service, emitAgentStarted } = createHarness()
@ -192,7 +209,7 @@ describe('StarNagService', () => {
emitAgentStarted(46)
await flushAsyncWork()
expect(window.webContents.send).toHaveBeenCalledWith('star-nag:show')
expect(window.webContents.send).toHaveBeenCalledWith('star-nag:show', { mode: 'gh' })
expect(consoleInfoMock).toHaveBeenCalledWith({
event: 'star_nag_shown',
app_version: '1.2.3',
@ -265,7 +282,7 @@ describe('StarNagService', () => {
browserWindowMock.getAllWindows.mockReturnValue([window])
forceShow()
expect(window.webContents.send).toHaveBeenCalledWith('star-nag:show')
expect(window.webContents.send).toHaveBeenCalledWith('star-nag:show', { mode: 'gh' })
expect(consoleInfoMock).toHaveBeenCalledWith({
event: 'star_nag_shown',
app_version: '1.2.3',
@ -320,7 +337,7 @@ describe('StarNagService', () => {
await flushAsyncWork()
getIpcHandler('star-nag:dismiss')()
emitAgentStarted(115)
emitAgentStarted(114)
await flushAsyncWork()
expect(window.webContents.send).toHaveBeenCalledTimes(1)
@ -368,7 +385,7 @@ describe('StarNagService', () => {
expect(consoleInfoMock).not.toHaveBeenCalled()
})
it('replays force_show after an in-flight threshold evaluation exits without showing', async () => {
it('keeps threshold source when an in-flight star check falls back to the browser', async () => {
const window = createWindow()
browserWindowMock.getAllWindows.mockReturnValue([window])
const deferredStarCheck = createDeferred<boolean | null>()
@ -388,12 +405,13 @@ describe('StarNagService', () => {
expect(window.webContents.send).toHaveBeenCalledTimes(1)
expect(consoleInfoMock).toHaveBeenCalledTimes(1)
expect(window.webContents.send).toHaveBeenCalledWith('star-nag:show', { mode: 'web' })
expect(consoleInfoMock).toHaveBeenCalledWith({
event: 'star_nag_shown',
app_version: '1.2.3',
threshold: STAR_NAG_INITIAL_THRESHOLD,
agents_since_baseline: 35,
source: 'force_show'
source: 'threshold'
})
})

View File

@ -5,6 +5,7 @@ import type { Store } from '../persistence'
import type { StatsCollector } from '../stats/collector'
type StarNagPromptSource = 'threshold' | 'force_show'
type StarNagPromptMode = 'gh' | 'web'
type StarNagPromptSession = {
source: StarNagPromptSource
@ -13,7 +14,7 @@ type StarNagPromptSession = {
/**
* Service that decides when to prompt the user with the "star Orca on GitHub"
* notification. Counts agents spawned since the current app version was first
* seen; crosses a doubling threshold (default 50 100 200 ) to fire the
* seen; crosses a doubling threshold (default 35 70 140 ) to fire the
* renderer notification via 'star-nag:show'.
*
* State lives in PersistedUIState so it survives restarts alongside the rest
@ -27,7 +28,7 @@ export class StarNagService {
// dismisses or stars. Without this in-memory guard, every subsequent
// agent_start past the threshold would re-enter maybeShow() and spawn a new
// `gh api` subprocess on each spawn — cheap individually, but a power user
// at 55 agents with threshold 50 would fork gh on every spawn until they
// at 40 agents with threshold 35 would fork gh on every spawn until they
// act on the card.
private promptVisible = false
// Why: prevent concurrent gh invocations if agents spawn rapidly during the
@ -64,6 +65,7 @@ export class StarNagService {
registerIpcHandlers(): void {
ipcMain.handle('star-nag:dismiss', () => this.dismiss())
ipcMain.handle('star-nag:complete', () => this.markCompleted())
ipcMain.handle('star-nag:disable', () => this.markCompleted())
ipcMain.handle('star-nag:forceShow', () => this.forceShow())
}
@ -118,14 +120,17 @@ export class StarNagService {
}
this.evaluating = true
try {
// Why: the notification is only useful for users whose gh CLI can
// actually perform the star. Calling checkOrcaStarred both gates on gh
// availability and skips users who already starred outside the app.
// Errors (network, gh missing) map to null — skip silently and leave
// state unchanged so we retry on the next spawn without racing forward
// to the next threshold.
// Why: checkOrcaStarred lets us skip users who already starred outside
// the app. When gh cannot tell us, keep the prompt available but route
// the renderer to the browser fallback instead of a dead direct-star
// button.
const starred = await checkOrcaStarred()
if (this.store.getUI().starNagCompleted) {
this.pendingForceShow = false
return
}
if (starred === null) {
this.broadcastShow(source, 'web')
return
}
if (starred) {
@ -134,14 +139,10 @@ export class StarNagService {
this.markCompleted()
return
}
if (this.store.getUI().starNagCompleted) {
this.pendingForceShow = false
return
}
if (this.promptVisible) {
return
}
this.broadcastShow(source)
this.broadcastShow(source, 'gh')
} finally {
this.evaluating = false
this.flushPendingForceShow()
@ -156,17 +157,17 @@ export class StarNagService {
if (this.promptVisible) {
return
}
this.broadcastShow('force_show')
this.broadcastShow('force_show', 'gh')
}
private broadcastShow(source: StarNagPromptSource): boolean {
private broadcastShow(source: StarNagPromptSource, mode: StarNagPromptMode): boolean {
const win = BrowserWindow.getAllWindows().find((w) => !w.isDestroyed())
if (!win) {
this.promptVisible = false
this.promptSession = null
return false
}
win.webContents.send('star-nag:show')
win.webContents.send('star-nag:show', { mode })
this.promptVisible = true
this.promptSession = { source }
this.logConsoleEvent('star_nag_shown', source)
@ -198,7 +199,7 @@ export class StarNagService {
* User closed the notification without starring double the threshold and
* rebase the baseline so the next fire is "threshold more agents since this
* dismissal" (not "threshold total since install"). This matches the
* product intent of exponential back-off: 50 more, then 100 more, then 200
* product intent of exponential back-off: 35 more, then 70 more, then 140
* more, etc.
*/
private dismiss(): void {
@ -219,7 +220,7 @@ export class StarNagService {
this.promptSession = null
}
/** User successfully starred → never nag again. */
/** User successfully starred or opted out → never nag again. */
private markCompleted(): void {
this.store.updateUI({ starNagCompleted: true })
this.promptVisible = false
@ -236,6 +237,6 @@ export class StarNagService {
this.pendingForceShow = true
return
}
this.broadcastShow('force_show')
this.broadcastShow('force_show', 'gh')
}
}

View File

@ -198,12 +198,7 @@ describe('mergePathSegments', () => {
})
it('moves user-local shell paths ahead of packaged Homebrew fallbacks', () => {
process.env.PATH = joinPath(
'/opt/homebrew/bin',
'/Users/tester/.local/bin',
'/usr/bin',
'/bin'
)
process.env.PATH = joinPath('/opt/homebrew/bin', '/Users/tester/.local/bin', '/usr/bin', '/bin')
const added = mergePathSegments(['/Users/tester/.local/bin', '/opt/homebrew/bin'])

View File

@ -1576,9 +1576,10 @@ export type PreloadApi = {
listTransitions: (args: { key: string; siteId?: string }) => Promise<JiraTransition[]>
}
starNag: {
onShow: (callback: () => void) => () => void
onShow: (callback: (payload?: { mode?: 'gh' | 'web' }) => void) => () => void
dismiss: () => Promise<void>
complete: () => Promise<void>
disable: () => Promise<void>
forceShow: () => Promise<void>
}
/** Fire-and-forget track. Loose typing at the IPC boundary on purpose

View File

@ -1455,13 +1455,17 @@ const api = {
},
starNag: {
onShow: (callback: () => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent): void => callback()
onShow: (callback: (payload?: { mode?: 'gh' | 'web' }) => void): (() => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
payload?: { mode?: 'gh' | 'web' }
): void => callback(payload)
ipcRenderer.on('star-nag:show', listener)
return () => ipcRenderer.removeListener('star-nag:show', listener)
},
dismiss: (): Promise<void> => ipcRenderer.invoke('star-nag:dismiss'),
complete: (): Promise<void> => ipcRenderer.invoke('star-nag:complete'),
disable: (): Promise<void> => ipcRenderer.invoke('star-nag:disable'),
forceShow: (): Promise<void> => ipcRenderer.invoke('star-nag:forceShow')
},

View File

@ -69,7 +69,9 @@ function getPreflightIssues(status: {
return issues
}
type StarState = 'loading' | 'starred' | 'not-starred' | 'hidden'
const ORCA_STARGAZERS_URL = 'https://github.com/stablyai/orca/stargazers'
type StarState = 'loading' | 'starred' | 'not-starred' | 'web-fallback' | 'hidden'
function GitHubStarButton({ hasRepos }: { hasRepos: boolean }): React.JSX.Element | null {
const [state, setState] = useState<StarState>('loading')
@ -84,7 +86,7 @@ function GitHubStarButton({ hasRepos }: { hasRepos: boolean }): React.JSX.Elemen
return
}
if (result === null) {
setState('hidden')
setState('web-fallback')
} else {
setState(result ? 'starred' : 'not-starred')
}
@ -112,6 +114,11 @@ function GitHubStarButton({ hasRepos }: { hasRepos: boolean }): React.JSX.Elemen
setMenuOpen((v) => !v)
return
}
if (state === 'web-fallback') {
await window.api.shell.openUrl(ORCA_STARGAZERS_URL)
await window.api.starNag.complete()
return
}
if (state !== 'not-starred') {
return
}
@ -119,7 +126,7 @@ function GitHubStarButton({ hasRepos }: { hasRepos: boolean }): React.JSX.Elemen
const ok = await window.api.gh.starOrca('landing')
if (!ok) {
if (mountedRef.current) {
setState('not-starred')
setState('web-fallback')
}
return
}
@ -129,7 +136,7 @@ function GitHubStarButton({ hasRepos }: { hasRepos: boolean }): React.JSX.Elemen
await window.api.starNag.complete()
}
// Hide if gh CLI is unavailable, or if the user has already starred and added a repo
// Hide once the user has already starred and added a repo.
if (state === 'hidden' || (state === 'starred' && hasRepos)) {
return null
}
@ -140,7 +147,7 @@ function GitHubStarButton({ hasRepos }: { hasRepos: boolean }): React.JSX.Elemen
className={cn(
'inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-[13px] font-medium transition-all duration-300',
state === 'loading' && 'pointer-events-none opacity-0',
state === 'not-starred' &&
state !== 'starred' &&
'cursor-pointer border-amber-500/60 text-amber-700 hover:border-amber-500/80 hover:bg-amber-400/10 dark:border-amber-400/30 dark:text-amber-300/90 dark:hover:border-amber-400/50 dark:hover:bg-amber-400/[0.08]',
state === 'starred' &&
'cursor-pointer border-amber-500/50 bg-amber-400/10 text-amber-700 dark:border-amber-400/25 dark:bg-amber-400/[0.06] dark:text-amber-400/60'
@ -148,17 +155,23 @@ function GitHubStarButton({ hasRepos }: { hasRepos: boolean }): React.JSX.Elemen
onClick={handleClick}
disabled={state === 'loading'}
>
<Star
className={cn(
'size-3.5 transition-all duration-300',
state === 'starred'
? 'fill-amber-500/70 text-amber-500/70 dark:fill-amber-400/60 dark:text-amber-400/60'
: 'text-amber-600 dark:text-amber-400/80'
)}
/>
{state === 'web-fallback' ? (
<ExternalLink className="size-3.5 text-amber-600 transition-all duration-300 dark:text-amber-400/80" />
) : (
<Star
className={cn(
'size-3.5 transition-all duration-300',
state === 'starred'
? 'fill-amber-500/70 text-amber-500/70 dark:fill-amber-400/60 dark:text-amber-400/60'
: 'text-amber-600 dark:text-amber-400/80'
)}
/>
)}
{state === 'starred'
? translate('auto.components.Landing.ec43b38ba7', 'Starred on GitHub')
: translate('auto.components.Landing.0d0ace8861', 'Star on GitHub')}
: state === 'web-fallback'
? translate('auto.components.Landing.157bb5ecbb', 'Open GitHub')
: translate('auto.components.Landing.0d0ace8861', 'Star on GitHub')}
</button>
{state === 'starred' && menuOpen && (
<div className="absolute right-0 top-[calc(100%+4px)] z-10 min-w-[100px] rounded-md border border-border bg-popover py-1 shadow-md">

View File

@ -1,11 +1,14 @@
import { useEffect, useState } from 'react'
import { Star, X } from 'lucide-react'
import { ExternalLink, Star, X } from 'lucide-react'
import { Card } from './ui/card'
import { Button } from './ui/button'
import { useAppStore } from '../store'
import { useMountedRef } from '@/hooks/useMountedRef'
import { translate } from '@/i18n/i18n'
const ORCA_STARGAZERS_URL = 'https://github.com/stablyai/orca/stargazers'
type StarNagMode = 'gh' | 'web'
/**
* Persistent "star Orca on GitHub" notification card.
*
@ -20,7 +23,7 @@ import { translate } from '@/i18n/i18n'
export function StarNagCard(): React.JSX.Element | null {
const [visible, setVisible] = useState(false)
const [busy, setBusy] = useState(false)
const [error, setError] = useState(false)
const [mode, setMode] = useState<StarNagMode>('gh')
const mountedRef = useMountedRef()
// Why: UpdateCard lives at the same bottom-right slot. When it is visible
// (any non-idle / non-not-available state), stack the star-nag card above
@ -30,8 +33,8 @@ export function StarNagCard(): React.JSX.Element | null {
const updateCardVisible = updateStatus.state !== 'idle' && updateStatus.state !== 'not-available'
useEffect(() => {
return window.api.starNag.onShow(() => {
setError(false)
return window.api.starNag.onShow((payload) => {
setMode(payload?.mode === 'web' ? 'web' : 'gh')
setVisible(true)
})
}, [])
@ -44,6 +47,11 @@ export function StarNagCard(): React.JSX.Element | null {
void window.api.starNag.dismiss()
}
const handleDisable = (): void => {
setVisible(false)
void window.api.starNag.disable()
}
useEffect(() => {
if (!visible) {
return
@ -67,15 +75,24 @@ export function StarNagCard(): React.JSX.Element | null {
if (busy) {
return
}
if (mode === 'web') {
setBusy(true)
await window.api.shell.openUrl(ORCA_STARGAZERS_URL)
await window.api.starNag.disable()
if (mountedRef.current) {
setBusy(false)
setVisible(false)
}
return
}
setBusy(true)
setError(false)
const ok = await window.api.gh.starOrca('star_nag')
if (mountedRef.current) {
setBusy(false)
}
if (!ok) {
if (mountedRef.current) {
setError(true)
setMode('web')
}
return
}
@ -122,20 +139,6 @@ export function StarNagCard(): React.JSX.Element | null {
)}
</p>
{error ? (
<p className="text-xs text-destructive">
{translate(
'auto.components.StarNagCard.cf82170065',
'Could not star the repo. Make sure'
)}
<code>{translate('auto.components.StarNagCard.cd8c34aac1', 'gh')}</code>{' '}
{translate(
'auto.components.StarNagCard.92b0f9d921',
'is authenticated and try again.'
)}
</p>
) : null}
<Button
variant="default"
size="sm"
@ -143,11 +146,23 @@ export function StarNagCard(): React.JSX.Element | null {
disabled={busy}
className="mt-0.5 w-full gap-1.5"
>
<Star className="size-3.5" />
{mode === 'web' ? <ExternalLink className="size-3.5" /> : <Star className="size-3.5" />}
{busy
? translate('auto.components.StarNagCard.af3c9bbb37', 'Starring…')
: translate('auto.components.StarNagCard.2d67b6c849', 'Star on GitHub')}
? mode === 'web'
? translate('auto.components.StarNagCard.d32015fec7', 'Opening...')
: translate('auto.components.StarNagCard.af3c9bbb37', 'Starring…')
: mode === 'web'
? translate('auto.components.StarNagCard.157bb5ecbb', 'Open GitHub')
: translate('auto.components.StarNagCard.2d67b6c849', 'Star on GitHub')}
</Button>
<div className="flex items-center justify-between gap-2">
<Button variant="ghost" size="sm" className="h-7 px-2" onClick={handleClose}>
{translate('auto.components.StarNagCard.8c967b4d15', 'Not now')}
</Button>
<Button variant="ghost" size="sm" className="h-7 px-2" onClick={handleDisable}>
{translate('auto.components.StarNagCard.73dfd4eb8d', "Don't ask again")}
</Button>
</div>
</div>
</Card>
</div>

View File

@ -28,8 +28,14 @@ function TaskSourceNameList(props: { names: readonly string[] }): React.JSX.Elem
{index > 0
? index === props.names.length - 1
? props.names.length > 2
? translate('auto.components.feature.wall.ConnectIntegrationsList.list_end', ', and ')
: translate('auto.components.feature.wall.ConnectIntegrationsList.list_pair', ' and ')
? translate(
'auto.components.feature.wall.ConnectIntegrationsList.list_end',
', and '
)
: translate(
'auto.components.feature.wall.ConnectIntegrationsList.list_pair',
' and '
)
: translate('auto.components.feature.wall.ConnectIntegrationsList.list_mid', ', ')
: null}
<span className="font-semibold text-foreground">{name}</span>

View File

@ -1,6 +1,6 @@
import type React from 'react'
import { useEffect, useState } from 'react'
import { Loader2, Star } from 'lucide-react'
import { ExternalLink, Loader2, Star } from 'lucide-react'
import { useMountedRef } from '@/hooks/useMountedRef'
import { Button } from '../ui/button'
import { Label } from '../ui/label'
@ -9,7 +9,16 @@ import { SearchableSetting } from './SearchableSetting'
import { SettingsSubsectionHeader } from './SettingsFormControls'
import { translate } from '@/i18n/i18n'
type SupportState = 'loading' | 'not-starred' | 'starring' | 'starred' | 'hidden' | 'error'
const ORCA_STARGAZERS_URL = 'https://github.com/stablyai/orca/stargazers'
type SupportState =
| 'loading'
| 'not-starred'
| 'web-fallback'
| 'opening-github'
| 'starring'
| 'starred'
| 'hidden'
type GeneralSupportSectionProps = {
hasPrecedingSections: boolean
@ -20,9 +29,8 @@ export function GeneralSupportSection({
}: GeneralSupportSectionProps): React.JSX.Element {
const mountedRef = useMountedRef()
// Why: the star state is derived from gh, not from settings, so it does not
// live in the global settings store. 'hidden' covers the gh-unavailable and
// already-starred-on-a-previous-session cases so the section drops out for
// users who can't or don't need to act.
// live in the global settings store. 'hidden' covers already-starred users
// so the section drops out for people who don't need to act.
//
// We start in 'loading' and render a placeholder at the exact same
// dimensions as the resolved section. When gh resolves to 'hidden', the
@ -37,7 +45,7 @@ export function GeneralSupportSection({
return
}
if (result === null) {
setStarState('hidden')
setStarState('web-fallback')
} else {
setStarState(result ? 'starred' : 'not-starred')
}
@ -48,14 +56,23 @@ export function GeneralSupportSection({
}, [])
const handleStarClick = async (): Promise<void> => {
if (starState !== 'not-starred' && starState !== 'error') {
if (starState === 'web-fallback') {
setStarState('opening-github')
await window.api.shell.openUrl(ORCA_STARGAZERS_URL)
await window.api.starNag.complete()
if (mountedRef.current) {
setStarState('web-fallback')
}
return
}
if (starState !== 'not-starred') {
return
}
setStarState('starring')
const ok = await window.api.gh.starOrca('settings')
if (!ok) {
if (mountedRef.current) {
setStarState('error')
setStarState('web-fallback')
}
return
}
@ -133,7 +150,7 @@ function SupportRow({
state,
onStarClick
}: {
state: 'not-starred' | 'starring' | 'starred' | 'error'
state: 'not-starred' | 'web-fallback' | 'opening-github' | 'starring' | 'starred'
onStarClick: () => void | Promise<void>
}): React.JSX.Element {
// Why: the left-hand label is the setting's identity and must not change
@ -147,7 +164,7 @@ function SupportRow({
)}
description={translate(
'auto.components.settings.GeneralSupportSection.511782265b',
'Support the project with a GitHub star via the gh CLI.'
'Support the project with a GitHub star.'
)}
keywords={['star', 'github', 'support', 'feedback', 'like']}
className="flex items-center justify-between gap-4 py-2"
@ -165,19 +182,26 @@ function SupportRow({
variant="default"
size="sm"
onClick={() => void onStarClick()}
disabled={state === 'starring'}
disabled={state === 'starring' || state === 'opening-github'}
className="shrink-0 gap-1.5"
>
{state === 'starring' ? (
{state === 'starring' || state === 'opening-github' ? (
<Loader2 className="size-3.5 animate-spin" />
) : state === 'web-fallback' ? (
<ExternalLink className="size-3.5" />
) : (
<Star className="size-3.5" />
<Star className="size-3.5 fill-amber-400 text-amber-400" />
)}
{state === 'starring'
? translate('auto.components.settings.GeneralSupportSection.397719bee5', 'Starring...')
: state === 'error'
? translate('auto.components.settings.GeneralSupportSection.73b327e793', 'Try Again')
: translate('auto.components.settings.GeneralSupportSection.964acc6bb4', 'Star')}
: state === 'opening-github'
? translate('auto.components.settings.GeneralSupportSection.cb65c75b11', 'Opening...')
: state === 'web-fallback'
? translate(
'auto.components.settings.GeneralSupportSection.f2d4f877b2',
'Open GitHub'
)
: translate('auto.components.settings.GeneralSupportSection.964acc6bb4', 'Star')}
</Button>
)}
</SearchableSetting>

View File

@ -1,21 +1,10 @@
import React from 'react'
import { Bell, CalendarClock, EyeOff, Github, Gitlab, List, Search, Smartphone } from 'lucide-react'
import { Bell, CalendarClock, EyeOff, Search, Smartphone } from 'lucide-react'
import { useAppStore } from '@/store'
import { useRepoMap } from '@/store/selectors'
import { cn } from '@/lib/utils'
import { isGitRepoKind } from '../../../../shared/repo-kind'
import type { GlobalSettings } from '../../../../shared/types'
import { getTaskPresetQuery, PER_REPO_FETCH_LIMIT } from '@/lib/new-workspace'
import { LinearIcon } from '@/components/icons/LinearIcon'
import { JiraIcon } from '@/components/icons/JiraIcon'
import {
normalizeVisibleTaskProviders,
restoreAvailableDefaultTaskProvider,
resolveVisibleTaskProvider
} from '../../../../shared/task-providers'
import { useActivityUnreadCount } from '@/components/activity/useActivityUnreadCount'
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
import { getLocalPreflightContext, localPreflightContextKey } from '@/lib/local-preflight-context'
import { useMobileSidebarOnboardingBadge } from './mobile-sidebar-onboarding-badge'
import {
ContextMenu,
@ -24,6 +13,7 @@ import {
ContextMenuTrigger
} from '@/components/ui/context-menu'
import { SetupGuideSidebarEntry } from './SetupGuideSidebarEntry'
import { SidebarTaskNavButton } from './SidebarTaskNavButton'
import { translate } from '@/i18n/i18n'
export { getSetupGuideSidebarEntryReady, shouldShowSetupGuideEntry } from './SetupGuideSidebarEntry'
@ -57,157 +47,23 @@ function HideSidebarMenu({ onHide }: { onHide: () => void }): React.JSX.Element
)
}
function TaskProviderShortcut({
canBrowseTasks,
label,
onOpen,
children
}: {
canBrowseTasks: boolean
label: string
onOpen: () => void
children: React.ReactNode
}): React.JSX.Element {
return (
<span
role={canBrowseTasks ? 'button' : undefined}
tabIndex={-1}
onClick={(e) => {
e.stopPropagation()
if (!canBrowseTasks) {
return
}
onOpen()
}}
className={cn(
'rounded p-0.5 text-muted-foreground/70',
canBrowseTasks ? 'transition-colors hover:text-foreground' : 'cursor-default'
)}
aria-label={canBrowseTasks ? label : undefined}
aria-hidden={canBrowseTasks ? undefined : true}
>
{children}
</span>
)
}
const SidebarNav = React.memo(function SidebarNav() {
const worktreePaletteShortcut = useShortcutLabel('worktree.palette')
const openTaskPage = useAppStore((s) => s.openTaskPage)
const openAutomationsPage = useAppStore((s) => s.openAutomationsPage)
const openActivityPage = useAppStore((s) => s.openActivityPage)
const openMobilePage = useAppStore((s) => s.openMobilePage)
const openModal = useAppStore((s) => s.openModal)
const updateSettings = useAppStore((s) => s.updateSettings)
const activeView = useAppStore((s) => s.activeView)
const repos = useAppStore((s) => s.repos)
const repoMap = useRepoMap()
const canBrowseTasks = repos.some((repo) => isGitRepoKind(repo))
// Why: the setting is opt-out (default true). `!== false` keeps the button
// visible for users whose persisted settings predate this field.
const showTasksButton = useAppStore((s) => s.settings?.showTasksButton !== false)
const rawVisibleTaskProviders = useAppStore((s) => s.settings?.visibleTaskProviders)
const defaultTaskSource = useAppStore((s) => s.settings?.defaultTaskSource ?? 'github')
const preflightStatus = useAppStore((s) => s.preflightStatus)
const preflightStatusChecked = useAppStore((s) => s.preflightStatusChecked)
const preflightStatusContextKey = useAppStore((s) => s.preflightStatusContextKey)
const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus)
const expectedPreflightContextKey = useAppStore((s) =>
localPreflightContextKey(getLocalPreflightContext(s))
)
const linearStatus = useAppStore((s) => s.linearStatus)
const linearStatusChecked = useAppStore((s) => s.linearStatusChecked)
const checkLinearConnection = useAppStore((s) => s.checkLinearConnection)
const showAgentsButton = useAppStore((s) => shouldShowAgentsButton(s.settings))
const showAutomationsButton = useAppStore((s) => shouldShowAutomationsButton(s.settings))
const showMobileButton = useAppStore((s) => shouldShowMobileButton(s.settings))
const preferredVisibleTaskProviders = React.useMemo(
() => normalizeVisibleTaskProviders(rawVisibleTaskProviders),
[rawVisibleTaskProviders]
)
const preflightStatusCurrent = preflightStatusContextKey === expectedPreflightContextKey
const visibleTaskProviders = React.useMemo(
() =>
restoreAvailableDefaultTaskProvider(
preferredVisibleTaskProviders,
{
gitlabInstalled: preflightStatusCurrent && preflightStatus?.glab?.installed === true,
linearConnected: linearStatus.connected === true
},
defaultTaskSource
),
[
defaultTaskSource,
linearStatus.connected,
preferredVisibleTaskProviders,
preflightStatusCurrent,
preflightStatus?.glab?.installed
]
)
const resolvedDefaultTaskSource = React.useMemo(
() => resolveVisibleTaskProvider(defaultTaskSource, visibleTaskProviders),
[defaultTaskSource, visibleTaskProviders]
)
React.useEffect(() => {
if (!preflightStatusChecked || !preflightStatusCurrent) {
void refreshPreflightStatus()
}
if (!linearStatusChecked) {
void checkLinearConnection()
}
}, [
checkLinearConnection,
linearStatusChecked,
preflightStatusChecked,
preflightStatusCurrent,
refreshPreflightStatus
])
// Why: warm the GitHub work-item cache on hover/focus so by the time the
// user's click finishes the round-trip has either completed or is already
// in-flight. Shaves ~200600ms off perceived page-load latency.
const prefetchWorkItems = useAppStore((s) => s.prefetchWorkItems)
const activeRepoId = useAppStore((s) => s.activeRepoId)
const defaultTaskViewPreset = useAppStore((s) => s.settings?.defaultTaskViewPreset ?? 'all')
const handlePrefetch = React.useCallback(() => {
if (!canBrowseTasks || resolvedDefaultTaskSource !== 'github') {
return
}
const activeRepo = activeRepoId ? (repoMap.get(activeRepoId) ?? null) : null
const activeGitRepo = activeRepo && isGitRepoKind(activeRepo) ? activeRepo : null
const firstGitRepo = activeGitRepo ?? repos.find((r) => isGitRepoKind(r))
if (firstGitRepo?.path) {
// Why: warm the exact cache key the page will read on mount — must
// match TaskPage's `initialTaskQuery` derived from the same default
// preset, otherwise the prefetch lands in a key the page never reads
// and we pay the full round-trip after click.
prefetchWorkItems(
firstGitRepo.id,
firstGitRepo.path,
PER_REPO_FETCH_LIMIT,
getTaskPresetQuery(defaultTaskViewPreset)
)
}
}, [
activeRepoId,
canBrowseTasks,
defaultTaskViewPreset,
prefetchWorkItems,
repoMap,
repos,
resolvedDefaultTaskSource
])
const tasksActive = activeView === 'tasks'
const automationsActive = activeView === 'automations'
const activityActive = activeView === 'activity'
const mobileActive = activeView === 'mobile'
const activityUnreadCount = useActivityUnreadCount(showAgentsButton, 'sidebar-badge')
const mobileOnboardingBadge = useMobileSidebarOnboardingBadge(showMobileButton)
const hideTasksButton = React.useCallback(() => {
void updateSettings({ showTasksButton: false })
}, [updateSettings])
const hideAutomationsButton = React.useCallback(() => {
void updateSettings({ showAutomationsButton: false })
}, [updateSettings])
@ -221,103 +77,7 @@ const SidebarNav = React.memo(function SidebarNav() {
data-contextual-tour-target="sidebar-navigation"
>
<SetupGuideSidebarEntry />
{showTasksButton ? (
<ContextMenu>
<ContextMenuTrigger asChild>
<button
type="button"
onClick={() => {
if (!canBrowseTasks) {
return
}
openTaskPage()
}}
onPointerEnter={handlePrefetch}
onFocus={handlePrefetch}
aria-disabled={!canBrowseTasks}
aria-current={tasksActive ? 'page' : undefined}
data-contextual-tour-target="sidebar-tasks"
className={cn(
'group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-medium tracking-tight transition-colors',
tasksActive
? 'bg-worktree-sidebar-accent text-worktree-sidebar-accent-foreground'
: 'text-worktree-sidebar-foreground/60 hover:bg-worktree-sidebar-foreground/8',
!canBrowseTasks && 'cursor-not-allowed opacity-50 hover:bg-transparent'
)}
>
<List
className={cn(
'size-4 shrink-0',
!tasksActive && 'text-worktree-sidebar-foreground/30'
)}
strokeWidth={tasksActive ? 2.25 : 1.75}
/>
<span className="flex-1">
{translate('auto.components.sidebar.SidebarNav.fee535205b', 'Tasks')}
</span>
<span className="hidden items-center gap-1 group-hover:flex group-focus-within:flex">
{visibleTaskProviders.includes('github') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate(
'auto.components.sidebar.SidebarNav.0ccba862b8',
'Open GitHub tasks'
)}
onOpen={() => {
openTaskPage({ taskSource: 'github' })
}}
>
<Github className="size-3.5" aria-hidden />
</TaskProviderShortcut>
) : null}
{visibleTaskProviders.includes('gitlab') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate(
'auto.components.sidebar.SidebarNav.196c1b5362',
'Open GitLab tasks'
)}
onOpen={() => {
openTaskPage({ taskSource: 'gitlab' })
}}
>
<Gitlab className="size-3.5" aria-hidden />
</TaskProviderShortcut>
) : null}
{visibleTaskProviders.includes('linear') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate(
'auto.components.sidebar.SidebarNav.c39ab10000',
'Open Linear tasks'
)}
onOpen={() => {
openTaskPage({ taskSource: 'linear' })
}}
>
<LinearIcon className="size-3.5" />
</TaskProviderShortcut>
) : null}
{visibleTaskProviders.includes('jira') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate(
'auto.components.sidebar.SidebarNav.e7ad3c540d',
'Open Jira tasks'
)}
onOpen={() => {
openTaskPage({ taskSource: 'jira' })
}}
>
<JiraIcon className="size-3.5" />
</TaskProviderShortcut>
) : null}
</span>
</button>
</ContextMenuTrigger>
<HideSidebarMenu onHide={hideTasksButton} />
</ContextMenu>
) : null}
<SidebarTaskNavButton />
{showAutomationsButton ? (
<ContextMenu>
<ContextMenuTrigger asChild>

View File

@ -0,0 +1,256 @@
import React from 'react'
import { EyeOff, Github, Gitlab, List } from 'lucide-react'
import { JiraIcon } from '@/components/icons/JiraIcon'
import { LinearIcon } from '@/components/icons/LinearIcon'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger
} from '@/components/ui/context-menu'
import { getTaskPresetQuery, PER_REPO_FETCH_LIMIT } from '@/lib/new-workspace'
import { getLocalPreflightContext, localPreflightContextKey } from '@/lib/local-preflight-context'
import { cn } from '@/lib/utils'
import { useAppStore } from '@/store'
import { useRepoMap } from '@/store/selectors'
import { translate } from '@/i18n/i18n'
import { isGitRepoKind } from '../../../../shared/repo-kind'
import {
normalizeVisibleTaskProviders,
restoreAvailableDefaultTaskProvider,
resolveVisibleTaskProvider
} from '../../../../shared/task-providers'
function HideTaskSidebarMenu({ onHide }: { onHide: () => void }): React.JSX.Element {
return (
<ContextMenuContent>
<ContextMenuItem onSelect={onHide}>
<EyeOff className="size-3.5" />
{translate('auto.components.sidebar.SidebarNav.d599269755', 'Hide from sidebar')}
</ContextMenuItem>
</ContextMenuContent>
)
}
function TaskProviderShortcut({
canBrowseTasks,
label,
onOpen,
children
}: {
canBrowseTasks: boolean
label: string
onOpen: () => void
children: React.ReactNode
}): React.JSX.Element {
return (
<span
role={canBrowseTasks ? 'button' : undefined}
tabIndex={-1}
onClick={(e) => {
e.stopPropagation()
if (!canBrowseTasks) {
return
}
onOpen()
}}
className={cn(
'rounded p-0.5 text-muted-foreground/70',
canBrowseTasks ? 'transition-colors hover:text-foreground' : 'cursor-default'
)}
aria-label={canBrowseTasks ? label : undefined}
aria-hidden={canBrowseTasks ? undefined : true}
>
{children}
</span>
)
}
export function SidebarTaskNavButton(): React.JSX.Element | null {
const openTaskPage = useAppStore((s) => s.openTaskPage)
const updateSettings = useAppStore((s) => s.updateSettings)
const activeView = useAppStore((s) => s.activeView)
const repos = useAppStore((s) => s.repos)
const repoMap = useRepoMap()
const canBrowseTasks = repos.some((repo) => isGitRepoKind(repo))
const showTasksButton = useAppStore((s) => s.settings?.showTasksButton !== false)
const rawVisibleTaskProviders = useAppStore((s) => s.settings?.visibleTaskProviders)
const defaultTaskSource = useAppStore((s) => s.settings?.defaultTaskSource ?? 'github')
const preflightStatus = useAppStore((s) => s.preflightStatus)
const preflightStatusChecked = useAppStore((s) => s.preflightStatusChecked)
const preflightStatusContextKey = useAppStore((s) => s.preflightStatusContextKey)
const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus)
const expectedPreflightContextKey = useAppStore((s) =>
localPreflightContextKey(getLocalPreflightContext(s))
)
const linearStatus = useAppStore((s) => s.linearStatus)
const linearStatusChecked = useAppStore((s) => s.linearStatusChecked)
const checkLinearConnection = useAppStore((s) => s.checkLinearConnection)
const prefetchWorkItems = useAppStore((s) => s.prefetchWorkItems)
const activeRepoId = useAppStore((s) => s.activeRepoId)
const defaultTaskViewPreset = useAppStore((s) => s.settings?.defaultTaskViewPreset ?? 'all')
const preferredVisibleTaskProviders = React.useMemo(
() => normalizeVisibleTaskProviders(rawVisibleTaskProviders),
[rawVisibleTaskProviders]
)
const preflightStatusCurrent = preflightStatusContextKey === expectedPreflightContextKey
const visibleTaskProviders = React.useMemo(
() =>
restoreAvailableDefaultTaskProvider(
preferredVisibleTaskProviders,
{
gitlabInstalled: preflightStatusCurrent && preflightStatus?.glab?.installed === true,
linearConnected: linearStatus.connected === true
},
defaultTaskSource
),
[
defaultTaskSource,
linearStatus.connected,
preferredVisibleTaskProviders,
preflightStatusCurrent,
preflightStatus?.glab?.installed
]
)
const resolvedDefaultTaskSource = React.useMemo(
() => resolveVisibleTaskProvider(defaultTaskSource, visibleTaskProviders),
[defaultTaskSource, visibleTaskProviders]
)
React.useEffect(() => {
if (!preflightStatusChecked || !preflightStatusCurrent) {
void refreshPreflightStatus()
}
if (!linearStatusChecked) {
void checkLinearConnection()
}
}, [
checkLinearConnection,
linearStatusChecked,
preflightStatusChecked,
preflightStatusCurrent,
refreshPreflightStatus
])
const handlePrefetch = React.useCallback(() => {
if (!canBrowseTasks || resolvedDefaultTaskSource !== 'github') {
return
}
const activeRepo = activeRepoId ? (repoMap.get(activeRepoId) ?? null) : null
const activeGitRepo = activeRepo && isGitRepoKind(activeRepo) ? activeRepo : null
const firstGitRepo = activeGitRepo ?? repos.find((r) => isGitRepoKind(r))
if (firstGitRepo?.path) {
prefetchWorkItems(
firstGitRepo.id,
firstGitRepo.path,
PER_REPO_FETCH_LIMIT,
getTaskPresetQuery(defaultTaskViewPreset)
)
}
}, [
activeRepoId,
canBrowseTasks,
defaultTaskViewPreset,
prefetchWorkItems,
repoMap,
repos,
resolvedDefaultTaskSource
])
const hideTasksButton = React.useCallback(() => {
void updateSettings({ showTasksButton: false })
}, [updateSettings])
if (!showTasksButton) {
return null
}
const tasksActive = activeView === 'tasks'
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<button
type="button"
onClick={() => {
if (!canBrowseTasks) {
return
}
openTaskPage()
}}
onPointerEnter={handlePrefetch}
onFocus={handlePrefetch}
aria-disabled={!canBrowseTasks}
aria-current={tasksActive ? 'page' : undefined}
data-contextual-tour-target="sidebar-tasks"
className={cn(
'group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-medium tracking-tight transition-colors',
tasksActive
? 'bg-worktree-sidebar-accent text-worktree-sidebar-accent-foreground'
: 'text-worktree-sidebar-foreground/60 hover:bg-worktree-sidebar-foreground/8',
!canBrowseTasks && 'cursor-not-allowed opacity-50 hover:bg-transparent'
)}
>
<List
className={cn('size-4 shrink-0', !tasksActive && 'text-worktree-sidebar-foreground/30')}
strokeWidth={tasksActive ? 2.25 : 1.75}
/>
<span className="flex-1">
{translate('auto.components.sidebar.SidebarNav.fee535205b', 'Tasks')}
</span>
<span className="hidden items-center gap-1 group-hover:flex group-focus-within:flex">
{visibleTaskProviders.includes('github') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate(
'auto.components.sidebar.SidebarNav.0ccba862b8',
'Open GitHub tasks'
)}
onOpen={() => openTaskPage({ taskSource: 'github' })}
>
<Github className="size-3.5" aria-hidden />
</TaskProviderShortcut>
) : null}
{visibleTaskProviders.includes('gitlab') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate(
'auto.components.sidebar.SidebarNav.196c1b5362',
'Open GitLab tasks'
)}
onOpen={() => openTaskPage({ taskSource: 'gitlab' })}
>
<Gitlab className="size-3.5" aria-hidden />
</TaskProviderShortcut>
) : null}
{visibleTaskProviders.includes('linear') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate(
'auto.components.sidebar.SidebarNav.c39ab10000',
'Open Linear tasks'
)}
onOpen={() => openTaskPage({ taskSource: 'linear' })}
>
<LinearIcon className="size-3.5" />
</TaskProviderShortcut>
) : null}
{visibleTaskProviders.includes('jira') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate(
'auto.components.sidebar.SidebarNav.e7ad3c540d',
'Open Jira tasks'
)}
onOpen={() => openTaskPage({ taskSource: 'jira' })}
>
<JiraIcon className="size-3.5" />
</TaskProviderShortcut>
) : null}
</span>
</button>
</ContextMenuTrigger>
<HideTaskSidebarMenu onHide={hideTasksButton} />
</ContextMenu>
)
}

View File

@ -869,7 +869,8 @@
"9c00bd4adf": "Select a workspace from the sidebar to begin.",
"16e9e3df89": "starred",
"0d0ace8861": "Star on GitHub",
"ec43b38ba7": "Starred on GitHub"
"ec43b38ba7": "Starred on GitHub",
"157bb5ecbb": "Open GitHub"
},
"LinearIssueMarkdownDescriptionEditor": {
"d9c47069ef": "Markdown",
@ -1201,7 +1202,13 @@
"b5e685e4d9": "Dismiss",
"5f6df21046": "Enjoying Orca?",
"2d67b6c849": "Star on GitHub",
"af3c9bbb37": "Starring…"
"af3c9bbb37": "Starring…",
"68a41bc3aa": "Could not star with",
"996bf76e46": "Open GitHub to finish in your browser.",
"d32015fec7": "Opening...",
"157bb5ecbb": "Open GitHub",
"8c967b4d15": "Not now",
"73dfd4eb8d": "Don't ask again"
},
"TaskPage": {
"513cddfa7a": "Verifying…",
@ -3344,7 +3351,10 @@
"2991a0106c": "Help",
"4e8f5710d3": "Couldn't restart Orca.",
"5161eef55d": "Restarting Orca…",
"d396773ef0": "checking"
"d396773ef0": "checking",
"f8a2c91d4e": "Milestones",
"b7e4d2a19c": "Onboarding",
"c4f8e1b72a": "X"
},
"SidebarToolbar": {
"19e32d0e5f": "Open folder picker to add a project",
@ -4369,7 +4379,9 @@
"1e29570462": "starring",
"9d181300e3": "starred",
"5c49f02662": "hidden",
"b3f0584f5d": "loading"
"b3f0584f5d": "loading",
"cb65c75b11": "Opening...",
"f2d4f877b2": "Open GitHub"
},
"GeneralUpdateSettingsSection": {
"8a52ca1d02": "Release notes",

View File

@ -869,7 +869,8 @@
"9c00bd4adf": "Seleccione un espacio de trabajo de la barra lateral para comenzar.",
"16e9e3df89": "sembrado de estrellas",
"0d0ace8861": "Estrella en GitHub",
"ec43b38ba7": "Destacado en GitHub"
"ec43b38ba7": "Destacado en GitHub",
"157bb5ecbb": "Open GitHub"
},
"LinearIssueMarkdownDescriptionEditor": {
"d9c47069ef": "Markdown",
@ -1201,7 +1202,13 @@
"b5e685e4d9": "Despedir",
"5f6df21046": "¿Disfrutando de Orca?",
"2d67b6c849": "Estrella en GitHub",
"af3c9bbb37": "Protagonizada…"
"af3c9bbb37": "Protagonizada…",
"68a41bc3aa": "Could not star with",
"996bf76e46": "Open GitHub to finish in your browser.",
"d32015fec7": "Opening...",
"157bb5ecbb": "Open GitHub",
"8c967b4d15": "Not now",
"73dfd4eb8d": "Don't ask again"
},
"TaskPage": {
"513cddfa7a": "Verificando…",
@ -3344,7 +3351,10 @@
"2991a0106c": "Ayuda",
"4e8f5710d3": "No se pudo reiniciar Orca.",
"5161eef55d": "Reiniciando Orca...",
"d396773ef0": "de cheques"
"d396773ef0": "de cheques",
"f8a2c91d4e": "Milestones",
"b7e4d2a19c": "Onboarding",
"c4f8e1b72a": "X"
},
"SidebarToolbar": {
"19e32d0e5f": "Abra el selector de carpetas para agregar un proyecto",
@ -4369,7 +4379,9 @@
"1e29570462": "protagonizada",
"9d181300e3": "sembrado de estrellas",
"5c49f02662": "oculto",
"b3f0584f5d": "cargando"
"b3f0584f5d": "cargando",
"cb65c75b11": "Opening...",
"f2d4f877b2": "Open GitHub"
},
"GeneralUpdateSettingsSection": {
"8a52ca1d02": "Notas de la versión",

View File

@ -869,7 +869,8 @@
"9c00bd4adf": "開始するには、サイドバーからワークスペースを選択します。",
"16e9e3df89": "星付き",
"0d0ace8861": "GitHub でスターを付ける",
"ec43b38ba7": "GitHub でスターを獲得"
"ec43b38ba7": "GitHub でスターを獲得",
"157bb5ecbb": "Open GitHub"
},
"LinearIssueMarkdownDescriptionEditor": {
"d9c47069ef": "Markdown",
@ -1201,7 +1202,13 @@
"b5e685e4d9": "閉じる",
"5f6df21046": "Orcaを楽しんでいますか",
"2d67b6c849": "GitHub でスターを付ける",
"af3c9bbb37": "主演…"
"af3c9bbb37": "主演…",
"68a41bc3aa": "Could not star with",
"996bf76e46": "Open GitHub to finish in your browser.",
"d32015fec7": "Opening...",
"157bb5ecbb": "Open GitHub",
"8c967b4d15": "Not now",
"73dfd4eb8d": "Don't ask again"
},
"TaskPage": {
"513cddfa7a": "確認中…",
@ -3325,7 +3332,10 @@
"2991a0106c": "ヘルプ",
"4e8f5710d3": "Orca を再起動できませんでした。",
"5161eef55d": "Orca を再起動しています…",
"d396773ef0": "チェック中"
"d396773ef0": "チェック中",
"f8a2c91d4e": "Milestones",
"b7e4d2a19c": "Onboarding",
"c4f8e1b72a": "X"
},
"SidebarToolbar": {
"19e32d0e5f": "フォルダーピッカーを開いてプロジェクトを追加します",
@ -4354,7 +4364,9 @@
"1e29570462": "主演",
"9d181300e3": "星付き",
"5c49f02662": "隠れた",
"b3f0584f5d": "読み込み中"
"b3f0584f5d": "読み込み中",
"cb65c75b11": "Opening...",
"f2d4f877b2": "Open GitHub"
},
"GeneralUpdateSettingsSection": {
"8a52ca1d02": "リリースノート",

View File

@ -869,7 +869,8 @@
"9c00bd4adf": "시작하려면 사이드바에서 워크스페이스를 선택하세요.",
"16e9e3df89": "별표가 붙은",
"0d0ace8861": "GitHub의 스타",
"ec43b38ba7": "GitHub에 별표 표시됨"
"ec43b38ba7": "GitHub에 별표 표시됨",
"157bb5ecbb": "Open GitHub"
},
"LinearIssueMarkdownDescriptionEditor": {
"d9c47069ef": "Markdown",
@ -1201,7 +1202,13 @@
"b5e685e4d9": "닫기",
"5f6df21046": "Orca를 즐기고 있나요?",
"2d67b6c849": "GitHub의 스타",
"af3c9bbb37": "스타 표시 중…"
"af3c9bbb37": "스타 표시 중…",
"68a41bc3aa": "Could not star with",
"996bf76e46": "Open GitHub to finish in your browser.",
"d32015fec7": "Opening...",
"157bb5ecbb": "Open GitHub",
"8c967b4d15": "Not now",
"73dfd4eb8d": "Don't ask again"
},
"TaskPage": {
"513cddfa7a": "확인 중…",
@ -3325,7 +3332,10 @@
"2991a0106c": "돕다",
"4e8f5710d3": "Orca를 다시 시작할 수 없습니다.",
"5161eef55d": "Orca를 다시 시작하는 중…",
"d396773ef0": "확인 중"
"d396773ef0": "확인 중",
"f8a2c91d4e": "Milestones",
"b7e4d2a19c": "Onboarding",
"c4f8e1b72a": "X"
},
"SidebarToolbar": {
"19e32d0e5f": "폴더 선택기를 열어 프로젝트를 추가하세요.",
@ -4354,7 +4364,9 @@
"1e29570462": "스타 표시",
"9d181300e3": "별표가 붙은",
"5c49f02662": "숨겨진",
"b3f0584f5d": "로드 중"
"b3f0584f5d": "로드 중",
"cb65c75b11": "Opening...",
"f2d4f877b2": "Open GitHub"
},
"GeneralUpdateSettingsSection": {
"8a52ca1d02": "릴리스 노트",

View File

@ -869,7 +869,8 @@
"9c00bd4adf": "从侧边栏中选择一个工作区开始。",
"16e9e3df89": "已加星标",
"0d0ace8861": "在 GitHub 上加星标",
"ec43b38ba7": "在 GitHub 上加星标"
"ec43b38ba7": "在 GitHub 上加星标",
"157bb5ecbb": "Open GitHub"
},
"LinearIssueMarkdownDescriptionEditor": {
"d9c47069ef": "Markdown",
@ -1201,7 +1202,13 @@
"b5e685e4d9": "关闭",
"5f6df21046": "喜欢 Orca 吗?",
"2d67b6c849": "在 GitHub 上加星标",
"af3c9bbb37": "主演…"
"af3c9bbb37": "主演…",
"68a41bc3aa": "Could not star with",
"996bf76e46": "Open GitHub to finish in your browser.",
"d32015fec7": "Opening...",
"157bb5ecbb": "Open GitHub",
"8c967b4d15": "Not now",
"73dfd4eb8d": "Don't ask again"
},
"TaskPage": {
"513cddfa7a": "正在验证...",
@ -3325,7 +3332,10 @@
"2991a0106c": "帮助",
"4e8f5710d3": "无法重新启动 Orca。",
"5161eef55d": "正在重启 Orca…",
"d396773ef0": "检查中"
"d396773ef0": "检查中",
"f8a2c91d4e": "Milestones",
"b7e4d2a19c": "Onboarding",
"c4f8e1b72a": "X"
},
"SidebarToolbar": {
"19e32d0e5f": "打开文件夹选择器以添加项目",
@ -4354,7 +4364,9 @@
"1e29570462": "主演",
"9d181300e3": "已加星标",
"5c49f02662": "隐",
"b3f0584f5d": "加载中"
"b3f0584f5d": "加载中",
"cb65c75b11": "Opening...",
"f2d4f877b2": "Open GitHub"
},
"GeneralUpdateSettingsSection": {
"8a52ca1d02": "发行说明",

View File

@ -2737,7 +2737,7 @@ export type PersistedUIState = {
* spawn effectively restarting the nag countdown after each update. */
starNagAppVersion?: string | null
/** Next threshold (agents spawned since baseline) at which the star-nag
* notification should fire. Starts at 50 and doubles each time the user
* notification should fire. Starts at 35 and doubles each time the user
* dismisses the notification without starring. */
starNagNextThreshold?: number
/** Once the user has starred Orca (from any entry point) we permanently