Add feature interaction usage bucket telemetry (#5119)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson
2026-06-10 13:38:51 -07:00
committed by GitHub
co-authored by Orca
parent 2fa2cc8cb1
commit 47927d019e
418 changed files with 13870 additions and 3196 deletions
+6 -2
View File
@@ -74,7 +74,9 @@ function editForCandidate(candidate, key, translation, sourceFile) {
}
function sourceKindForPath(filePath) {
return filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
return filePath.endsWith('.tsx') || filePath.endsWith('.jsx')
? ts.ScriptKind.TSX
: ts.ScriptKind.TS
}
function findNodeByRange(sourceFile, start, end) {
@@ -201,7 +203,9 @@ async function collectCandidateFiles(root) {
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
if (!['.git', 'assets', 'dist', 'node_modules', 'out', '__snapshots__'].includes(entry.name)) {
if (
!['.git', 'assets', 'dist', 'node_modules', 'out', '__snapshots__'].includes(entry.name)
) {
stack.push(fullPath)
}
continue
+6
View File
@@ -160,6 +160,12 @@ describe('telemetry IPC handlers', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
const handler = handlers.get('telemetry:track')!
handler({}, 'app_starred_orca', { source: 'settings' })
handler({}, 'feature_interaction_usage_bucket_reached', {
feature_id: 'tasks',
feature_category: 'task_management',
count_bucket: 'count_1',
bucket_source: 'crossed_now'
})
expect(trackMock).not.toHaveBeenCalled()
expect(getCohortAtEmitMock).not.toHaveBeenCalled()
})
+4 -1
View File
@@ -53,7 +53,10 @@ import type { OptInVia } from '../../shared/telemetry-events'
// mirrors how other core-handlers accept the store explicitly.
let storeRef: Store | null = null
const MAIN_OWNED_TELEMETRY_EVENTS = new Set<EventName>(['app_starred_orca'])
const MAIN_OWNED_TELEMETRY_EVENTS = new Set<EventName>([
'app_starred_orca',
'feature_interaction_usage_bucket_reached'
])
/**
* Derive the `via` discriminator for a `telemetry:setOptIn` call from
+245 -1
View File
@@ -81,6 +81,11 @@ const WORKFLOW_DEFAULT_WORKSPACE_STATUSES = [
{ id: 'todo', label: 'Todo', color: 'neutral', icon: 'circle' }
]
const { trackMock, getCohortAtEmitMock } = vi.hoisted(() => ({
trackMock: vi.fn(),
getCohortAtEmitMock: vi.fn()
}))
vi.mock('electron', () => ({
app: {
getPath: () => testState.dir
@@ -102,6 +107,14 @@ vi.mock('./git/repo', () => ({
getGitUsername: vi.fn().mockReturnValue('testuser')
}))
vi.mock('./telemetry/client', () => ({
track: trackMock
}))
vi.mock('./telemetry/cohort-classifier', () => ({
getCohortAtEmit: getCohortAtEmitMock
}))
/** Reset modules and dynamically import Store so the data-file path picks up the current testState.dir */
async function createStore() {
vi.resetModules()
@@ -257,6 +270,9 @@ function makeBalancedLegacyPaneLayout(start: number, end: number): TerminalPaneL
describe('Store', () => {
beforeEach(() => {
testState.dir = mkdtempSync(join(tmpdir(), 'orca-test-'))
trackMock.mockReset()
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
})
afterEach(() => {
@@ -268,7 +284,7 @@ describe('Store', () => {
it('returns empty repos when no data file exists', async () => {
const store = await createStore()
expect(store.getRepos()).toEqual([])
})
}, 15_000)
it('returns default settings when no data file exists', async () => {
const store = await createStore()
@@ -3037,6 +3053,61 @@ describe('Store', () => {
})
})
it('normalizes malformed main-owned feature telemetry bucket markers on read', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: {},
ui: {},
githubCache: { pr: {}, issue: {} },
workspaceSession: {},
featureInteractionTelemetryBuckets: {
tasks: 'count_2',
browser: 'count_4',
unknown: 'count_1'
}
})
const store = await createStore()
store.flush()
const persisted = readDataFile() as PersistedState
expect(persisted.featureInteractionTelemetryBuckets).toEqual({ tasks: 'count_2' })
})
it('does not expose or accept UI shadow writes for main-owned feature telemetry markers', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: {},
ui: {
featureInteractionTelemetryBuckets: { tasks: 'count_1000_plus' }
},
githubCache: { pr: {}, issue: {} },
workspaceSession: {},
featureInteractionTelemetryBuckets: { tasks: 'count_2' }
})
const store = await createStore()
expect('featureInteractionTelemetryBuckets' in (store.getUI() as Record<string, unknown>)).toBe(
false
)
store.updateUI({
featureInteractionTelemetryBuckets: { tasks: 'count_500_999' }
} as never)
store.flush()
const persisted = readDataFile() as PersistedState & {
ui: Record<string, unknown>
}
expect(persisted.featureInteractionTelemetryBuckets).toEqual({ tasks: 'count_2' })
expect(persisted.ui.featureInteractionTelemetryBuckets).toBeUndefined()
})
it('normalizes feature tip ids from direct UI writes', async () => {
const store = await createStore()
@@ -3068,6 +3139,179 @@ describe('Store', () => {
})
})
it('emits feature interaction telemetry only when a higher bucket is reached', async () => {
const store = await createStore()
store.recordFeatureInteraction('tasks')
store.recordFeatureInteraction('tasks')
store.recordFeatureInteraction('tasks')
store.recordFeatureInteraction('tasks')
store.flush()
expect(trackMock).toHaveBeenCalledTimes(3)
expect(trackMock).toHaveBeenNthCalledWith(1, 'feature_interaction_usage_bucket_reached', {
feature_id: 'tasks',
feature_category: 'task_management',
count_bucket: 'count_1',
bucket_source: 'crossed_now',
nth_repo_added: 2
})
expect(trackMock).toHaveBeenNthCalledWith(2, 'feature_interaction_usage_bucket_reached', {
feature_id: 'tasks',
feature_category: 'task_management',
count_bucket: 'count_2',
bucket_source: 'crossed_now',
nth_repo_added: 2
})
expect(trackMock).toHaveBeenNthCalledWith(3, 'feature_interaction_usage_bucket_reached', {
feature_id: 'tasks',
feature_category: 'task_management',
count_bucket: 'count_3_4',
bucket_source: 'crossed_now',
nth_repo_added: 2
})
expect((readDataFile() as PersistedState).featureInteractionTelemetryBuckets).toEqual({
tasks: 'count_3_4'
})
})
it('emits one observed-existing bucket for pre-rollout interaction counts', async () => {
const store = await createStore()
store.updateUI({
featureInteractions: {
tasks: { firstInteractedAt: 100, interactionCount: 137 }
}
})
trackMock.mockClear()
store.recordFeatureInteraction('tasks')
store.recordFeatureInteraction('tasks')
store.flush()
expect(trackMock).toHaveBeenCalledTimes(1)
expect(trackMock).toHaveBeenCalledWith('feature_interaction_usage_bucket_reached', {
feature_id: 'tasks',
feature_category: 'task_management',
count_bucket: 'count_100_199',
bucket_source: 'observed_existing',
nth_repo_added: 2
})
expect((readDataFile() as PersistedState).featureInteractionTelemetryBuckets).toEqual({
tasks: 'count_100_199'
})
})
it('emits only the top-coded observed-existing bucket for pre-rollout power users', async () => {
const store = await createStore()
store.updateUI({
featureInteractions: {
tasks: { firstInteractedAt: 100, interactionCount: 1200 }
}
})
trackMock.mockClear()
store.recordFeatureInteraction('tasks')
expect(trackMock).toHaveBeenCalledTimes(1)
expect(trackMock).toHaveBeenCalledWith('feature_interaction_usage_bucket_reached', {
feature_id: 'tasks',
feature_category: 'task_management',
count_bucket: 'count_1000_plus',
bucket_source: 'observed_existing',
nth_repo_added: 2
})
})
it('emits high bucket crossings once and ignores same-range increments', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: {},
ui: {
featureInteractions: {
tasks: { firstInteractedAt: 100, interactionCount: 198 }
}
},
githubCache: { pr: {}, issue: {} },
workspaceSession: {},
featureInteractionTelemetryBuckets: { tasks: 'count_100_199' }
})
const store = await createStore()
store.recordFeatureInteraction('tasks')
store.recordFeatureInteraction('tasks')
expect(trackMock).toHaveBeenCalledTimes(1)
expect(trackMock).toHaveBeenCalledWith('feature_interaction_usage_bucket_reached', {
feature_id: 'tasks',
feature_category: 'task_management',
count_bucket: 'count_200_499',
bucket_source: 'crossed_now',
nth_repo_added: 2
})
})
it('does not emit for count 4 but emits the count_1000_plus crossing', async () => {
const store = await createStore()
store.recordFeatureInteraction('tasks')
store.recordFeatureInteraction('tasks')
store.recordFeatureInteraction('tasks')
trackMock.mockClear()
store.recordFeatureInteraction('tasks')
expect(trackMock).not.toHaveBeenCalled()
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: {},
ui: {
featureInteractions: {
tasks: { firstInteractedAt: 100, interactionCount: 999 }
}
},
githubCache: { pr: {}, issue: {} },
workspaceSession: {},
featureInteractionTelemetryBuckets: { tasks: 'count_500_999' }
})
const reloaded = await createStore()
reloaded.recordFeatureInteraction('tasks')
expect(trackMock).toHaveBeenCalledTimes(1)
expect(trackMock).toHaveBeenCalledWith('feature_interaction_usage_bucket_reached', {
feature_id: 'tasks',
feature_category: 'task_management',
count_bucket: 'count_1000_plus',
bucket_source: 'crossed_now',
nth_repo_added: 2
})
})
it('dedupes against the persisted bucket marker', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: {},
ui: {
featureInteractions: {
tasks: { firstInteractedAt: 100, interactionCount: 100 }
}
},
githubCache: { pr: {}, issue: {} },
workspaceSession: {},
featureInteractionTelemetryBuckets: { tasks: 'count_100_199' }
})
const store = await createStore()
store.recordFeatureInteraction('tasks')
expect(trackMock).not.toHaveBeenCalled()
})
it('updateUI restores fixed card properties from direct UI writes', async () => {
const store = await createStore()
store.updateUI({ worktreeCardProperties: ['inline-agents'] })
+81 -21
View File
@@ -94,7 +94,11 @@ import { normalizeTerminalShortcutPolicy } from '../shared/keybindings'
import { normalizeAppIconId } from '../shared/app-icon'
import { normalizeTerminalCustomThemes } from '../shared/terminal-custom-themes'
import {
compareFeatureInteractionUsageBuckets,
getFeatureInteractionCategory,
getFeatureInteractionUsageBucket,
normalizeFeatureInteractions,
normalizeFeatureInteractionTelemetryBuckets,
type FeatureInteractionId
} from '../shared/feature-interactions'
import { normalizeContextualTourIds } from '../shared/contextual-tours'
@@ -134,6 +138,8 @@ import {
migrateWorkspaceSessionTerminalScrollbackSnapshots,
readTerminalScrollbackSnapshotSync
} from './terminal-scrollback-snapshots'
import { track } from './telemetry/client'
import { getCohortAtEmit } from './telemetry/cohort-classifier'
function encrypt(plaintext: string): string {
if (!plaintext || !safeStorage.isEncryptionAvailable()) {
@@ -324,6 +330,21 @@ function mergeContextualTourSeenIds(
return [...merged]
}
function stripMainOwnedTelemetryMarkerFromUI(
value: Partial<PersistedState['ui']> | undefined
): Partial<PersistedState['ui']> {
if (!value || typeof value !== 'object') {
return {}
}
const { featureInteractionTelemetryBuckets: _reserved, ...ui } = value as Partial<
PersistedState['ui']
> & {
featureInteractionTelemetryBuckets?: unknown
}
void _reserved
return ui
}
function normalizeSortBy(sortBy: unknown): PersistedState['ui']['sortBy'] {
if (
sortBy === 'smart' ||
@@ -1900,6 +1921,9 @@ export class Store {
result = {
...defaults,
...parsed,
featureInteractionTelemetryBuckets: normalizeFeatureInteractionTelemetryBuckets(
parsed.featureInteractionTelemetryBuckets
),
projectGroups: normalizeProjectGroups(parsed.projectGroups),
worktreeLineageById: parsed.worktreeLineageById ?? {},
settings: {
@@ -2106,7 +2130,7 @@ export class Store {
}
return {
...defaults.ui,
...parsed.ui,
...stripMainOwnedTelemetryMarkerFromUI(parsed.ui),
// Why: migrate once from the retired Appearance setting only
// when no explicit persisted chrome preference exists yet.
rightSidebarOpen,
@@ -3175,9 +3199,10 @@ export class Store {
// ── UI State ───────────────────────────────────────────────────────
getUI(): PersistedState['ui'] {
const uiState = stripMainOwnedTelemetryMarkerFromUI(this.state.ui)
return {
...getDefaultUIState(),
...this.state.ui,
...uiState,
groupBy: normalizeGroupBy(this.state.ui?.groupBy),
sortBy: normalizeSortBy(this.state.ui?.sortBy),
projectOrderBy: normalizeProjectOrderBy(this.state.ui?.projectOrderBy),
@@ -3206,39 +3231,44 @@ export class Store {
}
updateUI(updates: Partial<PersistedState['ui']>): void {
const sanitizedUpdates = stripMainOwnedTelemetryMarkerFromUI(updates)
const currentUI = {
...getDefaultUIState(),
...stripMainOwnedTelemetryMarkerFromUI(this.state.ui)
}
this.state.ui = {
...this.state.ui,
...updates,
groupBy: updates.groupBy
? normalizeGroupBy(updates.groupBy)
...currentUI,
...sanitizedUpdates,
groupBy: sanitizedUpdates.groupBy
? normalizeGroupBy(sanitizedUpdates.groupBy)
: normalizeGroupBy(this.state.ui?.groupBy),
sortBy: updates.sortBy
? normalizeSortBy(updates.sortBy)
sortBy: sanitizedUpdates.sortBy
? normalizeSortBy(sanitizedUpdates.sortBy)
: normalizeSortBy(this.state.ui?.sortBy),
projectOrderBy: updates.projectOrderBy
? normalizeProjectOrderBy(updates.projectOrderBy)
: normalizeProjectOrderBy(this.state.ui?.projectOrderBy),
rightSidebarTab:
updates.rightSidebarTab !== undefined
? normalizeRightSidebarTab(updates.rightSidebarTab)
sanitizedUpdates.rightSidebarTab !== undefined
? normalizeRightSidebarTab(sanitizedUpdates.rightSidebarTab)
: normalizeRightSidebarTab(this.state.ui?.rightSidebarTab),
worktreeCardProperties:
updates.worktreeCardProperties !== undefined
? normalizeWorktreeCardProperties(updates.worktreeCardProperties)
sanitizedUpdates.worktreeCardProperties !== undefined
? normalizeWorktreeCardProperties(sanitizedUpdates.worktreeCardProperties)
: normalizeWorktreeCardProperties(this.state.ui?.worktreeCardProperties),
agentActivityDisplayMode:
updates.agentActivityDisplayMode !== undefined
? normalizeAgentActivityDisplayMode(updates.agentActivityDisplayMode)
: normalizeAgentActivityDisplayMode(this.state.ui?.agentActivityDisplayMode),
workspaceStatuses:
updates.workspaceStatuses !== undefined
? normalizeWorkspaceStatuses(updates.workspaceStatuses)
sanitizedUpdates.workspaceStatuses !== undefined
? normalizeWorkspaceStatuses(sanitizedUpdates.workspaceStatuses)
: normalizeWorkspaceStatuses(this.state.ui?.workspaceStatuses),
workspaceBoardOpacity: clampWorkspaceBoardOpacity(
updates.workspaceBoardOpacity ?? this.state.ui?.workspaceBoardOpacity
sanitizedUpdates.workspaceBoardOpacity ?? this.state.ui?.workspaceBoardOpacity
),
workspaceBoardColumnWidth: clampWorkspaceBoardColumnWidth(
updates.workspaceBoardColumnWidth ?? this.state.ui?.workspaceBoardColumnWidth
sanitizedUpdates.workspaceBoardColumnWidth ?? this.state.ui?.workspaceBoardColumnWidth
),
browserDefaultZoomLevel: normalizeBrowserPageZoomLevel(
updates.browserDefaultZoomLevel ?? this.state.ui?.browserDefaultZoomLevel
@@ -3248,8 +3278,8 @@ export class Store {
? normalizeShowDotfilesByWorktree(updates.showDotfilesByWorktree)
: normalizeShowDotfilesByWorktree(this.state.ui?.showDotfilesByWorktree),
featureTipsSeenIds:
updates.featureTipsSeenIds !== undefined
? normalizeFeatureTipIds(updates.featureTipsSeenIds)
sanitizedUpdates.featureTipsSeenIds !== undefined
? normalizeFeatureTipIds(sanitizedUpdates.featureTipsSeenIds)
: normalizeFeatureTipIds(this.state.ui?.featureTipsSeenIds),
// Why: renderer and paired clients can mark different tours seen from
// stale UI snapshots; union them so completed tours stay suppressed.
@@ -3264,10 +3294,10 @@ export class Store {
// Merge instead of replacing so a stale renderer snapshot cannot erase
// runtime-only feature interactions.
featureInteractions:
updates.featureInteractions !== undefined
sanitizedUpdates.featureInteractions !== undefined
? mergeFeatureInteractions(
this.state.ui?.featureInteractions,
updates.featureInteractions
sanitizedUpdates.featureInteractions
)
: normalizeFeatureInteractions(this.state.ui?.featureInteractions)
}
@@ -3276,16 +3306,46 @@ export class Store {
recordFeatureInteraction(id: FeatureInteractionId): PersistedState['ui'] {
const featureInteractions = normalizeFeatureInteractions(this.state.ui?.featureInteractions)
const telemetryBuckets = normalizeFeatureInteractionTelemetryBuckets(
this.state.featureInteractionTelemetryBuckets
)
const existing = featureInteractions[id]
const previousCount = existing?.interactionCount ?? 0
const nextCount = previousCount + 1
const previousBucket = getFeatureInteractionUsageBucket(previousCount)
const nextBucket = getFeatureInteractionUsageBucket(nextCount)
const lastEmittedBucket = telemetryBuckets[id] ?? null
const shouldEmit =
nextBucket !== null &&
(lastEmittedBucket === null ||
compareFeatureInteractionUsageBuckets(nextBucket, lastEmittedBucket) > 0)
this.updateUI({
featureInteractions: {
...featureInteractions,
[id]: {
firstInteractedAt: existing?.firstInteractedAt ?? Date.now(),
interactionCount: (existing?.interactionCount ?? 0) + 1
interactionCount: nextCount
}
}
})
this.state.featureInteractionTelemetryBuckets = shouldEmit
? { ...telemetryBuckets, [id]: nextBucket }
: telemetryBuckets
this.scheduleSave()
if (shouldEmit) {
track('feature_interaction_usage_bucket_reached', {
feature_id: id,
feature_category: getFeatureInteractionCategory(id),
count_bucket: nextBucket,
bucket_source:
lastEmittedBucket === null && previousBucket !== null && previousBucket === nextBucket
? 'observed_existing'
: 'crossed_now',
...getCohortAtEmit()
})
}
return this.getUI()
}
@@ -74,7 +74,11 @@ export default function CodexRestartChip({
<div className="pointer-events-none absolute right-3 top-3 z-20">
<div className="pointer-events-auto flex items-center gap-2 rounded-lg border border-border/80 bg-popover/95 px-2 py-1.5 shadow-lg backdrop-blur-sm">
<span className="text-[11px] text-muted-foreground">
{translate("auto.components.CodexRestartChip.9263e75f49", "Codex is using the previous account")}</span>
{translate(
'auto.components.CodexRestartChip.9263e75f49',
'Codex is using the previous account'
)}
</span>
<div className="flex items-center gap-1.5">
<button
type="button"
@@ -82,13 +86,15 @@ export default function CodexRestartChip({
className="inline-flex items-center gap-1.5 rounded-md bg-foreground px-2 py-1 text-[11px] font-medium text-background transition-colors hover:opacity-90"
>
<RefreshCw className="size-3" />
{translate("auto.components.CodexRestartChip.c72a5fb234", "Restart")}</button>
{translate('auto.components.CodexRestartChip.c72a5fb234', 'Restart')}
</button>
<button
type="button"
onClick={() => dismissStaleWorktreePtyIds(staleWorktreePtyIds, clearCodexRestartNotice)}
className="rounded-md px-1.5 py-1 text-[11px] text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
>
{translate("auto.components.CodexRestartChip.9132779820", "Dismiss")}</button>
{translate('auto.components.CodexRestartChip.9132779820', 'Dismiss')}
</button>
</div>
</div>
</div>
@@ -128,21 +128,30 @@ export function FirstLaunchBanner({
<div
className="fixed left-1/2 top-2 z-40 flex w-[min(44.625rem,calc(100vw-2rem))] -translate-x-1/2 items-start gap-4 rounded-lg border border-border bg-card/95 py-3 pl-4 pr-3 shadow-lg backdrop-blur"
role="region"
aria-label={translate("auto.components.FirstLaunchBanner.fcbee32f08", "Telemetry notice")}
aria-label={translate('auto.components.FirstLaunchBanner.fcbee32f08', 'Telemetry notice')}
aria-live="polite"
>
{/* Text column — title + body stack on the left, takes remaining
width so the action column never pushes copy into a wrap. */}
<div className="flex-1 space-y-0.5 pr-1 text-sm">
<p className="font-medium leading-snug">{translate("auto.components.FirstLaunchBanner.9784b4d7bc", "Help us decide what to build next")}</p>
<p className="font-medium leading-snug">
{translate(
'auto.components.FirstLaunchBanner.9784b4d7bc',
'Help us decide what to build next'
)}
</p>
<p className="text-xs leading-snug text-muted-foreground">
{translate("auto.components.FirstLaunchBanner.958d2cc31b", "Anonymous counts of which features you use help us prioritize what to build. No file contents, prompts, terminal output, or anything that identifies you. Change anytime in Settings -> Privacy & Telemetry.")}{' '}
{translate(
'auto.components.FirstLaunchBanner.958d2cc31b',
'Anonymous counts of which features you use help us prioritize what to build. No file contents, prompts, terminal output, or anything that identifies you. Change anytime in Settings -> Privacy & Telemetry.'
)}{' '}
<button
type="button"
className="underline underline-offset-2 hover:text-foreground"
onClick={() => void window.api.shell.openUrl(PRIVACY_URL)}
>
{translate("auto.components.FirstLaunchBanner.d1deebb050", "Privacy policy")}</button>
{translate('auto.components.FirstLaunchBanner.d1deebb050', 'Privacy policy')}
</button>
.
</p>
</div>
@@ -160,15 +169,17 @@ export function FirstLaunchBanner({
disabled={inFlight}
className="border-border/60 text-muted-foreground"
>
{translate("auto.components.FirstLaunchBanner.fc5cc29955", "Opt out")}</Button>
{translate('auto.components.FirstLaunchBanner.fc5cc29955', 'Opt out')}
</Button>
<Button size="sm" onClick={handleAcknowledge} disabled={inFlight}>
{translate("auto.components.FirstLaunchBanner.94cc673726", "Got it")}</Button>
{translate('auto.components.FirstLaunchBanner.94cc673726', 'Got it')}
</Button>
</div>
{/* aria-label says "Dismiss" — the action persists silent opt-in,
not just hides the UI. */}
<button
type="button"
aria-label={translate("auto.components.FirstLaunchBanner.b9e1b966c7", "Dismiss notice")}
aria-label={translate('auto.components.FirstLaunchBanner.b9e1b966c7', 'Dismiss notice')}
onClick={handleAcknowledge}
disabled={inFlight}
className="absolute right-1.5 top-1.5 rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
@@ -716,6 +716,7 @@ function PRReviewersPanel({
patchWorkItem(item.id, { reviewRequests: nextReviewRequests }, item.repoId)
onReviewersRequested(nextReviewRequests)
setReviewerInput('')
useAppStore.getState().recordFeatureInteraction('github-tasks')
toast.success(
logins.length === 1
? translate('auto.components.GitHubItemDialog.ea985e657f', 'Reviewer requested')
@@ -789,6 +790,7 @@ function PRReviewersPanel({
patchWorkItem(item.id, { reviewRequests: nextReviewRequests }, item.repoId)
onReviewersRequested(nextReviewRequests)
setReviewerInput('')
useAppStore.getState().recordFeatureInteraction('github-tasks')
toast.success(
logins.length === 1
? translate('auto.components.GitHubItemDialog.69515bff81', 'Reviewer removed')
@@ -2571,6 +2573,7 @@ function ConversationTab({
})
onBodyUpdated(resolvedBodyDraft)
setBodyEditing(false)
useAppStore.getState().recordFeatureInteraction('github-tasks')
toast.success(
translate('auto.components.GitHubItemDialog.5221548274', 'Description updated.')
)
@@ -3116,6 +3119,7 @@ function PRActionsPanel({
number: item.number,
updates: { state: nextState }
})
useAppStore.getState().recordFeatureInteraction('github-tasks')
toast.success(
nextState === 'closed'
? translate('auto.components.GitHubItemDialog.9f88657c4e', 'Pull request closed')
@@ -3170,6 +3174,7 @@ function PRActionsPanel({
return
}
applyStatePatch('merged')
useAppStore.getState().recordFeatureInteraction('github-tasks')
toast.success(translate('auto.components.GitHubItemDialog.dbe5e2448e', 'Pull request merged'))
onMutated()
} catch {
@@ -3199,6 +3204,7 @@ function PRActionsPanel({
toast.error(result.error)
return
}
useAppStore.getState().recordFeatureInteraction('github-tasks')
toast.success(
enabled
? translate('auto.components.GitHubItemDialog.a35ea5a0f6', 'Auto-merge enabled')
@@ -4550,6 +4556,7 @@ function GHEditSection({
patchProjectRowIfNeeded({ state: prevState })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('github-tasks')
patchWorkItem(item.id, { state: newState }, item.repoId)
patchProjectRowIfNeeded({ state: newState })
onMutated()
@@ -4594,6 +4601,7 @@ function GHEditSection({
patchProjectRowIfNeeded({ labels: newLabels })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('github-tasks')
onMutated()
},
onRevert: () => {
@@ -4624,6 +4632,7 @@ function GHEditSection({
patchProjectRowIfNeeded({ labels: prevLabels })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('github-tasks')
onMutated()
},
onError: (err) => toast.error(err)
@@ -4675,6 +4684,7 @@ function GHEditSection({
patchProjectRowIfNeeded({ assignees: prevAssignees })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('github-tasks')
onMutated()
},
onError: (err) => toast.error(err)
@@ -4694,6 +4704,7 @@ function GHEditSection({
patchProjectRowIfNeeded({ assignees: newAssignees })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('github-tasks')
onMutated()
},
onRevert: () => {
@@ -5798,6 +5809,7 @@ export default function GitHubItemDialog({
const appendOptimisticComment = useCallback(
(comment: PRComment) => {
useAppStore.getState().recordFeatureInteraction('github-tasks')
// Why: skip refreshDetails() — gh api --cache 60s returns stale data
// that overwrites the optimistic comment. The next dialog open (after
// cache expiry) will pick up the server-confirmed version.
+230 -65
View File
@@ -31,6 +31,7 @@ import CommentMarkdown from '@/components/sidebar/CommentMarkdown'
import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
import { useMountedRef } from '@/hooks/useMountedRef'
import { cn } from '@/lib/utils'
import { useAppStore } from '@/store'
import type {
GitLabAssignableUser,
GitLabPipelineJob,
@@ -174,7 +175,8 @@ function CommentCard({
<span className="font-medium text-foreground">{comment.author}</span>
{comment.isResolved ? (
<span className="rounded-full bg-emerald-500/15 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700 dark:text-emerald-300">
{translate("auto.components.GitLabItemDialog.f23ea85341", "resolved")}</span>
{translate('auto.components.GitLabItemDialog.f23ea85341', 'resolved')}
</span>
) : null}
</div>
<div className="flex items-center gap-2">
@@ -188,7 +190,9 @@ function CommentCard({
className="h-6"
>
{resolving ? <LoaderCircle className="size-3 animate-spin" /> : null}
{comment.isResolved ? translate("auto.components.GitLabItemDialog.65e784c1f1", "Reopen") : translate("auto.components.GitLabItemDialog.4168eb2c51", "Resolve")}
{comment.isResolved
? translate('auto.components.GitLabItemDialog.65e784c1f1', 'Reopen')
: translate('auto.components.GitLabItemDialog.4168eb2c51', 'Resolve')}
</Button>
) : null}
<span>{comment.createdAt ? new Date(comment.createdAt).toLocaleDateString() : ''}</span>
@@ -260,7 +264,8 @@ function PipelineJobRow({
className="h-6"
>
{retrying ? <LoaderCircle className="size-3 animate-spin" /> : null}
{translate("auto.components.GitLabItemDialog.fa3e042203", "Retry")}</Button>
{translate('auto.components.GitLabItemDialog.fa3e042203', 'Retry')}
</Button>
) : null}
{job.webUrl ? (
<Button
@@ -268,7 +273,7 @@ function PipelineJobRow({
variant="ghost"
size="icon-xs"
onClick={() => void window.api.shell.openUrl(job.webUrl)}
title={translate("auto.components.GitLabItemDialog.032ae1312b", "Open job in GitLab")}
title={translate('auto.components.GitLabItemDialog.032ae1312b', 'Open job in GitLab')}
>
<ExternalLink className="size-3" />
</Button>
@@ -278,19 +283,23 @@ function PipelineJobRow({
{expanded ? (
<div className="mx-3 mb-2 rounded-md border border-border/50 bg-muted/20">
<div className="flex items-center justify-between border-b border-border/40 px-2.5 py-1.5 text-[11px] text-muted-foreground">
<span>{translate("auto.components.GitLabItemDialog.2f9b27f838", "Job log")}</span>
<span>{translate('auto.components.GitLabItemDialog.2f9b27f838', 'Job log')}</span>
<Button type="button" variant="ghost" size="xs" onClick={() => onToggleTrace(job)}>
{translate("auto.components.GitLabItemDialog.028bde664e", "Hide")}</Button>
{translate('auto.components.GitLabItemDialog.028bde664e', 'Hide')}
</Button>
</div>
{traceState?.loading ? (
<div className="flex items-center gap-2 px-2.5 py-3 text-xs text-muted-foreground">
<LoaderCircle className="size-3.5 animate-spin" />
{translate("auto.components.GitLabItemDialog.d600c2619a", "Loading log")}</div>
{translate('auto.components.GitLabItemDialog.d600c2619a', 'Loading log')}
</div>
) : traceState?.error ? (
<div className="px-2.5 py-3 text-xs text-destructive">{traceState.error}</div>
) : (
<pre className="max-h-64 overflow-auto whitespace-pre-wrap break-words px-2.5 py-2 font-mono text-[11px] leading-4 text-foreground scrollbar-sleek">
{traceState?.trace?.trim() ? traceState.trace : translate("auto.components.GitLabItemDialog.32f8bef818", "No log output.")}
{traceState?.trace?.trim()
? traceState.trace
: translate('auto.components.GitLabItemDialog.32f8bef818', 'No log output.')}
</pre>
)}
</div>
@@ -484,7 +493,7 @@ export default function GitLabItemDialog({
const nextBody = bodyDraft
const nextLabels = parseGitLabLabelDraft(labelDraft)
if (!nextTitle) {
toast.error(translate("auto.components.GitLabItemDialog.98718490e4", "MR title is required."))
toast.error(translate('auto.components.GitLabItemDialog.98718490e4', 'MR title is required.'))
return
}
@@ -531,6 +540,7 @@ export default function GitLabItemDialog({
setTitleDraft('')
setBodyDraft('')
setLabelDraft('')
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
}
} else if (mountedRef.current) {
toast.error(res.error)
@@ -611,7 +621,11 @@ export default function GitLabItemDialog({
return
}
if (result.ok) {
toast.success(translate("auto.components.GitLabItemDialog.f7cb495a12", "Retried {{value0}}", { value0: job.name }))
toast.success(
translate('auto.components.GitLabItemDialog.f7cb495a12', 'Retried {{value0}}', {
value0: job.name
})
)
if (result.job) {
setDetails((current) =>
current
@@ -646,7 +660,12 @@ export default function GitLabItemDialog({
.map((reviewer) => reviewer.id)
.filter((id): id is number => typeof id === 'number')
if (reviewerIds.length !== nextReviewers.length) {
toast.error(translate("auto.components.GitLabItemDialog.ceaf7c30c7", "Reviewer id is unavailable for this GitLab user."))
toast.error(
translate(
'auto.components.GitLabItemDialog.ceaf7c30c7',
'Reviewer id is unavailable for this GitLab user.'
)
)
return
}
setReviewerUpdating(true)
@@ -668,6 +687,7 @@ export default function GitLabItemDialog({
setReviewerOptions((current) =>
current ? dedupeGitLabUsers([...current, ...result.reviewers]) : current
)
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
} else {
toast.error(result.error)
}
@@ -688,11 +708,21 @@ export default function GitLabItemDialog({
const line = Number.parseInt(inlineCommentLine, 10)
const body = inlineCommentBody.trim()
if (!file || !Number.isFinite(line) || line <= 0 || !body) {
toast.error(translate("auto.components.GitLabItemDialog.00d0d25825", "File, line, and comment are required."))
toast.error(
translate(
'auto.components.GitLabItemDialog.00d0d25825',
'File, line, and comment are required.'
)
)
return
}
if (!details.baseSha || !details.startSha || !details.headSha) {
toast.error(translate("auto.components.GitLabItemDialog.ffdd9a78e1", "MR diff refs are unavailable for inline comments."))
toast.error(
translate(
'auto.components.GitLabItemDialog.ffdd9a78e1',
'MR diff refs are unavailable for inline comments.'
)
)
return
}
setInlineCommentSubmitting(true)
@@ -719,7 +749,10 @@ export default function GitLabItemDialog({
current ? { ...current, comments: [...current.comments, result.comment] } : current
)
setInlineCommentBody('')
toast.success(translate("auto.components.GitLabItemDialog.60c13320c4", "Inline comment added"))
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
toast.success(
translate('auto.components.GitLabItemDialog.60c13320c4', 'Inline comment added')
)
} else {
toast.error(result.error)
}
@@ -747,7 +780,12 @@ export default function GitLabItemDialog({
const res = await window.api.gl.closeMR({ repoPath, iid: item.number })
if (res.ok) {
if (mountedRef.current) {
toast.success(translate("auto.components.GitLabItemDialog.9b11cd233f", "Closed MR !{{value0}}", { value0: item.number }))
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
toast.success(
translate('auto.components.GitLabItemDialog.9b11cd233f', 'Closed MR !{{value0}}', {
value0: item.number
})
)
handleRefresh()
}
} else {
@@ -771,7 +809,12 @@ export default function GitLabItemDialog({
const res = await window.api.gl.reopenMR({ repoPath, iid: item.number })
if (res.ok) {
if (mountedRef.current) {
toast.success(translate("auto.components.GitLabItemDialog.865ea2703e", "Reopened MR !{{value0}}", { value0: item.number }))
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
toast.success(
translate('auto.components.GitLabItemDialog.865ea2703e', 'Reopened MR !{{value0}}', {
value0: item.number
})
)
handleRefresh()
}
} else {
@@ -795,7 +838,12 @@ export default function GitLabItemDialog({
const res = await window.api.gl.mergeMR({ repoPath, iid: item.number })
if (res.ok) {
if (mountedRef.current) {
toast.success(translate("auto.components.GitLabItemDialog.e089f62594", "Merged MR !{{value0}}", { value0: item.number }))
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
toast.success(
translate('auto.components.GitLabItemDialog.e089f62594', 'Merged MR !{{value0}}', {
value0: item.number
})
)
handleRefresh()
}
} else {
@@ -828,6 +876,7 @@ export default function GitLabItemDialog({
setCommentDraftState((current) =>
current.itemId === itemId ? { itemId, value: '' } : current
)
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
handleRefresh()
}
} else {
@@ -867,6 +916,7 @@ export default function GitLabItemDialog({
}
: current
)
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
}
} else if (mountedRef.current) {
toast.error(res.error)
@@ -907,8 +957,14 @@ export default function GitLabItemDialog({
<Sheet open={item !== null} onOpenChange={(open) => !open && onClose()}>
<SheetContent side="right" className="flex w-full flex-col gap-0 p-0 sm:max-w-2xl">
<VisuallyHidden.Root>
<SheetTitle>{item ? visibleTitle : translate("auto.components.GitLabItemDialog.3a051b8ade", "Work item")}</SheetTitle>
<SheetDescription>{translate("auto.components.GitLabItemDialog.30c97083c2", "GitLab work item detail")}</SheetDescription>
<SheetTitle>
{item
? visibleTitle
: translate('auto.components.GitLabItemDialog.3a051b8ade', 'Work item')}
</SheetTitle>
<SheetDescription>
{translate('auto.components.GitLabItemDialog.30c97083c2', 'GitLab work item detail')}
</SheetDescription>
</VisuallyHidden.Root>
{item ? (
@@ -923,7 +979,12 @@ export default function GitLabItemDialog({
{item.number}
</span>
<StateBadge state={item.state} />
{item.author ? <span>{translate("auto.components.GitLabItemDialog.9bfb4a24d7", "by")}{item.author}</span> : null}
{item.author ? (
<span>
{translate('auto.components.GitLabItemDialog.9bfb4a24d7', 'by')}
{item.author}
</span>
) : null}
</div>
<h2 className="mt-1.5 text-lg font-semibold leading-tight text-foreground">
{visibleTitle}
@@ -944,7 +1005,7 @@ export default function GitLabItemDialog({
<Button
variant="ghost"
size="icon-sm"
aria-label={translate("auto.components.GitLabItemDialog.b3c156dd51", "Refresh")}
aria-label={translate('auto.components.GitLabItemDialog.b3c156dd51', 'Refresh')}
disabled={loading}
onClick={handleRefresh}
className="size-7"
@@ -960,9 +1021,12 @@ export default function GitLabItemDialog({
<Tabs defaultValue="description" className="flex min-h-0 flex-1 flex-col">
<TabsList className="mx-5 mt-3 self-start">
<TabsTrigger value="description">{translate("auto.components.GitLabItemDialog.908d8d2a73", "Description")}</TabsTrigger>
<TabsTrigger value="description">
{translate('auto.components.GitLabItemDialog.908d8d2a73', 'Description')}
</TabsTrigger>
<TabsTrigger value="conversation">
{translate("auto.components.GitLabItemDialog.c996e2962c", "Conversation")}{details?.comments?.length ? (
{translate('auto.components.GitLabItemDialog.c996e2962c', 'Conversation')}
{details?.comments?.length ? (
<span className="ml-1.5 rounded-full bg-muted px-1.5 text-[10px] font-medium">
{details.comments.length}
</span>
@@ -970,7 +1034,8 @@ export default function GitLabItemDialog({
</TabsTrigger>
{isMR ? (
<TabsTrigger value="files">
{translate("auto.components.GitLabItemDialog.be3d291837", "Files")}{details?.files?.length ? (
{translate('auto.components.GitLabItemDialog.be3d291837', 'Files')}
{details?.files?.length ? (
<span className="ml-1.5 rounded-full bg-muted px-1.5 text-[10px] font-medium">
{details.files.length}
</span>
@@ -979,7 +1044,8 @@ export default function GitLabItemDialog({
) : null}
{isMR ? (
<TabsTrigger value="pipeline">
{translate("auto.components.GitLabItemDialog.02cbe2de44", "Pipeline")}{details?.pipelineJobs?.length ? (
{translate('auto.components.GitLabItemDialog.02cbe2de44', 'Pipeline')}
{details?.pipelineJobs?.length ? (
<span className="ml-1.5 rounded-full bg-muted px-1.5 text-[10px] font-medium">
{details.pipelineJobs.length}
</span>
@@ -1000,14 +1066,30 @@ export default function GitLabItemDialog({
<div className="mb-4 rounded-md border border-border/50 bg-muted/20 p-3">
<div className="flex items-center justify-between gap-2">
<div>
<div className="text-xs font-medium text-foreground">{translate("auto.components.GitLabItemDialog.4f9313984d", "Reviewers")}</div>
<div className="text-xs font-medium text-foreground">
{translate('auto.components.GitLabItemDialog.4f9313984d', 'Reviewers')}
</div>
{approvalState ? (
<div className="mt-0.5 text-[11px] text-muted-foreground">
{approvalState.approvalsLeft === 0
? translate("auto.components.GitLabItemDialog.22511537d2", "Approved")
: translate("auto.components.GitLabItemDialog.40c56b95e2", "{{value0}} approval{{value1}} remaining", { value0: approvalState.approvalsLeft ?? 0, value1: approvalState.approvalsLeft === 1 ? '' : 's' })}
{typeof approvalState.approvalsRequired === "number"
? translate("auto.components.GitLabItemDialog.00f3bab87b", " of {{value0}} required", { value0: approvalState.approvalsRequired })
? translate(
'auto.components.GitLabItemDialog.22511537d2',
'Approved'
)
: translate(
'auto.components.GitLabItemDialog.40c56b95e2',
'{{value0}} approval{{value1}} remaining',
{
value0: approvalState.approvalsLeft ?? 0,
value1: approvalState.approvalsLeft === 1 ? '' : 's'
}
)}
{typeof approvalState.approvalsRequired === 'number'
? translate(
'auto.components.GitLabItemDialog.00f3bab87b',
' of {{value0}} required',
{ value0: approvalState.approvalsRequired }
)
: ''}
</div>
) : null}
@@ -1022,7 +1104,8 @@ export default function GitLabItemDialog({
{reviewerOptionsLoading ? (
<LoaderCircle className="size-3 animate-spin" />
) : null}
{translate("auto.components.GitLabItemDialog.cb55b0390f", "Manage")}</Button>
{translate('auto.components.GitLabItemDialog.cb55b0390f', 'Manage')}
</Button>
</div>
<div className="mt-2 flex flex-wrap gap-1.5">
{currentReviewers.length > 0 ? (
@@ -1043,14 +1126,23 @@ export default function GitLabItemDialog({
)
}
className="rounded-full p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50"
aria-label={translate("auto.components.GitLabItemDialog.1b19cdc510", "Remove reviewer {{value0}}", { value0: reviewer.username })}
aria-label={translate(
'auto.components.GitLabItemDialog.1b19cdc510',
'Remove reviewer {{value0}}',
{ value0: reviewer.username }
)}
>
<X className="size-3" />
</button>
</span>
))
) : (
<span className="text-[11px] text-muted-foreground">{translate("auto.components.GitLabItemDialog.474b50d988", "No reviewers.")}</span>
<span className="text-[11px] text-muted-foreground">
{translate(
'auto.components.GitLabItemDialog.474b50d988',
'No reviewers.'
)}
</span>
)}
</div>
{reviewerOptions ? (
@@ -1061,7 +1153,12 @@ export default function GitLabItemDialog({
onChange={(event) => setReviewerDraftId(event.target.value)}
className="h-8 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-xs text-foreground"
>
<option value="">{translate("auto.components.GitLabItemDialog.05939e977d", "Add reviewer")}</option>
<option value="">
{translate(
'auto.components.GitLabItemDialog.05939e977d',
'Add reviewer'
)}
</option>
{reviewerOptionRows.map((reviewer) => (
<option key={gitLabUserKey(reviewer)} value={gitLabUserKey(reviewer)}>
{reviewer.username}
@@ -1084,7 +1181,8 @@ export default function GitLabItemDialog({
{reviewerUpdating ? (
<LoaderCircle className="size-3 animate-spin" />
) : null}
{translate("auto.components.GitLabItemDialog.7a2117129a", "Add")}</Button>
{translate('auto.components.GitLabItemDialog.7a2117129a', 'Add')}
</Button>
</div>
) : null}
{approvalState?.rules.length ? (
@@ -1096,7 +1194,16 @@ export default function GitLabItemDialog({
>
<span className="min-w-0 truncate">{rule.name}</span>
<span>
{rule.approved ? translate("auto.components.GitLabItemDialog.22511537d2", "Approved") : translate("auto.components.GitLabItemDialog.6de8ce0cc6", "{{value0}} required", { value0: rule.approvalsRequired })}
{rule.approved
? translate(
'auto.components.GitLabItemDialog.22511537d2',
'Approved'
)
: translate(
'auto.components.GitLabItemDialog.6de8ce0cc6',
'{{value0}} required',
{ value0: rule.approvalsRequired }
)}
</span>
</div>
))}
@@ -1112,7 +1219,8 @@ export default function GitLabItemDialog({
<div className="space-y-3">
<div>
<label className="mb-1 block text-xs font-medium text-muted-foreground">
{translate("auto.components.GitLabItemDialog.89f3f19368", "Title")}</label>
{translate('auto.components.GitLabItemDialog.89f3f19368', 'Title')}
</label>
<input
value={titleDraft}
onChange={(event) => setTitleDraft(event.target.value)}
@@ -1122,7 +1230,8 @@ export default function GitLabItemDialog({
</div>
<div>
<label className="mb-1 block text-xs font-medium text-muted-foreground">
{translate("auto.components.GitLabItemDialog.908d8d2a73", "Description")}</label>
{translate('auto.components.GitLabItemDialog.908d8d2a73', 'Description')}
</label>
<textarea
value={bodyDraft}
onChange={(event) => setBodyDraft(event.target.value)}
@@ -1133,12 +1242,16 @@ export default function GitLabItemDialog({
</div>
<div>
<label className="mb-1 block text-xs font-medium text-muted-foreground">
{translate("auto.components.GitLabItemDialog.dde24ade55", "Labels")}</label>
{translate('auto.components.GitLabItemDialog.dde24ade55', 'Labels')}
</label>
<input
value={labelDraft}
onChange={(event) => setLabelDraft(event.target.value)}
disabled={detailsSaving}
placeholder={translate("auto.components.GitLabItemDialog.3c0b6ccca7", "bug, backend")}
placeholder={translate(
'auto.components.GitLabItemDialog.3c0b6ccca7',
'bug, backend'
)}
className="h-9 w-full rounded-md border border-input bg-transparent px-2.5 text-sm shadow-xs focus:border-ring focus:outline-none focus:ring-[3px] focus:ring-ring/50"
/>
{labelOptionsLoading || labelSuggestionOptions.length > 0 ? (
@@ -1146,7 +1259,11 @@ export default function GitLabItemDialog({
{labelOptionsLoading ? (
<span className="inline-flex h-6 items-center gap-1 rounded-full border border-border/50 px-2 text-[11px] text-muted-foreground">
<LoaderCircle className="size-3 animate-spin" />
{translate("auto.components.GitLabItemDialog.717b706849", "Loading labels")}</span>
{translate(
'auto.components.GitLabItemDialog.717b706849',
'Loading labels'
)}
</span>
) : null}
{labelSuggestionOptions.map((label) => {
const selected = parseGitLabLabelDraft(labelDraft).some(
@@ -1184,7 +1301,8 @@ export default function GitLabItemDialog({
onClick={handleCancelDetailsEdit}
>
<X className="size-3.5" />
{translate("auto.components.GitLabItemDialog.f72fad3b16", "Cancel")}</Button>
{translate('auto.components.GitLabItemDialog.f72fad3b16', 'Cancel')}
</Button>
<Button
type="button"
size="sm"
@@ -1196,7 +1314,8 @@ export default function GitLabItemDialog({
) : (
<Check className="size-3.5" />
)}
{translate("auto.components.GitLabItemDialog.93f79a3fc1", "Save")}</Button>
{translate('auto.components.GitLabItemDialog.93f79a3fc1', 'Save')}
</Button>
</div>
</div>
) : details?.body ? (
@@ -1211,7 +1330,8 @@ export default function GitLabItemDialog({
className="gap-1.5"
>
<Pencil className="size-3.5" />
{translate("auto.components.GitLabItemDialog.da4174b00f", "Edit")}</Button>
{translate('auto.components.GitLabItemDialog.da4174b00f', 'Edit')}
</Button>
</div>
) : null}
<CommentMarkdown content={details.body} />
@@ -1228,10 +1348,16 @@ export default function GitLabItemDialog({
className="gap-1.5"
>
<Pencil className="size-3.5" />
{translate("auto.components.GitLabItemDialog.da4174b00f", "Edit")}</Button>
{translate('auto.components.GitLabItemDialog.da4174b00f', 'Edit')}
</Button>
</div>
) : null}
<p className="text-sm text-muted-foreground">{translate("auto.components.GitLabItemDialog.14423484db", "No description.")}</p>
<p className="text-sm text-muted-foreground">
{translate(
'auto.components.GitLabItemDialog.14423484db',
'No description.'
)}
</p>
</div>
)}
</TabsContent>
@@ -1254,7 +1380,9 @@ export default function GitLabItemDialog({
/>
))
) : (
<p className="text-sm text-muted-foreground">{translate("auto.components.GitLabItemDialog.85a8170279", "No comments yet.")}</p>
<p className="text-sm text-muted-foreground">
{translate('auto.components.GitLabItemDialog.85a8170279', 'No comments yet.')}
</p>
)}
</TabsContent>
@@ -1273,7 +1401,9 @@ export default function GitLabItemDialog({
onChange={(event) => setInlineCommentFilePath(event.target.value)}
className="h-8 min-w-0 rounded-md border border-input bg-background px-2 text-xs text-foreground"
>
<option value="">{translate("auto.components.GitLabItemDialog.ceb08a733d", "File")}</option>
<option value="">
{translate('auto.components.GitLabItemDialog.ceb08a733d', 'File')}
</option>
{details.files.map((file) => (
<option key={file.path} value={file.path}>
{file.path}
@@ -1284,7 +1414,10 @@ export default function GitLabItemDialog({
value={inlineCommentLine}
onChange={(event) => setInlineCommentLine(event.target.value)}
inputMode="numeric"
placeholder={translate("auto.components.GitLabItemDialog.7a7204417f", "Line")}
placeholder={translate(
'auto.components.GitLabItemDialog.7a7204417f',
'Line'
)}
className="h-8 rounded-md border border-input bg-background px-2 text-xs text-foreground"
/>
</div>
@@ -1292,7 +1425,10 @@ export default function GitLabItemDialog({
value={inlineCommentBody}
onChange={(event) => setInlineCommentBody(event.target.value)}
rows={2}
placeholder={translate("auto.components.GitLabItemDialog.21f8dde18a", "Inline comment")}
placeholder={translate(
'auto.components.GitLabItemDialog.21f8dde18a',
'Inline comment'
)}
className="mt-2 w-full resize-none rounded-md border border-input bg-background px-2.5 py-1.5 text-sm shadow-xs focus:border-ring focus:outline-none focus:ring-[3px] focus:ring-ring/50"
/>
<div className="mt-2 flex justify-end">
@@ -1312,7 +1448,8 @@ export default function GitLabItemDialog({
) : (
<Send className="size-3.5" />
)}
{translate("auto.components.GitLabItemDialog.84012fa8fb", "Comment")}</Button>
{translate('auto.components.GitLabItemDialog.84012fa8fb', 'Comment')}
</Button>
</div>
</div>
<div className="space-y-2">
@@ -1328,7 +1465,11 @@ export default function GitLabItemDialog({
</div>
{file.oldPath ? (
<div className="break-all font-mono text-[11px] text-muted-foreground">
{translate("auto.components.GitLabItemDialog.a7eb4f4916", "from")}{file.oldPath}
{translate(
'auto.components.GitLabItemDialog.a7eb4f4916',
'from'
)}
{file.oldPath}
</div>
) : null}
</div>
@@ -1343,14 +1484,23 @@ export default function GitLabItemDialog({
</pre>
) : (
<div className="px-3 py-3 text-xs text-muted-foreground">
{translate("auto.components.GitLabItemDialog.007423f585", "Diff content unavailable.")}</div>
{translate(
'auto.components.GitLabItemDialog.007423f585',
'Diff content unavailable.'
)}
</div>
)}
</div>
))}
</div>
</>
) : (
<p className="text-sm text-muted-foreground">{translate("auto.components.GitLabItemDialog.808b1ca1ba", "No changed files.")}</p>
<p className="text-sm text-muted-foreground">
{translate(
'auto.components.GitLabItemDialog.808b1ca1ba',
'No changed files.'
)}
</p>
)}
</TabsContent>
) : null}
@@ -1376,7 +1526,12 @@ export default function GitLabItemDialog({
))}
</div>
) : (
<p className="text-sm text-muted-foreground">{translate("auto.components.GitLabItemDialog.f11e3e7675", "No pipeline runs for this MR.")}</p>
<p className="text-sm text-muted-foreground">
{translate(
'auto.components.GitLabItemDialog.f11e3e7675',
'No pipeline runs for this MR.'
)}
</p>
)}
</TabsContent>
) : null}
@@ -1390,7 +1545,11 @@ export default function GitLabItemDialog({
<textarea
value={commentDraft}
onChange={(e) => updateCommentDraft(e.target.value)}
placeholder={translate("auto.components.GitLabItemDialog.c08e1d5a57", "Comment on {{value0}}{{value1}}…", { value0: prefix, value1: item.number })}
placeholder={translate(
'auto.components.GitLabItemDialog.c08e1d5a57',
'Comment on {{value0}}{{value1}}…',
{ value0: prefix, value1: item.number }
)}
rows={2}
disabled={commentSubmitting}
className="min-h-9 w-full resize-none rounded-md border border-input bg-transparent px-2.5 py-1.5 text-sm shadow-xs focus:border-ring focus:outline-none focus:ring-[3px] focus:ring-ring/50"
@@ -1414,7 +1573,8 @@ export default function GitLabItemDialog({
) : (
<Send className="size-3.5" />
)}
{translate("auto.components.GitLabItemDialog.84012fa8fb", "Comment")}</Button>
{translate('auto.components.GitLabItemDialog.84012fa8fb', 'Comment')}
</Button>
</div>
<div className="flex items-center justify-between gap-2">
@@ -1425,11 +1585,13 @@ export default function GitLabItemDialog({
className="gap-1.5"
>
<ExternalLink className="size-3.5" />
{translate("auto.components.GitLabItemDialog.f2e64d1c20", "Open in GitLab")}</Button>
{translate('auto.components.GitLabItemDialog.f2e64d1c20', 'Open in GitLab')}
</Button>
<div className="flex items-center gap-2">
{onCreateWorkspace ? (
<Button variant="outline" size="sm" onClick={() => onCreateWorkspace(item)}>
{translate("auto.components.GitLabItemDialog.131865e231", "Create workspace")}</Button>
{translate('auto.components.GitLabItemDialog.131865e231', 'Create workspace')}
</Button>
) : null}
{canMerge ? (
<Button
@@ -1437,10 +1599,11 @@ export default function GitLabItemDialog({
disabled={actionInFlight !== null}
onClick={() => void handleMerge()}
>
{actionInFlight === "merge" ? (
{actionInFlight === 'merge' ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : null}
{translate("auto.components.GitLabItemDialog.16b3412570", "Merge")}</Button>
{translate('auto.components.GitLabItemDialog.16b3412570', 'Merge')}
</Button>
) : null}
{canClose ? (
<Button
@@ -1449,10 +1612,11 @@ export default function GitLabItemDialog({
disabled={actionInFlight !== null}
onClick={() => void handleClose()}
>
{actionInFlight === "close" ? (
{actionInFlight === 'close' ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : null}
{translate("auto.components.GitLabItemDialog.a199eb364b", "Close")}</Button>
{translate('auto.components.GitLabItemDialog.a199eb364b', 'Close')}
</Button>
) : null}
{canReopen ? (
<Button
@@ -1461,10 +1625,11 @@ export default function GitLabItemDialog({
disabled={actionInFlight !== null}
onClick={() => void handleReopen()}
>
{actionInFlight === "reopen" ? (
{actionInFlight === 'reopen' ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : null}
{translate("auto.components.GitLabItemDialog.65e784c1f1", "Reopen")}</Button>
{translate('auto.components.GitLabItemDialog.65e784c1f1', 'Reopen')}
</Button>
) : null}
</div>
</div>
@@ -94,9 +94,17 @@ function jiraStatusClass(categoryKey: string): string {
async function copyTextToClipboard(text: string, label: string): Promise<void> {
try {
await window.api.ui.writeClipboardText(text)
toast.success(translate("auto.components.JiraIssueWorkspace.2ff69a3545", "{{value0}} copied", { value0: label }))
toast.success(
translate('auto.components.JiraIssueWorkspace.2ff69a3545', '{{value0}} copied', {
value0: label
})
)
} catch {
toast.error(translate("auto.components.JiraIssueWorkspace.6c41a9bcea", "Failed to copy {{value0}}", { value0: label.toLowerCase() }))
toast.error(
translate('auto.components.JiraIssueWorkspace.6c41a9bcea', 'Failed to copy {{value0}}', {
value0: label.toLowerCase()
})
)
}
}
@@ -253,7 +261,14 @@ export default function JiraIssueWorkspace({
} catch (error) {
setFullIssue(previous)
patchJiraIssue(previous.key, previous)
toast.error(error instanceof Error ? error.message : translate("auto.components.JiraIssueWorkspace.ea21952aa3", "Failed to update Jira issue."))
toast.error(
error instanceof Error
? error.message
: translate(
'auto.components.JiraIssueWorkspace.ea21952aa3',
'Failed to update Jira issue.'
)
)
} finally {
setPendingField(null)
}
@@ -308,7 +323,11 @@ export default function JiraIssueWorkspace({
setComments((prev) => [...prev, comment])
setCommentDraft('')
} catch (error) {
toast.error(error instanceof Error ? error.message : translate("auto.components.JiraIssueWorkspace.fa132c8aed", "Failed to add comment."))
toast.error(
error instanceof Error
? error.message
: translate('auto.components.JiraIssueWorkspace.fa132c8aed', 'Failed to add comment.')
)
} finally {
setCommentSubmitting(false)
}
@@ -320,27 +339,30 @@ export default function JiraIssueWorkspace({
}
return [
{
label: translate("auto.components.JiraIssueWorkspace.69da9a208c", "Open in Jira"),
label: translate('auto.components.JiraIssueWorkspace.69da9a208c', 'Open in Jira'),
icon: ExternalLink,
action: () => window.api.shell.openUrl(displayed.url)
},
{
label: translate("auto.components.JiraIssueWorkspace.779bb91ee0", "Copy URL"),
label: translate('auto.components.JiraIssueWorkspace.779bb91ee0', 'Copy URL'),
icon: Clipboard,
action: () => void copyTextToClipboard(displayed.url, 'URL')
},
{
label: translate("auto.components.JiraIssueWorkspace.38839801e8", "Copy key"),
label: translate('auto.components.JiraIssueWorkspace.38839801e8', 'Copy key'),
icon: Clipboard,
action: () => void copyTextToClipboard(displayed.key, 'Key')
},
{
label: translate("auto.components.JiraIssueWorkspace.80efa101c5", "Copy suggested branch name"),
label: translate(
'auto.components.JiraIssueWorkspace.80efa101c5',
'Copy suggested branch name'
),
icon: GitBranch,
action: () => void copyTextToClipboard(buildJiraBranchName(displayed), 'Branch name')
},
{
label: translate("auto.components.JiraIssueWorkspace.0cc62bd690", "Copy prompt"),
label: translate('auto.components.JiraIssueWorkspace.0cc62bd690', 'Copy prompt'),
icon: Clipboard,
action: () => void copyTextToClipboard(buildJiraPrompt(displayed), 'Prompt')
}
@@ -356,11 +378,18 @@ export default function JiraIssueWorkspace({
onOpenAutoFocus={(event) => event.preventDefault()}
>
<VisuallyHidden.Root asChild>
<SheetTitle>{displayed?.title ?? translate("auto.components.JiraIssueWorkspace.ef21405c6d", "Jira issue")}</SheetTitle>
<SheetTitle>
{displayed?.title ??
translate('auto.components.JiraIssueWorkspace.ef21405c6d', 'Jira issue')}
</SheetTitle>
</VisuallyHidden.Root>
<VisuallyHidden.Root asChild>
<SheetDescription>
{translate("auto.components.JiraIssueWorkspace.857bd2f88f", "Preview, edit, and start work from the selected issue.")}</SheetDescription>
{translate(
'auto.components.JiraIssueWorkspace.857bd2f88f',
'Preview, edit, and start work from the selected issue.'
)}
</SheetDescription>
</VisuallyHidden.Root>
{displayed ? (
@@ -384,7 +413,8 @@ export default function JiraIssueWorkspace({
className="hidden shrink-0 gap-2 sm:inline-flex"
size="sm"
>
{translate("auto.components.JiraIssueWorkspace.2441be6f9f", "Start workspace")}<ArrowRight className="size-4" />
{translate('auto.components.JiraIssueWorkspace.2441be6f9f', 'Start workspace')}
<ArrowRight className="size-4" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
@@ -393,13 +423,17 @@ export default function JiraIssueWorkspace({
size="icon-sm"
className="shrink-0"
onClick={onClose}
aria-label={translate("auto.components.JiraIssueWorkspace.76513c7898", "Close Jira issue preview")}
aria-label={translate(
'auto.components.JiraIssueWorkspace.76513c7898',
'Close Jira issue preview'
)}
>
<X className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.JiraIssueWorkspace.7a96985ca0", "Close")}</TooltipContent>
{translate('auto.components.JiraIssueWorkspace.7a96985ca0', 'Close')}
</TooltipContent>
</Tooltip>
</div>
</div>
@@ -416,7 +450,7 @@ export default function JiraIssueWorkspace({
)}
>
{displayed.status.name}
{pendingField === "transition" ? (
{pendingField === 'transition' ? (
<LoaderCircle className="size-3 animate-spin" />
) : null}
</button>
@@ -451,8 +485,9 @@ export default function JiraIssueWorkspace({
disabled={pendingField === 'priority'}
className="rounded-md px-1.5 py-0.5 text-[11px] text-muted-foreground transition hover:bg-muted/40 disabled:opacity-50"
>
{displayed.priority?.name ?? translate("auto.components.JiraIssueWorkspace.51bed73f88", "No priority")}
{pendingField === "priority" ? (
{displayed.priority?.name ??
translate('auto.components.JiraIssueWorkspace.51bed73f88', 'No priority')}
{pendingField === 'priority' ? (
<LoaderCircle className="ml-1 inline size-3 animate-spin" />
) : null}
</button>
@@ -468,7 +503,8 @@ export default function JiraIssueWorkspace({
}
className="flex w-full items-center rounded-sm px-2 py-1.5 text-left text-[12px] hover:bg-accent"
>
{translate("auto.components.JiraIssueWorkspace.51bed73f88", "No priority")}</button>
{translate('auto.components.JiraIssueWorkspace.51bed73f88', 'No priority')}
</button>
{priorities.map((priority) => (
<button
key={priority.id}
@@ -491,8 +527,9 @@ export default function JiraIssueWorkspace({
disabled={pendingField === 'assignee'}
className="flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-muted-foreground transition hover:bg-muted/40 disabled:opacity-50"
>
{displayed.assignee?.displayName ?? translate("auto.components.JiraIssueWorkspace.54649eaeab", "+ Assignee")}
{pendingField === "assignee" ? (
{displayed.assignee?.displayName ??
translate('auto.components.JiraIssueWorkspace.54649eaeab', '+ Assignee')}
{pendingField === 'assignee' ? (
<LoaderCircle className="size-3 animate-spin" />
) : null}
</button>
@@ -512,7 +549,8 @@ export default function JiraIssueWorkspace({
}
className="flex w-full items-center rounded-sm px-2 py-1.5 text-left text-[12px] hover:bg-accent"
>
{translate("auto.components.JiraIssueWorkspace.0b6b5646ed", "Unassigned")}</button>
{translate('auto.components.JiraIssueWorkspace.0b6b5646ed', 'Unassigned')}
</button>
{users.map((user) => (
<button
key={user.accountId}
@@ -540,7 +578,9 @@ export default function JiraIssueWorkspace({
<div className="min-h-0 overflow-y-auto scrollbar-sleek">
<section className="border-b border-border/40 px-4 py-4">
<div className="grid gap-2">
<label className="text-[11px] font-medium text-muted-foreground">{translate("auto.components.JiraIssueWorkspace.444865b4a8", "Title")}</label>
<label className="text-[11px] font-medium text-muted-foreground">
{translate('auto.components.JiraIssueWorkspace.444865b4a8', 'Title')}
</label>
<div className="flex gap-2">
<Input
value={titleDraft}
@@ -559,7 +599,7 @@ export default function JiraIssueWorkspace({
onClick={handleSaveTitle}
disabled={pendingField === 'title'}
>
{pendingField === "title" ? (
{pendingField === 'title' ? (
<LoaderCircle className="size-4 animate-spin" />
) : (
<Save className="size-4" />
@@ -567,12 +607,16 @@ export default function JiraIssueWorkspace({
</Button>
</div>
<label className="mt-2 text-[11px] font-medium text-muted-foreground">
{translate("auto.components.JiraIssueWorkspace.aee97b6913", "Labels")}</label>
{translate('auto.components.JiraIssueWorkspace.aee97b6913', 'Labels')}
</label>
<div className="flex gap-2">
<Input
value={labelsDraft}
onChange={(event) => setLabelsDraft(event.target.value)}
placeholder={translate("auto.components.JiraIssueWorkspace.0f3c07a901", "backend, bug")}
placeholder={translate(
'auto.components.JiraIssueWorkspace.0f3c07a901',
'backend, bug'
)}
className="h-8 text-xs"
/>
<Button
@@ -581,7 +625,7 @@ export default function JiraIssueWorkspace({
onClick={handleSaveLabels}
disabled={pendingField === 'labels'}
>
{pendingField === "labels" ? (
{pendingField === 'labels' ? (
<LoaderCircle className="size-4 animate-spin" />
) : (
<Save className="size-4" />
@@ -598,7 +642,9 @@ export default function JiraIssueWorkspace({
{displayed.issueType.name}
</span>
<span className="text-xs text-muted-foreground">
{displayed.project.key} · {displayed.assignee?.displayName ?? translate("auto.components.JiraIssueWorkspace.0b6b5646ed", "Unassigned")}
{displayed.project.key} ·{' '}
{displayed.assignee?.displayName ??
translate('auto.components.JiraIssueWorkspace.0b6b5646ed', 'Unassigned')}
</span>
</div>
{displayed.description?.trim() ? (
@@ -607,14 +653,21 @@ export default function JiraIssueWorkspace({
className="text-[14px] leading-relaxed"
/>
) : (
<p className="text-sm italic text-muted-foreground">{translate("auto.components.JiraIssueWorkspace.c4889a47e4", "No description provided.")}</p>
<p className="text-sm italic text-muted-foreground">
{translate(
'auto.components.JiraIssueWorkspace.c4889a47e4',
'No description provided.'
)}
</p>
)}
</section>
<section className="px-4 py-4">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<span className="text-[13px] font-medium text-foreground">{translate("auto.components.JiraIssueWorkspace.9a980b06b9", "Comments")}</span>
<span className="text-[13px] font-medium text-foreground">
{translate('auto.components.JiraIssueWorkspace.9a980b06b9', 'Comments')}
</span>
{comments.length > 0 ? (
<span className="text-[12px] text-muted-foreground">{comments.length}</span>
) : null}
@@ -632,7 +685,8 @@ export default function JiraIssueWorkspace({
) : (
<RefreshCw className="size-3" />
)}
{translate("auto.components.JiraIssueWorkspace.5cd09beaf9", "Retry")}</Button>
{translate('auto.components.JiraIssueWorkspace.5cd09beaf9', 'Retry')}
</Button>
) : null}
</div>
{commentsError ? (
@@ -644,7 +698,12 @@ export default function JiraIssueWorkspace({
<LoaderCircle className="size-4 animate-spin text-muted-foreground" />
</div>
) : comments.length === 0 ? (
<p className="text-sm text-muted-foreground">{translate("auto.components.JiraIssueWorkspace.9178090e26", "No comments yet.")}</p>
<p className="text-sm text-muted-foreground">
{translate(
'auto.components.JiraIssueWorkspace.9178090e26',
'No comments yet.'
)}
</p>
) : (
<div className="flex flex-col gap-3">
{comments.map((comment) => (
@@ -661,7 +720,11 @@ export default function JiraIssueWorkspace({
/>
) : null}
<span className="truncate text-[13px] font-semibold text-foreground">
{comment.user?.displayName ?? translate("auto.components.JiraIssueWorkspace.666cfdd835", "Unknown")}
{comment.user?.displayName ??
translate(
'auto.components.JiraIssueWorkspace.666cfdd835',
'Unknown'
)}
</span>
<span className="shrink-0 text-[12px] text-muted-foreground">
{formatRelativeTime(comment.createdAt)}
@@ -685,7 +748,8 @@ export default function JiraIssueWorkspace({
onClick={() => onUse(displayed)}
className="mb-3 w-full justify-center gap-2 sm:hidden"
>
{translate("auto.components.JiraIssueWorkspace.2441be6f9f", "Start workspace")}<ArrowRight className="size-4" />
{translate('auto.components.JiraIssueWorkspace.2441be6f9f', 'Start workspace')}
<ArrowRight className="size-4" />
</Button>
<div className="grid gap-1">
{actionItems.map((item) => {
@@ -717,7 +781,10 @@ export default function JiraIssueWorkspace({
<textarea
value={commentDraft}
onChange={(event) => setCommentDraft(event.target.value)}
placeholder={translate("auto.components.JiraIssueWorkspace.a585fd204e", "Add a Jira comment...")}
placeholder={translate(
'auto.components.JiraIssueWorkspace.a585fd204e',
'Add a Jira comment...'
)}
rows={2}
disabled={commentSubmitting}
className="min-h-10 flex-1 resize-none rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
@@ -732,7 +799,8 @@ export default function JiraIssueWorkspace({
) : (
<Send className="size-4" />
)}
{translate("auto.components.JiraIssueWorkspace.b0b92666c9", "Comment")}</Button>
{translate('auto.components.JiraIssueWorkspace.b0b92666c9', 'Comment')}
</Button>
</div>
</div>
</div>
@@ -45,7 +45,10 @@ type LinearIssueMarkdownToolbarButtonProps = {
const linearIssueMarkdownExtensions = [
...createRichMarkdownExtensions(),
Placeholder.configure({
placeholder: translate("auto.components.LinearIssueMarkdownDescriptionEditor.4f2fddc2b7", "No description provided.")
placeholder: translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.4f2fddc2b7',
'No description provided.'
)
})
]
@@ -91,7 +94,10 @@ function applyLinearIssueLink(editor: Editor | null): void {
}
const previousHref = editor.getAttributes('link').href as string | undefined
const href = window.prompt(translate("auto.components.LinearIssueMarkdownDescriptionEditor.5c16ec8f14", "Link URL"), previousHref ?? '')
const href = window.prompt(
translate('auto.components.LinearIssueMarkdownDescriptionEditor.5c16ec8f14', 'Link URL'),
previousHref ?? ''
)
if (href === null) {
editor.chain().focus().run()
return
@@ -124,16 +130,28 @@ function LinearIssueMarkdownToolbar({
)
return (
<div className="linear-issue-markdown-toolbar" aria-label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.7c52151156", "Issue description formatting")}>
<div
className="linear-issue-markdown-toolbar"
aria-label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.7c52151156',
'Issue description formatting'
)}
>
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.68a41d5665", "Body text")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.68a41d5665',
'Body text'
)}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().setParagraph().run())}
>
<Pilcrow className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.e3f741d258", "Heading 1")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.e3f741d258',
'Heading 1'
)}
active={editor?.isActive('heading', { level: 1 }) ?? false}
disabled={disabled}
onClick={() =>
@@ -143,7 +161,10 @@ function LinearIssueMarkdownToolbar({
<Heading1 className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.dddaa7a0a6", "Heading 2")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.dddaa7a0a6',
'Heading 2'
)}
active={editor?.isActive('heading', { level: 2 }) ?? false}
disabled={disabled}
onClick={() =>
@@ -154,7 +175,7 @@ function LinearIssueMarkdownToolbar({
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarSeparator />
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.caa88f50d0", "Bold")}
label={translate('auto.components.LinearIssueMarkdownDescriptionEditor.caa88f50d0', 'Bold')}
active={editor?.isActive('bold') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleBold().run())}
@@ -162,7 +183,10 @@ function LinearIssueMarkdownToolbar({
<Bold className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.5666b4493d", "Italic")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.5666b4493d',
'Italic'
)}
active={editor?.isActive('italic') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleItalic().run())}
@@ -170,7 +194,10 @@ function LinearIssueMarkdownToolbar({
<Italic className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.28fd951b83", "Strike")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.28fd951b83',
'Strike'
)}
active={editor?.isActive('strike') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleStrike().run())}
@@ -178,7 +205,10 @@ function LinearIssueMarkdownToolbar({
<Strikethrough className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.ad1869bd54", "Inline code")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.ad1869bd54',
'Inline code'
)}
active={editor?.isActive('code') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleCode().run())}
@@ -187,7 +217,10 @@ function LinearIssueMarkdownToolbar({
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarSeparator />
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.c82917e06e", "Bullet list")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.c82917e06e',
'Bullet list'
)}
active={editor?.isActive('bulletList') ?? false}
disabled={disabled}
onClick={() =>
@@ -197,7 +230,10 @@ function LinearIssueMarkdownToolbar({
<List className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.d6b2f3d35b", "Numbered list")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.d6b2f3d35b',
'Numbered list'
)}
active={editor?.isActive('orderedList') ?? false}
disabled={disabled}
onClick={() =>
@@ -207,7 +243,10 @@ function LinearIssueMarkdownToolbar({
<ListOrdered className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.e2a0267c8c", "Checklist")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.e2a0267c8c',
'Checklist'
)}
active={editor?.isActive('taskList') ?? false}
disabled={disabled}
onClick={() =>
@@ -218,7 +257,10 @@ function LinearIssueMarkdownToolbar({
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarSeparator />
<LinearIssueMarkdownToolbarButton
label={translate("auto.components.LinearIssueMarkdownDescriptionEditor.9eaf02ac01", "Quote")}
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.9eaf02ac01',
'Quote'
)}
active={editor?.isActive('blockquote') ?? false}
disabled={disabled}
onClick={() =>
@@ -228,7 +270,14 @@ function LinearIssueMarkdownToolbar({
<Quote className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={editor?.isActive('link') ? translate("auto.components.LinearIssueMarkdownDescriptionEditor.340160f4e8", "Remove link") : translate("auto.components.LinearIssueMarkdownDescriptionEditor.632096eb1c", "Link")}
label={
editor?.isActive('link')
? translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.340160f4e8',
'Remove link'
)
: translate('auto.components.LinearIssueMarkdownDescriptionEditor.632096eb1c', 'Link')
}
active={editor?.isActive('link') ?? false}
disabled={disabled}
onClick={() => runCommand(applyLinearIssueLink)}
@@ -333,10 +382,14 @@ export function LinearIssueMarkdownDescriptionEditor({
<div className="linear-issue-markdown-save-hint pointer-events-none absolute bottom-1.5 right-2 z-10 flex items-center gap-1.5 text-[10px] text-muted-foreground/75">
<span className="flex items-center gap-1">
<span>{submitShortcutLabel}</span>
<span>{translate("auto.components.LinearIssueMarkdownDescriptionEditor.a7301a11f3", "save")}</span>
<span>
{translate('auto.components.LinearIssueMarkdownDescriptionEditor.a7301a11f3', 'save')}
</span>
</span>
<span className="text-muted-foreground/35">·</span>
<span>{translate("auto.components.LinearIssueMarkdownDescriptionEditor.d9c47069ef", "Markdown")}</span>
<span>
{translate('auto.components.LinearIssueMarkdownDescriptionEditor.d9c47069ef', 'Markdown')}
</span>
</div>
{disabled ? (
<LoaderCircle className="absolute right-2 top-2 size-4 animate-spin text-muted-foreground" />
@@ -97,7 +97,9 @@ export function LinearIssueTextEditor({
})
if (savePlan.kind === 'empty-title') {
updateTitleDraft(issue.title)
toast.error(translate("auto.components.LinearIssueTextEditor.1e08a1ec80", "Title is required"))
toast.error(
translate('auto.components.LinearIssueTextEditor.1e08a1ec80', 'Title is required')
)
return
}
if (savePlan.kind === 'unchanged') {
@@ -130,7 +132,15 @@ export function LinearIssueTextEditor({
updateDescriptionDraft(issue.description ?? '')
}
}
toast.error(error instanceof Error ? error.message : translate("auto.components.LinearIssueTextEditor.e8ff595db3", "Failed to update {{value0}}", { value0: field }))
toast.error(
error instanceof Error
? error.message
: translate(
'auto.components.LinearIssueTextEditor.e8ff595db3',
'Failed to update {{value0}}',
{ value0: field }
)
)
} finally {
if (mountedRef.current && lastIssueIdRef.current === issue.id) {
setSavingField(null)
@@ -190,7 +200,7 @@ export function LinearIssueTextEditor({
: 'text-[15px] font-semibold leading-tight'
return (
<div className="min-w-0">
{fields !== "description" ? (
{fields !== 'description' ? (
<div className="relative">
<textarea
ref={titleRef}
@@ -200,7 +210,10 @@ export function LinearIssueTextEditor({
onKeyDown={handleTitleKeyDown}
disabled={savingField === 'title'}
rows={1}
aria-label={translate("auto.components.LinearIssueTextEditor.04d73b72dc", "Issue title")}
aria-label={translate(
'auto.components.LinearIssueTextEditor.04d73b72dc',
'Issue title'
)}
className={cn(
'peer scrollbar-sleek block w-full resize-none overflow-hidden rounded-md border border-transparent bg-transparent px-1 py-0 text-foreground outline-none transition hover:border-border/50 hover:bg-accent/40 focus-visible:border-border focus-visible:bg-background focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-80',
titleClass
@@ -210,15 +223,15 @@ export function LinearIssueTextEditor({
<kbd className="inline-flex h-4 min-w-4 select-none items-center justify-center rounded border border-border bg-muted/70 px-1 font-mono text-[9px] font-medium shadow-xs">
</kbd>
<span>{translate("auto.components.LinearIssueTextEditor.947ba2d6f4", "to save")}</span>
<span>{translate('auto.components.LinearIssueTextEditor.947ba2d6f4', 'to save')}</span>
</div>
{savingField === "title" ? (
{savingField === 'title' ? (
<LoaderCircle className="absolute right-2 top-2 size-4 animate-spin text-muted-foreground" />
) : null}
</div>
) : null}
{fields !== "title" ? (
{fields !== 'title' ? (
<div className="relative">
<LinearIssueMarkdownDescriptionEditor
value={descriptionDraft}
@@ -71,9 +71,17 @@ type LinearIssueWorkspaceProps = {
async function copyTextToClipboard(text: string, label: string): Promise<void> {
try {
await window.api.ui.writeClipboardText(text)
toast.success(translate("auto.components.LinearIssueWorkspace.7835483c43", "{{value0}} copied", { value0: label }))
toast.success(
translate('auto.components.LinearIssueWorkspace.7835483c43', '{{value0}} copied', {
value0: label
})
)
} catch {
toast.error(translate("auto.components.LinearIssueWorkspace.9bcbaa2737", "Failed to copy {{value0}}", { value0: label.toLowerCase() }))
toast.error(
translate('auto.components.LinearIssueWorkspace.9bcbaa2737', 'Failed to copy {{value0}}', {
value0: label.toLowerCase()
})
)
}
}
@@ -144,11 +152,20 @@ function LinearIssueSubIssueButton({
if (fullIssue) {
onOpenIssue(fullIssue)
} else {
toast.error(translate("auto.components.LinearIssueWorkspace.9a1317cdd3", "Failed to load sub-issue"))
toast.error(
translate('auto.components.LinearIssueWorkspace.9a1317cdd3', 'Failed to load sub-issue')
)
}
} catch (error) {
if (mountedRef.current) {
toast.error(error instanceof Error ? error.message : translate("auto.components.LinearIssueWorkspace.9a1317cdd3", "Failed to load sub-issue"))
toast.error(
error instanceof Error
? error.message
: translate(
'auto.components.LinearIssueWorkspace.9a1317cdd3',
'Failed to load sub-issue'
)
)
}
} finally {
if (mountedRef.current) {
@@ -190,14 +207,25 @@ function LinearIssueSubIssueButton({
}
return { issueId: issue.id, subIssues: [...currentSubIssues, child] }
})
toast.success(translate("auto.components.LinearIssueWorkspace.aeed19d003", "Created {{value0}}", { value0: result.identifier }))
toast.success(
translate('auto.components.LinearIssueWorkspace.aeed19d003', 'Created {{value0}}', {
value0: result.identifier
})
)
setTitle('')
setOpen(false)
} else {
toast.error(result.error)
}
} catch (error) {
toast.error(error instanceof Error ? error.message : translate("auto.components.LinearIssueWorkspace.b25e453c9d", "Failed to create sub-issue"))
toast.error(
error instanceof Error
? error.message
: translate(
'auto.components.LinearIssueWorkspace.b25e453c9d',
'Failed to create sub-issue'
)
)
} finally {
setSubmitting(false)
}
@@ -242,7 +270,9 @@ function LinearIssueSubIssueButton({
className="flex h-9 items-center gap-2 rounded-md px-1 text-sm font-medium text-muted-foreground transition hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<Plus className="size-4" />
<span>{translate("auto.components.LinearIssueWorkspace.8c55d6696a", "Add sub-issues")}</span>
<span>
{translate('auto.components.LinearIssueWorkspace.8c55d6696a', 'Add sub-issues')}
</span>
</button>
</PopoverTrigger>
<PopoverContent className="w-80 p-3" align="start">
@@ -256,7 +286,10 @@ function LinearIssueSubIssueButton({
void handleCreate()
}
}}
placeholder={translate("auto.components.LinearIssueWorkspace.c182e02de5", "Sub-issue title")}
placeholder={translate(
'auto.components.LinearIssueWorkspace.c182e02de5',
'Sub-issue title'
)}
className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<div className="flex justify-end">
@@ -266,7 +299,8 @@ function LinearIssueSubIssueButton({
disabled={!title.trim() || submitting}
>
{submitting ? <LoaderCircle className="size-3.5 animate-spin" /> : null}
{translate("auto.components.LinearIssueWorkspace.42589845bc", "Create")}</Button>
{translate('auto.components.LinearIssueWorkspace.42589845bc', 'Create')}
</Button>
</div>
</div>
</PopoverContent>
@@ -305,7 +339,14 @@ function LinearIssueSidebarProjectCard({
})
.catch((error) => {
if (!cancelled) {
toast.error(error instanceof Error ? error.message : translate("auto.components.LinearIssueWorkspace.38b80780c2", "Failed to load projects"))
toast.error(
error instanceof Error
? error.message
: translate(
'auto.components.LinearIssueWorkspace.38b80780c2',
'Failed to load projects'
)
)
}
})
.finally(() => {
@@ -333,13 +374,22 @@ function LinearIssueSidebarProjectCard({
if (result.ok) {
onProjectChanged(project)
patchLinearIssue(issue.id, { project })
toast.success(translate("auto.components.LinearIssueWorkspace.f9d4ef9807", "Project updated"))
toast.success(
translate('auto.components.LinearIssueWorkspace.f9d4ef9807', 'Project updated')
)
setOpen(false)
} else {
toast.error(result.error)
}
} catch (error) {
toast.error(error instanceof Error ? error.message : translate("auto.components.LinearIssueWorkspace.8b5b593053", "Failed to update project"))
toast.error(
error instanceof Error
? error.message
: translate(
'auto.components.LinearIssueWorkspace.8b5b593053',
'Failed to update project'
)
)
} finally {
setSavingProjectId(null)
}
@@ -350,7 +400,7 @@ function LinearIssueSidebarProjectCard({
return (
<section className="rounded-xl border border-border/60 bg-card text-card-foreground shadow-xs">
<div className="flex h-10 items-center gap-1 border-b border-border/50 px-4 text-sm font-medium text-muted-foreground">
<span>{translate("auto.components.LinearIssueWorkspace.b51276c8d6", "Project")}</span>
<span>{translate('auto.components.LinearIssueWorkspace.b51276c8d6', 'Project')}</span>
<ChevronDown className="size-3.5" />
</div>
<Popover open={open} onOpenChange={setOpen}>
@@ -361,7 +411,8 @@ function LinearIssueSidebarProjectCard({
>
<FolderKanban className="size-4 shrink-0" />
<span className="min-w-0 flex-1 truncate">
{issue.project?.name ?? translate("auto.components.LinearIssueWorkspace.519c3587f3", "Add to project")}
{issue.project?.name ??
translate('auto.components.LinearIssueWorkspace.519c3587f3', 'Add to project')}
</span>
<ChevronDown className="size-3.5 shrink-0" />
</button>
@@ -371,14 +422,18 @@ function LinearIssueSidebarProjectCard({
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={translate("auto.components.LinearIssueWorkspace.db3f269d98", "Search projects")}
placeholder={translate(
'auto.components.LinearIssueWorkspace.db3f269d98',
'Search projects'
)}
className="h-8 w-full rounded-md border border-input bg-background px-2 text-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<div className="max-h-64 overflow-y-auto scrollbar-sleek">
{loading ? (
<div className="flex items-center gap-2 px-2 py-3 text-sm text-muted-foreground">
<LoaderCircle className="size-3.5 animate-spin" />
{translate("auto.components.LinearIssueWorkspace.937ba6ad9a", "Loading projects")}</div>
{translate('auto.components.LinearIssueWorkspace.937ba6ad9a', 'Loading projects')}
</div>
) : projects.length > 0 ? (
projects.map((project) => (
<button
@@ -402,7 +457,15 @@ function LinearIssueSidebarProjectCard({
))
) : (
<div className="px-2 py-3 text-sm text-muted-foreground">
{query.trim() ? translate("auto.components.LinearIssueWorkspace.c11b4e3cc2", "No projects found.") : translate("auto.components.LinearIssueWorkspace.76ffd3c937", "Search for a project to add.")}
{query.trim()
? translate(
'auto.components.LinearIssueWorkspace.c11b4e3cc2',
'No projects found.'
)
: translate(
'auto.components.LinearIssueWorkspace.76ffd3c937',
'Search for a project to add.'
)}
</div>
)}
</div>
@@ -585,23 +648,26 @@ export default function LinearIssueWorkspace({
}
return [
{
label: translate("auto.components.LinearIssueWorkspace.9a9a884236", "Copy URL"),
label: translate('auto.components.LinearIssueWorkspace.9a9a884236', 'Copy URL'),
icon: Clipboard,
action: () => void copyTextToClipboard(displayed.url, 'URL')
},
{
label: translate("auto.components.LinearIssueWorkspace.30c1242f3a", "Copy identifier"),
label: translate('auto.components.LinearIssueWorkspace.30c1242f3a', 'Copy identifier'),
icon: Clipboard,
action: () => void copyTextToClipboard(displayed.identifier, 'Identifier')
},
{
label: translate("auto.components.LinearIssueWorkspace.5d670ec8dc", "Copy suggested branch name"),
label: translate(
'auto.components.LinearIssueWorkspace.5d670ec8dc',
'Copy suggested branch name'
),
icon: GitBranch,
action: () =>
void copyTextToClipboard(buildLinearIssueBranchName(displayed), 'Suggested branch name')
},
{
label: translate("auto.components.LinearIssueWorkspace.f6c6381593", "Copy prompt"),
label: translate('auto.components.LinearIssueWorkspace.f6c6381593', 'Copy prompt'),
icon: Clipboard,
action: () => {
const renderedText = buildLinearIssueContextSnapshot(displayed, comments)
@@ -621,7 +687,7 @@ export default function LinearIssueWorkspace({
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-background">
<header className="flex h-[61px] flex-none items-center justify-between gap-4 border-b border-border/60 px-5">
<div className="flex min-w-0 items-center gap-2 text-sm text-muted-foreground">
{variant === "page" ? (
{variant === 'page' ? (
<Button
type="button"
variant="ghost"
@@ -636,10 +702,13 @@ export default function LinearIssueWorkspace({
) : null}
<LinearIcon className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate font-medium text-foreground">
{displayed.workspaceName ?? translate("auto.components.LinearIssueWorkspace.65239a714b", "Linear")}
{displayed.workspaceName ??
translate('auto.components.LinearIssueWorkspace.65239a714b', 'Linear')}
</span>
<ChevronRight className="size-3.5 shrink-0" />
<span className="shrink-0">{translate("auto.components.LinearIssueWorkspace.f63ef94ea8", "Issues")}</span>
<span className="shrink-0">
{translate('auto.components.LinearIssueWorkspace.f63ef94ea8', 'Issues')}
</span>
<ChevronRight className="size-3.5 shrink-0" />
<span className="shrink-0 font-mono">{displayed.identifier}</span>
<span className="min-w-0 truncate font-medium text-foreground">{displayed.title}</span>
@@ -653,13 +722,17 @@ export default function LinearIssueWorkspace({
variant="ghost"
size="icon-sm"
onClick={() => void copyTextToClipboard(displayed.url, 'URL')}
aria-label={translate("auto.components.LinearIssueWorkspace.97c19a84f1", "Copy Linear URL")}
aria-label={translate(
'auto.components.LinearIssueWorkspace.97c19a84f1',
'Copy Linear URL'
)}
>
<Link className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.LinearIssueWorkspace.9a9a884236", "Copy URL")}</TooltipContent>
{translate('auto.components.LinearIssueWorkspace.9a9a884236', 'Copy URL')}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
@@ -667,13 +740,17 @@ export default function LinearIssueWorkspace({
variant="ghost"
size="icon-sm"
onClick={() => void copyTextToClipboard(displayed.identifier, 'Identifier')}
aria-label={translate("auto.components.LinearIssueWorkspace.9e3c49beb8", "Copy issue identifier")}
aria-label={translate(
'auto.components.LinearIssueWorkspace.9e3c49beb8',
'Copy issue identifier'
)}
>
<Clipboard className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.LinearIssueWorkspace.30c1242f3a", "Copy identifier")}</TooltipContent>
{translate('auto.components.LinearIssueWorkspace.30c1242f3a', 'Copy identifier')}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
@@ -681,28 +758,36 @@ export default function LinearIssueWorkspace({
variant="ghost"
size="icon-sm"
onClick={handleUseIssue}
aria-label={translate("auto.components.LinearIssueWorkspace.30a7f56c0a", "Start workspace from issue")}
aria-label={translate(
'auto.components.LinearIssueWorkspace.30a7f56c0a',
'Start workspace from issue'
)}
>
<ArrowRight className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.LinearIssueWorkspace.e1e0a9bca9", "Start workspace")}</TooltipContent>
{translate('auto.components.LinearIssueWorkspace.e1e0a9bca9', 'Start workspace')}
</TooltipContent>
</Tooltip>
{variant === "sheet" ? (
{variant === 'sheet' ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
onClick={onClose}
aria-label={translate("auto.components.LinearIssueWorkspace.7a4997d8bb", "Close Linear issue preview")}
aria-label={translate(
'auto.components.LinearIssueWorkspace.7a4997d8bb',
'Close Linear issue preview'
)}
>
<X className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.LinearIssueWorkspace.df4c86ed12", "Close")}</TooltipContent>
{translate('auto.components.LinearIssueWorkspace.df4c86ed12', 'Close')}
</TooltipContent>
</Tooltip>
) : null}
</div>
@@ -717,7 +802,9 @@ export default function LinearIssueWorkspace({
<section className="mt-12 border-t border-border/60 pt-9">
<div className="mb-8 flex items-center justify-between gap-3">
<h2 className="text-xl font-semibold text-foreground">{translate("auto.components.LinearIssueWorkspace.543970c87a", "Activity")}</h2>
<h2 className="text-xl font-semibold text-foreground">
{translate('auto.components.LinearIssueWorkspace.543970c87a', 'Activity')}
</h2>
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<LinearIssueAvatar
avatarUrl={displayed.assignee?.avatarUrl}
@@ -734,7 +821,12 @@ export default function LinearIssueWorkspace({
className="size-5"
/>
<span>
{displayed.assignee?.displayName ?? translate("auto.components.LinearIssueWorkspace.8a33c85e9c", "Someone")} {translate("auto.components.LinearIssueWorkspace.fabbd3f974", "updated the issue ·")}{' '}
{displayed.assignee?.displayName ??
translate('auto.components.LinearIssueWorkspace.8a33c85e9c', 'Someone')}{' '}
{translate(
'auto.components.LinearIssueWorkspace.fabbd3f974',
'updated the issue ·'
)}{' '}
{formatLinearIssueRelativeTime(displayed.updatedAt)}
</span>
</div>
@@ -754,7 +846,8 @@ export default function LinearIssueWorkspace({
) : (
<RefreshCw className="size-3" />
)}
{translate("auto.components.LinearIssueWorkspace.b0eac92d85", "Retry")}</Button>
{translate('auto.components.LinearIssueWorkspace.b0eac92d85', 'Retry')}
</Button>
</div>
) : null}
@@ -774,7 +867,11 @@ export default function LinearIssueWorkspace({
<div className="min-w-0 flex-1">
<div className="mb-1 flex min-w-0 items-center gap-2 text-sm">
<span className="truncate font-semibold text-foreground">
{comment.user?.displayName ?? translate("auto.components.LinearIssueWorkspace.ca8778c124", "Unknown")}
{comment.user?.displayName ??
translate(
'auto.components.LinearIssueWorkspace.ca8778c124',
'Unknown'
)}
</span>
<span className="shrink-0 text-muted-foreground">
{formatLinearIssueRelativeTime(comment.createdAt)}
@@ -816,7 +913,9 @@ export default function LinearIssueWorkspace({
/>
<section className="rounded-xl border border-border/60 bg-card text-card-foreground shadow-xs">
<div className="flex h-10 items-center gap-1 border-b border-border/50 px-4 text-sm font-medium text-muted-foreground">
<span>{translate("auto.components.LinearIssueWorkspace.c23e79e5c0", "Actions")}</span>
<span>
{translate('auto.components.LinearIssueWorkspace.c23e79e5c0', 'Actions')}
</span>
<ChevronDown className="size-3.5" />
</div>
<div className="space-y-1 p-3">
@@ -867,11 +966,18 @@ export default function LinearIssueWorkspace({
}}
>
<VisuallyHidden.Root asChild>
<SheetTitle>{displayed?.title ?? translate("auto.components.LinearIssueWorkspace.61f424f8ca", "Linear issue")}</SheetTitle>
<SheetTitle>
{displayed?.title ??
translate('auto.components.LinearIssueWorkspace.61f424f8ca', 'Linear issue')}
</SheetTitle>
</VisuallyHidden.Root>
<VisuallyHidden.Root asChild>
<SheetDescription>
{translate("auto.components.LinearIssueWorkspace.ad5dec37b7", "Preview, edit, and start work from the selected issue.")}</SheetDescription>
{translate(
'auto.components.LinearIssueWorkspace.ad5dec37b7',
'Preview, edit, and start work from the selected issue.'
)}
</SheetDescription>
</VisuallyHidden.Root>
{content}
+147 -44
View File
@@ -193,6 +193,9 @@ export function LinearIssueEditSection({
onEditStateChange({ state: prevState })
patchLinearIssue(issue.id, { state: prevState })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('linear-tasks')
},
onError: (err) => toast.error(err)
})
},
@@ -222,6 +225,9 @@ export function LinearIssueEditSection({
onEditStateChange({ priority: prevPriority })
patchLinearIssue(issue.id, { priority: prevPriority })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('linear-tasks')
},
onError: (err) => toast.error(err)
})
},
@@ -242,6 +248,9 @@ export function LinearIssueEditSection({
onEditStateChange({ estimate: prevEstimate })
patchLinearIssue(issue.id, { estimate: prevEstimate })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('linear-tasks')
},
onError: (err) => toast.error(err)
})
},
@@ -257,7 +266,12 @@ export function LinearIssueEditSection({
const estimate = Number(trimmed)
if (!Number.isInteger(estimate) || estimate < 0) {
toast.error(translate("auto.components.LinearItemDrawer.0be31fef8e", "Estimate must be a non-negative integer"))
toast.error(
translate(
'auto.components.LinearItemDrawer.0be31fef8e',
'Estimate must be a non-negative integer'
)
)
return
}
@@ -282,6 +296,9 @@ export function LinearIssueEditSection({
onEditStateChange({ assignee: prevAssignee })
patchLinearIssue(issue.id, { assignee: prevAssignee })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('linear-tasks')
},
onError: (err) => toast.error(err)
})
},
@@ -320,6 +337,9 @@ export function LinearIssueEditSection({
onEditStateChange({ labelIds: prevLabelIds, labels: prevLabels })
patchLinearIssue(issue.id, { labelIds: prevLabelIds, labels: prevLabels })
},
onSuccess: () => {
useAppStore.getState().recordFeatureInteraction('linear-tasks')
},
onError: (err) => toast.error(err)
})
},
@@ -372,7 +392,7 @@ export function LinearIssueEditSection({
<div className="space-y-3">
<section className="rounded-xl border border-border/60 bg-card text-card-foreground shadow-xs">
<div className="flex h-10 items-center gap-1 border-b border-border/50 px-4 text-sm font-medium text-muted-foreground">
<span>{translate("auto.components.LinearItemDrawer.dd304de85a", "Properties")}</span>
<span>{translate('auto.components.LinearItemDrawer.dd304de85a', 'Properties')}</span>
<ChevronDown className="size-3.5" />
</div>
<div className="space-y-1 p-3">
@@ -403,7 +423,8 @@ export function LinearIssueEditSection({
) : states.loading ? (
<div className="flex items-center gap-2 px-2 py-3 text-[12px] text-muted-foreground">
<LoaderCircle className="size-3 animate-spin" />
{translate("auto.components.LinearItemDrawer.59b6cd3706", "Loading states")}</div>
{translate('auto.components.LinearItemDrawer.59b6cd3706', 'Loading states')}
</div>
) : states.data.length > 0 ? (
<div>
{states.data.map((s) => (
@@ -426,7 +447,8 @@ export function LinearIssueEditSection({
</div>
) : (
<div className="px-2 py-3 text-center text-[12px] text-muted-foreground">
{translate("auto.components.LinearItemDrawer.780ea6ed89", "No states found")}</div>
{translate('auto.components.LinearItemDrawer.780ea6ed89', 'No states found')}
</div>
)}
</PopoverContent>
</Popover>
@@ -482,7 +504,9 @@ export function LinearIssueEditSection({
<UserRound className={propertyIconClass} />
)}
<span className="min-w-0 flex-1 truncate">
{localAssignee ? localAssignee.displayName : translate("auto.components.LinearItemDrawer.866316f22c", "Unassigned")}
{localAssignee
? localAssignee.displayName
: translate('auto.components.LinearItemDrawer.866316f22c', 'Unassigned')}
</span>
<LinearEditChipAdornment loading={members.loading} pending={assigneePending} />
</button>
@@ -497,7 +521,8 @@ export function LinearIssueEditSection({
onClick={() => handleAssigneeChange('__unassign__')}
className={cn(LINEAR_EDIT_MENU_ITEM_CLASS, !localAssignee && 'bg-accent/50')}
>
{translate("auto.components.LinearItemDrawer.866316f22c", "Unassigned")}</button>
{translate('auto.components.LinearItemDrawer.866316f22c', 'Unassigned')}
</button>
{members.error ? (
<div className="px-2 py-3 text-center text-[12px] text-destructive">
{members.error}
@@ -505,7 +530,8 @@ export function LinearIssueEditSection({
) : members.loading ? (
<div className="flex items-center gap-2 px-2 py-3 text-[12px] text-muted-foreground">
<LoaderCircle className="size-3 animate-spin" />
{translate("auto.components.LinearItemDrawer.b2376d0179", "Loading members")}</div>
{translate('auto.components.LinearItemDrawer.b2376d0179', 'Loading members')}
</div>
) : (
members.data.map((m) => (
<button
@@ -567,7 +593,10 @@ export function LinearIssueEditSection({
}
}}
inputMode="numeric"
placeholder={translate("auto.components.LinearItemDrawer.fbb90300e2", "Custom estimate")}
placeholder={translate(
'auto.components.LinearItemDrawer.fbb90300e2',
'Custom estimate'
)}
className="h-8 text-sm"
/>
<div className="flex items-center justify-between gap-2">
@@ -577,7 +606,8 @@ export function LinearIssueEditSection({
size="sm"
onClick={() => handleEstimateChange(null)}
>
{translate("auto.components.LinearItemDrawer.ceeb8c6153", "Clear")}</Button>
{translate('auto.components.LinearItemDrawer.ceeb8c6153', 'Clear')}
</Button>
<Button
type="button"
size="sm"
@@ -585,7 +615,8 @@ export function LinearIssueEditSection({
disabled={estimatePending}
>
{estimatePending ? <LoaderCircle className="size-3.5 animate-spin" /> : null}
{translate("auto.components.LinearItemDrawer.b5675b0694", "Save")}</Button>
{translate('auto.components.LinearItemDrawer.b5675b0694', 'Save')}
</Button>
</div>
</div>
</PopoverContent>
@@ -595,7 +626,7 @@ export function LinearIssueEditSection({
<section className="rounded-xl border border-border/60 bg-card text-card-foreground shadow-xs">
<div className="flex h-10 items-center gap-1 border-b border-border/50 px-4 text-sm font-medium text-muted-foreground">
<span>{translate("auto.components.LinearItemDrawer.64bfffc4dd", "Labels")}</span>
<span>{translate('auto.components.LinearItemDrawer.64bfffc4dd', 'Labels')}</span>
<ChevronDown className="size-3.5" />
</div>
<div className="p-3">
@@ -606,13 +637,21 @@ export function LinearIssueEditSection({
disabled={labelsPending}
className={propertyRowClass}
aria-label={
localLabels.length ? translate("auto.components.LinearItemDrawer.7f7b89b631", "Labels: {{value0}}", { value0: localLabels.join(', ') }) : translate("auto.components.LinearItemDrawer.23886c7eec", "Add label")
localLabels.length
? translate(
'auto.components.LinearItemDrawer.7f7b89b631',
'Labels: {{value0}}',
{ value0: localLabels.join(', ') }
)
: translate('auto.components.LinearItemDrawer.23886c7eec', 'Add label')
}
aria-busy={labelsPending || labels.loading}
>
<Tag className={propertyIconClass} />
<span className="min-w-0 flex-1 truncate">
{localLabels.length ? labelSummary : translate("auto.components.LinearItemDrawer.23886c7eec", "Add label")}
{localLabels.length
? labelSummary
: translate('auto.components.LinearItemDrawer.23886c7eec', 'Add label')}
</span>
<LinearEditChipAdornment loading={labels.loading} pending={labelsPending} />
</button>
@@ -628,7 +667,8 @@ export function LinearIssueEditSection({
) : labels.loading ? (
<div className="flex items-center gap-2 px-2 py-3 text-[12px] text-muted-foreground">
<LoaderCircle className="size-3 animate-spin" />
{translate("auto.components.LinearItemDrawer.cddd9b04a7", "Loading labels")}</div>
{translate('auto.components.LinearItemDrawer.cddd9b04a7', 'Loading labels')}
</div>
) : labels.data.length > 0 ? (
<div>
{labels.data.map((label) => (
@@ -658,7 +698,8 @@ export function LinearIssueEditSection({
</div>
) : (
<div className="px-2 py-3 text-center text-[12px] text-muted-foreground">
{translate("auto.components.LinearItemDrawer.367f828482", "No labels found")}</div>
{translate('auto.components.LinearItemDrawer.367f828482', 'No labels found')}
</div>
)}
</PopoverContent>
</Popover>
@@ -694,7 +735,8 @@ export function LinearIssueEditSection({
) : states.loading ? (
<div className="flex items-center gap-2 px-2 py-3 text-[12px] text-muted-foreground">
<LoaderCircle className="size-3 animate-spin" />
{translate("auto.components.LinearItemDrawer.59b6cd3706", "Loading states")}</div>
{translate('auto.components.LinearItemDrawer.59b6cd3706', 'Loading states')}
</div>
) : states.data.length > 0 ? (
<div>
{states.data.map((s) => (
@@ -717,7 +759,8 @@ export function LinearIssueEditSection({
</div>
) : (
<div className="px-2 py-3 text-center text-[12px] text-muted-foreground">
{translate("auto.components.LinearItemDrawer.780ea6ed89", "No states found")}</div>
{translate('auto.components.LinearItemDrawer.780ea6ed89', 'No states found')}
</div>
)}
</PopoverContent>
</Popover>
@@ -796,7 +839,10 @@ export function LinearIssueEditSection({
}
}}
inputMode="numeric"
placeholder={translate("auto.components.LinearItemDrawer.fbb90300e2", "Custom estimate")}
placeholder={translate(
'auto.components.LinearItemDrawer.fbb90300e2',
'Custom estimate'
)}
className="h-8 text-sm"
/>
<div className="flex items-center justify-between gap-2">
@@ -806,7 +852,8 @@ export function LinearIssueEditSection({
size="sm"
onClick={() => handleEstimateChange(null)}
>
{translate("auto.components.LinearItemDrawer.ceeb8c6153", "Clear")}</Button>
{translate('auto.components.LinearItemDrawer.ceeb8c6153', 'Clear')}
</Button>
<Button
type="button"
size="sm"
@@ -814,7 +861,8 @@ export function LinearIssueEditSection({
disabled={estimatePending}
>
{estimatePending ? <LoaderCircle className="size-3.5 animate-spin" /> : null}
{translate("auto.components.LinearItemDrawer.b5675b0694", "Save")}</Button>
{translate('auto.components.LinearItemDrawer.b5675b0694', 'Save')}
</Button>
</div>
</div>
</PopoverContent>
@@ -830,7 +878,9 @@ export function LinearIssueEditSection({
aria-busy={assigneePending || members.loading}
>
<span className="truncate">
{localAssignee ? localAssignee.displayName : translate("auto.components.LinearItemDrawer.d71cd3003e", "+ Assignee")}
{localAssignee
? localAssignee.displayName
: translate('auto.components.LinearItemDrawer.d71cd3003e', '+ Assignee')}
</span>
<LinearEditChipAdornment loading={members.loading} pending={assigneePending} />
</button>
@@ -842,7 +892,8 @@ export function LinearIssueEditSection({
onClick={() => handleAssigneeChange('__unassign__')}
className={cn(LINEAR_EDIT_MENU_ITEM_CLASS, !localAssignee && 'bg-accent/50')}
>
{translate("auto.components.LinearItemDrawer.866316f22c", "Unassigned")}</button>
{translate('auto.components.LinearItemDrawer.866316f22c', 'Unassigned')}
</button>
{members.error ? (
<div className="px-2 py-3 text-center text-[12px] text-destructive">
{members.error}
@@ -850,7 +901,8 @@ export function LinearIssueEditSection({
) : members.loading ? (
<div className="flex items-center gap-2 px-2 py-3 text-[12px] text-muted-foreground">
<LoaderCircle className="size-3 animate-spin" />
{translate("auto.components.LinearItemDrawer.b2376d0179", "Loading members")}</div>
{translate('auto.components.LinearItemDrawer.b2376d0179', 'Loading members')}
</div>
) : (
members.data.map((m) => (
<button
@@ -877,7 +929,13 @@ export function LinearIssueEditSection({
type="button"
disabled={labelsPending}
className={LINEAR_EDIT_CHIP_CLASS}
aria-label={localLabels.length ? translate("auto.components.LinearItemDrawer.7f7b89b631", "Labels: {{value0}}", { value0: localLabels.join(', ') }) : translate("auto.components.LinearItemDrawer.23886c7eec", "Add label")}
aria-label={
localLabels.length
? translate('auto.components.LinearItemDrawer.7f7b89b631', 'Labels: {{value0}}', {
value0: localLabels.join(', ')
})
: translate('auto.components.LinearItemDrawer.23886c7eec', 'Add label')
}
aria-busy={labelsPending || labels.loading}
>
<span className="truncate">{labelSummary}</span>
@@ -890,7 +948,8 @@ export function LinearIssueEditSection({
) : labels.loading ? (
<div className="flex items-center gap-2 px-2 py-3 text-[12px] text-muted-foreground">
<LoaderCircle className="size-3 animate-spin" />
{translate("auto.components.LinearItemDrawer.cddd9b04a7", "Loading labels")}</div>
{translate('auto.components.LinearItemDrawer.cddd9b04a7', 'Loading labels')}
</div>
) : labels.data.length > 0 ? (
<div>
{labels.data.map((label) => (
@@ -920,7 +979,8 @@ export function LinearIssueEditSection({
</div>
) : (
<div className="px-2 py-3 text-center text-[12px] text-muted-foreground">
{translate("auto.components.LinearItemDrawer.367f828482", "No labels found")}</div>
{translate('auto.components.LinearItemDrawer.367f828482', 'No labels found')}
</div>
)}
</PopoverContent>
</Popover>
@@ -977,17 +1037,25 @@ export function LinearIssueCommentFooter({
}
if (typed.ok) {
setBody('')
useAppStore.getState().recordFeatureInteraction('linear-tasks')
onCommentAdded({
id: typed.id ?? createBrowserUuid(),
body: trimmed,
createdAt: new Date().toISOString()
})
} else {
toast.error(typed.error ?? translate("auto.components.LinearItemDrawer.6ab35eafd5", "Failed to add comment"))
toast.error(
typed.error ??
translate('auto.components.LinearItemDrawer.6ab35eafd5', 'Failed to add comment')
)
}
} catch (err) {
if (mountedRef.current) {
toast.error(err instanceof Error ? err.message : translate("auto.components.LinearItemDrawer.6ab35eafd5", "Failed to add comment"))
toast.error(
err instanceof Error
? err.message
: translate('auto.components.LinearItemDrawer.6ab35eafd5', 'Failed to add comment')
)
}
} finally {
if (mountedRef.current) {
@@ -1020,19 +1088,26 @@ export function LinearIssueCommentFooter({
autoGrow()
}}
onKeyDown={handleKeyDown}
placeholder={translate("auto.components.LinearItemDrawer.2820f0f0f0", "Leave a comment...")}
placeholder={translate(
'auto.components.LinearItemDrawer.2820f0f0f0',
'Leave a comment...'
)}
rows={3}
className="scrollbar-sleek min-h-24 max-h-40 w-full resize-none overflow-y-auto rounded-t-xl bg-transparent px-5 py-4 text-sm placeholder:text-muted-foreground focus-visible:outline-none"
/>
<div className="flex items-center justify-between px-4 pb-3">
<span className="text-[11px] text-muted-foreground">
{submitShortcutLabel !== "Unassigned" ? translate("auto.components.LinearItemDrawer.fda549766e", "{{value0}} to comment", { value0: submitShortcutLabel }) : ''}
{submitShortcutLabel !== 'Unassigned'
? translate('auto.components.LinearItemDrawer.fda549766e', '{{value0}} to comment', {
value0: submitShortcutLabel
})
: ''}
</span>
<Button
size="icon-sm"
onClick={handleSubmit}
disabled={!body.trim() || submitting}
aria-label={translate("auto.components.LinearItemDrawer.d369841269", "Send comment")}
aria-label={translate('auto.components.LinearItemDrawer.d369841269', 'Send comment')}
>
{submitting ? (
<LoaderCircle className="size-3.5 animate-spin" />
@@ -1058,7 +1133,7 @@ export function LinearIssueCommentFooter({
autoGrow()
}}
onKeyDown={handleKeyDown}
placeholder={translate("auto.components.LinearItemDrawer.2fcff829a8", "Add a comment…")}
placeholder={translate('auto.components.LinearItemDrawer.2fcff829a8', 'Add a comment…')}
rows={1}
className="scrollbar-sleek min-h-[32px] max-h-[96px] flex-1 resize-none overflow-y-auto rounded-md border border-input bg-transparent px-3 py-2 text-[13px] placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
@@ -1067,7 +1142,7 @@ export function LinearIssueCommentFooter({
onClick={handleSubmit}
disabled={!body.trim() || submitting}
className="size-8 shrink-0"
aria-label={translate("auto.components.LinearItemDrawer.d369841269", "Send comment")}
aria-label={translate('auto.components.LinearItemDrawer.d369841269', 'Send comment')}
>
{submitting ? (
<LoaderCircle className="size-3.5 animate-spin" />
@@ -1238,10 +1313,18 @@ export default function LinearItemDrawer({
}}
>
<VisuallyHidden.Root asChild>
<SheetTitle>{displayed?.title ?? translate("auto.components.LinearItemDrawer.39883467f4", "Linear issue")}</SheetTitle>
<SheetTitle>
{displayed?.title ??
translate('auto.components.LinearItemDrawer.39883467f4', 'Linear issue')}
</SheetTitle>
</VisuallyHidden.Root>
<VisuallyHidden.Root asChild>
<SheetDescription>{translate("auto.components.LinearItemDrawer.04a442f796", "Preview and edit the selected Linear issue.")}</SheetDescription>
<SheetDescription>
{translate(
'auto.components.LinearItemDrawer.04a442f796',
'Preview and edit the selected Linear issue.'
)}
</SheetDescription>
</VisuallyHidden.Root>
{displayed && (
@@ -1276,13 +1359,17 @@ export default function LinearItemDrawer({
size="icon"
className="size-7"
onClick={() => window.api.shell.openUrl(displayed.url)}
aria-label={translate("auto.components.LinearItemDrawer.0190b760c1", "Open on Linear")}
aria-label={translate(
'auto.components.LinearItemDrawer.0190b760c1',
'Open on Linear'
)}
>
<ExternalLink className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.LinearItemDrawer.0190b760c1", "Open on Linear")}</TooltipContent>
{translate('auto.components.LinearItemDrawer.0190b760c1', 'Open on Linear')}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
@@ -1291,13 +1378,17 @@ export default function LinearItemDrawer({
size="icon"
className="size-7"
onClick={onClose}
aria-label={translate("auto.components.LinearItemDrawer.858d0630da", "Close preview")}
aria-label={translate(
'auto.components.LinearItemDrawer.858d0630da',
'Close preview'
)}
>
<X className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.LinearItemDrawer.9dc54172db", "Close · Esc")}</TooltipContent>
{translate('auto.components.LinearItemDrawer.9dc54172db', 'Close · Esc')}
</TooltipContent>
</Tooltip>
</div>
</div>
@@ -1325,7 +1416,9 @@ export default function LinearItemDrawer({
<div className="border-t border-border/40 px-4 py-4">
<div className="flex items-center gap-2 pb-3">
<span className="text-[13px] font-medium text-foreground">{translate("auto.components.LinearItemDrawer.fde849b2b6", "Comments")}</span>
<span className="text-[13px] font-medium text-foreground">
{translate('auto.components.LinearItemDrawer.fde849b2b6', 'Comments')}
</span>
{comments.length > 0 && (
<span className="text-[12px] text-muted-foreground">{comments.length}</span>
)}
@@ -1335,7 +1428,9 @@ export default function LinearItemDrawer({
<LoaderCircle className="size-4 animate-spin text-muted-foreground" />
</div>
) : comments.length === 0 ? (
<p className="text-[13px] text-muted-foreground">{translate("auto.components.LinearItemDrawer.a4fcc57522", "No comments yet.")}</p>
<p className="text-[13px] text-muted-foreground">
{translate('auto.components.LinearItemDrawer.a4fcc57522', 'No comments yet.')}
</p>
) : (
<div className="flex flex-col gap-3">
{comments.map((comment) => (
@@ -1352,7 +1447,8 @@ export default function LinearItemDrawer({
/>
)}
<span className="text-[13px] font-semibold text-foreground">
{comment.user?.displayName ?? translate("auto.components.LinearItemDrawer.48e17e8cbd", "Unknown")}
{comment.user?.displayName ??
translate('auto.components.LinearItemDrawer.48e17e8cbd', 'Unknown')}
</span>
<span className="text-[12px] text-muted-foreground">
· {formatRelativeTime(comment.createdAt)}
@@ -1381,9 +1477,16 @@ export default function LinearItemDrawer({
<Button
onClick={() => onUse(displayed)}
className="w-full justify-center gap-2"
aria-label={translate("auto.components.LinearItemDrawer.04008e6c46", "Start workspace from issue")}
aria-label={translate(
'auto.components.LinearItemDrawer.04008e6c46',
'Start workspace from issue'
)}
>
{translate("auto.components.LinearItemDrawer.04008e6c46", "Start workspace from issue")}<ArrowRight className="size-4" />
{translate(
'auto.components.LinearItemDrawer.04008e6c46',
'Start workspace from issue'
)}
<ArrowRight className="size-4" />
</Button>
</div>
</div>
@@ -229,7 +229,11 @@ function QuickTabBody({
<DialogHeader className="gap-1">
<DialogTitle className="text-base font-semibold">{primaryActionLabel}</DialogTitle>
<DialogDescription className="sr-only">
{translate("auto.components.NewWorkspaceComposerModal.fa90f739a5", "Choose the project, workspace name, and agent before creating the workspace.")}</DialogDescription>
{translate(
'auto.components.NewWorkspaceComposerModal.fa90f739a5',
'Choose the project, workspace name, and agent before creating the workspace.'
)}
</DialogDescription>
</DialogHeader>
<NewWorkspaceComposerCard
contextualTourSource={modalData.contextualTourSource}
+40 -16
View File
@@ -122,11 +122,21 @@ function InstallRgGuidance({
className="flex items-start gap-2.5 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2.5 text-amber-700 dark:text-amber-300"
>
<AlertTriangle size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
<p className="text-[13px] leading-5">{translate("auto.components.QuickOpen.4725b0e931", "Quick Open scan too large (")}{reason}).</p>
<p className="text-[13px] leading-5">
{translate('auto.components.QuickOpen.4725b0e931', 'Quick Open scan too large (')}
{reason}).
</p>
</div>
<p>
{translate("auto.components.QuickOpen.2ca749c15d", "Install")}{' '}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-foreground">{translate("auto.components.QuickOpen.5d80dc39bb", "ripgrep")}</code> {translate("auto.components.QuickOpen.1cf8561ab4", "on the remote to enable fast, gitignore-aware listing:")}</p>
{translate('auto.components.QuickOpen.2ca749c15d', 'Install')}{' '}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-foreground">
{translate('auto.components.QuickOpen.5d80dc39bb', 'ripgrep')}
</code>{' '}
{translate(
'auto.components.QuickOpen.1cf8561ab4',
'on the remote to enable fast, gitignore-aware listing:'
)}
</p>
{command ? (
<div className="flex items-center gap-2 rounded border border-border bg-muted/50 px-3 py-2 font-mono text-xs text-foreground">
<span className="flex-1 truncate">{command}</span>
@@ -135,10 +145,12 @@ function InstallRgGuidance({
type="button"
onClick={handleCopy}
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
aria-label={translate("auto.components.QuickOpen.73b44e7bde", "Copy install command")}
aria-label={translate('auto.components.QuickOpen.73b44e7bde', 'Copy install command')}
>
{copied ? <Check size={12} /> : <Copy size={12} />}
{copied ? translate("auto.components.QuickOpen.cf144856dc", "Copied") : translate("auto.components.QuickOpen.995be8ea22", "Copy")}
{copied
? translate('auto.components.QuickOpen.cf144856dc', 'Copied')
: translate('auto.components.QuickOpen.995be8ea22', 'Copy')}
</button>
</div>
) : guidance ? (
@@ -218,13 +230,19 @@ export default function QuickOpen(): React.JSX.Element | null {
onOpenChange={handleOpenChange}
shouldFilter={false}
onCloseAutoFocus={handleCloseAutoFocus}
title={translate("auto.components.QuickOpen.ec31e058f7", "Go to file")}
description={translate("auto.components.QuickOpen.9e97f08d0f", "Search for a file to open")}
title={translate('auto.components.QuickOpen.ec31e058f7', 'Go to file')}
description={translate('auto.components.QuickOpen.9e97f08d0f', 'Search for a file to open')}
>
<CommandInput placeholder={translate("auto.components.QuickOpen.1cb6ef47b7", "Go to file...")} value={query} onValueChange={setQuery} />
<CommandInput
placeholder={translate('auto.components.QuickOpen.1cb6ef47b7', 'Go to file...')}
value={query}
onValueChange={setQuery}
/>
<CommandList className="p-2">
{loading ? (
<div className="py-6 text-center text-sm text-muted-foreground">{translate("auto.components.QuickOpen.722a21e1a8", "Loading files...")}</div>
<div className="py-6 text-center text-sm text-muted-foreground">
{translate('auto.components.QuickOpen.722a21e1a8', 'Loading files...')}
</div>
) : loadError ? (
(() => {
const guidance = parseInstallRgGuidance(loadError)
@@ -241,7 +259,9 @@ export default function QuickOpen(): React.JSX.Element | null {
)
})()
) : filtered.length === 0 ? (
<CommandEmpty>{translate("auto.components.QuickOpen.74e2e1b3e4", "No matching files.")}</CommandEmpty>
<CommandEmpty>
{translate('auto.components.QuickOpen.74e2e1b3e4', 'No matching files.')}
</CommandEmpty>
) : (
filtered.map((item) => {
const lastSlash = item.path.lastIndexOf('/')
@@ -266,17 +286,21 @@ export default function QuickOpen(): React.JSX.Element | null {
</CommandList>
<div className="flex items-center justify-end border-t border-border/60 px-3.5 py-2.5 text-[11px] text-muted-foreground/82">
<div className="flex items-center gap-2">
<FooterKey>{translate("auto.components.QuickOpen.250e5b2dfb", "Enter")}</FooterKey>
<span>{translate("auto.components.QuickOpen.61b1c871a6", "Open")}</span>
<FooterKey>{translate("auto.components.QuickOpen.95fccbae88", "Esc")}</FooterKey>
<span>{translate("auto.components.QuickOpen.73b2c581f1", "Close")}</span>
<FooterKey>{translate('auto.components.QuickOpen.250e5b2dfb', 'Enter')}</FooterKey>
<span>{translate('auto.components.QuickOpen.61b1c871a6', 'Open')}</span>
<FooterKey>{translate('auto.components.QuickOpen.95fccbae88', 'Esc')}</FooterKey>
<span>{translate('auto.components.QuickOpen.73b2c581f1', 'Close')}</span>
<FooterKey></FooterKey>
<span>{translate("auto.components.QuickOpen.1dbd3f59ff", "Move")}</span>
<span>{translate('auto.components.QuickOpen.1dbd3f59ff', 'Move')}</span>
</div>
</div>
{/* Accessibility: announce result count changes */}
<div aria-live="polite" className="sr-only">
{deferredQuery.trim() ? translate("auto.components.QuickOpen.b227d88520", "{{value0}} files found", { value0: filtered.length }) : ''}
{deferredQuery.trim()
? translate('auto.components.QuickOpen.b227d88520', '{{value0}} files found', {
value0: filtered.length
})
: ''}
</div>
</CommandDialog>
)
@@ -115,7 +115,8 @@ export function SelectedTextCopyMenu({
onClick={handleCopy}
>
<Copy className="size-3.5 text-muted-foreground" />
{translate("auto.components.SelectedTextCopyMenu.9b40d7b018", "Copy")}</button>
{translate('auto.components.SelectedTextCopyMenu.9b40d7b018', 'Copy')}
</button>
</div>,
document.body
)}
+21 -5
View File
@@ -101,25 +101,39 @@ export function StarNagCard(): React.JSX.Element | null {
<div className="flex items-center gap-2">
<Star className="size-4 fill-amber-400/60 text-amber-400/80" />
<h3 id="star-nag-heading" className="text-sm font-semibold">
{translate("auto.components.StarNagCard.5f6df21046", "Enjoying Orca?")}</h3>
{translate('auto.components.StarNagCard.5f6df21046', 'Enjoying Orca?')}
</h3>
</div>
<Button
variant="ghost"
size="icon"
className="size-7 shrink-0"
onClick={handleClose}
aria-label={translate("auto.components.StarNagCard.b5e685e4d9", "Dismiss")}
aria-label={translate('auto.components.StarNagCard.b5e685e4d9', 'Dismiss')}
>
<X className="size-3.5" />
</Button>
</div>
<p className="text-sm text-muted-foreground">
{translate("auto.components.StarNagCard.30c36231c1", "If Orca has saved you time, a GitHub star goes a long way. It helps other developers discover the project and keeps the team motivated to ship improvements.")}</p>
{translate(
'auto.components.StarNagCard.30c36231c1',
'If Orca has saved you time, a GitHub star goes a long way. It helps other developers discover the project and keeps the team motivated to ship improvements.'
)}
</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>
{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
@@ -130,7 +144,9 @@ export function StarNagCard(): React.JSX.Element | null {
className="mt-0.5 w-full gap-1.5"
>
<Star className="size-3.5" />
{busy ? translate("auto.components.StarNagCard.af3c9bbb37", "Starring…") : translate("auto.components.StarNagCard.2d67b6c849", "Star on GitHub")}
{busy
? translate('auto.components.StarNagCard.af3c9bbb37', 'Starring…')
: translate('auto.components.StarNagCard.2d67b6c849', 'Star on GitHub')}
</Button>
</div>
</Card>
+40 -11
View File
@@ -488,7 +488,9 @@ function LinearStateCell({
result.error ??
translate('auto.components.TaskPage.6775c05483', 'Failed to update Linear state')
)
return
}
useAppStore.getState().recordFeatureInteraction('linear-tasks')
})
.catch(() => {
if (reqId !== reqRef.current) {
@@ -904,7 +906,9 @@ function GHStatusCell({
typed.error ??
translate('auto.components.TaskPage.1c893195ac', 'Failed to update state')
)
return
}
useAppStore.getState().recordFeatureInteraction('github-tasks')
})
.catch(() => {
if (reqId !== reqRef.current) {
@@ -1349,6 +1353,7 @@ function GHAssigneesCell({
} else {
throw new Error('No GitHub repository context available for this issue.')
}
useAppStore.getState().recordFeatureInteraction('github-tasks')
} catch (err) {
patchWorkItem(item.id, { assignees: previousAssignees }, item.repoId)
toast.error(
@@ -1814,6 +1819,7 @@ function PRReviewCell({
setLocalReviewRequests(nextReviewRequests)
patchWorkItem(item.id, { reviewRequests: nextReviewRequests }, item.repoId)
setReviewerInput('')
useAppStore.getState().recordFeatureInteraction('github-tasks')
} else {
toast.error(result.error)
}
@@ -2200,6 +2206,7 @@ function PRMergeCell({
prRepo: item.prRepo ?? null
})
if (result.ok) {
useAppStore.getState().recordFeatureInteraction('github-tasks')
toast.success(translate('auto.components.TaskPage.a161925adc', 'Pull request merged'))
onRefresh()
} else {
@@ -2227,6 +2234,7 @@ function PRMergeCell({
prRepo: item.prRepo ?? null
})
if (result.ok) {
useAppStore.getState().recordFeatureInteraction('github-tasks')
toast.success(
enabled
? translate('auto.components.TaskPage.fed317634c', 'Auto-merge enabled')
@@ -2850,12 +2858,15 @@ export default function TaskPage(): React.JSX.Element {
const openGitHubDetailPage = useCallback(
(item: GitHubWorkItem, initialTab: ItemDialogTab = 'conversation') => {
openTaskPage({
taskSource: 'github',
preselectedRepoId: item.repoId,
openGitHubWorkItem: item,
openGitHubInitialTab: initialTab
})
openTaskPage(
{
taskSource: 'github',
preselectedRepoId: item.repoId,
openGitHubWorkItem: item,
openGitHubInitialTab: initialTab
},
{ recordTasksInteraction: false }
)
},
[openTaskPage]
)
@@ -3078,7 +3089,10 @@ export default function TaskPage(): React.JSX.Element {
const openLinearDetailPage = useCallback(
(issue: LinearIssue) => {
openTaskPage({ taskSource: 'linear', openLinearIssue: issue })
openTaskPage(
{ taskSource: 'linear', openLinearIssue: issue },
{ recordTasksInteraction: false }
)
},
[openTaskPage]
)
@@ -4192,7 +4206,9 @@ export default function TaskPage(): React.JSX.Element {
result.error ??
translate('auto.components.TaskPage.6775c05483', 'Failed to update Linear state')
)
return
}
useAppStore.getState().recordFeatureInteraction('linear-tasks')
} catch {
patchLinearIssue(issue.id, { state: previousState })
patchScopedLinearIssue(issue.id, { state: previousState })
@@ -5229,6 +5245,7 @@ export default function TaskPage(): React.JSX.Element {
// the worktree appeared in the sidebar before the user had a chance
// to review it. The composer already owns the prefill flow. Telemetry
// attribution flows via `openComposerForItem` (sets telemetrySource).
useAppStore.getState().recordFeatureInteraction('github-tasks')
openComposerForItem(item)
},
[openComposerForItem]
@@ -5260,7 +5277,9 @@ export default function TaskPage(): React.JSX.Element {
'Unable to open the workspace attached to this issue.'
)
)
return
}
useAppStore.getState().recordFeatureInteraction('github-tasks')
},
[handleUseWorkItem]
)
@@ -5285,6 +5304,7 @@ export default function TaskPage(): React.JSX.Element {
const handleUseGitLabItem = useCallback(
(item: GitLabWorkItem): void => {
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
openComposerForGitLabItem(item)
},
[openComposerForGitLabItem]
@@ -5557,13 +5577,14 @@ export default function TaskPage(): React.JSX.Element {
setNewLinearIssueProjectId(null)
setNewLinearIssueLabelIds([])
setLinearRefreshNonce((n) => n + 1)
useAppStore.getState().recordFeatureInteraction('linear-tasks')
// Why: auto-select the new issue in the inline workspace so the user
// sees exactly what was filed, mirroring the GitHub create-issue flow.
void linearGetIssue(settings, result.id, newLinearIssueTargetTeam.workspaceId)
.then((full) => {
if (full) {
openLinearDetailPage(full)
setSelectedLinearIssue(full, { allowOutsideList: true })
}
})
.catch(() => {})
@@ -5580,8 +5601,8 @@ export default function TaskPage(): React.JSX.Element {
newLinearIssueAssigneeId,
newLinearIssueProjectId,
newLinearIssueLabelIds,
openLinearDetailPage,
selectedLinearProject,
setSelectedLinearIssue,
settings
])
@@ -6314,6 +6335,7 @@ export default function TaskPage(): React.JSX.Element {
// dialog pre-filled rather than yolo-creating the worktree, so the
// user can confirm name / agent / setup before the worktree lands in
// the sidebar. Telemetry attribution flows via openComposerForLinearItem.
useAppStore.getState().recordFeatureInteraction('linear-tasks')
openComposerForLinearItem(issue, renderedText)
},
[openComposerForLinearItem]
@@ -6503,7 +6525,10 @@ export default function TaskPage(): React.JSX.Element {
disabled={source.disabled}
onClick={() => {
taskSourceManuallyChangedRef.current = true
openTaskPage({ taskSource: source.id })
openTaskPage(
{ taskSource: source.id },
{ recordTasksInteraction: false }
)
void updateSettings({ defaultTaskSource: source.id }).catch(() => {
toast.error(
translate(
@@ -8205,10 +8230,14 @@ export default function TaskPage(): React.JSX.Element {
role="button"
tabIndex={0}
key={item.id}
onClick={() => setGitlabDialogItem(item)}
onClick={() => {
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
setGitlabDialogItem(item)
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
useAppStore.getState().recordFeatureInteraction('gitlab-tasks')
setGitlabDialogItem(item)
}
}}
+46 -15
View File
@@ -546,7 +546,12 @@ function Terminal(): React.JSX.Element | null {
releaseCloseDialogGuardAfterDebounce()
return
}
toast.error(translate("auto.components.Terminal.a2a279b32a", "Save timed out or failed. Fix errors before closing."))
toast.error(
translate(
'auto.components.Terminal.a2a279b32a',
'Save timed out or failed. Fix errors before closing.'
)
)
setSaveDialogFileId(fileId)
// Why: a genuine timeout leaves the user back on the same dialog, so
// release the guard immediately — a new click here is a deliberate
@@ -861,7 +866,7 @@ function Terminal(): React.JSX.Element | null {
return
}
createBrowserTab(activeWorktreeId, defaultUrl, {
title: translate("auto.components.Terminal.37da0d736f", "New Browser Tab"),
title: translate('auto.components.Terminal.37da0d736f', 'New Browser Tab'),
focusAddressBar: true
})
}, [
@@ -1292,7 +1297,12 @@ function Terminal(): React.JSX.Element | null {
if (floatingWorkspaceFocused) {
void createFloatingWorkspaceMarkdownTab(useAppStore.getState()).catch((err) => {
toast.error(
err instanceof Error ? err.message : translate("auto.components.Terminal.f0600556b3", "Failed to create untitled markdown file.")
err instanceof Error
? err.message
: translate(
'auto.components.Terminal.f0600556b3',
'Failed to create untitled markdown file.'
)
)
})
return
@@ -1806,11 +1816,12 @@ function Terminal(): React.JSX.Element | null {
})}
</div>
{renderedActiveWorktreeId && activeTabType === "editor" && worktreeFiles.length > 0 && (
{renderedActiveWorktreeId && activeTabType === 'editor' && worktreeFiles.length > 0 && (
<Suspense
fallback={
<div className="flex-1 flex items-center justify-center text-muted-foreground text-sm">
{translate("auto.components.Terminal.5c1d2a32bb", "Loading editor...")}</div>
{translate('auto.components.Terminal.5c1d2a32bb', 'Loading editor...')}
</div>
}
>
<EditorPanel />
@@ -1830,20 +1841,32 @@ function Terminal(): React.JSX.Element | null {
>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle className="text-sm">{translate("auto.components.Terminal.21295c6b8c", "Unsaved Changes")}</DialogTitle>
<DialogTitle className="text-sm">
{translate('auto.components.Terminal.21295c6b8c', 'Unsaved Changes')}
</DialogTitle>
<DialogDescription className="text-xs">
{saveDialogFile
? translate("auto.components.Terminal.61ed600d29", "\"{{value0}}\" has unsaved changes. Do you want to save before closing?", { value0: basename(saveDialogFile.relativePath) })
: translate("auto.components.Terminal.46e08bc5c8", "This file has unsaved changes.")}
? translate(
'auto.components.Terminal.61ed600d29',
'"{{value0}}" has unsaved changes. Do you want to save before closing?',
{ value0: basename(saveDialogFile.relativePath) }
)
: translate(
'auto.components.Terminal.46e08bc5c8',
'This file has unsaved changes.'
)}
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button type="button" variant="outline" size="sm" onClick={handleSaveDialogCancel}>
{translate("auto.components.Terminal.f82e9f02df", "Cancel")}</Button>
{translate('auto.components.Terminal.f82e9f02df', 'Cancel')}
</Button>
<Button type="button" variant="outline" size="sm" onClick={handleSaveDialogDiscard}>
{translate("auto.components.Terminal.0037b21794", "Don't Save")}</Button>
{translate('auto.components.Terminal.0037b21794', "Don't Save")}
</Button>
<Button type="button" size="sm" onClick={handleSaveDialogSave}>
{translate("auto.components.Terminal.cd51e28d8b", "Save")}</Button>
{translate('auto.components.Terminal.cd51e28d8b', 'Save')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -1859,9 +1882,15 @@ function Terminal(): React.JSX.Element | null {
>
<DialogContent className="max-w-sm" showCloseButton={false}>
<DialogHeader>
<DialogTitle className="text-sm">{translate("auto.components.Terminal.2fa9c69ff3", "Close Window?")}</DialogTitle>
<DialogTitle className="text-sm">
{translate('auto.components.Terminal.2fa9c69ff3', 'Close Window?')}
</DialogTitle>
<DialogDescription className="text-xs">
{translate("auto.components.Terminal.7958465754", "There are local terminals with running processes. Close the window anyway?")}</DialogDescription>
{translate(
'auto.components.Terminal.7958465754',
'There are local terminals with running processes. Close the window anyway?'
)}
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button
@@ -1870,7 +1899,8 @@ function Terminal(): React.JSX.Element | null {
size="sm"
onClick={() => setWindowCloseDialogOpen(false)}
>
{translate("auto.components.Terminal.f82e9f02df", "Cancel")}</Button>
{translate('auto.components.Terminal.f82e9f02df', 'Cancel')}
</Button>
<Button
type="button"
variant="destructive"
@@ -1881,7 +1911,8 @@ function Terminal(): React.JSX.Element | null {
window.api.ui.confirmWindowClose()
}}
>
{translate("auto.components.Terminal.73768427cf", "Close")}</Button>
{translate('auto.components.Terminal.73768427cf', 'Close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -110,7 +110,7 @@ export default function TerminalSearch({
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={translate("auto.components.TerminalSearch.e07012f26e", "Search...")}
placeholder={translate('auto.components.TerminalSearch.e07012f26e', 'Search...')}
className="min-w-0 flex-1 border-none bg-transparent text-sm text-white outline-none placeholder:text-zinc-500"
/>
@@ -122,7 +122,7 @@ export default function TerminalSearch({
className={`flex size-6 shrink-0 items-center justify-center rounded ${
caseSensitive ? 'bg-zinc-700/50 text-blue-400' : 'text-zinc-400 hover:text-zinc-200'
}`}
title={translate("auto.components.TerminalSearch.90c61387d9", "Case sensitive")}
title={translate('auto.components.TerminalSearch.90c61387d9', 'Case sensitive')}
>
<CaseSensitive size={14} />
</Button>
@@ -135,7 +135,7 @@ export default function TerminalSearch({
className={`flex size-6 shrink-0 items-center justify-center rounded ${
regex ? 'bg-zinc-700/50 text-blue-400' : 'text-zinc-400 hover:text-zinc-200'
}`}
title={translate("auto.components.TerminalSearch.42e466b9f1", "Regex")}
title={translate('auto.components.TerminalSearch.42e466b9f1', 'Regex')}
>
<Regex size={14} />
</Button>
@@ -148,7 +148,7 @@ export default function TerminalSearch({
size="icon-xs"
onClick={findPrevious}
className="flex size-6 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-200"
title={translate("auto.components.TerminalSearch.0f3066256e", "Previous match")}
title={translate('auto.components.TerminalSearch.0f3066256e', 'Previous match')}
>
<ChevronUp size={14} />
</Button>
@@ -159,7 +159,7 @@ export default function TerminalSearch({
size="icon-xs"
onClick={findNext}
className="flex size-6 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-200"
title={translate("auto.components.TerminalSearch.7cb40c04eb", "Next match")}
title={translate('auto.components.TerminalSearch.7cb40c04eb', 'Next match')}
>
<ChevronDown size={14} />
</Button>
@@ -172,7 +172,7 @@ export default function TerminalSearch({
size="icon-xs"
onClick={onClose}
className="flex size-6 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-200"
title={translate("auto.components.TerminalSearch.db234b7519", "Close")}
title={translate('auto.components.TerminalSearch.db234b7519', 'Close')}
>
<X size={14} />
</Button>
@@ -255,6 +255,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
const openModal = useAppStore((s) => s.openModal)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
const recordFeatureInteraction = useAppStore((s) => s.recordFeatureInteraction)
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
const allWorktrees = useAllWorktrees()
const repos = useAppStore((s) => s.repos)
@@ -799,6 +800,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
useEffect(() => {
if (visible && !wasVisibleRef.current) {
recordFeatureInteraction('cmd-j')
createLookupGuard.invalidate()
activeGroupSnapshotRef.current = captureCmdJActiveGroupSnapshot(
useAppStore.getState(),
@@ -845,6 +847,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
activeWorktreeId,
browserTabsByWorktree,
createLookupGuard,
recordFeatureInteraction,
visible
])
@@ -953,12 +956,13 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
return
}
activateAndRevealWorktree(worktreeId)
recordFeatureInteraction('cmd-j-workspace-open')
skipRestoreFocusRef.current = true
closeModal()
setSelectedItemId('')
focusFallbackSurface()
},
[closeModal, focusFallbackSurface]
[closeModal, focusFallbackSurface, recordFeatureInteraction]
)
const handleSelectBrowserPage = useCallback(
@@ -989,6 +993,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
const state = useAppStore.getState()
state.setActiveBrowserTab(workspace.id)
state.setActiveBrowserPage(workspace.id, pageId)
recordFeatureInteraction('cmd-j-browser-page-open')
skipRestoreFocusRef.current = true
closeModal()
setSelectedItemId('')
@@ -997,7 +1002,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
target: isBlankBrowserUrl(page.url) ? 'address-bar' : 'webview'
})
},
[closeModal, requestBrowserFocus]
[closeModal, recordFeatureInteraction, requestBrowserFocus]
)
const handleSelectSimulatorTab = useCallback(
@@ -1046,8 +1051,9 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
setSelectedItemId('')
openSettingsTarget(target)
openSettingsPage()
recordFeatureInteraction('cmd-j-settings-open')
},
[closeModal, openSettingsPage, openSettingsTarget]
[closeModal, openSettingsPage, openSettingsTarget, recordFeatureInteraction]
)
const handleSelectQuickAction = useCallback(
@@ -1059,10 +1065,16 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
void action.run(ctx).then((result) => {
if (result.status === 'unavailable') {
toast.error(getUnavailableQuickActionMessage(action.title, result.reason))
return
}
if (action.id === 'create-workspace') {
recordFeatureInteraction('cmd-j-create-workspace')
return
}
recordFeatureInteraction('cmd-j-quick-action')
})
},
[buildQuickActionContext, closeModal]
[buildQuickActionContext, closeModal, recordFeatureInteraction]
)
const handleSelectItem = useCallback(
@@ -1099,6 +1111,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
typeof data.initialRepoId === 'string' ? data.initialRepoId : undefined
)
closeModal()
recordFeatureInteraction('cmd-j-create-workspace')
// Why: defer opening so Radix fully unmounts the palette's dialog before
// the composer modal mounts, avoiding focus churn between the two.
queueMicrotask(() =>
@@ -1122,6 +1135,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
if (activeMatch) {
closeModal()
activateAndRevealWorktree(activeMatch.id)
recordFeatureInteraction('cmd-j-workspace-open')
return
}
@@ -1141,6 +1155,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
// composer once the lookup returns.
const lookupToken = createLookupGuard.start()
preserveCreateLookupOnCloseRef.current = true
recordFeatureInteraction('cmd-j-create-workspace')
closeModal()
void window.api.gh
.workItemByOwnerRepo({
@@ -1199,6 +1214,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
if (activeMatch) {
closeModal()
activateAndRevealWorktree(activeMatch.id)
recordFeatureInteraction('cmd-j-workspace-open')
return
}
@@ -1213,6 +1229,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
prefetchCreateWorkspaceBaseForComposer(repoForLookup.id)
const lookupToken = createLookupGuard.start()
preserveCreateLookupOnCloseRef.current = true
recordFeatureInteraction('cmd-j-create-workspace')
closeModal()
void window.api.gh
.workItem({ repoPath: repoForLookup.path, repoId: repoForLookup.id, number: ghNumber })
@@ -1263,6 +1280,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
createWorktreeName,
openModal,
prefetchCreateWorkspaceBaseForComposer,
recordFeatureInteraction,
repoMap
])
@@ -911,7 +911,13 @@ export function getActivityThreadGroup(
if (groupBy === 'project') {
return thread.repo
? { key: `project:${thread.repo.id}`, label: thread.repo.displayName }
: { key: 'project:unknown', label: translate("auto.components.activity.ActivityPrototypePage.5651b216c6", "Unknown project") }
: {
key: 'project:unknown',
label: translate(
'auto.components.activity.ActivityPrototypePage.5651b216c6',
'Unknown project'
)
}
}
if (groupBy === 'worktree') {
return { key: `worktree:${thread.worktree.id}`, label: thread.worktree.displayName }
@@ -1162,7 +1168,10 @@ function ThreadRow({
{thread.unread ? (
<FilledBellIcon
className="size-[13px] shrink-0 text-amber-500 drop-shadow-sm"
aria-label={translate("auto.components.activity.ActivityPrototypePage.beb2c19173", "Unread")}
aria-label={translate(
'auto.components.activity.ActivityPrototypePage.beb2c19173',
'Unread'
)}
/>
) : (
<Tooltip>
@@ -1179,12 +1188,20 @@ function ThreadRow({
'hover:bg-accent/80 active:scale-95',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
)}
aria-label={translate("auto.components.activity.ActivityPrototypePage.59b131fbd9", "Mark thread unread")}
aria-label={translate(
'auto.components.activity.ActivityPrototypePage.59b131fbd9',
'Mark thread unread'
)}
>
<Bell className="size-3 text-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100 group-hover/unread:opacity-100" />
</button>
</TooltipTrigger>
<TooltipContent side="left">{translate("auto.components.activity.ActivityPrototypePage.59b131fbd9", "Mark thread unread")}</TooltipContent>
<TooltipContent side="left">
{translate(
'auto.components.activity.ActivityPrototypePage.59b131fbd9',
'Mark thread unread'
)}
</TooltipContent>
</Tooltip>
)}
</span>
@@ -1216,7 +1233,10 @@ function ThreadRow({
type="button"
variant="outline"
size="icon-xs"
aria-label={translate("auto.components.activity.ActivityPrototypePage.4616ea39fd", "Jump to workspace")}
aria-label={translate(
'auto.components.activity.ActivityPrototypePage.4616ea39fd',
'Jump to workspace'
)}
onClick={(event) => {
event.stopPropagation()
onJump()
@@ -1226,7 +1246,12 @@ function ThreadRow({
<ExternalLink className="size-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">{translate("auto.components.activity.ActivityPrototypePage.4616ea39fd", "Jump to workspace")}</TooltipContent>
<TooltipContent side="left">
{translate(
'auto.components.activity.ActivityPrototypePage.4616ea39fd',
'Jump to workspace'
)}
</TooltipContent>
</Tooltip>
</span>
) : null}
@@ -1615,7 +1640,10 @@ export default function ActivityPrototypePage(): React.JSX.Element {
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={translate("auto.components.activity.ActivityPrototypePage.795cbf26e2", "Filter...")}
placeholder={translate(
'auto.components.activity.ActivityPrototypePage.795cbf26e2',
'Filter...'
)}
className="h-8 w-full pl-7 text-xs"
/>
</div>
@@ -1626,15 +1654,38 @@ export default function ActivityPrototypePage(): React.JSX.Element {
<SelectTrigger
size="sm"
className="h-8 w-[128px] shrink-0 px-2 text-xs"
aria-label={translate("auto.components.activity.ActivityPrototypePage.770d458144", "Group agent activity by")}
aria-label={translate(
'auto.components.activity.ActivityPrototypePage.770d458144',
'Group agent activity by'
)}
>
<SelectValue />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="status">{translate("auto.components.activity.ActivityPrototypePage.4a3986b200", "Status")}</SelectItem>
<SelectItem value="project">{translate("auto.components.activity.ActivityPrototypePage.8c3b621ddf", "Project")}</SelectItem>
<SelectItem value="worktree">{translate("auto.components.activity.ActivityPrototypePage.b29191b3e0", "Worktree")}</SelectItem>
<SelectItem value="agent">{translate("auto.components.activity.ActivityPrototypePage.f6396e1f85", "Agent")}</SelectItem>
<SelectItem value="status">
{translate(
'auto.components.activity.ActivityPrototypePage.4a3986b200',
'Status'
)}
</SelectItem>
<SelectItem value="project">
{translate(
'auto.components.activity.ActivityPrototypePage.8c3b621ddf',
'Project'
)}
</SelectItem>
<SelectItem value="worktree">
{translate(
'auto.components.activity.ActivityPrototypePage.b29191b3e0',
'Worktree'
)}
</SelectItem>
<SelectItem value="agent">
{translate(
'auto.components.activity.ActivityPrototypePage.f6396e1f85',
'Agent'
)}
</SelectItem>
</SelectContent>
</Select>
<Tooltip>
@@ -1650,12 +1701,20 @@ export default function ActivityPrototypePage(): React.JSX.Element {
? '!border-primary !bg-primary !text-primary-foreground shadow-xs ring-2 ring-primary/35 hover:!bg-primary/90 hover:!text-primary-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
aria-label={translate("auto.components.activity.ActivityPrototypePage.d1a88df9a8", "Show unread threads only")}
aria-label={translate(
'auto.components.activity.ActivityPrototypePage.d1a88df9a8',
'Show unread threads only'
)}
>
<BellDot className="size-3.5" />
</Toggle>
</TooltipTrigger>
<TooltipContent side="bottom">{translate("auto.components.activity.ActivityPrototypePage.d1a88df9a8", "Show unread threads only")}</TooltipContent>
<TooltipContent side="bottom">
{translate(
'auto.components.activity.ActivityPrototypePage.d1a88df9a8',
'Show unread threads only'
)}
</TooltipContent>
</Tooltip>
{/* Why (overflow menu): "Mark all read" is a low-frequency,
destructive-feeling action parking it behind a `` keeps
@@ -1671,13 +1730,21 @@ export default function ActivityPrototypePage(): React.JSX.Element {
variant="outline"
size="sm"
className="size-8 shrink-0 border-input bg-transparent p-0 text-muted-foreground shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-transparent dark:hover:bg-accent dark:hover:text-accent-foreground"
aria-label={translate("auto.components.activity.ActivityPrototypePage.db8a1878b5", "Thread list options")}
aria-label={translate(
'auto.components.activity.ActivityPrototypePage.db8a1878b5',
'Thread list options'
)}
>
<MoreVertical className="size-3.5" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">{translate("auto.components.activity.ActivityPrototypePage.a472a14700", "More options")}</TooltipContent>
<TooltipContent side="bottom">
{translate(
'auto.components.activity.ActivityPrototypePage.a472a14700',
'More options'
)}
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" sideOffset={6}>
<DropdownMenuCheckboxItem
@@ -1685,20 +1752,35 @@ export default function ActivityPrototypePage(): React.JSX.Element {
onCheckedChange={(checked) => setCompactMode(checked === true)}
onSelect={(event) => event.preventDefault()}
>
{translate("auto.components.activity.ActivityPrototypePage.f70e4bec47", "Compact mode")}</DropdownMenuCheckboxItem>
{translate(
'auto.components.activity.ActivityPrototypePage.f70e4bec47',
'Compact mode'
)}
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => markAllThreadsRead()}
disabled={!hasUnreadThreads}
>
{translate("auto.components.activity.ActivityPrototypePage.023ff75afe", "Mark all read")}</DropdownMenuItem>
{translate(
'auto.components.activity.ActivityPrototypePage.023ff75afe',
'Mark all read'
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="min-h-0 flex-1 overflow-auto scrollbar-sleek">
{visibleThreadGroups.map((group) => (
<section key={group.key} aria-label={translate("auto.components.activity.ActivityPrototypePage.a2b4437bfb", "{{value0}} activity", { value0: group.label })}>
<section
key={group.key}
aria-label={translate(
'auto.components.activity.ActivityPrototypePage.a2b4437bfb',
'{{value0}} activity',
{ value0: group.label }
)}
>
<ActivityStatusGroupHeader group={group} />
{group.threads.map((thread) => (
<ThreadRow
@@ -1716,12 +1798,22 @@ export default function ActivityPrototypePage(): React.JSX.Element {
))}
{visibleThreads.length === 0 ? (
<div className="px-3 py-8 text-sm text-muted-foreground">
{translate("auto.components.activity.ActivityPrototypePage.7cd632006b", "No agent activity matches these filters.")}</div>
{translate(
'auto.components.activity.ActivityPrototypePage.7cd632006b',
'No agent activity matches these filters.'
)}
</div>
) : null}
</div>
<div
aria-label={translate("auto.components.activity.ActivityPrototypePage.443690186e", "Resize activity thread list")}
title={translate("auto.components.activity.ActivityPrototypePage.866083500b", "Drag to resize")}
aria-label={translate(
'auto.components.activity.ActivityPrototypePage.443690186e',
'Resize activity thread list'
)}
title={translate(
'auto.components.activity.ActivityPrototypePage.866083500b',
'Drag to resize'
)}
className={cn(
'group absolute -right-1.5 top-0 z-20 flex h-full w-3 cursor-col-resize items-stretch justify-center',
isThreadListResizing && 'bg-ring/10'
@@ -1778,8 +1870,14 @@ export default function ActivityPrototypePage(): React.JSX.Element {
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-2 p-4 text-sm text-muted-foreground">
<TerminalSquare className="size-7" />
{storeData.worktreeMap.has(selectedThread.worktree.id)
? translate("auto.components.activity.ActivityPrototypePage.afdc2139a8", "Agent terminal closed. Open a new terminal in this workspace to continue.")
: translate("auto.components.activity.ActivityPrototypePage.22b22034bc", "Standalone terminal unavailable in Activity.")}
? translate(
'auto.components.activity.ActivityPrototypePage.afdc2139a8',
'Agent terminal closed. Open a new terminal in this workspace to continue.'
)
: translate(
'auto.components.activity.ActivityPrototypePage.22b22034bc',
'Standalone terminal unavailable in Activity.'
)}
</div>
)
}
@@ -1815,12 +1913,22 @@ export default function ActivityPrototypePage(): React.JSX.Element {
{visiblePortalUnavailable ? (
<div className="ml-3 mt-3 inline-flex items-center gap-2 rounded-md border border-border bg-background/85 px-2 py-1 text-xs text-muted-foreground shadow-xs">
<span className="h-3 w-1.5 rounded-sm bg-muted-foreground/70" />
<span>{translate("auto.components.activity.ActivityPrototypePage.8de7c5beaa", "Terminal unavailable")}</span>
<span>
{translate(
'auto.components.activity.ActivityPrototypePage.8de7c5beaa',
'Terminal unavailable'
)}
</span>
</div>
) : showTerminalLoadingLabel ? (
<div className="ml-3 mt-3 inline-flex items-center gap-2 rounded-md border border-border bg-background/85 px-2 py-1 text-xs text-muted-foreground shadow-xs">
<span className="h-3 w-1.5 animate-pulse rounded-sm bg-muted-foreground/70" />
<span>{translate("auto.components.activity.ActivityPrototypePage.1b633f5c1e", "Connecting terminal...")}</span>
<span>
{translate(
'auto.components.activity.ActivityPrototypePage.1b633f5c1e',
'Connecting terminal...'
)}
</span>
</div>
) : null}
</div>
@@ -1834,11 +1942,19 @@ export default function ActivityPrototypePage(): React.JSX.Element {
{visibleThreads.length === 0 ? (
<>
<MessageSquareText className="size-7" />
{translate("auto.components.activity.ActivityPrototypePage.e3db9892f6", "No activity yet.")}</>
{translate(
'auto.components.activity.ActivityPrototypePage.e3db9892f6',
'No activity yet.'
)}
</>
) : (
<>
<TerminalSquare className="size-7" />
{translate("auto.components.activity.ActivityPrototypePage.cf780197a1", "Select an agent to view its activity")}</>
{translate(
'auto.components.activity.ActivityPrototypePage.cf780197a1',
'Select an agent to view its activity'
)}
</>
)}
</div>
)}
@@ -27,18 +27,29 @@ export function ActivityTitlebarControls(): React.JSX.Element {
variant="ghost"
size="icon-xs"
onClick={closeActivityPage}
aria-label={translate("auto.components.activity.ActivityTitlebarControls.dc708f3eff", "Close agents")}
aria-label={translate(
'auto.components.activity.ActivityTitlebarControls.dc708f3eff',
'Close agents'
)}
>
<ArrowLeft className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.activity.ActivityTitlebarControls.dc708f3eff", "Close agents")}</TooltipContent>
{translate(
'auto.components.activity.ActivityTitlebarControls.dc708f3eff',
'Close agents'
)}
</TooltipContent>
</Tooltip>
<Bell className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate text-xs font-medium">{translate("auto.components.activity.ActivityTitlebarControls.d6a8de3934", "agents")}</span>
<span className="truncate text-xs font-medium">
{translate('auto.components.activity.ActivityTitlebarControls.d6a8de3934', 'agents')}
</span>
<Badge variant="secondary" className="h-5 px-1.5 text-[11px] font-normal">
{unreadCount} {translate("auto.components.activity.ActivityTitlebarControls.f915168c8e", "unread")}</Badge>
{unreadCount}{' '}
{translate('auto.components.activity.ActivityTitlebarControls.f915168c8e', 'unread')}
</Badge>
</div>
</div>
)
@@ -102,7 +102,9 @@ function renderItem({
<ContextMenuContent className="z-[70]">
<ContextMenuItem onSelect={onSetDefault} disabled={isDefault}>
<Star className="size-3.5" />
{isDefault ? translate("auto.components.agent.AgentCombobox.1b0d6965fa", "Current default") : translate("auto.components.agent.AgentCombobox.9c6b59fe58", "Set as default")}
{isDefault
? translate('auto.components.agent.AgentCombobox.1b0d6965fa', 'Current default')
: translate('auto.components.agent.AgentCombobox.9c6b59fe58', 'Set as default')}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -286,7 +288,9 @@ export default function AgentCombobox({
) : (
<span className="inline-flex min-w-0 flex-1 items-center gap-1.5">
<Terminal className="size-3.5" />
<span className="truncate">{translate("auto.components.agent.AgentCombobox.986f946354", "Blank Terminal")}</span>
<span className="truncate">
{translate('auto.components.agent.AgentCombobox.986f946354', 'Blank Terminal')}
</span>
</span>
)}
<ChevronsUpDown className="size-3.5 opacity-50" />
@@ -307,12 +311,20 @@ export default function AgentCombobox({
<Command shouldFilter={false} value={commandValue} onValueChange={setCommandValue}>
<CommandInput
ref={setInputNode}
placeholder={translate("auto.components.agent.AgentCombobox.48c6a5a9b4", "Search agents...")}
placeholder={translate(
'auto.components.agent.AgentCombobox.48c6a5a9b4',
'Search agents...'
)}
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>{translate("auto.components.agent.AgentCombobox.579c768bde", "No agents match your search.")}</CommandEmpty>
<CommandEmpty>
{translate(
'auto.components.agent.AgentCombobox.579c768bde',
'No agents match your search.'
)}
</CommandEmpty>
{blankMatchesQuery
? renderItem({
key: BLANK_VALUE,
@@ -322,7 +334,10 @@ export default function AgentCombobox({
onSelect: () => handleSelect(null),
onSetDefault: onSetDefault ? () => onSetDefault('blank') : undefined,
icon: <Terminal className="size-3.5" />,
label: translate("auto.components.agent.AgentCombobox.986f946354", "Blank Terminal")
label: translate(
'auto.components.agent.AgentCombobox.986f946354',
'Blank Terminal'
)
})
: null}
{filteredAgents.map((agent) =>
@@ -348,7 +363,8 @@ export default function AgentCombobox({
onMouseEnter={() => setCommandValue('')}
className="h-9 w-full justify-start rounded-none px-3 text-xs font-normal text-muted-foreground"
>
{translate("auto.components.agent.AgentCombobox.19522e25ee", "Manage agents")}<ArrowRight className="ml-auto size-3" />
{translate('auto.components.agent.AgentCombobox.19522e25ee', 'Manage agents')}
<ArrowRight className="ml-auto size-3" />
</Button>
</div>
) : null}
@@ -34,9 +34,15 @@ export default function AgentSettingsDialog({
agents are detected. */}
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="text-sm">{translate("auto.components.agent.AgentSettingsDialog.fc0268e4ed", "Agents")}</DialogTitle>
<DialogTitle className="text-sm">
{translate('auto.components.agent.AgentSettingsDialog.fc0268e4ed', 'Agents')}
</DialogTitle>
<DialogDescription className="text-xs">
{translate("auto.components.agent.AgentSettingsDialog.50cdb57c03", "Manage AI agents, set a default, and customize commands.")}</DialogDescription>
{translate(
'auto.components.agent.AgentSettingsDialog.50cdb57c03',
'Manage AI agents, set a default, and customize commands.'
)}
</DialogDescription>
</DialogHeader>
<div className="scrollbar-sleek -mr-2 max-h-[70vh] overflow-y-auto pr-2">
<AgentsPane settings={settings} updateSettings={updateSettings} />
@@ -17,10 +17,22 @@ export function getCronScheduleStatusLabel(
): { kind: 'empty' | 'invalid' | 'valid'; label: string } {
const trimmed = schedule.trim()
if (!trimmed) {
return { kind: 'empty', label: translate("auto.components.automations.AutomationCustomCronPanel.968e66d686", "Enter a five-field cron.") }
return {
kind: 'empty',
label: translate(
'auto.components.automations.AutomationCustomCronPanel.968e66d686',
'Enter a five-field cron.'
)
}
}
if (!validateSchedule(trimmed)) {
return { kind: 'invalid', label: translate("auto.components.automations.AutomationCustomCronPanel.e81a02d61b", "Enter a valid five-field cron before saving.") }
return {
kind: 'invalid',
label: translate(
'auto.components.automations.AutomationCustomCronPanel.e81a02d61b',
'Enter a valid five-field cron before saving.'
)
}
}
const formatted = formatAutomationSchedule(trimmed)
return { kind: 'valid', label: formatted === 'Custom schedule' ? 'Valid custom cron' : formatted }
@@ -50,7 +62,12 @@ export function AutomationCustomCronPanel({
return (
<div className="grid gap-3">
<Field label={translate("auto.components.automations.AutomationCustomCronPanel.3e3b2c369f", "Cron expression")}>
<Field
label={translate(
'auto.components.automations.AutomationCustomCronPanel.3e3b2c369f',
'Cron expression'
)}
>
<Input
value={draft.customSchedule}
placeholder="0 9 * * 1-5"
@@ -88,7 +105,7 @@ export function AutomationCustomCronPanel({
: 'border-border/70 bg-muted/30 text-muted-foreground'
)}
>
{customScheduleStatus.kind === "invalid" ? (
{customScheduleStatus.kind === 'invalid' ? (
<CircleAlert className="size-3.5 shrink-0" />
) : (
<CheckCircle2 className="size-3.5 shrink-0" />
@@ -69,17 +69,35 @@ export function AutomationEditorDialogHeader({
<div className="min-w-0 flex-1 space-y-2">
<DialogTitle className="text-sm font-medium">
{isEditing
? translate("auto.components.automations.AutomationEditorDialogHeader.17086b48ee", "Edit automation")
? translate(
'auto.components.automations.AutomationEditorDialogHeader.17086b48ee',
'Edit automation'
)
: isEditingExternal
? translate("auto.components.automations.AutomationEditorDialogHeader.03142e7721", "Edit Hermes automation")
? translate(
'auto.components.automations.AutomationEditorDialogHeader.03142e7721',
'Edit Hermes automation'
)
: isHermesCreate
? translate("auto.components.automations.AutomationEditorDialogHeader.0a75e5e2fa", "Create Hermes automation")
: translate("auto.components.automations.AutomationEditorDialogHeader.4133d33862", "Create automation")}
? translate(
'auto.components.automations.AutomationEditorDialogHeader.0a75e5e2fa',
'Create Hermes automation'
)
: translate(
'auto.components.automations.AutomationEditorDialogHeader.4133d33862',
'Create automation'
)}
</DialogTitle>
<Input
value={draftName}
placeholder={translate("auto.components.automations.AutomationEditorDialogHeader.1d9826933e", "Weekday repo audit")}
aria-label={translate("auto.components.automations.AutomationEditorDialogHeader.58f56b73d9", "Automation name")}
placeholder={translate(
'auto.components.automations.AutomationEditorDialogHeader.1d9826933e',
'Weekday repo audit'
)}
aria-label={translate(
'auto.components.automations.AutomationEditorDialogHeader.58f56b73d9',
'Automation name'
)}
className="h-10 max-w-md border-input bg-input/30 px-3 text-lg font-semibold text-foreground shadow-xs placeholder:text-muted-foreground dark:bg-input/30"
onChange={(event) => onDraftNameChange(event.target.value)}
/>
@@ -97,9 +115,17 @@ export function AutomationEditorDialogHeader({
className="grid grid-cols-2"
>
<ToggleGroupItem value="orca" className={modeToggleItemClassName}>
{translate("auto.components.automations.AutomationEditorDialogHeader.6f309eef8d", "Orca")}</ToggleGroupItem>
{translate(
'auto.components.automations.AutomationEditorDialogHeader.6f309eef8d',
'Orca'
)}
</ToggleGroupItem>
<ToggleGroupItem value="hermes" className={modeToggleItemClassName}>
{translate("auto.components.automations.AutomationEditorDialogHeader.7e35393632", "Hermes")}</ToggleGroupItem>
{translate(
'auto.components.automations.AutomationEditorDialogHeader.7e35393632',
'Hermes'
)}
</ToggleGroupItem>
</ToggleGroup>
<Popover open={templateOpen} onOpenChange={onTemplateOpenChange}>
<PopoverTrigger asChild>
@@ -110,7 +136,11 @@ export function AutomationEditorDialogHeader({
className={pickerTriggerClassName}
>
<Sparkles className="size-4" />
{translate("auto.components.automations.AutomationEditorDialogHeader.31f9253920", "Use template")}</Button>
{translate(
'auto.components.automations.AutomationEditorDialogHeader.31f9253920',
'Use template'
)}
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-96 p-3">
<div className="grid gap-2">
@@ -28,18 +28,29 @@ export function AutomationMissedRunGraceField({
<Field
label={
<span className="inline-flex items-center gap-1">
{translate("auto.components.automations.AutomationMissedRunGraceField.fc089e5fde", "Grace")}<Tooltip>
{translate(
'auto.components.automations.AutomationMissedRunGraceField.fc089e5fde',
'Grace'
)}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={translate("auto.components.automations.AutomationMissedRunGraceField.3df53d554a", "Missed-run grace help")}
aria-label={translate(
'auto.components.automations.AutomationMissedRunGraceField.3df53d554a',
'Missed-run grace help'
)}
className="rounded-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Info className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6} className="max-w-72">
{translate("auto.components.automations.AutomationMissedRunGraceField.3d70c185c8", "If Orca or the execution host was unavailable at the scheduled time, Orca runs one missed occurrence when it becomes available within this window. Older missed runs are skipped.")}</TooltipContent>
{translate(
'auto.components.automations.AutomationMissedRunGraceField.3d70c185c8',
'If Orca or the execution host was unavailable at the scheduled time, Orca runs one missed occurrence when it becomes available within this window. Older missed runs are skipped.'
)}
</TooltipContent>
</Tooltip>
</span>
}
@@ -55,13 +66,48 @@ export function AutomationMissedRunGraceField({
<SelectValue />
</SelectTrigger>
<SelectContent position="popper" side="bottom" align="start" sideOffset={4}>
<SelectItem value="0">{translate("auto.components.automations.AutomationMissedRunGraceField.529dc6c0b7", "No grace")}</SelectItem>
<SelectItem value="30">{translate("auto.components.automations.AutomationMissedRunGraceField.e5ad263ae5", "30 minutes")}</SelectItem>
<SelectItem value="60">{translate("auto.components.automations.AutomationMissedRunGraceField.521f77cd58", "1 hour")}</SelectItem>
<SelectItem value="180">{translate("auto.components.automations.AutomationMissedRunGraceField.2dc9ee84d0", "3 hours")}</SelectItem>
<SelectItem value="720">{translate("auto.components.automations.AutomationMissedRunGraceField.ba50e2a230", "12 hours")}</SelectItem>
<SelectItem value="1440">{translate("auto.components.automations.AutomationMissedRunGraceField.adbab51feb", "24 hours")}</SelectItem>
<SelectItem value="2880">{translate("auto.components.automations.AutomationMissedRunGraceField.0f4459e91d", "48 hours")}</SelectItem>
<SelectItem value="0">
{translate(
'auto.components.automations.AutomationMissedRunGraceField.529dc6c0b7',
'No grace'
)}
</SelectItem>
<SelectItem value="30">
{translate(
'auto.components.automations.AutomationMissedRunGraceField.e5ad263ae5',
'30 minutes'
)}
</SelectItem>
<SelectItem value="60">
{translate(
'auto.components.automations.AutomationMissedRunGraceField.521f77cd58',
'1 hour'
)}
</SelectItem>
<SelectItem value="180">
{translate(
'auto.components.automations.AutomationMissedRunGraceField.2dc9ee84d0',
'3 hours'
)}
</SelectItem>
<SelectItem value="720">
{translate(
'auto.components.automations.AutomationMissedRunGraceField.ba50e2a230',
'12 hours'
)}
</SelectItem>
<SelectItem value="1440">
{translate(
'auto.components.automations.AutomationMissedRunGraceField.adbab51feb',
'24 hours'
)}
</SelectItem>
<SelectItem value="2880">
{translate(
'auto.components.automations.AutomationMissedRunGraceField.0f4459e91d',
'48 hours'
)}
</SelectItem>
</SelectContent>
</Select>
</Field>
@@ -24,11 +24,19 @@ export function AutomationPrecheckFields({
}: AutomationPrecheckFieldsProps): React.JSX.Element {
return (
<>
<Field label={translate("auto.components.automations.AutomationPrecheckFields.c2a762a180", "Precheck")}>
<Field
label={translate(
'auto.components.automations.AutomationPrecheckFields.c2a762a180',
'Precheck'
)}
>
<textarea
value={draft.precheckCommand}
disabled={disabled}
placeholder={translate("auto.components.automations.AutomationPrecheckFields.99a577306c", "gh pr list --json number -q '.[0].number'")}
placeholder={translate(
'auto.components.automations.AutomationPrecheckFields.99a577306c',
"gh pr list --json number -q '.[0].number'"
)}
onChange={(event) =>
onDraftChange((current) => ({
...current,
@@ -38,7 +46,12 @@ export function AutomationPrecheckFields({
className="min-h-[68px] w-full resize-none rounded-md border border-input bg-transparent px-3 py-2 font-mono text-sm shadow-xs outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30"
/>
</Field>
<Field label={translate("auto.components.automations.AutomationPrecheckFields.bb2dfb3629", "Timeout")}>
<Field
label={translate(
'auto.components.automations.AutomationPrecheckFields.bb2dfb3629',
'Timeout'
)}
>
<Select
value={draft.precheckTimeoutSeconds}
disabled={disabled}
@@ -50,11 +63,36 @@ export function AutomationPrecheckFields({
<SelectValue />
</SelectTrigger>
<SelectContent position="popper" side="bottom" align="start" sideOffset={4}>
<SelectItem value="30">{translate("auto.components.automations.AutomationPrecheckFields.51e28cdad9", "30 sec")}</SelectItem>
<SelectItem value="60">{translate("auto.components.automations.AutomationPrecheckFields.c820119736", "1 min")}</SelectItem>
<SelectItem value="120">{translate("auto.components.automations.AutomationPrecheckFields.d84d3765fd", "2 min")}</SelectItem>
<SelectItem value="300">{translate("auto.components.automations.AutomationPrecheckFields.bf49585b3c", "5 min")}</SelectItem>
<SelectItem value="600">{translate("auto.components.automations.AutomationPrecheckFields.d2a2ac89ac", "10 min")}</SelectItem>
<SelectItem value="30">
{translate(
'auto.components.automations.AutomationPrecheckFields.51e28cdad9',
'30 sec'
)}
</SelectItem>
<SelectItem value="60">
{translate(
'auto.components.automations.AutomationPrecheckFields.c820119736',
'1 min'
)}
</SelectItem>
<SelectItem value="120">
{translate(
'auto.components.automations.AutomationPrecheckFields.d84d3765fd',
'2 min'
)}
</SelectItem>
<SelectItem value="300">
{translate(
'auto.components.automations.AutomationPrecheckFields.bf49585b3c',
'5 min'
)}
</SelectItem>
<SelectItem value="600">
{translate(
'auto.components.automations.AutomationPrecheckFields.d2a2ac89ac',
'10 min'
)}
</SelectItem>
</SelectContent>
</Select>
</Field>
@@ -48,16 +48,28 @@ export function AutomationRunHistory({
return (
<div className="rounded-md border border-border/50 bg-muted/20 shadow-sm">
<div className="flex items-center justify-between border-b border-border/50 px-3 py-2">
<div className="text-sm font-medium">{translate("auto.components.automations.AutomationRunHistory.53fc5f07ab", "Run history")}</div>
<div className="text-sm font-medium">
{translate('auto.components.automations.AutomationRunHistory.53fc5f07ab', 'Run history')}
</div>
<div className="text-xs text-muted-foreground">{runCountLabel}</div>
</div>
<div className="min-h-[18rem] min-w-0">
<div className="grid grid-cols-[minmax(9rem,1fr)_minmax(10rem,1.1fr)_minmax(5rem,.55fr)_minmax(5rem,.55fr)_minmax(6rem,auto)] gap-3 border-b border-border/50 px-3 py-1.5 text-[11px] font-medium uppercase text-muted-foreground">
<div>{translate("auto.components.automations.AutomationRunHistory.8faaa00726", "Run")}</div>
<div>{translate("auto.components.automations.AutomationRunHistory.149c0b49c7", "Workspace")}</div>
<div>{translate("auto.components.automations.AutomationRunHistory.86a248187e", "Spend")}</div>
<div>{translate("auto.components.automations.AutomationRunHistory.13988187b3", "Tokens")}</div>
<div>{translate("auto.components.automations.AutomationRunHistory.9974a2b429", "Status")}</div>
<div>
{translate('auto.components.automations.AutomationRunHistory.8faaa00726', 'Run')}
</div>
<div>
{translate('auto.components.automations.AutomationRunHistory.149c0b49c7', 'Workspace')}
</div>
<div>
{translate('auto.components.automations.AutomationRunHistory.86a248187e', 'Spend')}
</div>
<div>
{translate('auto.components.automations.AutomationRunHistory.13988187b3', 'Tokens')}
</div>
<div>
{translate('auto.components.automations.AutomationRunHistory.9974a2b429', 'Status')}
</div>
</div>
<div className="divide-y divide-border/50">
{runs.map((run) => {
@@ -115,9 +127,12 @@ export function AutomationRunHistory({
}
title={usageLabel}
>
{run.usage?.status === "known"
{run.usage?.status === 'known'
? formatAutomationTokens(run.usage.totalTokens)
: translate("auto.components.automations.AutomationRunHistory.a00e38d1a3", "n/a")}
: translate(
'auto.components.automations.AutomationRunHistory.a00e38d1a3',
'n/a'
)}
</div>
<div className="flex justify-start">
<Badge variant={getAutomationRunStatusVariant(run.status)}>
@@ -128,7 +143,12 @@ export function AutomationRunHistory({
)
})}
{runs.length === 0 ? (
<div className="px-3 py-6 text-center text-sm text-muted-foreground">{translate("auto.components.automations.AutomationRunHistory.402651bfb6", "No runs yet.")}</div>
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
{translate(
'auto.components.automations.AutomationRunHistory.402651bfb6',
'No runs yet.'
)}
</div>
) : null}
</div>
</div>
@@ -34,7 +34,10 @@ export function AutomationRunPageFrame({
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.automations.AutomationRunPageFrame.33741dd973", "Back to runs")}
aria-label={translate(
'auto.components.automations.AutomationRunPageFrame.33741dd973',
'Back to runs'
)}
onClick={onBack}
>
<ArrowLeft className="size-3.5" />
@@ -46,7 +49,10 @@ export function AutomationRunPageFrame({
</div>
{breadcrumbs.length > 0 ? (
<ol
aria-label={translate("auto.components.automations.AutomationRunPageFrame.40a511bed4", "Run context")}
aria-label={translate(
'auto.components.automations.AutomationRunPageFrame.40a511bed4',
'Run context'
)}
className="mt-0.5 flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs text-muted-foreground"
>
{breadcrumbs.map((breadcrumb, index) => (
@@ -172,7 +172,12 @@ export function AutomationSchedulePicker({
className="popover-scroll-content scrollbar-sleek max-h-[var(--radix-popover-content-available-height)] w-[min(var(--radix-popover-trigger-width),calc(100vw-2rem))] min-w-[min(22rem,calc(100vw-2rem))] max-w-[calc(100vw-2rem)] overflow-y-auto p-3"
>
<div className="grid gap-3">
<Field label={translate("auto.components.automations.AutomationSchedulePicker.233b8c94b6", "Cadence")}>
<Field
label={translate(
'auto.components.automations.AutomationSchedulePicker.233b8c94b6',
'Cadence'
)}
>
<Select
value={draft.preset}
onValueChange={(preset) =>
@@ -194,7 +199,7 @@ export function AutomationSchedulePicker({
</SelectContent>
</Select>
</Field>
{draft.preset === "custom" ? (
{draft.preset === 'custom' ? (
<AutomationCustomCronPanel
draft={draft}
customScheduleInvalid={customScheduleInvalid}
@@ -203,8 +208,13 @@ export function AutomationSchedulePicker({
/>
) : (
<>
{draft.preset === "weekly" ? (
<Field label={translate("auto.components.automations.AutomationSchedulePicker.6b914c5fbb", "Day")}>
{draft.preset === 'weekly' ? (
<Field
label={translate(
'auto.components.automations.AutomationSchedulePicker.6b914c5fbb',
'Day'
)}
>
<Select
value={draft.dayOfWeek}
onValueChange={(dayOfWeek) =>
@@ -224,8 +234,13 @@ export function AutomationSchedulePicker({
</Select>
</Field>
) : null}
{draft.preset === "hourly" ? (
<Field label={translate("auto.components.automations.AutomationSchedulePicker.9e677335b0", "Minute")}>
{draft.preset === 'hourly' ? (
<Field
label={translate(
'auto.components.automations.AutomationSchedulePicker.9e677335b0',
'Minute'
)}
>
<Select
value={String(clockParts.minute)}
onValueChange={(minute) =>
@@ -249,7 +264,12 @@ export function AutomationSchedulePicker({
</Select>
</Field>
) : (
<Field label={translate("auto.components.automations.AutomationSchedulePicker.d90981f766", "Time")}>
<Field
label={translate(
'auto.components.automations.AutomationSchedulePicker.d90981f766',
'Time'
)}
>
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,0.8fr)] gap-2">
<Select
value={String(clockParts.hour12)}
@@ -262,7 +282,10 @@ export function AutomationSchedulePicker({
}
>
<SelectTrigger
aria-label={translate("auto.components.automations.AutomationSchedulePicker.6b802ecc99", "Hour")}
aria-label={translate(
'auto.components.automations.AutomationSchedulePicker.6b802ecc99',
'Hour'
)}
className={cn('w-full min-w-0', FIELD_CONTROL_CLASS)}
>
<SelectValue />
@@ -286,7 +309,10 @@ export function AutomationSchedulePicker({
}
>
<SelectTrigger
aria-label={translate("auto.components.automations.AutomationSchedulePicker.9e677335b0", "Minute")}
aria-label={translate(
'auto.components.automations.AutomationSchedulePicker.9e677335b0',
'Minute'
)}
className={cn('w-full min-w-0', FIELD_CONTROL_CLASS)}
>
<SelectValue />
@@ -310,7 +336,10 @@ export function AutomationSchedulePicker({
}
>
<SelectTrigger
aria-label={translate("auto.components.automations.AutomationSchedulePicker.22359b186a", "AM or PM")}
aria-label={translate(
'auto.components.automations.AutomationSchedulePicker.22359b186a',
'AM or PM'
)}
className={cn('w-full min-w-0', FIELD_CONTROL_CLASS)}
>
<SelectValue />
@@ -21,18 +21,26 @@ export function AutomationSessionField({
<Field
label={
<span className="inline-flex items-center gap-1">
{translate("auto.components.automations.AutomationSessionField.5ad314118e", "Session")}<Tooltip>
{translate('auto.components.automations.AutomationSessionField.5ad314118e', 'Session')}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={translate("auto.components.automations.AutomationSessionField.4bdce31f37", "Session reuse help")}
aria-label={translate(
'auto.components.automations.AutomationSessionField.4bdce31f37',
'Session reuse help'
)}
className="rounded-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Info className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6} className="max-w-72">
{translate("auto.components.automations.AutomationSessionField.b675112193", "Reuse sends future runs to the previous live automation session. If that session is gone, Orca starts a fresh one.")}</TooltipContent>
{translate(
'auto.components.automations.AutomationSessionField.b675112193',
'Reuse sends future runs to the previous live automation session. If that session is gone, Orca starts a fresh one.'
)}
</TooltipContent>
</Tooltip>
</span>
}
@@ -55,9 +63,11 @@ export function AutomationSessionField({
className="grid w-full grid-cols-2"
>
<ToggleGroupItem value="fresh" className={toggleItemClassName}>
{translate("auto.components.automations.AutomationSessionField.c90888ee94", "Fresh")}</ToggleGroupItem>
{translate('auto.components.automations.AutomationSessionField.c90888ee94', 'Fresh')}
</ToggleGroupItem>
<ToggleGroupItem value="reuse" className={toggleItemClassName}>
{translate("auto.components.automations.AutomationSessionField.f3c76dce51", "Reuse")}</ToggleGroupItem>
{translate('auto.components.automations.AutomationSessionField.f3c76dce51', 'Reuse')}
</ToggleGroupItem>
</ToggleGroup>
</Field>
)
@@ -176,7 +176,12 @@ export function CreateFromPicker({
className={cn('h-9 w-full justify-between px-3 text-sm font-normal', triggerClassName)}
>
<span className="flex min-w-0 items-center gap-1.5">
<span className="shrink-0 text-muted-foreground">{translate("auto.components.automations.CreateFromPicker.dd3841b442", "Branch from")}</span>
<span className="shrink-0 text-muted-foreground">
{translate(
'auto.components.automations.CreateFromPicker.dd3841b442',
'Branch from'
)}
</span>
<span className="truncate">{selectedLabel}</span>
</span>
<ChevronsUpDown className="size-4 opacity-50" />
@@ -195,11 +200,22 @@ export function CreateFromPicker({
ref={setInputNode}
value={query}
onValueChange={setQuery}
placeholder={translate("auto.components.automations.CreateFromPicker.f061f49e3f", "Search repo branches...")}
placeholder={translate(
'auto.components.automations.CreateFromPicker.f061f49e3f',
'Search repo branches...'
)}
/>
<CommandList className="max-h-72">
<CommandEmpty>
{isSearching ? translate("auto.components.automations.CreateFromPicker.9ce96621f4", "Searching branches...") : translate("auto.components.automations.CreateFromPicker.79512f22a7", "No branches found.")}
{isSearching
? translate(
'auto.components.automations.CreateFromPicker.9ce96621f4',
'Searching branches...'
)
: translate(
'auto.components.automations.CreateFromPicker.79512f22a7',
'No branches found.'
)}
</CommandEmpty>
<CommandItem
value={effectiveDefault ? `${effectiveDefault} default` : 'project default'}
@@ -215,7 +231,16 @@ export function CreateFromPicker({
)}
/>
<span className="truncate">
{effectiveDefault ? translate("auto.components.automations.CreateFromPicker.e53d306056", "{{value0}} (default)", { value0: effectiveDefault }) : translate("auto.components.automations.CreateFromPicker.ef6d762538", "Project default")}
{effectiveDefault
? translate(
'auto.components.automations.CreateFromPicker.e53d306056',
'{{value0}} (default)',
{ value0: effectiveDefault }
)
: translate(
'auto.components.automations.CreateFromPicker.ef6d762538',
'Project default'
)}
</span>
</CommandItem>
{branchOptions
@@ -111,10 +111,24 @@ export function ExternalAutomationManagers({
<div className="rounded-md border border-border/50 bg-muted/20 shadow-sm">
<div className="flex items-center justify-between border-b border-border/50 px-3 py-2">
<div>
<div className="text-sm font-medium">{translate("auto.components.automations.ExternalAutomationManagers.c6695e6fbd", "External automations")}</div>
<div className="text-sm font-medium">
{translate(
'auto.components.automations.ExternalAutomationManagers.c6695e6fbd',
'External automations'
)}
</div>
</div>
<Badge variant="outline">
{automationCount} {automationCount === 1 ? translate("auto.components.automations.ExternalAutomationManagers.701515f010", "automation") : translate("auto.components.automations.ExternalAutomationManagers.e2532150ed", "automations")}
{automationCount}{' '}
{automationCount === 1
? translate(
'auto.components.automations.ExternalAutomationManagers.701515f010',
'automation'
)
: translate(
'auto.components.automations.ExternalAutomationManagers.e2532150ed',
'automations'
)}
</Badge>
</div>
<div className="divide-y divide-border/50">
@@ -127,9 +141,18 @@ export function ExternalAutomationManagers({
{getProviderLabel(manager)} / {getTargetKindLabel(manager)} ·{' '}
{manager.status === 'available'
? manager.canManage
? translate("auto.components.automations.ExternalAutomationManagers.0a2d4359a8", "Manageable")
: translate("auto.components.automations.ExternalAutomationManagers.dbdcec22bd", "Read-only")
: translate("auto.components.automations.ExternalAutomationManagers.92405f1431", "Unavailable")}
? translate(
'auto.components.automations.ExternalAutomationManagers.0a2d4359a8',
'Manageable'
)
: translate(
'auto.components.automations.ExternalAutomationManagers.dbdcec22bd',
'Read-only'
)
: translate(
'auto.components.automations.ExternalAutomationManagers.92405f1431',
'Unavailable'
)}
{manager.error ? ` - ${manager.error}` : null}
</div>
</div>
@@ -149,19 +172,45 @@ export function ExternalAutomationManagers({
<div className="flex min-w-0 items-center gap-2">
<span className="truncate font-medium">{job.name}</span>
<Badge variant={job.enabled ? 'secondary' : 'outline'}>
{job.enabled ? translate("auto.components.automations.ExternalAutomationManagers.b3feba84c7", "Active") : translate("auto.components.automations.ExternalAutomationManagers.2b0adbce21", "Paused")}
{job.enabled
? translate(
'auto.components.automations.ExternalAutomationManagers.b3feba84c7',
'Active'
)
: translate(
'auto.components.automations.ExternalAutomationManagers.2b0adbce21',
'Paused'
)}
</Badge>
</div>
<div className="mt-1 truncate text-xs font-medium text-foreground/80">
{scheduleDisplay.label}
</div>
<div className="mt-1 truncate text-xs text-muted-foreground">
{translate("auto.components.automations.ExternalAutomationManagers.20fd7a3a15", "next")} {formatExternalDate(job.nextRunAt, now)} · {getProviderLabel(manager)}{' '}
/ {manager.targetLabel}
{translate(
'auto.components.automations.ExternalAutomationManagers.20fd7a3a15',
'next'
)}{' '}
{formatExternalDate(job.nextRunAt, now)} · {getProviderLabel(manager)} /{' '}
{manager.targetLabel}
</div>
{manager.provider === "hermes" ? (
{manager.provider === 'hermes' ? (
<div className="mt-1 truncate text-xs text-muted-foreground">
{job.runCount} {job.runCount === 1 ? translate("auto.components.automations.ExternalAutomationManagers.8e9165af08", "run") : translate("auto.components.automations.ExternalAutomationManagers.e66091daf4", "runs")} {translate("auto.components.automations.ExternalAutomationManagers.844f1acb72", "found")}</div>
{job.runCount}{' '}
{job.runCount === 1
? translate(
'auto.components.automations.ExternalAutomationManagers.8e9165af08',
'run'
)
: translate(
'auto.components.automations.ExternalAutomationManagers.e66091daf4',
'runs'
)}{' '}
{translate(
'auto.components.automations.ExternalAutomationManagers.844f1acb72',
'found'
)}
</div>
) : null}
{job.promptPreview || job.lastError ? (
<div className="mt-1 truncate text-xs text-muted-foreground">
@@ -170,12 +219,19 @@ export function ExternalAutomationManagers({
) : null}
</div>
<div className="hidden min-w-0 text-xs text-muted-foreground md:block">
{translate("auto.components.automations.ExternalAutomationManagers.5820648765", "Last")}{formatExternalDate(job.lastRunAt, now)}
{translate(
'auto.components.automations.ExternalAutomationManagers.5820648765',
'Last'
)}
{formatExternalDate(job.lastRunAt, now)}
{job.lastStatus ? ` · ${job.lastStatus}` : null}
</div>
<div className="flex items-center justify-end gap-1">
<ExternalActionButton
label={translate("auto.components.automations.ExternalAutomationManagers.cc77ba88ff", "Run external automation")}
label={translate(
'auto.components.automations.ExternalAutomationManagers.cc77ba88ff',
'Run external automation'
)}
disabled={!manager.canManage || runningActionKey !== null}
onClick={() => onAction(manager, job, 'run')}
>
@@ -185,9 +241,12 @@ export function ExternalAutomationManagers({
<Play className="size-3.5" />
)}
</ExternalActionButton>
{manager.provider === "hermes" ? (
{manager.provider === 'hermes' ? (
<ExternalActionButton
label={translate("auto.components.automations.ExternalAutomationManagers.1df491fd00", "Edit external automation")}
label={translate(
'auto.components.automations.ExternalAutomationManagers.1df491fd00',
'Edit external automation'
)}
disabled={!manager.canManage || runningActionKey !== null}
onClick={() => onEdit?.(manager, job)}
>
@@ -196,7 +255,15 @@ export function ExternalAutomationManagers({
) : null}
<ExternalActionButton
label={
job.enabled ? translate("auto.components.automations.ExternalAutomationManagers.0def1693bb", "Pause external automation") : translate("auto.components.automations.ExternalAutomationManagers.1c3bfd38fe", "Resume external automation")
job.enabled
? translate(
'auto.components.automations.ExternalAutomationManagers.0def1693bb',
'Pause external automation'
)
: translate(
'auto.components.automations.ExternalAutomationManagers.1c3bfd38fe',
'Resume external automation'
)
}
disabled={!manager.canManage || runningActionKey !== null}
onClick={() => onAction(manager, job, job.enabled ? 'pause' : 'resume')}
@@ -211,7 +278,10 @@ export function ExternalAutomationManagers({
)}
</ExternalActionButton>
<ExternalActionButton
label={translate("auto.components.automations.ExternalAutomationManagers.a42bf2b27e", "Delete external automation")}
label={translate(
'auto.components.automations.ExternalAutomationManagers.a42bf2b27e',
'Delete external automation'
)}
className="text-destructive hover:text-destructive"
disabled={!manager.canManage || runningActionKey !== null}
onClick={() => onAction(manager, job, 'delete')}
@@ -223,7 +293,7 @@ export function ExternalAutomationManagers({
)}
</ExternalActionButton>
</div>
{manager.provider === "hermes" ? (
{manager.provider === 'hermes' ? (
<div className="col-span-3">
<ExternalAutomationRunTable
manager={manager}
@@ -239,15 +309,35 @@ export function ExternalAutomationManagers({
})}
{manager.jobs.length === 0 ? (
<div className="px-3 py-4 text-sm text-muted-foreground">
{translate("auto.components.automations.ExternalAutomationManagers.3d58d5b67d", "No")}{' '}
{manager.provider === 'hermes' ? translate("auto.components.automations.ExternalAutomationManagers.766abf833c", "Hermes") : translate("auto.components.automations.ExternalAutomationManagers.5524365227", "OpenClaw")} {translate("auto.components.automations.ExternalAutomationManagers.6da3bfba4b", "automations found.")}</div>
{translate(
'auto.components.automations.ExternalAutomationManagers.3d58d5b67d',
'No'
)}{' '}
{manager.provider === 'hermes'
? translate(
'auto.components.automations.ExternalAutomationManagers.766abf833c',
'Hermes'
)
: translate(
'auto.components.automations.ExternalAutomationManagers.5524365227',
'OpenClaw'
)}{' '}
{translate(
'auto.components.automations.ExternalAutomationManagers.6da3bfba4b',
'automations found.'
)}
</div>
) : null}
</div>
</div>
))}
{managers.length === 0 ? (
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
{translate("auto.components.automations.ExternalAutomationManagers.e02f970595", "No external automation managers found.")}</div>
{translate(
'auto.components.automations.ExternalAutomationManagers.e02f970595',
'No external automation managers found.'
)}
</div>
) : null}
</div>
</div>
@@ -183,7 +183,9 @@ export function ExternalAutomationRunTable({
<div className="mt-2 rounded-md border border-border/50 bg-background/50">
<div className="flex items-center justify-between border-b border-border/50 px-3 py-2">
<div className="flex min-w-0 items-center gap-2">
<div className="text-xs font-medium">{translate("auto.components.automations.ExternalAutomationRunTable.2d4388a908", "Runs")}</div>
<div className="text-xs font-medium">
{translate('auto.components.automations.ExternalAutomationRunTable.2d4388a908', 'Runs')}
</div>
{isLoading ? <Loader2 className="size-3.5 animate-spin text-muted-foreground" /> : null}
{fetchError ? (
<Tooltip>
@@ -197,7 +199,13 @@ export function ExternalAutomationRunTable({
) : null}
</div>
<div className="text-xs text-muted-foreground">
{totalCount} {totalCount === 1 ? translate("auto.components.automations.ExternalAutomationRunTable.872d032d05", "run") : translate("auto.components.automations.ExternalAutomationRunTable.d5527d8fe7", "runs")}
{totalCount}{' '}
{totalCount === 1
? translate('auto.components.automations.ExternalAutomationRunTable.872d032d05', 'run')
: translate(
'auto.components.automations.ExternalAutomationRunTable.d5527d8fe7',
'runs'
)}
</div>
</div>
@@ -205,9 +213,24 @@ export function ExternalAutomationRunTable({
<div>
<div className="min-w-0 border-b border-border/50">
<div className="grid grid-cols-[minmax(7.5rem,.45fr)_minmax(0,1fr)_auto] gap-3 border-b border-border/50 px-3 py-1.5 text-[11px] font-medium uppercase text-muted-foreground">
<span>{translate("auto.components.automations.ExternalAutomationRunTable.d4b34feb66", "Run time")}</span>
<span>{translate("auto.components.automations.ExternalAutomationRunTable.a813df9808", "Preview")}</span>
<span>{translate("auto.components.automations.ExternalAutomationRunTable.be551397ca", "Status")}</span>
<span>
{translate(
'auto.components.automations.ExternalAutomationRunTable.d4b34feb66',
'Run time'
)}
</span>
<span>
{translate(
'auto.components.automations.ExternalAutomationRunTable.a813df9808',
'Preview'
)}
</span>
<span>
{translate(
'auto.components.automations.ExternalAutomationRunTable.be551397ca',
'Status'
)}
</span>
</div>
<div className="divide-y divide-border/50">
{visibleRuns.map((run) => (
@@ -248,7 +271,15 @@ export function ExternalAutomationRunTable({
</div>
) : (
<div className="px-3 py-4 text-sm text-muted-foreground">
{isLoading ? translate("auto.components.automations.ExternalAutomationRunTable.8ea934cacf", "Loading runs...") : translate("auto.components.automations.ExternalAutomationRunTable.9c080765ff", "No Hermes runs found yet.")}
{isLoading
? translate(
'auto.components.automations.ExternalAutomationRunTable.8ea934cacf',
'Loading runs...'
)
: translate(
'auto.components.automations.ExternalAutomationRunTable.9c080765ff',
'No Hermes runs found yet.'
)}
</div>
)}
@@ -256,7 +287,9 @@ export function ExternalAutomationRunTable({
<div className="flex min-w-0 items-center gap-2 text-xs text-muted-foreground">
<FileText className="size-3.5" />
<span>
{pageStart}-{pageEnd} {translate("auto.components.automations.ExternalAutomationRunTable.7475c0ce96", "of")}{totalCount}
{pageStart}-{pageEnd}{' '}
{translate('auto.components.automations.ExternalAutomationRunTable.7475c0ce96', 'of')}
{totalCount}
</span>
</div>
<div className="flex items-center gap-1">
@@ -264,7 +297,10 @@ export function ExternalAutomationRunTable({
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.automations.ExternalAutomationRunTable.52d468a0b8", "Previous run page")}
aria-label={translate(
'auto.components.automations.ExternalAutomationRunTable.52d468a0b8',
'Previous run page'
)}
disabled={page === 0 || isLoading}
onClick={() => handlePageChange(Math.max(0, page - 1))}
>
@@ -277,7 +313,10 @@ export function ExternalAutomationRunTable({
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.automations.ExternalAutomationRunTable.0ba9c0a95c", "Next run page")}
aria-label={translate(
'auto.components.automations.ExternalAutomationRunTable.0ba9c0a95c',
'Next run page'
)}
disabled={page >= totalPages - 1 || isLoading}
onClick={() => handlePageChange(Math.min(totalPages - 1, page + 1))}
>
@@ -303,7 +303,10 @@ export function HermesCronOutputView({ content }: { content: string }): React.JS
) : null}
{errorSection ? (
<SectionCard title={translate("auto.components.automations.HermesCronOutputView.05affc68e3", "Error")} accent="error">
<SectionCard
title={translate('auto.components.automations.HermesCronOutputView.05affc68e3', 'Error')}
accent="error"
>
<CommentMarkdown
variant="document"
content={errorSection.body}
@@ -313,7 +316,13 @@ export function HermesCronOutputView({ content }: { content: string }): React.JS
) : null}
{responseSection ? (
<SectionCard title={translate("auto.components.automations.HermesCronOutputView.4557213074", "Response")} accent="response">
<SectionCard
title={translate(
'auto.components.automations.HermesCronOutputView.4557213074',
'Response'
)}
accent="response"
>
<CommentMarkdown
variant="document"
content={responseSection.body}
@@ -324,7 +333,7 @@ export function HermesCronOutputView({ content }: { content: string }): React.JS
{promptSection ? (
<CollapsibleSection
title={translate("auto.components.automations.HermesCronOutputView.e27c716b43", "Prompt")}
title={translate('auto.components.automations.HermesCronOutputView.e27c716b43', 'Prompt')}
tone="muted"
icon={MessageSquare}
iconClass="text-indigo-700 dark:text-indigo-400"
@@ -75,7 +75,11 @@ export function WorkspaceCombobox({
className={cn('h-9 w-full justify-between px-3 text-sm font-normal', triggerClassName)}
>
<span className={cn('truncate', !selected && 'text-muted-foreground')}>
{selected?.displayName ?? translate("auto.components.automations.WorkspaceCombobox.66a0cd9628", "Select workspace")}
{selected?.displayName ??
translate(
'auto.components.automations.WorkspaceCombobox.66a0cd9628',
'Select workspace'
)}
</span>
<ChevronsUpDown className="size-4 opacity-50" />
</Button>
@@ -89,9 +93,20 @@ export function WorkspaceCombobox({
}}
>
<Command>
<CommandInput ref={setInputNode} placeholder={translate("auto.components.automations.WorkspaceCombobox.8e9c8cc6b5", "Search workspaces...")} />
<CommandInput
ref={setInputNode}
placeholder={translate(
'auto.components.automations.WorkspaceCombobox.8e9c8cc6b5',
'Search workspaces...'
)}
/>
<CommandList className="max-h-72">
<CommandEmpty>{translate("auto.components.automations.WorkspaceCombobox.ee5b280eba", "No workspaces found.")}</CommandEmpty>
<CommandEmpty>
{translate(
'auto.components.automations.WorkspaceCombobox.ee5b280eba',
'No workspaces found.'
)}
</CommandEmpty>
{worktrees.map((worktree) => (
<CommandItem
key={worktree.id}
@@ -41,6 +41,9 @@ export function getExternalAutomationScheduleDisplay(
}
return {
label: translate("auto.components.automations.external.automation.schedule.display.a8e92b815a", "Schedule unavailable")
label: translate(
'auto.components.automations.external.automation.schedule.display.a8e92b815a',
'Schedule unavailable'
)
}
}
@@ -152,13 +152,22 @@ export default function BrowserFind({
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={translate("auto.components.browser.pane.BrowserFind.636a69cd66", "Find in page...")}
placeholder={translate(
'auto.components.browser.pane.BrowserFind.636a69cd66',
'Find in page...'
)}
className="min-w-0 flex-1 border-none bg-transparent text-sm text-white outline-none placeholder:text-zinc-500"
/>
{query ? (
<span className="shrink-0 text-xs text-zinc-400">
{totalMatches > 0 ? translate("auto.components.browser.pane.BrowserFind.fc63f336aa", "{{value0}} of {{value1}}", { value0: activeMatch, value1: totalMatches }) : translate("auto.components.browser.pane.BrowserFind.7baca7b1b8", "No matches")}
{totalMatches > 0
? translate(
'auto.components.browser.pane.BrowserFind.fc63f336aa',
'{{value0}} of {{value1}}',
{ value0: activeMatch, value1: totalMatches }
)
: translate('auto.components.browser.pane.BrowserFind.7baca7b1b8', 'No matches')}
</span>
) : null}
@@ -170,7 +179,7 @@ export default function BrowserFind({
size="icon-xs"
onClick={findPrevious}
className="flex size-6 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-200"
title={translate("auto.components.browser.pane.BrowserFind.ca7aebbd7f", "Previous match")}
title={translate('auto.components.browser.pane.BrowserFind.ca7aebbd7f', 'Previous match')}
>
<ChevronUp size={14} />
</Button>
@@ -181,7 +190,7 @@ export default function BrowserFind({
size="icon-xs"
onClick={findNext}
className="flex size-6 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-200"
title={translate("auto.components.browser.pane.BrowserFind.5c0c02ae76", "Next match")}
title={translate('auto.components.browser.pane.BrowserFind.5c0c02ae76', 'Next match')}
>
<ChevronDown size={14} />
</Button>
@@ -194,7 +203,7 @@ export default function BrowserFind({
size="icon-xs"
onClick={onClose}
className="flex size-6 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-200"
title={translate("auto.components.browser.pane.BrowserFind.c9d5f63fdc", "Close")}
title={translate('auto.components.browser.pane.BrowserFind.c9d5f63fdc', 'Close')}
>
<X size={14} />
</Button>
@@ -53,15 +53,33 @@ export function BrowserMobileDriverOverlay({ driver, onTakeBack }: Props): React
<div className="pointer-events-auto flex w-full max-w-[30rem] flex-col gap-3 rounded-lg border border-border bg-card p-6 pb-5 text-card-foreground shadow-xs">
<div className="flex items-center gap-1.5 text-xs font-medium text-foreground">
<span aria-hidden="true"></span>
<span>{translate("auto.components.browser.pane.BrowserMobileDriverOverlay.20539eca03", "Mobile is driving this browser")}</span>
<span>
{translate(
'auto.components.browser.pane.BrowserMobileDriverOverlay.20539eca03',
'Mobile is driving this browser'
)}
</span>
</div>
<div className="text-base font-semibold leading-tight">
{translate(
'auto.components.browser.pane.BrowserMobileDriverOverlay.d9768ec642',
'Browser input is paused'
)}
</div>
<div className="text-base font-semibold leading-tight">{translate("auto.components.browser.pane.BrowserMobileDriverOverlay.d9768ec642", "Browser input is paused")}</div>
<div className="text-sm leading-relaxed text-muted-foreground">
{translate("auto.components.browser.pane.BrowserMobileDriverOverlay.f4ecd61552", "This tab is being controlled from your phone. Take back to use it on desktop.")}</div>
{translate(
'auto.components.browser.pane.BrowserMobileDriverOverlay.f4ecd61552',
'This tab is being controlled from your phone. Take back to use it on desktop.'
)}
</div>
<div className="mt-1 flex justify-end">
{/* autoFocus puts keyboard users on the recovery action when the lock appears. */}
<Button type="button" size="sm" onClick={handleTakeBack} disabled={pending} autoFocus>
{translate("auto.components.browser.pane.BrowserMobileDriverOverlay.a6914ee43f", "Take back")}</Button>
{translate(
'auto.components.browser.pane.BrowserMobileDriverOverlay.a6914ee43f',
'Take back'
)}
</Button>
</div>
</div>
</div>
@@ -4173,6 +4173,10 @@ function BrowserPagePane({
[annotationBannerSendModeId, annotationTraySendModeId, closeAgentSendPopoverTargetMode]
)
const handleBrowserAnnotationsSentToAgent = useCallback((): void => {
recordFeatureInteraction('browser-annotations-sent-to-agent')
}, [recordFeatureInteraction])
const handleClearBrowserAnnotations = useCallback((): void => {
if (browserAnnotationsRef.current.length === 0) {
return
@@ -4877,6 +4881,7 @@ function BrowserPagePane({
prompt={browserAnnotationsPrompt}
promptDelivery="submit-after-ready"
launchSource="notes_send"
onPromptDelivered={handleBrowserAnnotationsSentToAgent}
/>
</DropdownMenuContent>
</DropdownMenu>
@@ -5136,6 +5141,7 @@ function BrowserPagePane({
prompt={browserAnnotationsPrompt}
promptDelivery="submit-after-ready"
launchSource="notes_send"
onPromptDelivered={handleBrowserAnnotationsSentToAgent}
/>
</DropdownMenuContent>
</DropdownMenu>
@@ -128,9 +128,14 @@ export default function GrabConfirmationSheet({
<div className="flex items-center justify-between border-b border-border/70 px-4 py-3">
<div className="flex items-center gap-2">
<div className="rounded-md bg-indigo-500/10 px-2 py-0.5 text-xs font-medium text-indigo-400">
{translate("auto.components.browser.pane.GrabConfirmationSheet.f3575229df", "Grab")}</div>
{translate('auto.components.browser.pane.GrabConfirmationSheet.f3575229df', 'Grab')}
</div>
<span className="text-sm text-muted-foreground">
{translate("auto.components.browser.pane.GrabConfirmationSheet.50f7114f99", "Review before attaching. Captured page context may include visible site content.")}</span>
{translate(
'auto.components.browser.pane.GrabConfirmationSheet.50f7114f99',
'Review before attaching. Captured page context may include visible site content.'
)}
</span>
</div>
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={onCancel}>
<X className="size-4" />
@@ -147,7 +152,10 @@ export default function GrabConfirmationSheet({
<div className="overflow-hidden rounded-lg border border-border/60">
<img
src={payload.screenshot.dataUrl}
alt={translate("auto.components.browser.pane.GrabConfirmationSheet.9c6ce0632a", "Selected element screenshot")}
alt={translate(
'auto.components.browser.pane.GrabConfirmationSheet.9c6ce0632a',
'Selected element screenshot'
)}
className="max-h-48 w-full object-contain bg-black/5"
/>
</div>
@@ -156,7 +164,11 @@ export default function GrabConfirmationSheet({
{/* Element summary */}
<div className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{translate("auto.components.browser.pane.GrabConfirmationSheet.a759d8f866", "Selected Element")}</h3>
{translate(
'auto.components.browser.pane.GrabConfirmationSheet.a759d8f866',
'Selected Element'
)}
</h3>
<div className="rounded-lg border border-border/60 bg-muted/20 p-3 text-sm">
<div className="flex items-baseline gap-2">
<span className="font-mono font-semibold text-foreground">
@@ -164,14 +176,20 @@ export default function GrabConfirmationSheet({
</span>
{target.accessibility.role ? (
<span className="text-xs text-muted-foreground">
{translate("auto.components.browser.pane.GrabConfirmationSheet.d053db279d", "role=")}<EscapedText text={target.accessibility.role} />
{translate(
'auto.components.browser.pane.GrabConfirmationSheet.d053db279d',
'role='
)}
<EscapedText text={target.accessibility.role} />
</span>
) : null}
</div>
{target.accessibility.accessibleName ? (
<div className="mt-1 text-muted-foreground">
{translate("auto.components.browser.pane.GrabConfirmationSheet.eb98a0971a", "\"")}<EscapedText text={target.accessibility.accessibleName} />
{translate("auto.components.browser.pane.GrabConfirmationSheet.eb98a0971a", "\"")}</div>
{translate('auto.components.browser.pane.GrabConfirmationSheet.eb98a0971a', '"')}
<EscapedText text={target.accessibility.accessibleName} />
{translate('auto.components.browser.pane.GrabConfirmationSheet.eb98a0971a', '"')}
</div>
) : null}
<div className="mt-1 font-mono text-xs text-muted-foreground/70">
<EscapedText text={target.selector} />
@@ -185,10 +203,19 @@ export default function GrabConfirmationSheet({
{/* Page info */}
<div className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{translate("auto.components.browser.pane.GrabConfirmationSheet.9098b118ab", "Page")}</h3>
{translate('auto.components.browser.pane.GrabConfirmationSheet.9098b118ab', 'Page')}
</h3>
<div className="rounded-lg border border-border/60 bg-muted/20 p-3 text-sm">
<div className="font-medium text-foreground">
<EscapedText text={page.title || translate("auto.components.browser.pane.GrabConfirmationSheet.405bb315da", "Untitled")} />
<EscapedText
text={
page.title ||
translate(
'auto.components.browser.pane.GrabConfirmationSheet.405bb315da',
'Untitled'
)
}
/>
</div>
<div className="mt-0.5 text-xs text-muted-foreground/70">
<EscapedText text={page.sanitizedUrl} />
@@ -200,7 +227,8 @@ export default function GrabConfirmationSheet({
{target.htmlSnippet ? (
<div className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{translate("auto.components.browser.pane.GrabConfirmationSheet.7d1480fbf1", "HTML")}</h3>
{translate('auto.components.browser.pane.GrabConfirmationSheet.7d1480fbf1', 'HTML')}
</h3>
<pre className="max-h-32 overflow-auto rounded-lg border border-border/60 bg-muted/20 p-3 font-mono text-xs text-foreground/80 scrollbar-sleek">
<EscapedText text={target.htmlSnippet} />
</pre>
@@ -211,7 +239,11 @@ export default function GrabConfirmationSheet({
{nearbyText.length > 0 ? (
<div className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{translate("auto.components.browser.pane.GrabConfirmationSheet.effd75e330", "Nearby Context")}</h3>
{translate(
'auto.components.browser.pane.GrabConfirmationSheet.effd75e330',
'Nearby Context'
)}
</h3>
<div className="rounded-lg border border-border/60 bg-muted/20 p-3">
<ul className="list-inside list-disc space-y-0.5 text-sm text-muted-foreground">
{nearbyText.map((text, i) => (
@@ -229,18 +261,28 @@ export default function GrabConfirmationSheet({
{/* Actions */}
<div className="flex items-center justify-end gap-2 border-t border-border/70 px-4 py-3">
<Button variant="ghost" size="sm" onClick={onCancel}>
{translate("auto.components.browser.pane.GrabConfirmationSheet.87d97bdd6d", "Cancel")}</Button>
{translate('auto.components.browser.pane.GrabConfirmationSheet.87d97bdd6d', 'Cancel')}
</Button>
<Button variant="outline" size="sm" className="gap-1.5" onClick={onCopy}>
<Copy className="size-3.5" />
{translate("auto.components.browser.pane.GrabConfirmationSheet.26fd87f4df", "Copy")}</Button>
{translate('auto.components.browser.pane.GrabConfirmationSheet.26fd87f4df', 'Copy')}
</Button>
{onCopyScreenshot ? (
<Button variant="outline" size="sm" className="gap-1.5" onClick={onCopyScreenshot}>
<Image className="size-3.5" />
{translate("auto.components.browser.pane.GrabConfirmationSheet.7095e98362", "Copy Screenshot")}</Button>
{translate(
'auto.components.browser.pane.GrabConfirmationSheet.7095e98362',
'Copy Screenshot'
)}
</Button>
) : null}
<Button size="sm" className="gap-1.5" onClick={onAttach}>
<MessageSquarePlus className="size-3.5" />
{translate("auto.components.browser.pane.GrabConfirmationSheet.314a0aaa5b", "Attach to AI")}</Button>
{translate(
'auto.components.browser.pane.GrabConfirmationSheet.314a0aaa5b',
'Attach to AI'
)}
</Button>
</div>
</div>
)
@@ -83,7 +83,11 @@ export function buildBrowserAddressBarSuggestions({
topAction = {
url: buildSearchUrl(trimmed, searchEngine, { kagiSessionLink }),
title: trimmed,
subtitle: translate("auto.components.browser.pane.browser.address.bar.suggestions.87fcdd0da9", "{{value0}} Search", { value0: SEARCH_ENGINE_LABELS[searchEngine] }),
subtitle: translate(
'auto.components.browser.pane.browser.address.bar.suggestions.87fcdd0da9',
'{{value0}} Search',
{ value0: SEARCH_ENGINE_LABELS[searchEngine] }
),
lastVisitedAt: 0,
visitCount: 0,
isSearch: true
@@ -99,14 +99,16 @@ export function ConfirmationDialogProvider({
</DialogHeader>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => settleActiveRequest(false)}>
{displayedRequest?.options.cancelLabel ?? translate("auto.components.confirmation.dialog.56f5c60e0c", "Cancel")}
{displayedRequest?.options.cancelLabel ??
translate('auto.components.confirmation.dialog.56f5c60e0c', 'Cancel')}
</Button>
<Button
type="button"
variant={displayedRequest?.options.confirmVariant ?? 'default'}
onClick={() => settleActiveRequest(true)}
>
{displayedRequest?.options.confirmLabel ?? translate("auto.components.confirmation.dialog.8490e5d36a", "Confirm")}
{displayedRequest?.options.confirmLabel ??
translate('auto.components.confirmation.dialog.8490e5d36a', 'Confirm')}
</Button>
</DialogFooter>
</DialogContent>
@@ -36,15 +36,27 @@ function AutoRenameBranchFromWorkControl(): JSX.Element {
<div className="mt-3 rounded-md border border-border/70 bg-muted/35 px-3 py-2.5">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-xs font-medium text-foreground">{translate("auto.components.contextual.tours.ContextualTourControl.731c5573df", "Auto-name from first message")}</div>
<div className="text-xs font-medium text-foreground">
{translate(
'auto.components.contextual.tours.ContextualTourControl.731c5573df',
'Auto-name from first message'
)}
</div>
<div className="mt-0.5 text-[11px] leading-4 text-muted-foreground">
{translate("auto.components.contextual.tours.ContextualTourControl.02e8373219", "Auto-generates a new name when you leave this text box empty.")}</div>
{translate(
'auto.components.contextual.tours.ContextualTourControl.02e8373219',
'Auto-generates a new name when you leave this text box empty.'
)}
</div>
</div>
<button
type="button"
role="switch"
aria-checked={enabled}
aria-label={translate("auto.components.contextual.tours.ContextualTourControl.186eecc34f", "Auto-name workspace from first agent message")}
aria-label={translate(
'auto.components.contextual.tours.ContextualTourControl.186eecc34f',
'Auto-name workspace from first agent message'
)}
onClick={() => {
toggleAutoRenameBranchFromWork({
enabled,
@@ -132,7 +132,17 @@ export function ContextualTourOverlaySurface({
type="button"
variant="ghost"
size="icon-xs"
aria-label={renderState.isLastStep ? translate("auto.components.contextual.tours.ContextualTourOverlaySurface.d974f32a83", "Dismiss tour") : translate("auto.components.contextual.tours.ContextualTourOverlaySurface.4f86e2a10b", "Skip tour")}
aria-label={
renderState.isLastStep
? translate(
'auto.components.contextual.tours.ContextualTourOverlaySurface.d974f32a83',
'Dismiss tour'
)
: translate(
'auto.components.contextual.tours.ContextualTourOverlaySurface.4f86e2a10b',
'Skip tour'
)
}
onClick={() => onSkip(activeTourId)}
className="absolute right-2 top-2 text-muted-foreground hover:text-foreground"
>
@@ -150,9 +160,22 @@ export function ContextualTourOverlaySurface({
/>
<div className="flex items-center gap-1.5">
{!renderState.isFirstStep ? (
<Button type="button" variant="ghost" size="xs" aria-label={translate("auto.components.contextual.tours.ContextualTourOverlaySurface.4a9568f773", "Back")} onClick={onBack}>
<Button
type="button"
variant="ghost"
size="xs"
aria-label={translate(
'auto.components.contextual.tours.ContextualTourOverlaySurface.4a9568f773',
'Back'
)}
onClick={onBack}
>
<ArrowLeft />
{translate("auto.components.contextual.tours.ContextualTourOverlaySurface.4a9568f773", "Back")}</Button>
{translate(
'auto.components.contextual.tours.ContextualTourOverlaySurface.4a9568f773',
'Back'
)}
</Button>
) : null}
{renderState.secondaryAction ? (
<Button
@@ -176,7 +199,7 @@ export function ContextualTourOverlaySurface({
}
>
{primaryAction.label}
{primaryAction.kind === "next" && !renderState.isLastStep ? <ArrowRight /> : null}
{primaryAction.kind === 'next' && !renderState.isLastStep ? <ArrowRight /> : null}
</Button>
) : null}
</div>
@@ -19,7 +19,11 @@ export function ContextualTourProgressDots({
aria-valuemin={1}
aria-valuemax={total}
aria-valuenow={current}
aria-label={translate("auto.components.contextual.tours.ContextualTourProgressDots.dcd6e6b03e", "Step {{value0}} of {{value1}}", { value0: current, value1: total })}
aria-label={translate(
'auto.components.contextual.tours.ContextualTourProgressDots.dcd6e6b03e',
'Step {{value0}} of {{value1}}',
{ value0: current, value1: total }
)}
>
<span className="flex items-center gap-1.5" aria-hidden="true">
{Array.from({ length: total }).map((_, index) => {
@@ -41,7 +45,9 @@ export function ContextualTourProgressDots({
})}
</span>
<span className="whitespace-nowrap text-[11px] font-medium leading-none text-muted-foreground">
{current} {translate("auto.components.contextual.tours.ContextualTourProgressDots.7734cb8ad3", "of")}{total}
{current}{' '}
{translate('auto.components.contextual.tours.ContextualTourProgressDots.7734cb8ad3', 'of')}
{total}
</span>
</div>
)
@@ -118,7 +118,13 @@ export function measureContextualTourOverlayRenderState(args: {
const sidebarAlreadyVisible =
activeStep.primaryAction?.kind === 'show-worktrees' && args.sidebarOpen
const primaryAction = sidebarAlreadyVisible
? ({ kind: 'next', label: translate("auto.components.contextual.tours.contextual.tour.overlay.measurement.38b3155418", "Next") } as const)
? ({
kind: 'next',
label: translate(
'auto.components.contextual.tours.contextual.tour.overlay.measurement.38b3155418',
'Next'
)
} as const)
: activeStep.primaryAction
const secondaryAction = sidebarAlreadyVisible ? undefined : activeStep.secondaryAction
@@ -52,7 +52,15 @@ export function DashboardAgentChildDisclosure({
onMouseDown={stopMouseDown}
onKeyDown={stopKeyDown}
className="-ml-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded-sm border border-sidebar-border/80 bg-sidebar text-foreground/80 shadow-xs hover:bg-sidebar-accent hover:text-foreground"
aria-label={translate("auto.components.dashboard.DashboardAgentChildDisclosure.1b57ce9fa4", "{{value0}} {{value1}} child {{value2}}", { value0: childAgentsExpanded ? 'Hide' : 'Show', value1: childAgentCount, value2: childAgentCount === 1 ? 'agent' : 'agents' })}
aria-label={translate(
'auto.components.dashboard.DashboardAgentChildDisclosure.1b57ce9fa4',
'{{value0}} {{value1}} child {{value2}}',
{
value0: childAgentsExpanded ? 'Hide' : 'Show',
value1: childAgentCount,
value2: childAgentCount === 1 ? 'agent' : 'agents'
}
)}
aria-expanded={childAgentsExpanded}
>
<ChevronRight
@@ -26,9 +26,16 @@ export function DashboardAgentRowMessage({
{isInterrupted ? (
<span
className="shrink-0 text-[10px] leading-snug text-muted-foreground/80"
aria-label={translate("auto.components.dashboard.DashboardAgentRowMessage.1ec01cef03", "Interrupted by user")}
aria-label={translate(
'auto.components.dashboard.DashboardAgentRowMessage.1ec01cef03',
'Interrupted by user'
)}
>
{translate("auto.components.dashboard.DashboardAgentRowMessage.0a01046763", "interrupted")}</span>
{translate(
'auto.components.dashboard.DashboardAgentRowMessage.0a01046763',
'interrupted'
)}
</span>
) : null}
{lastAssistantMessage ? (
<CommentMarkdown
@@ -159,15 +159,22 @@ export function DiffCommentCard({
<button
type="button"
className="orca-diff-comment-pill-btn"
title={translate("auto.components.diff.comments.DiffCommentCard.508ee678a5", "Open in browser")}
aria-label={translate("auto.components.diff.comments.DiffCommentCard.508ee678a5", "Open in browser")}
title={translate(
'auto.components.diff.comments.DiffCommentCard.508ee678a5',
'Open in browser'
)}
aria-label={translate(
'auto.components.diff.comments.DiffCommentCard.508ee678a5',
'Open in browser'
)}
onClick={(ev) => {
ev.preventDefault()
ev.stopPropagation()
void window.api.shell.openUrl(url)
}}
>
{translate("auto.components.diff.comments.DiffCommentCard.6978871a3d", "Open")}</button>
{translate('auto.components.diff.comments.DiffCommentCard.6978871a3d', 'Open')}
</button>
{(onSubmitEdit || onDelete) && (
<span className="orca-diff-comment-pill-divider" />
)}
@@ -178,8 +185,14 @@ export function DiffCommentCard({
<button
type="button"
className="orca-diff-comment-pill-btn"
title={translate("auto.components.diff.comments.DiffCommentCard.cad3384faa", "Edit note")}
aria-label={translate("auto.components.diff.comments.DiffCommentCard.cad3384faa", "Edit note")}
title={translate(
'auto.components.diff.comments.DiffCommentCard.cad3384faa',
'Edit note'
)}
aria-label={translate(
'auto.components.diff.comments.DiffCommentCard.cad3384faa',
'Edit note'
)}
onClick={(ev) => {
ev.preventDefault()
ev.stopPropagation()
@@ -195,8 +208,14 @@ export function DiffCommentCard({
<button
type="button"
className="orca-diff-comment-pill-btn orca-diff-comment-pill-btn-danger"
title={translate("auto.components.diff.comments.DiffCommentCard.cce596969e", "Delete note")}
aria-label={translate("auto.components.diff.comments.DiffCommentCard.cce596969e", "Delete note")}
title={translate(
'auto.components.diff.comments.DiffCommentCard.cce596969e',
'Delete note'
)}
aria-label={translate(
'auto.components.diff.comments.DiffCommentCard.cce596969e',
'Delete note'
)}
onClick={(ev) => {
ev.preventDefault()
ev.stopPropagation()
@@ -249,14 +268,23 @@ export function DiffCommentCard({
/>
<div className="orca-diff-comment-popover-footer">
<Button variant="ghost" size="sm" onClick={handleCancel} disabled={submitting}>
{translate("auto.components.diff.comments.DiffCommentCard.0203bed775", "Cancel")}</Button>
{translate('auto.components.diff.comments.DiffCommentCard.0203bed775', 'Cancel')}
</Button>
<Button
size="sm"
onClick={() => void handleSubmit()}
disabled={!canSubmit}
title={submitting ? translate("auto.components.diff.comments.DiffCommentCard.bb0a55f856", "Saving…") : undefined}
title={
submitting
? translate(
'auto.components.diff.comments.DiffCommentCard.bb0a55f856',
'Saving…'
)
: undefined
}
>
{translate("auto.components.diff.comments.DiffCommentCard.109a791e7b", "Save")}<CornerDownLeft className="ml-1 size-3 opacity-70" />
{translate('auto.components.diff.comments.DiffCommentCard.109a791e7b', 'Save')}
<CornerDownLeft className="ml-1 size-3 opacity-70" />
</Button>
</div>
</div>
@@ -130,8 +130,16 @@ export function DiffCommentPopover({
<div id={labelId} className="orca-diff-comment-popover-label">
{title ??
(startLine && startLine !== lineNumber
? translate("auto.components.diff.comments.DiffCommentPopover.c845170b3b", "Lines {{value0}}-{{value1}}", { value0: startLine, value1: lineNumber })
: translate("auto.components.diff.comments.DiffCommentPopover.e05063cfc1", "Line {{value0}}", { value0: lineNumber }))}
? translate(
'auto.components.diff.comments.DiffCommentPopover.c845170b3b',
'Lines {{value0}}-{{value1}}',
{ value0: startLine, value1: lineNumber }
)
: translate(
'auto.components.diff.comments.DiffCommentPopover.e05063cfc1',
'Line {{value0}}',
{ value0: lineNumber }
))}
</div>
<textarea
ref={focusTextareaRef}
@@ -170,7 +178,8 @@ export function DiffCommentPopover({
/>
<div className="orca-diff-comment-popover-footer">
<Button variant="ghost" size="sm" onClick={onCancel}>
{translate("auto.components.diff.comments.DiffCommentPopover.2b3ce6d394", "Cancel")}</Button>
{translate('auto.components.diff.comments.DiffCommentPopover.2b3ce6d394', 'Cancel')}
</Button>
<Button
size="sm"
onClick={handleSubmit}
@@ -106,7 +106,10 @@ function getSingleCommentSendScopes(
return [
{
id: 'note',
label: translate("auto.components.diff.comments.useDiffCommentDecorator.995fa28b50", "This note"),
label: translate(
'auto.components.diff.comments.useDiffCommentDecorator.995fa28b50',
'This note'
),
notes: comment.sentAt ? [] : [comment],
prompt: formatCommentPrompt ? formatCommentPrompt(comment) : formatDiffComments([comment])
}
@@ -38,16 +38,23 @@ export function ChangesModeView({
if (!dc) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
{translate("auto.components.editor.ChangesModeView.54e0035b15", "Loading diff...")}</div>
{translate('auto.components.editor.ChangesModeView.54e0035b15', 'Loading diff...')}
</div>
)
}
if (dc.kind === 'binary') {
return (
<div className="flex h-full items-center justify-center px-6 text-center">
<div className="space-y-2">
<div className="text-sm font-medium text-foreground">{translate("auto.components.editor.ChangesModeView.7dffb0f563", "Binary file")}</div>
<div className="text-sm font-medium text-foreground">
{translate('auto.components.editor.ChangesModeView.7dffb0f563', 'Binary file')}
</div>
<div className="text-xs text-muted-foreground">
{translate("auto.components.editor.ChangesModeView.052c184f24", "Text diff is unavailable for this file.")}</div>
{translate(
'auto.components.editor.ChangesModeView.052c184f24',
'Text diff is unavailable for this file.'
)}
</div>
</div>
</div>
)
@@ -68,7 +75,11 @@ export function ChangesModeView({
{activeFile.conflict && <ConflictBanner file={activeFile} entry={activeConflictEntry} />}
{isIdentical && (
<div className="border-b border-border/60 bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
{translate("auto.components.editor.ChangesModeView.ef25ae2d09", "No uncommitted changes.")}</div>
{translate(
'auto.components.editor.ChangesModeView.ef25ae2d09',
'No uncommitted changes.'
)}
</div>
)}
<div className="flex min-h-0 flex-1 flex-col">
<DiffViewer
@@ -73,13 +73,15 @@ export default function CodeBlockCopyButton({
type="button"
className="code-block-copy-btn"
onClick={handleCopy}
aria-label={translate("auto.components.editor.CodeBlockCopyButton.1f9f4def45", "Copy code")}
title={translate("auto.components.editor.CodeBlockCopyButton.1f9f4def45", "Copy code")}
aria-label={translate('auto.components.editor.CodeBlockCopyButton.1f9f4def45', 'Copy code')}
title={translate('auto.components.editor.CodeBlockCopyButton.1f9f4def45', 'Copy code')}
>
{copied ? (
<>
<Check size={14} />
<span className="code-block-copy-label">{translate("auto.components.editor.CodeBlockCopyButton.28921f5bf9", "Copied")}</span>
<span className="code-block-copy-label">
{translate('auto.components.editor.CodeBlockCopyButton.28921f5bf9', 'Copied')}
</span>
</>
) : (
<Copy size={14} />
@@ -171,12 +171,16 @@ export function CombinedDiffFileTree({
<div className="sticky top-0 z-20 shrink-0 bg-background">
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-1.5">
<div className="text-[11px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
{translate("auto.components.editor.CombinedDiffFileTree.481e63ca52", "Files")}</div>
{translate('auto.components.editor.CombinedDiffFileTree.481e63ca52', 'Files')}
</div>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.editor.CombinedDiffFileTree.21783df79f", "Collapse file tree")}
aria-label={translate(
'auto.components.editor.CombinedDiffFileTree.21783df79f',
'Collapse file tree'
)}
onClick={() => onCollapsedChange(true)}
>
<PanelLeftClose className="size-3.5" />
@@ -188,7 +192,10 @@ export function CombinedDiffFileTree({
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={translate("auto.components.editor.CombinedDiffFileTree.4cc7b83ffe", "Filter files...")}
placeholder={translate(
'auto.components.editor.CombinedDiffFileTree.4cc7b83ffe',
'Filter files...'
)}
className="h-8 pl-7 text-xs"
/>
</div>
@@ -198,7 +205,10 @@ export function CombinedDiffFileTree({
type="button"
variant="outline"
size="icon-sm"
aria-label={translate("auto.components.editor.CombinedDiffFileTree.cd0e0ed79e", "Filter diff files")}
aria-label={translate(
'auto.components.editor.CombinedDiffFileTree.cd0e0ed79e',
'Filter diff files'
)}
className={cn(activeFilterCount > 0 && 'border-foreground/30 text-foreground')}
>
<Filter className="size-3.5" />
@@ -206,7 +216,11 @@ export function CombinedDiffFileTree({
</PopoverTrigger>
<PopoverContent align="end" side="bottom" sideOffset={6} className="w-56 p-0">
<div className="border-b border-border px-3 py-2 text-xs font-semibold text-foreground">
{translate("auto.components.editor.CombinedDiffFileTree.c00020f081", "File extensions")}</div>
{translate(
'auto.components.editor.CombinedDiffFileTree.c00020f081',
'File extensions'
)}
</div>
<div className="max-h-60 overflow-auto py-1 scrollbar-sleek">
{availableExtensions.map((extension) => {
const checked = !excludedExtensions.has(extension)
@@ -234,7 +248,12 @@ export function CombinedDiffFileTree({
<Check
className={cn('size-3.5 shrink-0', includeViewed ? 'opacity-100' : 'opacity-0')}
/>
<span className="min-w-0 flex-1 truncate">{translate("auto.components.editor.CombinedDiffFileTree.be119cb9d1", "Viewed files")}</span>
<span className="min-w-0 flex-1 truncate">
{translate(
'auto.components.editor.CombinedDiffFileTree.be119cb9d1',
'Viewed files'
)}
</span>
</button>
{activeFilterCount > 0 && (
<button
@@ -242,7 +261,11 @@ export function CombinedDiffFileTree({
className="w-full px-3 py-1.5 text-left text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
onClick={resetFilters}
>
{translate("auto.components.editor.CombinedDiffFileTree.eafe1aeb53", "Reset filters")}</button>
{translate(
'auto.components.editor.CombinedDiffFileTree.eafe1aeb53',
'Reset filters'
)}
</button>
)}
</div>
</PopoverContent>
@@ -252,8 +275,12 @@ export function CombinedDiffFileTree({
<div className="min-h-0 flex-1 overflow-auto py-1 scrollbar-sleek">
{filteredEntries.length === 0 ? (
<div className="px-3 py-6 text-center text-xs text-muted-foreground">
{translate("auto.components.editor.CombinedDiffFileTree.f984289373", "No files match the current filters.")}</div>
) : mode === "uncommitted" ? (
{translate(
'auto.components.editor.CombinedDiffFileTree.f984289373',
'No files match the current filters.'
)}
</div>
) : mode === 'uncommitted' ? (
uncommittedGroups.map((group) => (
<div key={group.area} className="py-1">
<div className="px-3 pb-1 text-[11px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
@@ -1219,7 +1219,12 @@ export default function CombinedDiffViewer({
if (ok) {
setClearNotesDialogOpen(false)
} else {
toast.error(translate("auto.components.editor.CombinedDiffViewer.45cf23b418", "Failed to clear notes."))
toast.error(
translate(
'auto.components.editor.CombinedDiffViewer.45cf23b418',
'Failed to clear notes.'
)
)
}
} finally {
if (mountedRef.current) {
@@ -1269,9 +1274,17 @@ export default function CombinedDiffViewer({
<div className="flex flex-1 items-center justify-center px-6 text-center">
<div className="max-w-md space-y-3">
<div className="text-sm font-medium text-foreground">
{translate("auto.components.editor.CombinedDiffViewer.820ec01f24", "Conflicted files are reviewed separately")}</div>
{translate(
'auto.components.editor.CombinedDiffViewer.820ec01f24',
'Conflicted files are reviewed separately'
)}
</div>
<div className="text-xs text-muted-foreground">
{translate("auto.components.editor.CombinedDiffViewer.eb5f40e49c", "This diff view excludes unresolved conflicts because the normal two-way diff pipeline is not conflict-safe.")}</div>
{translate(
'auto.components.editor.CombinedDiffViewer.eb5f40e49c',
'This diff view excludes unresolved conflicts because the normal two-way diff pipeline is not conflict-safe.'
)}
</div>
<div className="text-xs text-muted-foreground">
{file.skippedConflicts!.map((entry) => entry.path).join(', ')}
</div>
@@ -1292,7 +1305,11 @@ export default function CombinedDiffViewer({
)
}
>
{translate("auto.components.editor.CombinedDiffViewer.39f8007549", "Review conflicts")}</Button>
{translate(
'auto.components.editor.CombinedDiffViewer.39f8007549',
'Review conflicts'
)}
</Button>
</div>
</div>
</div>
@@ -1305,7 +1322,11 @@ export default function CombinedDiffViewer({
<div className="flex h-full min-h-0 flex-col">
{commitHeader}
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
{translate("auto.components.editor.CombinedDiffViewer.fd8892b120", "No changes to display")}</div>
{translate(
'auto.components.editor.CombinedDiffViewer.fd8892b120',
'No changes to display'
)}
</div>
</div>
)
}
@@ -1313,9 +1334,21 @@ export default function CombinedDiffViewer({
const skippedConflictNotice =
(file.skippedConflicts?.length ?? 0) > 0 ? (
<div className="mx-4 mt-3 rounded-md border border-border/60 bg-muted/20 px-3 py-2 text-xs">
<div className="font-medium text-foreground">{translate("auto.components.editor.CombinedDiffViewer.820ec01f24", "Conflicted files are reviewed separately")}</div>
<div className="font-medium text-foreground">
{translate(
'auto.components.editor.CombinedDiffViewer.820ec01f24',
'Conflicted files are reviewed separately'
)}
</div>
<div className="mt-1 text-muted-foreground">
{file.skippedConflicts!.length} {translate("auto.components.editor.CombinedDiffViewer.689b99f8ad", "unresolved conflict")}{file.skippedConflicts!.length === 1 ? '' : 's'} {translate("auto.components.editor.CombinedDiffViewer.39e73e7181", "were excluded from this diff view.")}</div>
{file.skippedConflicts!.length}{' '}
{translate('auto.components.editor.CombinedDiffViewer.689b99f8ad', 'unresolved conflict')}
{file.skippedConflicts!.length === 1 ? '' : 's'}{' '}
{translate(
'auto.components.editor.CombinedDiffViewer.39e73e7181',
'were excluded from this diff view.'
)}
</div>
<div className="mt-2 flex items-center gap-2">
<Button
type="button"
@@ -1334,7 +1367,8 @@ export default function CombinedDiffViewer({
)
}
>
{translate("auto.components.editor.CombinedDiffViewer.39f8007549", "Review conflicts")}</Button>
{translate('auto.components.editor.CombinedDiffViewer.39f8007549', 'Review conflicts')}
</Button>
</div>
</div>
) : null
@@ -1352,19 +1386,40 @@ export default function CombinedDiffViewer({
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.editor.CombinedDiffViewer.b6c3b84476", "Show file tree")}
aria-label={translate(
'auto.components.editor.CombinedDiffViewer.b6c3b84476',
'Show file tree'
)}
onClick={() => setFileTreeCollapsed(false)}
>
<PanelLeftOpen className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.editor.CombinedDiffViewer.b6c3b84476", "Show file tree")}</TooltipContent>
{translate(
'auto.components.editor.CombinedDiffViewer.b6c3b84476',
'Show file tree'
)}
</TooltipContent>
</Tooltip>
)}
<span className="truncate text-xs text-muted-foreground">
{sections.length} {translate("auto.components.editor.CombinedDiffViewer.7e7ca60816", "changed files")}{isBranchMode && branchCompare ? translate("auto.components.editor.CombinedDiffViewer.6094135eec", " vs {{value0}}", { value0: branchCompare.baseRef }) : ''}
{isCommitMode && commitCompare ? translate("auto.components.editor.CombinedDiffViewer.724a13568d", " in {{value0}}", { value0: commitCompare.compareRef }) : ''}
{sections.length}{' '}
{translate('auto.components.editor.CombinedDiffViewer.7e7ca60816', 'changed files')}
{isBranchMode && branchCompare
? translate(
'auto.components.editor.CombinedDiffViewer.6094135eec',
' vs {{value0}}',
{ value0: branchCompare.baseRef }
)
: ''}
{isCommitMode && commitCompare
? translate(
'auto.components.editor.CombinedDiffViewer.724a13568d',
' in {{value0}}',
{ value0: commitCompare.compareRef }
)
: ''}
</span>
{diffCommentCount > 0 && (
<div className="ml-1 flex shrink-0 items-center overflow-hidden rounded-full border border-border/70 bg-muted/40">
@@ -1373,10 +1428,22 @@ export default function CombinedDiffViewer({
<button
type="button"
className="inline-flex h-6 items-center gap-1 pl-2 pr-1.5 text-[11px] font-medium leading-none text-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
aria-label={translate("auto.components.editor.CombinedDiffViewer.8f68ad9ca9", "Show {{value0}} AI {{value1}}", { value0: diffCommentCount, value1: diffCommentCount === 1 ? 'note' : 'notes' })}
aria-label={translate(
'auto.components.editor.CombinedDiffViewer.8f68ad9ca9',
'Show {{value0}} AI {{value1}}',
{
value0: diffCommentCount,
value1: diffCommentCount === 1 ? 'note' : 'notes'
}
)}
>
<Sparkles className="size-3 text-violet-500 dark:text-violet-400" />
<span>{translate("auto.components.editor.CombinedDiffViewer.bb84b4c374", "AI notes")}</span>
<span>
{translate(
'auto.components.editor.CombinedDiffViewer.bb84b4c374',
'AI notes'
)}
</span>
<span className="rounded-full bg-background/80 px-1 text-[10px] tabular-nums text-muted-foreground">
{diffCommentCount}
</span>
@@ -1409,22 +1476,32 @@ export default function CombinedDiffViewer({
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={openAlternateDiff}
>
{file.combinedAlternate.source === "combined-branch"
? translate("auto.components.editor.CombinedDiffViewer.3d909843bb", "Open Branch Diff")
: translate("auto.components.editor.CombinedDiffViewer.982d14bfa5", "Open Uncommitted Diff")}
{file.combinedAlternate.source === 'combined-branch'
? translate(
'auto.components.editor.CombinedDiffViewer.3d909843bb',
'Open Branch Diff'
)
: translate(
'auto.components.editor.CombinedDiffViewer.982d14bfa5',
'Open Uncommitted Diff'
)}
</button>
)}
<button
className="w-20 text-left text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setAllSectionsCollapsed(!allSectionsCollapsed)}
>
{allSectionsCollapsed ? translate("auto.components.editor.CombinedDiffViewer.19c45cfdc0", "Expand All") : translate("auto.components.editor.CombinedDiffViewer.ea08dae15b", "Collapse All")}
{allSectionsCollapsed
? translate('auto.components.editor.CombinedDiffViewer.19c45cfdc0', 'Expand All')
: translate('auto.components.editor.CombinedDiffViewer.ea08dae15b', 'Collapse All')}
</button>
<button
className="w-24 px-2 py-0.5 text-center text-xs rounded border border-border text-muted-foreground hover:text-foreground transition-colors"
onClick={toggleSideBySide}
>
{sideBySide ? translate("auto.components.editor.CombinedDiffViewer.f786fd54e1", "Inline") : translate("auto.components.editor.CombinedDiffViewer.ec5053c7f5", "Side by Side")}
{sideBySide
? translate('auto.components.editor.CombinedDiffViewer.f786fd54e1', 'Inline')
: translate('auto.components.editor.CombinedDiffViewer.ec5053c7f5', 'Side by Side')}
</button>
</div>
</div>
@@ -1538,9 +1615,20 @@ export default function CombinedDiffViewer({
>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="text-sm">{translate("auto.components.editor.CombinedDiffViewer.948a5fd6c8", "Clear Notes")}</DialogTitle>
<DialogTitle className="text-sm">
{translate('auto.components.editor.CombinedDiffViewer.948a5fd6c8', 'Clear Notes')}
</DialogTitle>
<DialogDescription className="text-xs">
{translate("auto.components.editor.CombinedDiffViewer.84898c548d", "Clear")}{diffCommentCount} {diffCommentCount === 1 ? translate("auto.components.editor.CombinedDiffViewer.8ab3248fd8", "note") : translate("auto.components.editor.CombinedDiffViewer.0fb870a0fe", "notes")} {translate("auto.components.editor.CombinedDiffViewer.80a286d8f5", "from this worktree?")}</DialogDescription>
{translate('auto.components.editor.CombinedDiffViewer.84898c548d', 'Clear')}
{diffCommentCount}{' '}
{diffCommentCount === 1
? translate('auto.components.editor.CombinedDiffViewer.8ab3248fd8', 'note')
: translate('auto.components.editor.CombinedDiffViewer.0fb870a0fe', 'notes')}{' '}
{translate(
'auto.components.editor.CombinedDiffViewer.80a286d8f5',
'from this worktree?'
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
@@ -1549,7 +1637,8 @@ export default function CombinedDiffViewer({
onClick={() => setClearNotesDialogOpen(false)}
disabled={isClearingNotes}
>
{translate("auto.components.editor.CombinedDiffViewer.0f806a2ab1", "Cancel")}</Button>
{translate('auto.components.editor.CombinedDiffViewer.0f806a2ab1', 'Cancel')}
</Button>
<Button
type="button"
variant="destructive"
@@ -1557,7 +1646,8 @@ export default function CombinedDiffViewer({
disabled={isClearingNotes || diffCommentCount === 0}
>
<Trash2 className="size-4" />
{translate("auto.components.editor.CombinedDiffViewer.948a5fd6c8", "Clear Notes")}</Button>
{translate('auto.components.editor.CombinedDiffViewer.948a5fd6c8', 'Clear Notes')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -1585,7 +1675,9 @@ function DiffNotesPreviewPopover({
<div className="flex items-center justify-between gap-2 border-b border-border/60 px-3 py-2">
<div className="flex min-w-0 items-center gap-1.5 font-medium text-foreground">
<MessageSquare className="size-3.5 shrink-0 text-muted-foreground" />
<span>{translate("auto.components.editor.CombinedDiffViewer.bb84b4c374", "AI notes")}</span>
<span>
{translate('auto.components.editor.CombinedDiffViewer.bb84b4c374', 'AI notes')}
</span>
<span className="text-[11px] font-normal tabular-nums text-muted-foreground">
{totalCount}
</span>
@@ -1600,7 +1692,8 @@ function DiffNotesPreviewPopover({
disabled={totalCount === 0}
>
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
{translate("auto.components.editor.CombinedDiffViewer.88b70d0ef5", "Copy")}</Button>
{translate('auto.components.editor.CombinedDiffViewer.88b70d0ef5', 'Copy')}
</Button>
<Button
type="button"
variant="ghost"
@@ -1610,7 +1703,8 @@ function DiffNotesPreviewPopover({
disabled={totalCount === 0}
>
<Trash2 className="size-3" />
{translate("auto.components.editor.CombinedDiffViewer.84898c548d", "Clear")}</Button>
{translate('auto.components.editor.CombinedDiffViewer.84898c548d', 'Clear')}
</Button>
</div>
</div>
<div className="max-h-72 overflow-y-auto p-2 scrollbar-sleek">
@@ -1620,7 +1714,8 @@ function DiffNotesPreviewPopover({
<span className="min-w-0 flex-1 truncate font-mono">{comment.filePath}</span>
{comment.sentAt ? (
<span className="shrink-0 rounded bg-muted px-1 py-0.5 text-[10px] leading-none">
{translate("auto.components.editor.CombinedDiffViewer.1da745c551", "Sent")}</span>
{translate('auto.components.editor.CombinedDiffViewer.1da745c551', 'Sent')}
</span>
) : null}
<span className="shrink-0 tabular-nums">
{getDiffCommentLineLabel(comment, true)}
@@ -1633,7 +1728,13 @@ function DiffNotesPreviewPopover({
))}
{remainingCount > 0 && (
<div className="px-2 py-1 text-[11px] text-muted-foreground">
{remainingCount} {translate("auto.components.editor.CombinedDiffViewer.e3b9a6ce02", "more")}{remainingCount === 1 ? translate("auto.components.editor.CombinedDiffViewer.8ab3248fd8", "note") : translate("auto.components.editor.CombinedDiffViewer.0fb870a0fe", "notes")} {translate("auto.components.editor.CombinedDiffViewer.35cc27aeb2", "in Source Control")}</div>
{remainingCount}{' '}
{translate('auto.components.editor.CombinedDiffViewer.e3b9a6ce02', 'more')}
{remainingCount === 1
? translate('auto.components.editor.CombinedDiffViewer.8ab3248fd8', 'note')
: translate('auto.components.editor.CombinedDiffViewer.0fb870a0fe', 'notes')}{' '}
{translate('auto.components.editor.CombinedDiffViewer.35cc27aeb2', 'in Source Control')}
</div>
)}
</div>
</div>
@@ -100,7 +100,9 @@ export function ConflictBanner({
<CircleCheck className="size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
)}
<span className="min-w-0 truncate font-medium text-foreground">
{label} {translate("auto.components.editor.ConflictComponents.55d61a0ccd", "conflict ·")}{CONFLICT_KIND_LABELS[conflict.conflictKind]}
{label}{' '}
{translate('auto.components.editor.ConflictComponents.55d61a0ccd', 'conflict ·')}
{CONFLICT_KIND_LABELS[conflict.conflictKind]}
</span>
{conflictNavigation && conflictNavigation.total > 0 && (
<span className="shrink-0 px-1 text-[11px] tabular-nums text-muted-foreground">
@@ -116,14 +118,21 @@ export function ConflictBanner({
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.editor.ConflictComponents.41d9af2e7a", "Previous conflict")}
aria-label={translate(
'auto.components.editor.ConflictComponents.41d9af2e7a',
'Previous conflict'
)}
onClick={() => conflictNavigation.onJump('previous')}
>
<ChevronUp className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.editor.ConflictComponents.41d9af2e7a", "Previous conflict")}</TooltipContent>
{translate(
'auto.components.editor.ConflictComponents.41d9af2e7a',
'Previous conflict'
)}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
@@ -131,14 +140,18 @@ export function ConflictBanner({
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.editor.ConflictComponents.9c2901ef8a", "Next conflict")}
aria-label={translate(
'auto.components.editor.ConflictComponents.9c2901ef8a',
'Next conflict'
)}
onClick={() => conflictNavigation.onJump('next')}
>
<ChevronDown className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.editor.ConflictComponents.9c2901ef8a", "Next conflict")}</TooltipContent>
{translate('auto.components.editor.ConflictComponents.9c2901ef8a', 'Next conflict')}
</TooltipContent>
</Tooltip>
</div>
)}
@@ -150,10 +163,17 @@ export function ConflictBanner({
list where it provides actionable guidance. */}
{!isUnresolved && (
<div className="mt-1 text-muted-foreground">
{translate("auto.components.editor.ConflictComponents.6e459867ad", "Session-local continuity state. Git is no longer reporting this file as unmerged.")}</div>
{translate(
'auto.components.editor.ConflictComponents.6e459867ad',
'Session-local continuity state. Git is no longer reporting this file as unmerged.'
)}
</div>
)}
{entry?.oldPath && (
<div className="mt-1 text-muted-foreground">{translate("auto.components.editor.ConflictComponents.d5edd81755", "Renamed from")}{entry.oldPath}</div>
<div className="mt-1 text-muted-foreground">
{translate('auto.components.editor.ConflictComponents.d5edd81755', 'Renamed from')}
{entry.oldPath}
</div>
)}
</div>
)
@@ -172,7 +192,11 @@ export function ConflictPlaceholderView({ file }: { file: OpenFile }): React.JSX
{CONFLICT_KIND_LABELS[conflict.conflictKind]}
</div>
<div className="text-xs text-muted-foreground">
{conflict.message ?? translate("auto.components.editor.ConflictComponents.da539359b6", "No working-tree file is available to edit for this conflict.")}
{conflict.message ??
translate(
'auto.components.editor.ConflictComponents.da539359b6',
'No working-tree file is available to edit for this conflict.'
)}
</div>
<div className="text-xs text-muted-foreground">
{conflict.guidance ?? CONFLICT_HINT_MAP[conflict.conflictKind]}
@@ -233,16 +257,27 @@ export function ConflictReviewPanel({
return (
<div className="flex h-full items-center justify-center px-6 text-center">
<div className="max-w-md space-y-3">
<div className="text-sm font-medium text-foreground">{translate("auto.components.editor.ConflictComponents.992145ff5a", "All conflicts resolved")}</div>
<div className="text-sm font-medium text-foreground">
{translate(
'auto.components.editor.ConflictComponents.992145ff5a',
'All conflicts resolved'
)}
</div>
<div className="text-xs text-muted-foreground">
{translate("auto.components.editor.ConflictComponents.31931dec46", "This review snapshot no longer has any live unresolved conflicts.")}</div>
{translate(
'auto.components.editor.ConflictComponents.31931dec46',
'This review snapshot no longer has any live unresolved conflicts.'
)}
</div>
<div className="flex items-center justify-center gap-2">
<Button type="button" size="sm" variant="outline" onClick={onReturnToSourceControl}>
<GitMerge className="size-3.5" />
{translate("auto.components.editor.ConflictComponents.28e7db4a90", "Source Control")}</Button>
{translate('auto.components.editor.ConflictComponents.28e7db4a90', 'Source Control')}
</Button>
<Button type="button" size="sm" variant="ghost" onClick={onDismiss}>
<X className="size-3.5" />
{translate("auto.components.editor.ConflictComponents.58ad5ad431", "Dismiss")}</Button>
{translate('auto.components.editor.ConflictComponents.58ad5ad431', 'Dismiss')}
</Button>
</div>
</div>
</div>
@@ -268,34 +303,55 @@ export function ConflictReviewPanel({
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.editor.ConflictComponents.c8ca989aea", "Show file tree")}
aria-label={translate(
'auto.components.editor.ConflictComponents.c8ca989aea',
'Show file tree'
)}
onClick={() => setFileTreeCollapsed(false)}
>
<PanelLeftOpen className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate("auto.components.editor.ConflictComponents.c8ca989aea", "Show file tree")}</TooltipContent>
{translate(
'auto.components.editor.ConflictComponents.c8ca989aea',
'Show file tree'
)}
</TooltipContent>
</Tooltip>
)}
<div className="flex min-w-0 flex-wrap items-baseline gap-x-1.5">
<span className="text-sm font-medium text-foreground">
{unresolvedCount} {translate("auto.components.editor.ConflictComponents.4be41eaafc", "unresolved conflict")}{unresolvedCount === 1 ? '' : 's'}
{unresolvedCount}{' '}
{translate(
'auto.components.editor.ConflictComponents.4be41eaafc',
'unresolved conflict'
)}
{unresolvedCount === 1 ? '' : 's'}
</span>
<span className="text-muted-foreground/50">·</span>
<span className="text-xs text-muted-foreground">
{translate("auto.components.editor.ConflictComponents.a1ce36f77d", "Snapshot captured at")}{snapshotTime}.
{translate(
'auto.components.editor.ConflictComponents.a1ce36f77d',
'Snapshot captured at'
)}
{snapshotTime}.
</span>
</div>
</div>
<Button type="button" size="sm" variant="outline" onClick={onRefreshSnapshot}>
<RefreshCw className="size-3.5" />
{translate("auto.components.editor.ConflictComponents.90d576adb2", "Refresh")}</Button>
{translate('auto.components.editor.ConflictComponents.90d576adb2', 'Refresh')}
</Button>
</div>
<div className="flex min-h-0 flex-1 flex-col">
{selectedContent ?? (
<div className="flex h-full min-h-0 items-center justify-center px-6 text-center text-sm text-muted-foreground">
{translate("auto.components.editor.ConflictComponents.f338288514", "Loading conflict contents...")}</div>
{translate(
'auto.components.editor.ConflictComponents.f338288514',
'Loading conflict contents...'
)}
</div>
)}
</div>
</div>
@@ -73,14 +73,18 @@ export function ConflictReviewFileTree({
<aside className="flex w-72 shrink-0 flex-col border-r border-border bg-background">
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-1.5">
<div className="text-[11px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
{translate("auto.components.editor.ConflictReviewFileTree.99496bab6e", "Files")}</div>
{translate('auto.components.editor.ConflictReviewFileTree.99496bab6e', 'Files')}
</div>
<div className="flex items-center gap-2">
<div className="text-[11px] text-muted-foreground tabular-nums">{entries.length}</div>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.editor.ConflictReviewFileTree.a54551c5a6", "Collapse file tree")}
aria-label={translate(
'auto.components.editor.ConflictReviewFileTree.a54551c5a6',
'Collapse file tree'
)}
onClick={() => onCollapsedChange(true)}
>
<PanelLeftClose className="size-3.5" />
@@ -90,7 +94,11 @@ export function ConflictReviewFileTree({
<div className="min-h-0 flex-1 overflow-auto py-1 scrollbar-sleek">
{rows.length === 0 ? (
<div className="px-3 py-6 text-center text-xs text-muted-foreground">
{translate("auto.components.editor.ConflictReviewFileTree.3449521a8c", "No conflicts in this snapshot.")}</div>
{translate(
'auto.components.editor.ConflictReviewFileTree.3449521a8c',
'No conflicts in this snapshot.'
)}
</div>
) : (
rows.map((node) => (
<ConflictReviewFileTreeRow
@@ -182,7 +190,11 @@ function ConflictReviewFileTreeRow({
: 'bg-muted text-muted-foreground'
)}
>
{isStillUnresolved ? translate("auto.components.editor.ConflictReviewFileTree.69d4e210bb", "Unresolved") : liveEntry ? translate("auto.components.editor.ConflictReviewFileTree.8528a5eaf5", "Resolved") : translate("auto.components.editor.ConflictReviewFileTree.496e28a932", "Gone")}
{isStillUnresolved
? translate('auto.components.editor.ConflictReviewFileTree.69d4e210bb', 'Unresolved')
: liveEntry
? translate('auto.components.editor.ConflictReviewFileTree.8528a5eaf5', 'Resolved')
: translate('auto.components.editor.ConflictReviewFileTree.496e28a932', 'Gone')}
</span>
</button>
)
@@ -92,7 +92,8 @@ export default function CsvViewer({ content, filePath }: CsvViewerProps): React.
if (parsed.rows.length === 0) {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
{translate("auto.components.editor.CsvViewer.a233d55b77", "Empty file")}</div>
{translate('auto.components.editor.CsvViewer.a233d55b77', 'Empty file')}
</div>
)
}
@@ -178,8 +179,13 @@ export default function CsvViewer({ content, filePath }: CsvViewerProps): React.
</div>
</div>
<div className="flex items-center gap-4 border-t border-border/60 px-3 py-1 text-xs text-muted-foreground">
<span>{bodyRows.length.toLocaleString()} {translate("auto.components.editor.CsvViewer.ac31d2cd60", "rows")}</span>
<span>{columnCount} {translate("auto.components.editor.CsvViewer.eedd0d37a7", "columns")}</span>
<span>
{bodyRows.length.toLocaleString()}{' '}
{translate('auto.components.editor.CsvViewer.ac31d2cd60', 'rows')}
</span>
<span>
{columnCount} {translate('auto.components.editor.CsvViewer.eedd0d37a7', 'columns')}
</span>
</div>
</div>
)
@@ -43,7 +43,7 @@ export function DiffNotesSendMenu({
const scopes = useMemo<NotesSendMenuScope<DiffComment>[]>(() => {
const allNotesScope = {
id: 'all',
label: translate("auto.components.editor.DiffNotesSendMenu.8b87612461", "All unsent notes"),
label: translate('auto.components.editor.DiffNotesSendMenu.8b87612461', 'All unsent notes'),
notes: unsentNotes,
prompt: unsentPrompt
}
@@ -53,7 +53,7 @@ export function DiffNotesSendMenu({
return [
{
id: 'file',
label: translate("auto.components.editor.DiffNotesSendMenu.f1aa04b5cf", "This file"),
label: translate('auto.components.editor.DiffNotesSendMenu.f1aa04b5cf', 'This file'),
notes: unsentFileNotes,
prompt: unsentFilePrompt
},
@@ -85,7 +85,9 @@ export function DiffSectionBody({
{section.loading ? (
<div className="flex h-full items-center gap-2 bg-muted/10 px-3 text-[11px] text-muted-foreground">
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/50" />
<span>{translate("auto.components.editor.DiffSectionBody.f5cf81cec2", "Loading diff...")}</span>
<span>
{translate('auto.components.editor.DiffSectionBody.f5cf81cec2', 'Loading diff...')}
</span>
</div>
) : section.error ? (
<div className="flex h-full items-center justify-between gap-3 bg-muted/10 px-3 text-[11px] text-muted-foreground">
@@ -104,7 +106,8 @@ export function DiffSectionBody({
}}
>
<RefreshCw className="size-3" />
{translate("auto.components.editor.DiffSectionBody.cef4cf0ff5", "Retry")}</Button>
{translate('auto.components.editor.DiffSectionBody.cef4cf0ff5', 'Retry')}
</Button>
</div>
) : section.diffResult?.kind === 'binary' ? (
section.diffResult.isImage ? (
@@ -119,11 +122,22 @@ export function DiffSectionBody({
) : (
<div className="flex h-full items-center justify-center px-6 text-center">
<div className="space-y-2">
<div className="text-sm font-medium text-foreground">{translate("auto.components.editor.DiffSectionBody.35d6afb5be", "Binary file changed")}</div>
<div className="text-sm font-medium text-foreground">
{translate(
'auto.components.editor.DiffSectionBody.35d6afb5be',
'Binary file changed'
)}
</div>
<div className="text-xs text-muted-foreground">
{isBranchMode
? translate("auto.components.editor.DiffSectionBody.7ce8436458", "Text diff is unavailable for this file in branch compare.")
: translate("auto.components.editor.DiffSectionBody.72f71f52eb", "Text diff is unavailable for this file.")}
? translate(
'auto.components.editor.DiffSectionBody.7ce8436458',
'Text diff is unavailable for this file in branch compare.'
)
: translate(
'auto.components.editor.DiffSectionBody.72f71f52eb',
'Text diff is unavailable for this file.'
)}
</div>
</div>
</div>
@@ -56,7 +56,7 @@ export function DiffSectionHeader({
console.error('Failed to copy diff path:', error)
})
}}
title={translate("auto.components.editor.DiffSectionHeader.8915726e93", "Copy path")}
title={translate('auto.components.editor.DiffSectionHeader.8915726e93', 'Copy path')}
>
{path}
</span>
@@ -81,11 +81,14 @@ function FileLoadErrorView({
<div className="flex max-w-xl items-start gap-3 rounded-md border border-border bg-background p-4">
<AlertCircle className="mt-0.5 size-4 flex-shrink-0 text-destructive" />
<div className="min-w-0">
<div className="font-medium text-foreground">{translate("auto.components.editor.EditorContent.39f018b052", "Unable to load file")}</div>
<div className="font-medium text-foreground">
{translate('auto.components.editor.EditorContent.39f018b052', 'Unable to load file')}
</div>
<div className="mt-1 break-words">{message}</div>
<Button type="button" variant="outline" size="sm" className="mt-3" onClick={onRetry}>
<RefreshCw className="size-3.5" />
{translate("auto.components.editor.EditorContent.2a512bb46a", "Retry")}</Button>
{translate('auto.components.editor.EditorContent.2a512bb46a', 'Retry')}
</Button>
</div>
</div>
</div>
@@ -253,8 +256,10 @@ export function EditorContent({
conflictKind: entry.conflictKind,
conflictStatus: entry.conflictStatus,
conflictStatusSource: entry.conflictStatusSource,
message:
translate("auto.components.editor.EditorContent.8b1a605bae", "This file is in a conflict state, but no working-tree file is available to edit."),
message: translate(
'auto.components.editor.EditorContent.8b1a605bae',
'This file is in a conflict state, but no working-tree file is available to edit.'
),
guidance: 'Resolve the conflict in Git or restore one side before reopening it.'
}
: {
@@ -477,7 +482,8 @@ export function EditorContent({
return (
<div className={className}>
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
{translate("auto.components.editor.EditorContent.b2735221f5", "Loading...")}</div>
{translate('auto.components.editor.EditorContent.b2735221f5', 'Loading...')}
</div>
</div>
)
}
@@ -506,7 +512,11 @@ export function EditorContent({
return (
<div className={className}>
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
{translate("auto.components.editor.EditorContent.b9de81ba52", "Binary file — cannot display")}</div>
{translate(
'auto.components.editor.EditorContent.b9de81ba52',
'Binary file — cannot display'
)}
</div>
</div>
)
}
@@ -650,7 +660,8 @@ export function EditorContent({
if (!fc) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
{translate("auto.components.editor.EditorContent.37a0e81fa6", "Loading preview...")}</div>
{translate('auto.components.editor.EditorContent.37a0e81fa6', 'Loading preview...')}
</div>
)
}
if (fc.loadError) {
@@ -661,7 +672,11 @@ export function EditorContent({
if (fc.isBinary) {
return (
<div className="flex h-full items-center justify-center px-6 text-center text-sm text-muted-foreground">
{translate("auto.components.editor.EditorContent.8608ce4cb1", "Markdown preview is unavailable for binary files.")}</div>
{translate(
'auto.components.editor.EditorContent.8608ce4cb1',
'Markdown preview is unavailable for binary files.'
)}
</div>
)
}
const previewSourceFileId = activeFile.markdownPreviewSourceFileId ?? activeFile.filePath
@@ -694,7 +709,8 @@ export function EditorContent({
if (!fc) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
{translate("auto.components.editor.EditorContent.b2735221f5", "Loading...")}</div>
{translate('auto.components.editor.EditorContent.b2735221f5', 'Loading...')}
</div>
)
}
if (fc.loadError) {
@@ -710,7 +726,11 @@ export function EditorContent({
}
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
{translate("auto.components.editor.EditorContent.b9de81ba52", "Binary file — cannot display")}</div>
{translate(
'auto.components.editor.EditorContent.b9de81ba52',
'Binary file — cannot display'
)}
</div>
)
}
if (isChangesMode) {
@@ -744,19 +764,19 @@ export function EditorContent({
<div className="min-h-0 flex-1 relative">
{isMarkdown ? (
renderMarkdownContent(fc)
) : isMermaid && mdViewMode === "rich" ? (
) : isMermaid && mdViewMode === 'rich' ? (
<MermaidViewer
key={activeFile.id}
content={editBuffers[activeFile.id] ?? fc.content}
filePath={activeFile.filePath}
/>
) : isCsv && mdViewMode === "rich" ? (
) : isCsv && mdViewMode === 'rich' ? (
<CsvViewer
key={activeFile.id}
content={editBuffers[activeFile.id] ?? fc.content}
filePath={activeFile.filePath}
/>
) : isNotebook && mdViewMode === "rich" ? (
) : isNotebook && mdViewMode === 'rich' ? (
<IpynbViewer
key={activeFile.id}
content={editBuffers[activeFile.id] ?? fc.content}
@@ -781,7 +801,8 @@ export function EditorContent({
if (!dc) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
{translate("auto.components.editor.EditorContent.c88c73a0d3", "Loading diff...")}</div>
{translate('auto.components.editor.EditorContent.c88c73a0d3', 'Loading diff...')}
</div>
)
}
const isEditable = activeFile.diffSource === 'unstaged'
@@ -800,11 +821,19 @@ export function EditorContent({
return (
<div className="flex h-full items-center justify-center px-6 text-center">
<div className="space-y-2">
<div className="text-sm font-medium text-foreground">{translate("auto.components.editor.EditorContent.78541e254e", "Binary file changed")}</div>
<div className="text-sm font-medium text-foreground">
{translate('auto.components.editor.EditorContent.78541e254e', 'Binary file changed')}
</div>
<div className="text-xs text-muted-foreground">
{activeFile.diffSource === "branch"
? translate("auto.components.editor.EditorContent.3c6e71df22", "Text diff is unavailable for this file in branch compare.")
: translate("auto.components.editor.EditorContent.8a0898ae4c", "Text diff is unavailable for this file.")}
{activeFile.diffSource === 'branch'
? translate(
'auto.components.editor.EditorContent.3c6e71df22',
'Text diff is unavailable for this file in branch compare.'
)
: translate(
'auto.components.editor.EditorContent.8a0898ae4c',
'Text diff is unavailable for this file.'
)}
</div>
</div>
</div>
@@ -819,7 +848,11 @@ export function EditorContent({
deletions simultaneously, so preview mode intentionally shows the
modified side of the diff. Source mode remains available for the
actual line-by-line comparison. */}
{translate("auto.components.editor.EditorContent.9640d1d3db", "Previewing the modified version of this diff. Switch to source mode to inspect changes.")}</div>
{translate(
'auto.components.editor.EditorContent.9640d1d3db',
'Previewing the modified version of this diff. Switch to source mode to inspect changes.'
)}
</div>
<div className="min-h-0 flex-1">
<MarkdownPreview
key={viewStateScopeId}
@@ -878,8 +911,10 @@ function FrontMatterBanner({ raw }: { raw: string }): React.JSX.Element {
return (
<div className="border-b border-border/60 bg-muted/40 px-3 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
{translate("auto.components.editor.EditorContent.e4b074749d", "Front Matter")}<span className="ml-2 font-normal normal-case tracking-normal opacity-70">
{translate("auto.components.editor.EditorContent.56dba34e1a", "(edit in source mode)")}</span>
{translate('auto.components.editor.EditorContent.e4b074749d', 'Front Matter')}
<span className="ml-2 font-normal normal-case tracking-normal opacity-70">
{translate('auto.components.editor.EditorContent.56dba34e1a', '(edit in source mode)')}
</span>
</div>
<pre className="max-h-32 overflow-auto whitespace-pre-wrap text-xs text-muted-foreground font-mono scrollbar-editor">
{inner}
@@ -39,8 +39,14 @@ export function EditorPanelMarkdownActionsMenu({
<button
type="button"
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
aria-label={translate("auto.components.editor.EditorPanelMarkdownActionsMenu.561251019a", "More actions")}
title={translate("auto.components.editor.EditorPanelMarkdownActionsMenu.561251019a", "More actions")}
aria-label={translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.561251019a',
'More actions'
)}
title={translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.561251019a',
'More actions'
)}
>
<MoreHorizontal size={14} />
</button>
@@ -54,7 +60,15 @@ export function EditorPanelMarkdownActionsMenu({
onToggleMarkdownFrontmatter()
}}
>
{markdownFrontmatterVisible ? translate("auto.components.editor.EditorPanelMarkdownActionsMenu.10c39d58c1", "Hide front matter") : translate("auto.components.editor.EditorPanelMarkdownActionsMenu.8c8b7f5ff5", "Show front matter")}
{markdownFrontmatterVisible
? translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.10c39d58c1',
'Hide front matter'
)
: translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.8c8b7f5ff5',
'Show front matter'
)}
</DropdownMenuItem>
{hasViewModeToggle ? <DropdownMenuSeparator /> : null}
</>
@@ -67,7 +81,11 @@ export function EditorPanelMarkdownActionsMenu({
disabled={mdViewMode === 'source'}
onSelect={onExportMarkdownToPdf}
>
{translate("auto.components.editor.EditorPanelMarkdownActionsMenu.3e0ce48c24", "Export as PDF")}</DropdownMenuItem>
{translate(
'auto.components.editor.EditorPanelMarkdownActionsMenu.3e0ce48c24',
'Export as PDF'
)}
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
@@ -185,6 +185,7 @@ export function EditorPanelShell({
function EditorLoadingFallback(): JSX.Element {
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
{translate("auto.components.editor.EditorPanelShell.e2c4dec350", "Loading editor...")}</div>
{translate('auto.components.editor.EditorPanelShell.e2c4dec350', 'Loading editor...')}
</div>
)
}
@@ -27,29 +27,29 @@ type ViewModeMetadata = { label: string; icon: LucideIcon; title?: string }
const DEFAULT_VIEW_MODE_METADATA: Record<EditorToggleValue, ViewModeMetadata> = {
source: {
label: translate("auto.components.editor.EditorViewToggle.4d6ccb7ba6", "Source"),
label: translate('auto.components.editor.EditorViewToggle.4d6ccb7ba6', 'Source'),
icon: Code
},
rich: {
label: translate("auto.components.editor.EditorViewToggle.aff15f94f5", "Rich Editor"),
label: translate('auto.components.editor.EditorViewToggle.aff15f94f5', 'Rich Editor'),
icon: Pencil
},
preview: {
label: translate("auto.components.editor.EditorViewToggle.0d193dc03c", "Preview"),
label: translate('auto.components.editor.EditorViewToggle.0d193dc03c', 'Preview'),
icon: Eye
},
edit: {
label: translate("auto.components.editor.EditorViewToggle.ac3bb87913", "Edit"),
label: translate('auto.components.editor.EditorViewToggle.ac3bb87913', 'Edit'),
icon: FileText
},
changes: {
label: translate("auto.components.editor.EditorViewToggle.4837f3f578", "Changes"),
label: translate('auto.components.editor.EditorViewToggle.4837f3f578', 'Changes'),
icon: GitCompareArrows,
// Why: "Changes" collides with the Source Control sidebar's "Branch
// Changes" section, which diffs against the base ref. This toggle shows
// uncommitted changes (working tree vs HEAD), so disambiguate in the
// hover title without repeating the button label.
title: translate("auto.components.editor.EditorViewToggle.167f45888c", "Uncommitted changes")
title: translate('auto.components.editor.EditorViewToggle.167f45888c', 'Uncommitted changes')
}
}
@@ -58,14 +58,14 @@ const DEFAULT_VIEW_MODE_METADATA: Record<EditorToggleValue, ViewModeMetadata> =
// which we don't offer, so callers can override the per-mode presentation.
export const CSV_VIEW_MODE_METADATA: Partial<Record<MarkdownViewMode, ViewModeMetadata>> = {
rich: {
label: translate("auto.components.editor.EditorViewToggle.e408aa9cd5", "Table"),
label: translate('auto.components.editor.EditorViewToggle.e408aa9cd5', 'Table'),
icon: TableIcon
}
}
export const NOTEBOOK_VIEW_MODE_METADATA: Partial<Record<MarkdownViewMode, ViewModeMetadata>> = {
rich: {
label: translate("auto.components.editor.EditorViewToggle.b3410cd5e0", "Notebook"),
label: translate('auto.components.editor.EditorViewToggle.b3410cd5e0', 'Notebook'),
icon: NotebookText
}
}
@@ -42,7 +42,8 @@ function ImageDiffPane({
isIntrinsicLayout ? 'min-h-32' : 'flex-1'
)}
>
{translate("auto.components.editor.ImageDiffViewer.fb0ae4f3c0", "No preview")}</div>
{translate('auto.components.editor.ImageDiffViewer.fb0ae4f3c0', 'No preview')}
</div>
</div>
)
}
@@ -94,14 +95,14 @@ export default function ImageDiffViewer({
style={gridRowStyle}
>
<ImageDiffPane
label={translate("auto.components.editor.ImageDiffViewer.57aac3979a", "Original")}
label={translate('auto.components.editor.ImageDiffViewer.57aac3979a', 'Original')}
content={originalContent}
filePath={filePath}
mimeType={mimeType}
layout={layout}
/>
<ImageDiffPane
label={translate("auto.components.editor.ImageDiffViewer.a651be62b0", "Modified")}
label={translate('auto.components.editor.ImageDiffViewer.a651be62b0', 'Modified')}
content={modifiedContent}
filePath={filePath}
mimeType={mimeType}
@@ -219,7 +219,12 @@ export default function ImageViewer({
)}
>
<ImageIcon size={40} />
<div>{translate("auto.components.editor.ImageViewer.d9d2944855", "Failed to load file preview")}</div>
<div>
{translate(
'auto.components.editor.ImageViewer.d9d2944855',
'Failed to load file preview'
)}
</div>
<div className="max-w-md break-all text-center text-xs">{filename}</div>
</div>
)
@@ -233,7 +238,8 @@ export default function ImageViewer({
isIntrinsicLayout ? 'min-h-64' : 'h-full'
)}
>
{translate("auto.components.editor.ImageViewer.3ef9551ba2", "Loading preview...")}</div>
{translate('auto.components.editor.ImageViewer.3ef9551ba2', 'Loading preview...')}
</div>
)
}
@@ -249,7 +255,7 @@ export default function ImageViewer({
: 'flex-1 overflow-auto scrollbar-editor'
)}
onClick={openPopup}
title={translate("auto.components.editor.ImageViewer.77bfc9b35a", "Open image in popup")}
title={translate('auto.components.editor.ImageViewer.77bfc9b35a', 'Open image in popup')}
>
<div
className={cn(
@@ -299,7 +305,7 @@ export default function ImageViewer({
applyInlineZoomChange((currentZoom) => currentZoom / IMAGE_VIEWER_ZOOM_STEP)
}
disabled={inlineZoom <= MIN_IMAGE_VIEWER_ZOOM}
title={translate("auto.components.editor.ImageViewer.be27304574", "Zoom out")}
title={translate('auto.components.editor.ImageViewer.be27304574', 'Zoom out')}
>
<ZoomOut size={14} />
</button>
@@ -308,7 +314,7 @@ export default function ImageViewer({
className="rounded p-1 hover:bg-accent hover:text-foreground disabled:opacity-50"
onClick={() => applyInlineZoomChange(() => 1)}
disabled={inlineZoom === 1}
title={translate("auto.components.editor.ImageViewer.6c89c73d9f", "Reset zoom")}
title={translate('auto.components.editor.ImageViewer.6c89c73d9f', 'Reset zoom')}
>
<RotateCcw size={14} />
</button>
@@ -319,7 +325,7 @@ export default function ImageViewer({
applyInlineZoomChange((currentZoom) => currentZoom * IMAGE_VIEWER_ZOOM_STEP)
}
disabled={inlineZoom >= MAX_IMAGE_VIEWER_ZOOM}
title={translate("auto.components.editor.ImageViewer.3c9217f5a6", "Zoom in")}
title={translate('auto.components.editor.ImageViewer.3c9217f5a6', 'Zoom in')}
>
<ZoomIn size={14} />
</button>
@@ -33,7 +33,12 @@ export default function ImageViewerPopup({
className="top-1/2 left-1/2 flex h-[80vh] w-[70vw] max-w-[70vw] -translate-x-1/2 -translate-y-1/2 flex-col gap-0 overflow-hidden border border-border/60 bg-background p-0 shadow-2xl sm:max-w-[70vw]"
>
<DialogTitle className="sr-only">{filename}</DialogTitle>
<DialogDescription className="sr-only">{translate("auto.components.editor.ImageViewerPopup.9e27b2ecaf", "Full-size image preview")}</DialogDescription>
<DialogDescription className="sr-only">
{translate(
'auto.components.editor.ImageViewerPopup.9e27b2ecaf',
'Full-size image preview'
)}
</DialogDescription>
<div className="flex shrink-0 items-center justify-between border-b border-border/60 bg-background/95 px-3 py-2">
<div className="min-w-0 truncate text-sm font-medium text-foreground">{filename}</div>
<button
@@ -42,7 +47,7 @@ export default function ImageViewerPopup({
onClick={() => onOpenChange(false)}
>
<X size={14} />
<span>{translate("auto.components.editor.ImageViewerPopup.535f4e2b56", "Close")}</span>
<span>{translate('auto.components.editor.ImageViewerPopup.535f4e2b56', 'Close')}</span>
</button>
</div>
<div
@@ -63,7 +68,9 @@ export default function ImageViewerPopup({
</div>
</div>
<div className="flex shrink-0 items-center justify-between border-t border-border/60 bg-background/95 px-3 py-2 text-xs text-muted-foreground">
<div>{translate("auto.components.editor.ImageViewerPopup.0ef78475e7", "Press Esc to close")}</div>
<div>
{translate('auto.components.editor.ImageViewerPopup.0ef78475e7', 'Press Esc to close')}
</div>
<div className="tabular-nums">{zoomPercent}%</div>
</div>
</DialogContent>
@@ -181,29 +181,56 @@ function NotebookCellHeader({
onChange={(event) => onKindChange(event.target.value as IpynbCellKind)}
className="h-7 rounded-md border border-input bg-background px-2 text-xs text-foreground"
>
<option value="code">{translate("auto.components.editor.IpynbViewer.7005960d73", "Code")}</option>
<option value="markdown">{translate("auto.components.editor.IpynbViewer.1833dbbc43", "Markdown")}</option>
<option value="raw">{translate("auto.components.editor.IpynbViewer.3e4cbf15ea", "Raw")}</option>
<option value="code">
{translate('auto.components.editor.IpynbViewer.7005960d73', 'Code')}
</option>
<option value="markdown">
{translate('auto.components.editor.IpynbViewer.1833dbbc43', 'Markdown')}
</option>
<option value="raw">
{translate('auto.components.editor.IpynbViewer.3e4cbf15ea', 'Raw')}
</option>
</select>
{cell.kind === "code" ? (
<NotebookHeaderButton label={translate("auto.components.editor.IpynbViewer.859bf9fc21", "Run cell")} disabled={running} onClick={onRun}>
{cell.kind === 'code' ? (
<NotebookHeaderButton
label={translate('auto.components.editor.IpynbViewer.859bf9fc21', 'Run cell')}
disabled={running}
onClick={onRun}
>
{running ? <Loader2 className="size-3.5 animate-spin" /> : <Play className="size-3.5" />}
</NotebookHeaderButton>
) : null}
<NotebookHeaderButton label={translate("auto.components.editor.IpynbViewer.fd8ac707bc", "Move cell up")} disabled={!canMoveUp} onClick={onMoveUp}>
<NotebookHeaderButton
label={translate('auto.components.editor.IpynbViewer.fd8ac707bc', 'Move cell up')}
disabled={!canMoveUp}
onClick={onMoveUp}
>
<MoveUp className="size-3.5" />
</NotebookHeaderButton>
<NotebookHeaderButton label={translate("auto.components.editor.IpynbViewer.27e064e2db", "Move cell down")} disabled={!canMoveDown} onClick={onMoveDown}>
<NotebookHeaderButton
label={translate('auto.components.editor.IpynbViewer.27e064e2db', 'Move cell down')}
disabled={!canMoveDown}
onClick={onMoveDown}
>
<MoveDown className="size-3.5" />
</NotebookHeaderButton>
<NotebookHeaderButton label={translate("auto.components.editor.IpynbViewer.53b839b8a0", "Insert code cell above")} onClick={() => onInsertAbove('code')}>
<NotebookHeaderButton
label={translate('auto.components.editor.IpynbViewer.53b839b8a0', 'Insert code cell above')}
onClick={() => onInsertAbove('code')}
>
<ArrowUpToLine className="size-3.5" />
</NotebookHeaderButton>
<NotebookHeaderButton label={translate("auto.components.editor.IpynbViewer.b4208cad7e", "Insert code cell below")} onClick={() => onInsertBelow('code')}>
<NotebookHeaderButton
label={translate('auto.components.editor.IpynbViewer.b4208cad7e', 'Insert code cell below')}
onClick={() => onInsertBelow('code')}
>
<ArrowDownToLine className="size-3.5" />
</NotebookHeaderButton>
<NotebookHeaderButton
label={translate("auto.components.editor.IpynbViewer.ffc1ac2699", "Insert markdown cell above")}
label={translate(
'auto.components.editor.IpynbViewer.ffc1ac2699',
'Insert markdown cell above'
)}
onClick={() => onInsertAbove('markdown')}
>
<span className="relative size-4">
@@ -212,7 +239,10 @@ function NotebookCellHeader({
</span>
</NotebookHeaderButton>
<NotebookHeaderButton
label={translate("auto.components.editor.IpynbViewer.b42f6a9547", "Insert markdown cell below")}
label={translate(
'auto.components.editor.IpynbViewer.b42f6a9547',
'Insert markdown cell below'
)}
onClick={() => onInsertBelow('markdown')}
>
<span className="relative size-4">
@@ -220,7 +250,10 @@ function NotebookCellHeader({
<MoveDown className="absolute -bottom-0.5 -right-0.5 size-2.5" />
</span>
</NotebookHeaderButton>
<NotebookHeaderButton label={translate("auto.components.editor.IpynbViewer.781abd6926", "Delete cell")} onClick={onDelete}>
<NotebookHeaderButton
label={translate('auto.components.editor.IpynbViewer.781abd6926', 'Delete cell')}
onClick={onDelete}
>
<Trash2 className="size-3.5" />
</NotebookHeaderButton>
<span className="ml-auto font-mono">#{index + 1}</span>
@@ -438,7 +471,7 @@ function OutputItem({ item }: { item: IpynbOutputItem }): React.JSX.Element | nu
})
return (
<iframe
title={translate("auto.components.editor.IpynbViewer.66a3f7d330", "Notebook HTML output")}
title={translate('auto.components.editor.IpynbViewer.66a3f7d330', 'Notebook HTML output')}
sandbox=""
referrerPolicy="no-referrer"
loading="lazy"
@@ -730,7 +763,12 @@ export default function IpynbViewer({
<div className="flex max-w-md items-start gap-3 rounded-md border border-border bg-background p-4">
<AlertCircle className="mt-0.5 size-4 text-destructive" />
<div>
<div className="font-medium text-foreground">{translate("auto.components.editor.IpynbViewer.c1601b23b2", "Unable to render notebook")}</div>
<div className="font-medium text-foreground">
{translate(
'auto.components.editor.IpynbViewer.c1601b23b2',
'Unable to render notebook'
)}
</div>
<div className="mt-1">{parsed.error}</div>
</div>
</div>
@@ -837,27 +875,35 @@ export default function IpynbViewer({
>
<div className="sticky top-0 z-10 flex items-center gap-3 border-b border-border/60 bg-background/95 px-4 py-2 text-xs text-muted-foreground backdrop-blur">
<span className="font-medium text-foreground">{filePath.split(/[/\\]/).pop()}</span>
<span>{notebook.cells.length} {translate("auto.components.editor.IpynbViewer.07e7d96612", "cells")}</span>
<span>
{notebook.cells.length}{' '}
{translate('auto.components.editor.IpynbViewer.07e7d96612', 'cells')}
</span>
<span>{notebook.language}</span>
{notebook.kernelName ? <span>{notebook.kernelName}</span> : null}
{runError ? <span className="text-destructive">{runError}</span> : null}
<div className="ml-auto flex items-center gap-2">
<NotebookHeaderButton
label={translate("auto.components.editor.IpynbViewer.15ec40a735", "Save notebook")}
label={translate('auto.components.editor.IpynbViewer.15ec40a735', 'Save notebook')}
shortcutKeys={saveShortcutKeys}
onClick={() => void saveNotebook()}
>
<Save className="size-3.5" />
</NotebookHeaderButton>
<span className="rounded-sm border border-border bg-muted px-1.5 py-0.5 font-medium text-muted-foreground">
{translate("auto.components.editor.IpynbViewer.329764e9fc", "BETA")}</span>
<span className="font-mono">{translate("auto.components.editor.IpynbViewer.8c3b21369a", "nbformat")}{notebook.nbformat}</span>
{translate('auto.components.editor.IpynbViewer.329764e9fc', 'BETA')}
</span>
<span className="font-mono">
{translate('auto.components.editor.IpynbViewer.8c3b21369a', 'nbformat')}
{notebook.nbformat}
</span>
</div>
</div>
<div className="mx-auto flex max-w-[980px] flex-col gap-3 px-5 py-5">
{notebook.cells.length === 0 ? (
<div className="flex items-center justify-center rounded-md border border-border bg-background p-8 text-sm text-muted-foreground">
{translate("auto.components.editor.IpynbViewer.d6f37a640b", "Empty notebook")}</div>
{translate('auto.components.editor.IpynbViewer.d6f37a640b', 'Empty notebook')}
</div>
) : (
notebook.cells.map((cell, index) => {
const cellKey = getCellKey(cell, index)
@@ -883,7 +929,7 @@ export default function IpynbViewer({
onMoveDown={() => moveCell(index, 1)}
onDelete={() => deleteCell(index)}
/>
{cell.kind === "markdown" ? (
{cell.kind === 'markdown' ? (
<div className="grid gap-0 lg:grid-cols-2">
<EditableTextCell
source={source}
@@ -893,7 +939,7 @@ export default function IpynbViewer({
<MarkdownCell source={source} />
</div>
</div>
) : cell.kind === "code" ? (
) : cell.kind === 'code' ? (
<MemoizedCodeCell
cell={cell}
source={source}
@@ -927,15 +973,23 @@ export default function IpynbViewer({
>
<DialogContent className="max-w-md sm:max-w-md" showCloseButton={false}>
<DialogHeader>
<DialogTitle className="text-sm">{translate("auto.components.editor.IpynbViewer.9e06ae5d36", "Run Notebook Code?")}</DialogTitle>
<DialogTitle className="text-sm">
{translate('auto.components.editor.IpynbViewer.9e06ae5d36', 'Run Notebook Code?')}
</DialogTitle>
<DialogDescription className="text-xs">
{translate("auto.components.editor.IpynbViewer.10ed04a685", "Notebook cells execute local Python on this machine from the notebook folder. Only run cells from files you trust.")}</DialogDescription>
{translate(
'auto.components.editor.IpynbViewer.10ed04a685',
'Notebook cells execute local Python on this machine from the notebook folder. Only run cells from files you trust.'
)}
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button type="button" variant="outline" size="sm" onClick={cancelPendingRun}>
{translate("auto.components.editor.IpynbViewer.7f0d7077c6", "Cancel")}</Button>
{translate('auto.components.editor.IpynbViewer.7f0d7077c6', 'Cancel')}
</Button>
<Button type="button" size="sm" autoFocus onClick={confirmPendingRun}>
{translate("auto.components.editor.IpynbViewer.859bf9fc21", "Run cell")}</Button>
{translate('auto.components.editor.IpynbViewer.859bf9fc21', 'Run cell')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -595,7 +595,7 @@ export default function MarkdownPreview({
() => [
{
id: 'all',
label: translate("auto.components.editor.MarkdownPreview.ddf087d12e", "All unsent notes"),
label: translate('auto.components.editor.MarkdownPreview.ddf087d12e', 'All unsent notes'),
notes: unsentMarkdownReviewNotes,
prompt: unsentMarkdownReviewPrompt
}
@@ -1035,8 +1035,8 @@ export default function MarkdownPreview({
<button
type="button"
className="markdown-annotation-add"
aria-label={translate("auto.components.editor.MarkdownPreview.13f94d760c", "Add note")}
title={translate("auto.components.editor.MarkdownPreview.13f94d760c", "Add note")}
aria-label={translate('auto.components.editor.MarkdownPreview.13f94d760c', 'Add note')}
title={translate('auto.components.editor.MarkdownPreview.13f94d760c', 'Add note')}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
@@ -1081,10 +1081,26 @@ export default function MarkdownPreview({
type="button"
className="orca-diff-comment-pill-btn"
title={
copiedReviewNoteId === comment.id ? translate("auto.components.editor.MarkdownPreview.94b520a96a", "Copied note") : translate("auto.components.editor.MarkdownPreview.f961e94057", "Copy note for agent")
copiedReviewNoteId === comment.id
? translate(
'auto.components.editor.MarkdownPreview.94b520a96a',
'Copied note'
)
: translate(
'auto.components.editor.MarkdownPreview.f961e94057',
'Copy note for agent'
)
}
aria-label={
copiedReviewNoteId === comment.id ? translate("auto.components.editor.MarkdownPreview.94b520a96a", "Copied note") : translate("auto.components.editor.MarkdownPreview.f961e94057", "Copy note for agent")
copiedReviewNoteId === comment.id
? translate(
'auto.components.editor.MarkdownPreview.94b520a96a',
'Copied note'
)
: translate(
'auto.components.editor.MarkdownPreview.f961e94057',
'Copy note for agent'
)
}
onClick={(event) => {
event.preventDefault()
@@ -1264,7 +1280,11 @@ export default function MarkdownPreview({
void window.api.shell.pathExists(classified.absolutePath).then((exists) => {
if (!exists) {
toast.error(
translate("auto.components.editor.MarkdownPreview.6c043947ae", "File not found: {{value0}}", { value0: classified.relativePath ?? classified.absolutePath })
translate(
'auto.components.editor.MarkdownPreview.6c043947ae',
'File not found: {{value0}}',
{ value0: classified.relativePath ?? classified.absolutePath }
)
)
return
}
@@ -1356,11 +1376,23 @@ export default function MarkdownPreview({
absolutePath
)
if (stats.isDirectory) {
toast.error(translate("auto.components.editor.MarkdownPreview.759463a221", "Cannot open directory: {{value0}}", { value0: relativePath }))
toast.error(
translate(
'auto.components.editor.MarkdownPreview.759463a221',
'Cannot open directory: {{value0}}',
{ value0: relativePath }
)
)
return
}
} catch {
toast.error(translate("auto.components.editor.MarkdownPreview.6c043947ae", "File not found: {{value0}}", { value0: relativePath }))
toast.error(
translate(
'auto.components.editor.MarkdownPreview.6c043947ae',
'File not found: {{value0}}',
{ value0: relativePath }
)
)
return
}
@@ -1663,14 +1695,20 @@ export default function MarkdownPreview({
rootRef.current?.focus()
}
}}
placeholder={translate("auto.components.editor.MarkdownPreview.517aea303b", "Find in preview")}
placeholder={translate(
'auto.components.editor.MarkdownPreview.517aea303b',
'Find in preview'
)}
className="markdown-preview-search-input h-7 !border-0 bg-transparent px-2 shadow-none focus-visible:!border-0 focus-visible:ring-0"
aria-label={translate("auto.components.editor.MarkdownPreview.ec77985138", "Find in markdown preview")}
aria-label={translate(
'auto.components.editor.MarkdownPreview.ec77985138',
'Find in markdown preview'
)}
/>
</div>
<div className="markdown-preview-search-status">
{query && matchCount === 0
? translate("auto.components.editor.MarkdownPreview.c5dc92cfe3", "No results")
? translate('auto.components.editor.MarkdownPreview.c5dc92cfe3', 'No results')
: `${matchCount === 0 ? 0 : activeMatchIndex + 1}/${matchCount}`}
</div>
<Button
@@ -1679,8 +1717,14 @@ export default function MarkdownPreview({
size="icon-xs"
onClick={() => moveToMatch(-1)}
disabled={matchCount === 0}
title={translate("auto.components.editor.MarkdownPreview.1febd97f5c", "Previous match")}
aria-label={translate("auto.components.editor.MarkdownPreview.1febd97f5c", "Previous match")}
title={translate(
'auto.components.editor.MarkdownPreview.1febd97f5c',
'Previous match'
)}
aria-label={translate(
'auto.components.editor.MarkdownPreview.1febd97f5c',
'Previous match'
)}
className="markdown-preview-search-button"
>
<ChevronUp size={14} />
@@ -1691,8 +1735,11 @@ export default function MarkdownPreview({
size="icon-xs"
onClick={() => moveToMatch(1)}
disabled={matchCount === 0}
title={translate("auto.components.editor.MarkdownPreview.b42c41bd0d", "Next match")}
aria-label={translate("auto.components.editor.MarkdownPreview.b42c41bd0d", "Next match")}
title={translate('auto.components.editor.MarkdownPreview.b42c41bd0d', 'Next match')}
aria-label={translate(
'auto.components.editor.MarkdownPreview.b42c41bd0d',
'Next match'
)}
className="markdown-preview-search-button"
>
<ChevronDown size={14} />
@@ -1703,8 +1750,11 @@ export default function MarkdownPreview({
variant="ghost"
size="icon-xs"
onClick={closeSearch}
title={translate("auto.components.editor.MarkdownPreview.12052c639c", "Close search")}
aria-label={translate("auto.components.editor.MarkdownPreview.12052c639c", "Close search")}
title={translate('auto.components.editor.MarkdownPreview.12052c639c', 'Close search')}
aria-label={translate(
'auto.components.editor.MarkdownPreview.12052c639c',
'Close search'
)}
className="markdown-preview-search-button"
>
<X size={14} />
@@ -1723,11 +1773,19 @@ export default function MarkdownPreview({
}
}}
disabled={markdownReviewNotes.length === 0}
title={translate("auto.components.editor.MarkdownPreview.0f9969a159", "Jump to first review note")}
aria-label={translate("auto.components.editor.MarkdownPreview.0f9969a159", "Jump to first review note")}
title={translate(
'auto.components.editor.MarkdownPreview.0f9969a159',
'Jump to first review note'
)}
aria-label={translate(
'auto.components.editor.MarkdownPreview.0f9969a159',
'Jump to first review note'
)}
>
<MessageSquare className="size-3.5" />
<span>{translate("auto.components.editor.MarkdownPreview.322afab6ff", "Review notes")}</span>
<span>
{translate('auto.components.editor.MarkdownPreview.322afab6ff', 'Review notes')}
</span>
<span className="markdown-review-count">{markdownReviewNotes.length}</span>
</button>
<button
@@ -1735,8 +1793,14 @@ export default function MarkdownPreview({
className="markdown-review-icon-button"
onClick={() => void handleCopyMarkdownReviewNotes()}
disabled={markdownReviewNotes.length === 0}
title={translate("auto.components.editor.MarkdownPreview.bb629de58a", "Copy notes for agent")}
aria-label={translate("auto.components.editor.MarkdownPreview.bb629de58a", "Copy notes for agent")}
title={translate(
'auto.components.editor.MarkdownPreview.bb629de58a',
'Copy notes for agent'
)}
aria-label={translate(
'auto.components.editor.MarkdownPreview.bb629de58a',
'Copy notes for agent'
)}
>
{reviewNotesCopied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
</button>
@@ -1759,7 +1823,8 @@ export default function MarkdownPreview({
{frontMatter && frontmatterVisible ? (
<div className="mb-4 rounded border border-border/60 bg-muted/40 px-3 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
{translate("auto.components.editor.MarkdownPreview.2b2b31382c", "Front Matter")}</div>
{translate('auto.components.editor.MarkdownPreview.2b2b31382c', 'Front Matter')}
</div>
<pre className="max-h-48 overflow-auto whitespace-pre-wrap text-xs text-muted-foreground font-mono scrollbar-editor">
{frontMatterInner}
</pre>
@@ -1820,7 +1885,7 @@ function MarkdownSingleNoteSendMenu({
scopes={[
{
id: 'note',
label: translate("auto.components.editor.MarkdownPreview.f37b98999e", "This note"),
label: translate('auto.components.editor.MarkdownPreview.f37b98999e', 'This note'),
notes: note.sentAt ? [] : [note],
prompt: formatMarkdownReviewNotes([note], content)
}
@@ -1876,11 +1941,16 @@ function MarkdownAnnotationComposer({
return (
<div className="markdown-annotation-composer" onClick={(event) => event.stopPropagation()}>
<div className="orca-diff-comment-popover-label">{translate("auto.components.editor.MarkdownPreview.b1bfc04034", "Selected text")}</div>
<div className="orca-diff-comment-popover-label">
{translate('auto.components.editor.MarkdownPreview.b1bfc04034', 'Selected text')}
</div>
<textarea
ref={focusTextareaRef}
className="orca-diff-comment-popover-textarea"
placeholder={translate("auto.components.editor.MarkdownPreview.d737791433", "Add note for the AI")}
placeholder={translate(
'auto.components.editor.MarkdownPreview.d737791433',
'Add note for the AI'
)}
value={body}
onChange={(event) => {
setBody(event.target.value)
@@ -1903,9 +1973,12 @@ function MarkdownAnnotationComposer({
/>
<div className="orca-diff-comment-popover-footer">
<Button variant="ghost" size="sm" onClick={onCancel} disabled={submitting}>
{translate("auto.components.editor.MarkdownPreview.e4683f70c4", "Cancel")}</Button>
{translate('auto.components.editor.MarkdownPreview.e4683f70c4', 'Cancel')}
</Button>
<Button size="sm" onClick={() => void submit()} disabled={submitting || !trimmed}>
{submitting ? translate("auto.components.editor.MarkdownPreview.d652c87c91", "Saving…") : translate("auto.components.editor.MarkdownPreview.13f94d760c", "Add note")}
{submitting
? translate('auto.components.editor.MarkdownPreview.d652c87c91', 'Saving…')
: translate('auto.components.editor.MarkdownPreview.13f94d760c', 'Add note')}
{!submitting && <CornerDownLeft className="ml-1 size-3 opacity-70" />}
</Button>
</div>
@@ -52,7 +52,19 @@ function MarkdownTocRow({
<button
type="button"
className="markdown-toc-disclosure"
aria-label={expanded ? translate("auto.components.editor.MarkdownTableOfContentsPanel.97ad46f11f", "Collapse {{value0}}", { value0: item.title }) : translate("auto.components.editor.MarkdownTableOfContentsPanel.65b036a6c8", "Expand {{value0}}", { value0: item.title })}
aria-label={
expanded
? translate(
'auto.components.editor.MarkdownTableOfContentsPanel.97ad46f11f',
'Collapse {{value0}}',
{ value0: item.title }
)
: translate(
'auto.components.editor.MarkdownTableOfContentsPanel.65b036a6c8',
'Expand {{value0}}',
{ value0: item.title }
)
}
aria-expanded={expanded}
onClick={() => onToggleCollapsed(item.id)}
>
@@ -108,12 +120,30 @@ export function MarkdownTableOfContentsPanel({
}
return (
<aside className="markdown-toc-panel" aria-label={translate("auto.components.editor.MarkdownTableOfContentsPanel.27d0a9c49a", "Table of contents")}>
<aside
className="markdown-toc-panel"
aria-label={translate(
'auto.components.editor.MarkdownTableOfContentsPanel.27d0a9c49a',
'Table of contents'
)}
>
<div className="markdown-toc-header">
<ListTree className="size-3.5 text-muted-foreground" />
<span>{translate("auto.components.editor.MarkdownTableOfContentsPanel.06357eea60", "Table of Contents")}</span>
<span>
{translate(
'auto.components.editor.MarkdownTableOfContentsPanel.06357eea60',
'Table of Contents'
)}
</span>
<div className="markdown-toc-header-actions">
<div className="markdown-toc-level-controls" role="group" aria-label={translate("auto.components.editor.MarkdownTableOfContentsPanel.0dc7b2f05a", "Collapse by level")}>
<div
className="markdown-toc-level-controls"
role="group"
aria-label={translate(
'auto.components.editor.MarkdownTableOfContentsPanel.0dc7b2f05a',
'Collapse by level'
)}
>
{TOC_LEVELS.map((level) => (
<Button
key={level}
@@ -122,9 +152,29 @@ export function MarkdownTableOfContentsPanel({
size="icon-xs"
className="markdown-toc-level-button"
aria-label={
level === 3 ? translate("auto.components.editor.MarkdownTableOfContentsPanel.f3de856175", "Expand all heading levels") : translate("auto.components.editor.MarkdownTableOfContentsPanel.111e66b85d", "Collapse to heading level {{value0}}", { value0: level })
level === 3
? translate(
'auto.components.editor.MarkdownTableOfContentsPanel.f3de856175',
'Expand all heading levels'
)
: translate(
'auto.components.editor.MarkdownTableOfContentsPanel.111e66b85d',
'Collapse to heading level {{value0}}',
{ value0: level }
)
}
title={
level === 3
? translate(
'auto.components.editor.MarkdownTableOfContentsPanel.a5daadd68b',
'Expand all'
)
: translate(
'auto.components.editor.MarkdownTableOfContentsPanel.4680a4b808',
'Collapse to H{{value0}}',
{ value0: level }
)
}
title={level === 3 ? translate("auto.components.editor.MarkdownTableOfContentsPanel.a5daadd68b", "Expand all") : translate("auto.components.editor.MarkdownTableOfContentsPanel.4680a4b808", "Collapse to H{{value0}}", { value0: level })}
onClick={() => collapseToLevel(level)}
>
H{level}
@@ -135,8 +185,14 @@ export function MarkdownTableOfContentsPanel({
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate("auto.components.editor.MarkdownTableOfContentsPanel.bbe8369097", "Close table of contents")}
title={translate("auto.components.editor.MarkdownTableOfContentsPanel.bbe8369097", "Close table of contents")}
aria-label={translate(
'auto.components.editor.MarkdownTableOfContentsPanel.bbe8369097',
'Close table of contents'
)}
title={translate(
'auto.components.editor.MarkdownTableOfContentsPanel.bbe8369097',
'Close table of contents'
)}
onClick={onClose}
>
<X className="size-3.5" />
@@ -156,7 +212,12 @@ export function MarkdownTableOfContentsPanel({
/>
))
) : (
<div className="markdown-toc-empty">{translate("auto.components.editor.MarkdownTableOfContentsPanel.de3928b6e4", "No headings")}</div>
<div className="markdown-toc-empty">
{translate(
'auto.components.editor.MarkdownTableOfContentsPanel.de3928b6e4',
'No headings'
)}
</div>
)}
</div>
</aside>
@@ -53,13 +53,26 @@ export function MarkdownTemplatePicker(): JSX.Element {
resolveRequest({ type: 'cancel' })
}
}}
title={translate("auto.components.editor.MarkdownTemplatePicker.1829437fce", "New Markdown")}
description={translate("auto.components.editor.MarkdownTemplatePicker.7b458e0b7f", "Choose a Markdown template.")}
title={translate('auto.components.editor.MarkdownTemplatePicker.1829437fce', 'New Markdown')}
description={translate(
'auto.components.editor.MarkdownTemplatePicker.7b458e0b7f',
'Choose a Markdown template.'
)}
contentClassName="w-[520px]"
>
<CommandInput placeholder={translate("auto.components.editor.MarkdownTemplatePicker.22fd4890ad", "Search templates...")} />
<CommandInput
placeholder={translate(
'auto.components.editor.MarkdownTemplatePicker.22fd4890ad',
'Search templates...'
)}
/>
<CommandList>
<CommandEmpty>{translate("auto.components.editor.MarkdownTemplatePicker.df667919ca", "No matching templates.")}</CommandEmpty>
<CommandEmpty>
{translate(
'auto.components.editor.MarkdownTemplatePicker.df667919ca',
'No matching templates.'
)}
</CommandEmpty>
<CommandGroup heading="New Document">
<CommandItem
value="blank markdown document"
@@ -68,8 +81,18 @@ export function MarkdownTemplatePicker(): JSX.Element {
>
<FileText className="mt-0.5 size-4 text-muted-foreground" />
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">{translate("auto.components.editor.MarkdownTemplatePicker.6e2e6c04ad", "Blank Markdown")}</span>
<span className="block truncate text-xs text-muted-foreground">{translate("auto.components.editor.MarkdownTemplatePicker.22cd94426f", "untitled.md")}</span>
<span className="block truncate text-sm font-medium">
{translate(
'auto.components.editor.MarkdownTemplatePicker.6e2e6c04ad',
'Blank Markdown'
)}
</span>
<span className="block truncate text-xs text-muted-foreground">
{translate(
'auto.components.editor.MarkdownTemplatePicker.22cd94426f',
'untitled.md'
)}
</span>
</span>
</CommandItem>
</CommandGroup>
@@ -84,7 +84,10 @@ export default function MermaidBlock({
if (error) {
return (
<div className="mermaid-block">
<div className="mermaid-error">{translate("auto.components.editor.MermaidBlock.dcc132e691", "Diagram error:")}{error}</div>
<div className="mermaid-error">
{translate('auto.components.editor.MermaidBlock.dcc132e691', 'Diagram error:')}
{error}
</div>
<pre>
<code>{content}</code>
</pre>
@@ -382,7 +382,7 @@ export default function MonacoEditor({
)
const searchInFilesAction = editorInstance.addAction({
id: 'orca.searchInFiles',
label: translate("auto.components.editor.MonacoEditor.fd68ae03b3", "Search in Files"),
label: translate('auto.components.editor.MonacoEditor.fd68ae03b3', 'Search in Files'),
contextMenuGroupId: 'navigation',
contextMenuOrder: 2,
run: () => {
@@ -751,8 +751,14 @@ export default function MonacoEditor({
top: Math.max(4, selectionAnnotationTarget.top - 22),
left: selectionAnnotationTarget.left ?? 4
}}
title={translate("auto.components.editor.MonacoEditor.68cb83f4a7", "Add note on selected text")}
aria-label={translate("auto.components.editor.MonacoEditor.68cb83f4a7", "Add note on selected text")}
title={translate(
'auto.components.editor.MonacoEditor.68cb83f4a7',
'Add note on selected text'
)}
aria-label={translate(
'auto.components.editor.MonacoEditor.68cb83f4a7',
'Add note on selected text'
)}
onMouseDown={(event) => {
event.preventDefault()
event.stopPropagation()
@@ -45,14 +45,22 @@ export function MonacoGutterContextMenu({
onSelect={() => window.api.ui.writeClipboardText(formatPathLineReference(filePath, line))}
>
<Copy className="w-3.5 h-3.5 mr-1.5" />
{translate("auto.components.editor.MonacoGutterContextMenu.4eaa991bde", "Copy Path to Line")}</DropdownMenuItem>
{translate(
'auto.components.editor.MonacoGutterContextMenu.4eaa991bde',
'Copy Path to Line'
)}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() =>
window.api.ui.writeClipboardText(formatPathLineReference(relativePath, line))
}
>
<Copy className="w-3.5 h-3.5 mr-1.5" />
{translate("auto.components.editor.MonacoGutterContextMenu.2e0b1cdc05", "Copy Rel. Path to Line")}</DropdownMenuItem>
{translate(
'auto.components.editor.MonacoGutterContextMenu.2e0b1cdc05',
'Copy Rel. Path to Line'
)}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={async () => {
const state = useAppStore.getState()
@@ -80,7 +88,11 @@ export function MonacoGutterContextMenu({
}}
>
<ExternalLink className="w-3.5 h-3.5 mr-1.5" />
{translate("auto.components.editor.MonacoGutterContextMenu.7b57b1b468", "Copy Remote URL")}</DropdownMenuItem>
{translate(
'auto.components.editor.MonacoGutterContextMenu.7b57b1b468',
'Copy Remote URL'
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
@@ -151,7 +151,15 @@ export function NotesSendMenu<TNote>({
)}
disabled={!hasDeliverableNotes}
title={hasDeliverableNotes ? ENABLED_SEND_TOOLTIP : disabledTooltip}
aria-label={triggerLabel ? translate("auto.components.editor.NotesSendMenu.433928cd9f", "Send {{value0}} to an agent", { value0: triggerLabel }) : ENABLED_SEND_TOOLTIP}
aria-label={
triggerLabel
? translate(
'auto.components.editor.NotesSendMenu.433928cd9f',
'Send {{value0}} to an agent',
{ value0: triggerLabel }
)
: ENABLED_SEND_TOOLTIP
}
onMouseDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
@@ -184,7 +192,9 @@ export function NotesSendMenu<TNote>({
>
{scopes.length > 1 ? (
<>
<DropdownMenuLabel>{translate("auto.components.editor.NotesSendMenu.44dc5e60a6", "Send notes")}</DropdownMenuLabel>
<DropdownMenuLabel>
{translate('auto.components.editor.NotesSendMenu.44dc5e60a6', 'Send notes')}
</DropdownMenuLabel>
{scopes.map((scope) => (
<DropdownMenuSub key={scope.id}>
<DropdownMenuSubTrigger
+10 -5
View File
@@ -125,12 +125,17 @@ export default function PdfFind({
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={translate("auto.components.editor.PdfFind.2fc3ba0ea8", "Find in page...")}
placeholder={translate('auto.components.editor.PdfFind.2fc3ba0ea8', 'Find in page...')}
className="min-w-0 flex-1 border-none bg-transparent text-sm text-white outline-none placeholder:text-zinc-500"
/>
{query ? (
<span className="shrink-0 text-xs text-zinc-400">
{totalMatches > 0 ? translate("auto.components.editor.PdfFind.db56fcd6d2", "{{value0}} of {{value1}}", { value0: activeMatch, value1: totalMatches }) : translate("auto.components.editor.PdfFind.d080ab37d6", "No matches")}
{totalMatches > 0
? translate('auto.components.editor.PdfFind.db56fcd6d2', '{{value0}} of {{value1}}', {
value0: activeMatch,
value1: totalMatches
})
: translate('auto.components.editor.PdfFind.d080ab37d6', 'No matches')}
</span>
) : null}
<div className="mx-0.5 h-4 w-px bg-zinc-700" />
@@ -140,7 +145,7 @@ export default function PdfFind({
size="icon-xs"
onClick={findPrevious}
className="flex size-6 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-200"
title={translate("auto.components.editor.PdfFind.30de726ad0", "Previous match")}
title={translate('auto.components.editor.PdfFind.30de726ad0', 'Previous match')}
>
<ChevronUp size={14} />
</Button>
@@ -150,7 +155,7 @@ export default function PdfFind({
size="icon-xs"
onClick={findNext}
className="flex size-6 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-200"
title={translate("auto.components.editor.PdfFind.eeba2547a1", "Next match")}
title={translate('auto.components.editor.PdfFind.eeba2547a1', 'Next match')}
>
<ChevronDown size={14} />
</Button>
@@ -161,7 +166,7 @@ export default function PdfFind({
size="icon-xs"
onClick={onClose}
className="flex size-6 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-200"
title={translate("auto.components.editor.PdfFind.cd65b1d6b0", "Close")}
title={translate('auto.components.editor.PdfFind.cd65b1d6b0', 'Close')}
>
<X size={14} />
</Button>
@@ -216,7 +216,7 @@ export default function PdfViewer({ content, filePath }: PdfViewerProps): JSX.El
<span className="min-w-0 truncate" title={filename}>
{filename}
</span>
<span>{translate("auto.components.editor.PdfViewer.3e98d500d2", "PDF preview")}</span>
<span>{translate('auto.components.editor.PdfViewer.3e98d500d2', 'PDF preview')}</span>
</div>
</div>
)
@@ -252,7 +252,7 @@ export default function PdfViewer({ content, filePath }: PdfViewerProps): JSX.El
className="rounded p-1 hover:bg-accent hover:text-foreground disabled:opacity-50"
onClick={zoomOut}
disabled={scale <= MIN_SCALE}
title={translate("auto.components.editor.PdfViewer.fa5d096b00", "Zoom out")}
title={translate('auto.components.editor.PdfViewer.fa5d096b00', 'Zoom out')}
>
<ZoomOut size={14} />
</button>
@@ -260,7 +260,7 @@ export default function PdfViewer({ content, filePath }: PdfViewerProps): JSX.El
type="button"
className="rounded p-1 hover:bg-accent hover:text-foreground"
onClick={zoomReset}
title={translate("auto.components.editor.PdfViewer.c0119616d6", "Fit to width")}
title={translate('auto.components.editor.PdfViewer.c0119616d6', 'Fit to width')}
>
<RotateCcw size={14} />
</button>
@@ -269,7 +269,7 @@ export default function PdfViewer({ content, filePath }: PdfViewerProps): JSX.El
className="rounded p-1 hover:bg-accent hover:text-foreground disabled:opacity-50"
onClick={zoomIn}
disabled={scale >= MAX_SCALE}
title={translate("auto.components.editor.PdfViewer.2b6eb1ccd6", "Zoom in")}
title={translate('auto.components.editor.PdfViewer.2b6eb1ccd6', 'Zoom in')}
>
<ZoomIn size={14} />
</button>
@@ -279,14 +279,18 @@ export default function PdfViewer({ content, filePath }: PdfViewerProps): JSX.El
type="button"
className="rounded p-1 hover:bg-accent hover:text-foreground"
onClick={() => setFindOpen(true)}
title={translate("auto.components.editor.PdfViewer.069ff59932", "Find in PDF ({{value0}})", { value0: findShortcutLabel })}
title={translate(
'auto.components.editor.PdfViewer.069ff59932',
'Find in PDF ({{value0}})',
{ value0: findShortcutLabel }
)}
>
<Search size={14} />
</button>
<span className="min-w-0 truncate" title={filename}>
{filename}
</span>
<span>{translate("auto.components.editor.PdfViewer.3e98d500d2", "PDF preview")}</span>
<span>{translate('auto.components.editor.PdfViewer.3e98d500d2', 'PDF preview')}</span>
</div>
</div>
)
@@ -38,19 +38,34 @@ export function ReviewNotesSendMenuContent({
if (!hasPrompt || !canSendToActiveAgent) {
return
}
const pending = toast.loading(translate("auto.components.editor.ReviewNotesSendMenuContent.50f7e753ea", "Sending notes to active agent..."))
const pending = toast.loading(
translate(
'auto.components.editor.ReviewNotesSendMenuContent.50f7e753ea',
'Sending notes to active agent...'
)
)
void sendNotesToActiveAgentSession({ worktreeId, prompt })
.then((result) => {
if (result.status === 'sent') {
onPromptDelivered?.()
toast.success(translate("auto.components.editor.ReviewNotesSendMenuContent.bb9c69a0c9", "Notes sent to active agent."))
toast.success(
translate(
'auto.components.editor.ReviewNotesSendMenuContent.bb9c69a0c9',
'Notes sent to active agent.'
)
)
return
}
toast.message(activeAgentNotesSendFailureMessage(result.status))
})
.catch((error) => {
console.error('Failed to send notes to active agent:', error)
toast.error(translate("auto.components.editor.ReviewNotesSendMenuContent.f5096c6e4e", "Could not send notes to the active agent."))
toast.error(
translate(
'auto.components.editor.ReviewNotesSendMenuContent.f5096c6e4e',
'Could not send notes to the active agent.'
)
)
})
.finally(() => {
toast.dismiss(pending)
@@ -59,16 +74,24 @@ export function ReviewNotesSendMenuContent({
return (
<>
<DropdownMenuLabel>{translate("auto.components.editor.ReviewNotesSendMenuContent.03378aea75", "Send notes to")}</DropdownMenuLabel>
<DropdownMenuLabel>
{translate('auto.components.editor.ReviewNotesSendMenuContent.03378aea75', 'Send notes to')}
</DropdownMenuLabel>
<DropdownMenuItem
disabled={!hasPrompt || !canSendToActiveAgent}
onSelect={sendToActiveAgent}
className="gap-2 rounded-[7px] px-2 py-1.5 text-[12px] leading-5 font-medium"
>
<SquareTerminal className="size-3.5" />
{translate("auto.components.editor.ReviewNotesSendMenuContent.e84705f223", "Active agent session")}</DropdownMenuItem>
{translate(
'auto.components.editor.ReviewNotesSendMenuContent.e84705f223',
'Active agent session'
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>{translate("auto.components.editor.ReviewNotesSendMenuContent.a49800405b", "New agent")}</DropdownMenuLabel>
<DropdownMenuLabel>
{translate('auto.components.editor.ReviewNotesSendMenuContent.a49800405b', 'New agent')}
</DropdownMenuLabel>
<QuickLaunchAgentMenuItems
worktreeId={worktreeId}
groupId={groupId}
@@ -30,8 +30,14 @@ export function RichMarkdownAnnotationOverlay({
top: target.buttonTop ?? 56,
left: target.buttonLeft ?? 16
}}
title={translate("auto.components.editor.RichMarkdownAnnotationOverlay.6f2f3a6001", "Add review note")}
aria-label={translate("auto.components.editor.RichMarkdownAnnotationOverlay.6f2f3a6001", "Add review note")}
title={translate(
'auto.components.editor.RichMarkdownAnnotationOverlay.6f2f3a6001',
'Add review note'
)}
aria-label={translate(
'auto.components.editor.RichMarkdownAnnotationOverlay.6f2f3a6001',
'Add review note'
)}
onMouseDown={(event) => {
event.preventDefault()
event.stopPropagation()
@@ -56,7 +62,10 @@ export function RichMarkdownAnnotationOverlay({
}
top={popover.top}
left={popover.left}
title={translate("auto.components.editor.RichMarkdownAnnotationOverlay.069b5677b8", "Selected text")}
title={translate(
'auto.components.editor.RichMarkdownAnnotationOverlay.069b5677b8',
'Selected text'
)}
onCancel={onCancelPopover}
onSubmit={onSubmit}
/>
@@ -12,31 +12,103 @@ import { translate } from '@/i18n/i18n'
* this list is just for quick picking in the UI.
*/
const LANGUAGES = [
{ value: '', label: translate("auto.components.editor.RichMarkdownCodeBlock.13822cdfda", "Plain text") },
{ value: 'bash', label: translate("auto.components.editor.RichMarkdownCodeBlock.4227cf50fe", "Bash") },
{
value: '',
label: translate('auto.components.editor.RichMarkdownCodeBlock.13822cdfda', 'Plain text')
},
{
value: 'bash',
label: translate('auto.components.editor.RichMarkdownCodeBlock.4227cf50fe', 'Bash')
},
{ value: 'c', label: 'C' },
{ value: 'cpp', label: translate("auto.components.editor.RichMarkdownCodeBlock.4daed43ae3", "C++") },
{ value: 'css', label: translate("auto.components.editor.RichMarkdownCodeBlock.026653f21f", "CSS") },
{ value: 'diff', label: translate("auto.components.editor.RichMarkdownCodeBlock.bf6ee5caaa", "Diff") },
{ value: 'go', label: translate("auto.components.editor.RichMarkdownCodeBlock.edfcc64182", "Go") },
{ value: 'graphql', label: translate("auto.components.editor.RichMarkdownCodeBlock.706fd85738", "GraphQL") },
{ value: 'html', label: translate("auto.components.editor.RichMarkdownCodeBlock.8c4a3fa02d", "HTML") },
{ value: 'java', label: translate("auto.components.editor.RichMarkdownCodeBlock.36536ad539", "Java") },
{ value: 'javascript', label: translate("auto.components.editor.RichMarkdownCodeBlock.a209c57063", "JavaScript") },
{ value: 'json', label: translate("auto.components.editor.RichMarkdownCodeBlock.78eba32de4", "JSON") },
{ value: 'kotlin', label: translate("auto.components.editor.RichMarkdownCodeBlock.bcb236e2d8", "Kotlin") },
{ value: 'markdown', label: translate("auto.components.editor.RichMarkdownCodeBlock.983b9576b4", "Markdown") },
{ value: 'mermaid', label: translate("auto.components.editor.RichMarkdownCodeBlock.89d6cc14fb", "Mermaid") },
{ value: 'python', label: translate("auto.components.editor.RichMarkdownCodeBlock.2391f9cda9", "Python") },
{ value: 'ruby', label: translate("auto.components.editor.RichMarkdownCodeBlock.96182a2f64", "Ruby") },
{ value: 'rust', label: translate("auto.components.editor.RichMarkdownCodeBlock.e72e6b03f4", "Rust") },
{ value: 'scss', label: translate("auto.components.editor.RichMarkdownCodeBlock.5af8251002", "SCSS") },
{ value: 'shell', label: translate("auto.components.editor.RichMarkdownCodeBlock.d01f55be57", "Shell") },
{ value: 'sql', label: translate("auto.components.editor.RichMarkdownCodeBlock.3009f722b9", "SQL") },
{ value: 'swift', label: translate("auto.components.editor.RichMarkdownCodeBlock.9e384d48dc", "Swift") },
{ value: 'typescript', label: translate("auto.components.editor.RichMarkdownCodeBlock.88d777bc07", "TypeScript") },
{ value: 'xml', label: translate("auto.components.editor.RichMarkdownCodeBlock.5ef5605cb7", "XML") },
{ value: 'yaml', label: translate("auto.components.editor.RichMarkdownCodeBlock.74eab1d9b2", "YAML") }
{
value: 'cpp',
label: translate('auto.components.editor.RichMarkdownCodeBlock.4daed43ae3', 'C++')
},
{
value: 'css',
label: translate('auto.components.editor.RichMarkdownCodeBlock.026653f21f', 'CSS')
},
{
value: 'diff',
label: translate('auto.components.editor.RichMarkdownCodeBlock.bf6ee5caaa', 'Diff')
},
{
value: 'go',
label: translate('auto.components.editor.RichMarkdownCodeBlock.edfcc64182', 'Go')
},
{
value: 'graphql',
label: translate('auto.components.editor.RichMarkdownCodeBlock.706fd85738', 'GraphQL')
},
{
value: 'html',
label: translate('auto.components.editor.RichMarkdownCodeBlock.8c4a3fa02d', 'HTML')
},
{
value: 'java',
label: translate('auto.components.editor.RichMarkdownCodeBlock.36536ad539', 'Java')
},
{
value: 'javascript',
label: translate('auto.components.editor.RichMarkdownCodeBlock.a209c57063', 'JavaScript')
},
{
value: 'json',
label: translate('auto.components.editor.RichMarkdownCodeBlock.78eba32de4', 'JSON')
},
{
value: 'kotlin',
label: translate('auto.components.editor.RichMarkdownCodeBlock.bcb236e2d8', 'Kotlin')
},
{
value: 'markdown',
label: translate('auto.components.editor.RichMarkdownCodeBlock.983b9576b4', 'Markdown')
},
{
value: 'mermaid',
label: translate('auto.components.editor.RichMarkdownCodeBlock.89d6cc14fb', 'Mermaid')
},
{
value: 'python',
label: translate('auto.components.editor.RichMarkdownCodeBlock.2391f9cda9', 'Python')
},
{
value: 'ruby',
label: translate('auto.components.editor.RichMarkdownCodeBlock.96182a2f64', 'Ruby')
},
{
value: 'rust',
label: translate('auto.components.editor.RichMarkdownCodeBlock.e72e6b03f4', 'Rust')
},
{
value: 'scss',
label: translate('auto.components.editor.RichMarkdownCodeBlock.5af8251002', 'SCSS')
},
{
value: 'shell',
label: translate('auto.components.editor.RichMarkdownCodeBlock.d01f55be57', 'Shell')
},
{
value: 'sql',
label: translate('auto.components.editor.RichMarkdownCodeBlock.3009f722b9', 'SQL')
},
{
value: 'swift',
label: translate('auto.components.editor.RichMarkdownCodeBlock.9e384d48dc', 'Swift')
},
{
value: 'typescript',
label: translate('auto.components.editor.RichMarkdownCodeBlock.88d777bc07', 'TypeScript')
},
{
value: 'xml',
label: translate('auto.components.editor.RichMarkdownCodeBlock.5ef5605cb7', 'XML')
},
{
value: 'yaml',
label: translate('auto.components.editor.RichMarkdownCodeBlock.74eab1d9b2', 'YAML')
}
]
export function RichMarkdownCodeBlock({
@@ -128,13 +200,18 @@ export function RichMarkdownCodeBlock({
className="code-block-copy-btn"
contentEditable={false}
onClick={handleCopy}
aria-label={translate("auto.components.editor.RichMarkdownCodeBlock.c72beafc0f", "Copy code")}
title={translate("auto.components.editor.RichMarkdownCodeBlock.c72beafc0f", "Copy code")}
aria-label={translate(
'auto.components.editor.RichMarkdownCodeBlock.c72beafc0f',
'Copy code'
)}
title={translate('auto.components.editor.RichMarkdownCodeBlock.c72beafc0f', 'Copy code')}
>
{copied ? (
<>
<Check size={14} />
<span className="code-block-copy-label">{translate("auto.components.editor.RichMarkdownCodeBlock.232d9ed853", "Copied")}</span>
<span className="code-block-copy-label">
{translate('auto.components.editor.RichMarkdownCodeBlock.232d9ed853', 'Copied')}
</span>
</>
) : (
<Copy size={14} />
@@ -26,10 +26,18 @@ export function RichMarkdownDocLinkMenu({
className="rich-markdown-doc-link-menu"
style={{ left: menu.left, top: menu.top }}
role="listbox"
aria-label={translate("auto.components.editor.RichMarkdownDocLinkMenu.0e8489bc11", "Markdown document links")}
aria-label={translate(
'auto.components.editor.RichMarkdownDocLinkMenu.0e8489bc11',
'Markdown document links'
)}
>
{rows.length === 0 ? (
<div className="rich-markdown-doc-link-item is-empty">{translate("auto.components.editor.RichMarkdownDocLinkMenu.63ced7cb9b", "No documents found")}</div>
<div className="rich-markdown-doc-link-item is-empty">
{translate(
'auto.components.editor.RichMarkdownDocLinkMenu.63ced7cb9b',
'No documents found'
)}
</div>
) : (
rows.map((row, index) => {
const rowKey = row.kind === 'document' ? row.document.filePath : row.id
@@ -60,11 +68,18 @@ export function RichMarkdownDocLinkMenu({
)}
{overflow ? (
<div className="rich-markdown-doc-link-footer">
{translate("auto.components.editor.RichMarkdownDocLinkMenu.2aaf7d9678", "Showing")}{rows.length} {translate("auto.components.editor.RichMarkdownDocLinkMenu.90c5f0e1e4", "of")}{totalMatches}
{translate('auto.components.editor.RichMarkdownDocLinkMenu.2aaf7d9678', 'Showing')}
{rows.length}{' '}
{translate('auto.components.editor.RichMarkdownDocLinkMenu.90c5f0e1e4', 'of')}
{totalMatches}
</div>
) : null}
<div className="rich-markdown-doc-link-hint">
{translate("auto.components.editor.RichMarkdownDocLinkMenu.e17b987473", "↑↓ navigate&nbsp;&nbsp;↵ select&nbsp;&nbsp;esc dismiss")}</div>
{translate(
'auto.components.editor.RichMarkdownDocLinkMenu.e17b987473',
'↑↓ navigate&nbsp;&nbsp;↵ select&nbsp;&nbsp;esc dismiss'
)}
</div>
</div>
)
}
@@ -55,14 +55,23 @@ export class RichMarkdownErrorBoundary extends React.Component<Props, State> {
return (
<div className="flex h-full min-h-0 flex-col items-center justify-center gap-3 px-6 text-center text-sm text-muted-foreground">
<div>
{translate("auto.components.editor.RichMarkdownErrorBoundary.dfdf1cacd4", "The rich markdown editor hit an unexpected error and was reset to keep the rest of Orca responsive.")}</div>
{translate(
'auto.components.editor.RichMarkdownErrorBoundary.dfdf1cacd4',
'The rich markdown editor hit an unexpected error and was reset to keep the rest of Orca responsive.'
)}
</div>
<div className="text-xs opacity-70">
{translate("auto.components.editor.RichMarkdownErrorBoundary.4a5de9f2f0", "Switch to source mode, or click retry to reload the rich view.")}</div>
{translate(
'auto.components.editor.RichMarkdownErrorBoundary.4a5de9f2f0',
'Switch to source mode, or click retry to reload the rich view.'
)}
</div>
<button
className="rounded border border-border/60 px-3 py-1 text-xs hover:bg-accent"
onClick={this.handleReset}
>
{translate("auto.components.editor.RichMarkdownErrorBoundary.aad0998127", "Retry")}</button>
{translate('auto.components.editor.RichMarkdownErrorBoundary.aad0998127', 'Retry')}
</button>
</div>
)
}
@@ -81,7 +81,10 @@ function LinkEditInput({
onCancel()
}
}}
placeholder={translate("auto.components.editor.RichMarkdownLinkBubble.7b0b945fdc", "Paste or type a link…")}
placeholder={translate(
'auto.components.editor.RichMarkdownLinkBubble.7b0b945fdc',
'Paste or type a link…'
)}
className="rich-markdown-link-input"
/>
)
@@ -130,7 +133,10 @@ export function RichMarkdownLinkBubble({
type="button"
className="rich-markdown-link-button"
onClick={onOpen}
title={translate("auto.components.editor.RichMarkdownLinkBubble.bfc813e909", "Open link")}
title={translate(
'auto.components.editor.RichMarkdownLinkBubble.bfc813e909',
'Open link'
)}
>
<ExternalLink size={14} />
</button>
@@ -138,7 +144,10 @@ export function RichMarkdownLinkBubble({
type="button"
className="rich-markdown-link-button"
onClick={onEditStart}
title={translate("auto.components.editor.RichMarkdownLinkBubble.cdfe166f6f", "Edit link")}
title={translate(
'auto.components.editor.RichMarkdownLinkBubble.cdfe166f6f',
'Edit link'
)}
>
<Pencil size={14} />
</button>
@@ -146,7 +155,10 @@ export function RichMarkdownLinkBubble({
type="button"
className="rich-markdown-link-button"
onClick={onRemove}
title={translate("auto.components.editor.RichMarkdownLinkBubble.1c99b726e0", "Remove link")}
title={translate(
'auto.components.editor.RichMarkdownLinkBubble.1c99b726e0',
'Remove link'
)}
>
<Unlink size={14} />
</button>
@@ -49,7 +49,13 @@ export function RichMarkdownReviewNoteLayer({
onDelivered
}: RichMarkdownReviewNoteLayerProps): React.JSX.Element {
return (
<div className="rich-markdown-review-note-layer" aria-label={translate("auto.components.editor.RichMarkdownReviewNoteLayer.3ababd949d", "Review notes")}>
<div
className="rich-markdown-review-note-layer"
aria-label={translate(
'auto.components.editor.RichMarkdownReviewNoteLayer.3ababd949d',
'Review notes'
)}
>
{positions.map(({ comment, top }) => (
<div
key={comment.id}
@@ -81,9 +87,27 @@ export function RichMarkdownReviewNoteLayer({
<button
type="button"
className="rich-markdown-review-note-action"
title={copiedCommentId === comment.id ? translate("auto.components.editor.RichMarkdownReviewNoteLayer.117432e2c6", "Copied note") : translate("auto.components.editor.RichMarkdownReviewNoteLayer.9cde7ad994", "Copy note for agent")}
title={
copiedCommentId === comment.id
? translate(
'auto.components.editor.RichMarkdownReviewNoteLayer.117432e2c6',
'Copied note'
)
: translate(
'auto.components.editor.RichMarkdownReviewNoteLayer.9cde7ad994',
'Copy note for agent'
)
}
aria-label={
copiedCommentId === comment.id ? translate("auto.components.editor.RichMarkdownReviewNoteLayer.117432e2c6", "Copied note") : translate("auto.components.editor.RichMarkdownReviewNoteLayer.9cde7ad994", "Copy note for agent")
copiedCommentId === comment.id
? translate(
'auto.components.editor.RichMarkdownReviewNoteLayer.117432e2c6',
'Copied note'
)
: translate(
'auto.components.editor.RichMarkdownReviewNoteLayer.9cde7ad994',
'Copy note for agent'
)
}
onMouseDown={(event) => event.stopPropagation()}
onClick={(event) => {
@@ -105,7 +129,10 @@ export function RichMarkdownReviewNoteLayer({
scopes={[
{
id: 'note',
label: translate("auto.components.editor.RichMarkdownReviewNoteLayer.f3ef92952b", "This note"),
label: translate(
'auto.components.editor.RichMarkdownReviewNoteLayer.f3ef92952b',
'This note'
),
notes: comment.sentAt ? [] : [comment as MarkdownReviewNote],
prompt: formatMarkdownReviewNotes(
[comment as MarkdownReviewNote],
@@ -31,9 +31,29 @@ export function RichMarkdownReviewRailActions({
<button
type="button"
className="rich-markdown-review-rail-toggle"
aria-label={railOpen ? translate("auto.components.editor.RichMarkdownReviewRailActions.af02dc2456", "Hide review notes") : translate("auto.components.editor.RichMarkdownReviewRailActions.8aaf2c4c69", "Show review notes")}
aria-label={
railOpen
? translate(
'auto.components.editor.RichMarkdownReviewRailActions.af02dc2456',
'Hide review notes'
)
: translate(
'auto.components.editor.RichMarkdownReviewRailActions.8aaf2c4c69',
'Show review notes'
)
}
aria-expanded={railOpen}
title={railOpen ? translate("auto.components.editor.RichMarkdownReviewRailActions.af02dc2456", "Hide review notes") : translate("auto.components.editor.RichMarkdownReviewRailActions.8aaf2c4c69", "Show review notes")}
title={
railOpen
? translate(
'auto.components.editor.RichMarkdownReviewRailActions.af02dc2456',
'Hide review notes'
)
: translate(
'auto.components.editor.RichMarkdownReviewRailActions.8aaf2c4c69',
'Show review notes'
)
}
onClick={onToggleRail}
>
<MessageSquare className="size-3.5" />
@@ -42,8 +62,28 @@ export function RichMarkdownReviewRailActions({
<button
type="button"
className="rich-markdown-review-rail-action"
title={notesCopied ? translate("auto.components.editor.RichMarkdownReviewRailActions.a807596997", "Copied notes") : translate("auto.components.editor.RichMarkdownReviewRailActions.636394af72", "Copy notes for agent")}
aria-label={notesCopied ? translate("auto.components.editor.RichMarkdownReviewRailActions.a807596997", "Copied notes") : translate("auto.components.editor.RichMarkdownReviewRailActions.636394af72", "Copy notes for agent")}
title={
notesCopied
? translate(
'auto.components.editor.RichMarkdownReviewRailActions.a807596997',
'Copied notes'
)
: translate(
'auto.components.editor.RichMarkdownReviewRailActions.636394af72',
'Copy notes for agent'
)
}
aria-label={
notesCopied
? translate(
'auto.components.editor.RichMarkdownReviewRailActions.a807596997',
'Copied notes'
)
: translate(
'auto.components.editor.RichMarkdownReviewRailActions.636394af72',
'Copy notes for agent'
)
}
onClick={onCopyNotes}
>
{notesCopied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
@@ -59,14 +59,20 @@ export function RichMarkdownSearchBar({
onClose()
}
}}
placeholder={translate("auto.components.editor.RichMarkdownSearchBar.98b89276f3", "Find in rich editor")}
placeholder={translate(
'auto.components.editor.RichMarkdownSearchBar.98b89276f3',
'Find in rich editor'
)}
className="rich-markdown-search-input h-7 !border-0 bg-transparent px-2 shadow-none focus-visible:!border-0 focus-visible:ring-0"
aria-label={translate("auto.components.editor.RichMarkdownSearchBar.158c645829", "Find in rich markdown editor")}
aria-label={translate(
'auto.components.editor.RichMarkdownSearchBar.158c645829',
'Find in rich markdown editor'
)}
/>
</div>
<div className="rich-markdown-search-status">
{query && matchCount === 0
? translate("auto.components.editor.RichMarkdownSearchBar.a86958d508", "No results")
? translate('auto.components.editor.RichMarkdownSearchBar.a86958d508', 'No results')
: `${matchCount === 0 ? 0 : activeMatchIndex + 1}/${matchCount}`}
</div>
<Button
@@ -76,8 +82,14 @@ export function RichMarkdownSearchBar({
onMouseDown={keepSearchFocus}
onClick={() => onMoveToMatch(-1)}
disabled={matchCount === 0}
title={translate("auto.components.editor.RichMarkdownSearchBar.32ae8d7d57", "Previous match")}
aria-label={translate("auto.components.editor.RichMarkdownSearchBar.32ae8d7d57", "Previous match")}
title={translate(
'auto.components.editor.RichMarkdownSearchBar.32ae8d7d57',
'Previous match'
)}
aria-label={translate(
'auto.components.editor.RichMarkdownSearchBar.32ae8d7d57',
'Previous match'
)}
className="rich-markdown-search-button"
>
<ChevronUp size={14} />
@@ -89,8 +101,11 @@ export function RichMarkdownSearchBar({
onMouseDown={keepSearchFocus}
onClick={() => onMoveToMatch(1)}
disabled={matchCount === 0}
title={translate("auto.components.editor.RichMarkdownSearchBar.f7bcecbe26", "Next match")}
aria-label={translate("auto.components.editor.RichMarkdownSearchBar.f7bcecbe26", "Next match")}
title={translate('auto.components.editor.RichMarkdownSearchBar.f7bcecbe26', 'Next match')}
aria-label={translate(
'auto.components.editor.RichMarkdownSearchBar.f7bcecbe26',
'Next match'
)}
className="rich-markdown-search-button"
>
<ChevronDown size={14} />
@@ -102,8 +117,11 @@ export function RichMarkdownSearchBar({
size="icon-xs"
onMouseDown={keepSearchFocus}
onClick={onClose}
title={translate("auto.components.editor.RichMarkdownSearchBar.de68b75bde", "Close search")}
aria-label={translate("auto.components.editor.RichMarkdownSearchBar.de68b75bde", "Close search")}
title={translate('auto.components.editor.RichMarkdownSearchBar.de68b75bde', 'Close search')}
aria-label={translate(
'auto.components.editor.RichMarkdownSearchBar.de68b75bde',
'Close search'
)}
className="rich-markdown-search-button"
>
<X size={14} />
@@ -30,21 +30,35 @@ export function RichMarkdownSlashMenu({
className="rich-markdown-slash-menu"
style={{ left: slashMenu.left, top: slashMenu.top }}
role="dialog"
aria-label={translate("auto.components.editor.RichMarkdownSlashMenu.2e0400b958", "Slash commands")}
aria-label={translate(
'auto.components.editor.RichMarkdownSlashMenu.2e0400b958',
'Slash commands'
)}
>
<div className="rich-markdown-slash-search" onMouseDown={(event) => event.preventDefault()}>
<Search className="size-3.5" />
<input
aria-label={translate("auto.components.editor.RichMarkdownSlashMenu.550189b06c", "Search blocks")}
aria-label={translate(
'auto.components.editor.RichMarkdownSlashMenu.550189b06c',
'Search blocks'
)}
readOnly
type="text"
value={slashMenu.query}
placeholder={translate("auto.components.editor.RichMarkdownSlashMenu.dbdd2ad15f", "Search blocks...")}
placeholder={translate(
'auto.components.editor.RichMarkdownSlashMenu.dbdd2ad15f',
'Search blocks...'
)}
/>
</div>
<div className="rich-markdown-slash-results scrollbar-sleek" role="listbox">
{filteredCommands.length === 0 ? (
<div className="rich-markdown-slash-empty">{translate("auto.components.editor.RichMarkdownSlashMenu.82c6816ff8", "No blocks found")}</div>
<div className="rich-markdown-slash-empty">
{translate(
'auto.components.editor.RichMarkdownSlashMenu.82c6816ff8',
'No blocks found'
)}
</div>
) : (
filteredCommands.map((command, index) => {
const showGroup = command.group !== currentGroup
@@ -34,28 +34,28 @@ export function RichMarkdownToolbar({
<div className="rich-markdown-editor-toolbar">
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.b462641ed2", "Body text")}
label={translate('auto.components.editor.RichMarkdownToolbar.b462641ed2', 'Body text')}
onClick={() => editor?.chain().focus().setParagraph().run()}
>
<Pilcrow className="size-3.5" />
</RichMarkdownToolbarButton>
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.abb5100a3d", "Heading 1")}
label={translate('auto.components.editor.RichMarkdownToolbar.abb5100a3d', 'Heading 1')}
onClick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}
>
<Heading1 className="size-3.5" />
</RichMarkdownToolbarButton>
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.d34a2021c8", "Heading 2")}
label={translate('auto.components.editor.RichMarkdownToolbar.d34a2021c8', 'Heading 2')}
onClick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
>
<Heading2 className="size-3.5" />
</RichMarkdownToolbarButton>
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.cf5817d827", "Heading 3")}
label={translate('auto.components.editor.RichMarkdownToolbar.cf5817d827', 'Heading 3')}
onClick={() => editor?.chain().focus().toggleHeading({ level: 3 }).run()}
>
<Heading3 className="size-3.5" />
@@ -63,21 +63,21 @@ export function RichMarkdownToolbar({
<Separator />
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.4f9e789fe0", "Bold")}
label={translate('auto.components.editor.RichMarkdownToolbar.4f9e789fe0', 'Bold')}
onClick={() => editor?.chain().focus().toggleBold().run()}
>
B
</RichMarkdownToolbarButton>
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.6b4ccf9493", "Italic")}
label={translate('auto.components.editor.RichMarkdownToolbar.6b4ccf9493', 'Italic')}
onClick={() => editor?.chain().focus().toggleItalic().run()}
>
I
</RichMarkdownToolbarButton>
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.0bea19a988", "Strike")}
label={translate('auto.components.editor.RichMarkdownToolbar.0bea19a988', 'Strike')}
onClick={() => editor?.chain().focus().toggleStrike().run()}
>
S
@@ -85,21 +85,21 @@ export function RichMarkdownToolbar({
<Separator />
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.5d1539e5a9", "Bullet list")}
label={translate('auto.components.editor.RichMarkdownToolbar.5d1539e5a9', 'Bullet list')}
onClick={() => editor?.chain().focus().toggleBulletList().run()}
>
<List className="size-3.5" />
</RichMarkdownToolbarButton>
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.31630ed66e", "Numbered list")}
label={translate('auto.components.editor.RichMarkdownToolbar.31630ed66e', 'Numbered list')}
onClick={() => editor?.chain().focus().toggleOrderedList().run()}
>
<ListOrdered className="size-3.5" />
</RichMarkdownToolbarButton>
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.f97031be09", "Checklist")}
label={translate('auto.components.editor.RichMarkdownToolbar.f97031be09', 'Checklist')}
onClick={() => editor?.chain().focus().toggleTaskList().run()}
>
<ListTodo className="size-3.5" />
@@ -107,15 +107,23 @@ export function RichMarkdownToolbar({
<Separator />
<RichMarkdownToolbarButton
active={false}
label={translate("auto.components.editor.RichMarkdownToolbar.f6a51cb9af", "Quote")}
label={translate('auto.components.editor.RichMarkdownToolbar.f6a51cb9af', 'Quote')}
onClick={() => editor?.chain().focus().toggleBlockquote().run()}
>
<Quote className="size-3.5" />
</RichMarkdownToolbarButton>
<RichMarkdownToolbarButton active={false} label={translate("auto.components.editor.RichMarkdownToolbar.6d52624712", "Link")} onClick={onToggleLink}>
<RichMarkdownToolbarButton
active={false}
label={translate('auto.components.editor.RichMarkdownToolbar.6d52624712', 'Link')}
onClick={onToggleLink}
>
<LinkIcon className="size-3.5" />
</RichMarkdownToolbarButton>
<RichMarkdownToolbarButton active={false} label={translate("auto.components.editor.RichMarkdownToolbar.e935c6b61e", "Image")} onClick={onImagePick}>
<RichMarkdownToolbarButton
active={false}
label={translate('auto.components.editor.RichMarkdownToolbar.e935c6b61e', 'Image')}
onClick={onImagePick}
>
<ImageIcon className="size-3.5" />
</RichMarkdownToolbarButton>
</div>
@@ -133,13 +133,21 @@ export function UntitledFileRenameDialog({
}}
>
<DialogHeader>
<DialogTitle className="text-sm">{translate("auto.components.editor.UntitledFileRenameDialog.674b046582", "Save as")}</DialogTitle>
<DialogTitle className="text-sm">
{translate('auto.components.editor.UntitledFileRenameDialog.674b046582', 'Save as')}
</DialogTitle>
<DialogDescription className="text-xs">
{translate("auto.components.editor.UntitledFileRenameDialog.e365f3c638", "Name your markdown file and pick a folder.")}</DialogDescription>
{translate(
'auto.components.editor.UntitledFileRenameDialog.e365f3c638',
'Name your markdown file and pick a folder.'
)}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3">
<div>
<label className="text-[11px] font-medium text-muted-foreground mb-1 block">{translate("auto.components.editor.UntitledFileRenameDialog.b6ed807cc6", "Name")}</label>
<label className="text-[11px] font-medium text-muted-foreground mb-1 block">
{translate('auto.components.editor.UntitledFileRenameDialog.b6ed807cc6', 'Name')}
</label>
<div className="flex items-center gap-1.5">
<Input
ref={setNameInputNode}
@@ -154,16 +162,22 @@ export function UntitledFileRenameDialog({
handleSubmit()
}
}}
placeholder={translate("auto.components.editor.UntitledFileRenameDialog.c8ac7868e6", "file name")}
placeholder={translate(
'auto.components.editor.UntitledFileRenameDialog.c8ac7868e6',
'file name'
)}
className="h-8 text-sm"
aria-invalid={!!displayError}
/>
<span className="text-xs text-muted-foreground shrink-0">{translate("auto.components.editor.UntitledFileRenameDialog.2d7d39dc63", ".md")}</span>
<span className="text-xs text-muted-foreground shrink-0">
{translate('auto.components.editor.UntitledFileRenameDialog.2d7d39dc63', '.md')}
</span>
</div>
</div>
<div>
<label className="text-[11px] font-medium text-muted-foreground mb-1 block">
{translate("auto.components.editor.UntitledFileRenameDialog.30099dca46", "Folder")}</label>
{translate('auto.components.editor.UntitledFileRenameDialog.30099dca46', 'Folder')}
</label>
<div className="flex items-center gap-1.5">
<Input
value={dir}
@@ -187,7 +201,15 @@ export function UntitledFileRenameDialog({
disabled={disableBrowse}
onClick={() => void handleBrowse()}
title={
disableBrowse ? translate("auto.components.editor.UntitledFileRenameDialog.5e7f0d8a80", "Folder picker unavailable for remote files") : translate("auto.components.editor.UntitledFileRenameDialog.725868c75d", "Browse folders")
disableBrowse
? translate(
'auto.components.editor.UntitledFileRenameDialog.5e7f0d8a80',
'Folder picker unavailable for remote files'
)
: translate(
'auto.components.editor.UntitledFileRenameDialog.725868c75d',
'Browse folders'
)
}
>
<FolderOpen className="size-3.5" />
@@ -198,9 +220,11 @@ export function UntitledFileRenameDialog({
{displayError && <p className="text-xs text-destructive mt-1">{displayError}</p>}
<DialogFooter className="mt-1">
<Button variant="outline" size="sm" onClick={onClose}>
{translate("auto.components.editor.UntitledFileRenameDialog.949711deb4", "Cancel")}</Button>
{translate('auto.components.editor.UntitledFileRenameDialog.949711deb4', 'Cancel')}
</Button>
<Button size="sm" onClick={handleSubmit}>
{translate("auto.components.editor.UntitledFileRenameDialog.a7dd27b0bc", "Save")}</Button>
{translate('auto.components.editor.UntitledFileRenameDialog.a7dd27b0bc', 'Save')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -16,14 +16,23 @@ export async function exportActiveMarkdownToPdf(): Promise<void> {
return
}
const toastId = toast.loading(translate("auto.components.editor.export.active.markdown.d4a901e0ad", "Exporting PDF..."))
const toastId = toast.loading(
translate('auto.components.editor.export.active.markdown.d4a901e0ad', 'Exporting PDF...')
)
try {
const result = await window.api.export.htmlToPdf({
html: payload.html,
title: payload.title
})
if (result.success) {
toast.success(translate("auto.components.editor.export.active.markdown.51c4244904", "Exported to {{value0}}", { value0: result.filePath }), { id: toastId })
toast.success(
translate(
'auto.components.editor.export.active.markdown.51c4244904',
'Exported to {{value0}}',
{ value0: result.filePath }
),
{ id: toastId }
)
return
}
if (result.cancelled) {
@@ -32,7 +41,14 @@ export async function exportActiveMarkdownToPdf(): Promise<void> {
toast.dismiss(toastId)
return
}
toast.error(result.error ?? translate("auto.components.editor.export.active.markdown.eda2cea3ad", "Failed to export PDF"), { id: toastId })
toast.error(
result.error ??
translate(
'auto.components.editor.export.active.markdown.eda2cea3ad',
'Failed to export PDF'
),
{ id: toastId }
)
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to export PDF'
toast.error(message, { id: toastId })
@@ -17,7 +17,10 @@ type UnsupportedMatch = {
const UNSUPPORTED_PATTERNS: UnsupportedMatch[] = [
{
reason: 'html-or-jsx',
message: translate("auto.components.editor.markdown.rich.mode.57128b73e1", "Editable only in code mode because this file contains HTML, JSX, or MDX."),
message: translate(
'auto.components.editor.markdown.rich.mode.57128b73e1',
'Editable only in code mode because this file contains HTML, JSX, or MDX.'
),
// Why: the rich editor preserves common embedded markup via placeholder
// tokens before parsing, but any HTML shape that still fails round-trip
// must fall back instead of risking silent source corruption.
@@ -25,12 +28,18 @@ const UNSUPPORTED_PATTERNS: UnsupportedMatch[] = [
},
{
reason: 'reference-links',
message: translate("auto.components.editor.markdown.rich.mode.2fd2b44073", "Editable only in code mode because this file contains reference-style links."),
message: translate(
'auto.components.editor.markdown.rich.mode.2fd2b44073',
'Editable only in code mode because this file contains reference-style links.'
),
pattern: /^\[[^\]]+\]:\s+\S+/m
},
{
reason: 'footnotes',
message: translate("auto.components.editor.markdown.rich.mode.7a8ce7c7da", "Editable only in code mode because this file contains footnotes."),
message: translate(
'auto.components.editor.markdown.rich.mode.7a8ce7c7da',
'Editable only in code mode because this file contains footnotes.'
),
pattern: /^\[\^[^\]]+\]:\s+/m
}
]
@@ -198,7 +198,13 @@ function openMarkdownLinkInClientOs({
if (classified.kind === 'markdown') {
void window.api.shell.pathExists(classified.absolutePath).then((exists) => {
if (!exists) {
toast.error(translate("auto.components.editor.rich.markdown.editor.click.routing.2d5fb9335d", "File not found: {{value0}}", { value0: classified.relativePath }))
toast.error(
translate(
'auto.components.editor.rich.markdown.editor.click.routing.2d5fb9335d',
'File not found: {{value0}}',
{ value0: classified.relativePath }
)
)
return
}
void window.api.shell.openFileUri(toFileUrlForOsEscape(classified.absolutePath))
@@ -35,7 +35,8 @@ export function useContextualCopySetup() {
className="pointer-events-none fixed z-50 rounded-md bg-foreground px-2 py-1 text-xs text-background shadow-sm"
style={{ left: copyToast.left, top: copyToast.top }}
>
{translate("auto.components.editor.useContextualCopySetup.059bfb0d94", "Context copied")}</div>
{translate('auto.components.editor.useContextualCopySetup.059bfb0d94', 'Context copied')}
</div>
) : null
return { setupCopy, toastNode }
@@ -37,7 +37,12 @@ export function useLocalImagePick(
if (settings?.activeRuntimeEnvironmentId?.trim() || connectionId) {
const worktreePath = getWorktreePath(worktreeId)
if (settings?.activeRuntimeEnvironmentId?.trim() && !worktreePath) {
toast.error(translate("auto.components.editor.useLocalImagePick.91d835dc88", "Worktree path not available."))
toast.error(
translate(
'auto.components.editor.useLocalImagePick.91d835dc88',
'Worktree path not available.'
)
)
return
}
// Why: picked images are client-local files while remote markdown lives
@@ -55,7 +60,12 @@ export function useLocalImagePick(
)
const imported = results.find((result) => result.status === 'imported')
if (!imported) {
toast.error(translate("auto.components.editor.useLocalImagePick.175cb8b8ce", "Failed to insert image."))
toast.error(
translate(
'auto.components.editor.useLocalImagePick.175cb8b8ce',
'Failed to insert image.'
)
)
return
}
editor
@@ -57,7 +57,10 @@ export function useRichMarkdownReviewData({
return [
{
id: 'all',
label: translate("auto.components.editor.useRichMarkdownReviewData.f9d2acd6b0", "All unsent notes"),
label: translate(
'auto.components.editor.useRichMarkdownReviewData.f9d2acd6b0',
'All unsent notes'
),
notes: unsentNotes,
prompt: formatMarkdownReviewNotes(unsentNotes, markdownReviewContent)
}

Some files were not shown because too many files have changed in this diff Show More