feat(desktop): add session automation controls

(cherry picked from commit 8a61c2bcba8e4c7b3adcc172d4e5457721f76990)
This commit is contained in:
Jerry Gooch
2026-09-05 15:22:49 -05:00
committed by Teknium
parent bfddf556bf
commit dffd8d62c2
17 changed files with 3224 additions and 9 deletions
@@ -0,0 +1,200 @@
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { $composerActionsBySession } from '@/store/composer-actions'
import { $previewStatusBySession } from '@/store/preview-status'
import { $sessionControlBySession, type SessionControlEntry } from '@/store/session-control'
import { $todosBySession } from '@/store/todos'
import { useSessionStatusPresence } from './use-status-presence'
const SID = 'presence-session-1'
const mockEntry = (overrides?: Partial<SessionControlEntry>): SessionControlEntry => ({
capability: 'supported',
error: null,
loading: false,
pendingAction: null,
snapshot: {
goal: null,
heartbeat: null,
loop: null,
revision: 'rev-1',
updated_at: 1000
},
...overrides
})
describe('useSessionStatusPresence', () => {
beforeEach(() => {
$todosBySession.set({})
$composerActionsBySession.set({})
$previewStatusBySession.set({})
$sessionControlBySession.set({})
})
afterEach(() => {
cleanup()
$todosBySession.set({})
$composerActionsBySession.set({})
$previewStatusBySession.set({})
$sessionControlBySession.set({})
})
it('returns false when session is null or empty', () => {
const { result } = renderHook(() => useSessionStatusPresence(null))
expect(result.current).toBe(false)
const { result: emptyResult } = renderHook(() => useSessionStatusPresence(SID))
expect(emptyResult.current).toBe(false)
})
it('returns true when legacy status items exist', () => {
const { result } = renderHook(() => useSessionStatusPresence(SID))
expect(result.current).toBe(false)
act(() => {
$todosBySession.set({
[SID]: [{ content: 'task 1', id: '1', status: 'in_progress' }]
})
})
expect(result.current).toBe(true)
})
it('returns true when structured goal exists in session control', () => {
const { result } = renderHook(() => useSessionStatusPresence(SID))
expect(result.current).toBe(false)
act(() => {
$sessionControlBySession.set({
[SID]: mockEntry({
snapshot: {
goal: {
contract: {
boundaries: '',
constraints: '',
outcome: 'test outcome',
stop_when: '',
verification: ''
},
gates: [],
max_turns: 10,
status: 'active',
subgoals: [],
title: 'Structured Goal',
turns_used: 1
},
heartbeat: null,
loop: null,
revision: 'rev-2',
updated_at: 2000
}
})
})
})
expect(result.current).toBe(true)
})
it('returns true when structured loop exists in session control', () => {
const { result } = renderHook(() => useSessionStatusPresence(SID))
expect(result.current).toBe(false)
act(() => {
$sessionControlBySession.set({
[SID]: mockEntry({
snapshot: {
goal: null,
heartbeat: null,
loop: {
awaiting_response: false,
created_at: 1000,
current_delay: 60,
deferred_by_goal: false,
interval_seconds: 60,
last_fired_at: 1000,
max_ticks: 10,
mode: 'interval',
next_due_at: 2000,
prompt: 'Run loop',
status: 'active',
ticks_fired: 0,
times: 5,
until: ''
},
revision: 'rev-3',
updated_at: 2000
}
})
})
})
expect(result.current).toBe(true)
})
it('returns true when structured heartbeat exists in session control', () => {
const { result } = renderHook(() => useSessionStatusPresence(SID))
expect(result.current).toBe(false)
act(() => {
$sessionControlBySession.set({
[SID]: mockEntry({
snapshot: {
goal: null,
heartbeat: {
created_at: 1000,
fire_count: 1,
interval_seconds: 300,
last_fired_at: 1000,
prompt: 'Heartbeat check',
status: 'active'
},
loop: null,
revision: 'rev-4',
updated_at: 2000
}
})
})
})
expect(result.current).toBe(true)
})
it('returns false when session control entry has empty snapshot (null goal, loop, heartbeat)', () => {
const { result } = renderHook(() => useSessionStatusPresence(SID))
expect(result.current).toBe(false)
act(() => {
$sessionControlBySession.set({
[SID]: mockEntry({
snapshot: {
goal: null,
heartbeat: null,
loop: null,
revision: 'rev-empty',
updated_at: 2000
}
})
})
})
expect(result.current).toBe(false)
})
it('returns true when session control entry has only an error', () => {
const { result } = renderHook(() => useSessionStatusPresence(SID))
expect(result.current).toBe(false)
act(() => {
$sessionControlBySession.set({
[SID]: mockEntry({
error: 'Gateway connection failed',
snapshot: null
})
})
})
expect(result.current).toBe(true)
})
})
@@ -3,8 +3,9 @@ import { useSyncExternalStore } from 'react'
import { $composerActionsBySession } from '@/store/composer-actions'
import { $statusItemsBySession } from '@/store/composer-status'
import { $previewStatusBySession } from '@/store/preview-status'
import { $sessionControlBySession } from '@/store/session-control'
/** Structural view of the three per-session feeds — they hold different item
/** Structural view of the per-session feeds — they hold different item
* types, and all this hook needs from each is "does this key have rows". */
interface PresenceFeed {
get(): Record<string, undefined | unknown[]>
@@ -14,7 +15,7 @@ interface PresenceFeed {
const FEEDS: PresenceFeed[] = [$statusItemsBySession, $composerActionsBySession, $previewStatusBySession]
const subscribe = (onChange: () => void) => {
const offs = FEEDS.map(feed => feed.listen(onChange))
const offs = [...FEEDS.map(feed => feed.listen(onChange)), $sessionControlBySession.listen(onChange)]
return () => {
for (const off of offs) {
@@ -24,8 +25,9 @@ const subscribe = (onChange: () => void) => {
}
/**
* Whether a session has any status items, micro actions, or previews, as a
* coarse *edge*: the boolean only flips when the stack appears/disappears.
* Whether a session has any status items, micro actions, previews, or
* structured session controls (goal, loop, heartbeat), as a coarse *edge*:
* the boolean only flips when the stack appears/disappears.
* ChatBar uses it to toggle a styling data-attr — subscribing to the whole
* `$statusItemsBySession` (a `computed` that rebuilds the entire map) /
* `$previewStatusBySession` maps re-rendered the ~1.4k ChatBar on every
@@ -39,6 +41,16 @@ export function useSessionStatusPresence(sessionId: string | null): boolean {
return false
}
return FEEDS.some(feed => (feed.get()[sessionId]?.length ?? 0) > 0)
if (FEEDS.some(feed => (feed.get()[sessionId]?.length ?? 0) > 0)) {
return true
}
const control = $sessionControlBySession.get()[sessionId]
return Boolean(
control?.error ||
(control?.snapshot &&
(control.snapshot.goal !== null || control.snapshot.loop !== null || control.snapshot.heartbeat !== null))
)
})
}
@@ -1195,6 +1195,7 @@ export function ChatBar({
grows upward over the thread and the dock's own measurement covers
it. Collapses to nothing when every status is empty. */}
<ComposerStatusStack
onSubmit={onSubmit}
queue={
activeQueueSessionKey && queuedPrompts.length > 0 ? (
<QueuePanel
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router'
import { blurComposerInput } from '@/app/chat/composer/focus'
import { AGENTS_ROUTE } from '@/app/routes'
import type { SubmitTextOptions } from '@/app/session/hooks/use-prompt-actions/utils'
import { BillingBanner } from '@/components/billing-banner'
import { composerDockCard } from '@/components/chat/composer-dock'
import { StatusSection } from '@/components/chat/status-section'
@@ -24,12 +25,14 @@ import {
type StatusGroup,
stopBackgroundProcess
} from '@/store/composer-status'
import { refreshSessionGoal } from '@/store/goals'
import { $previewStatusBySession, dismissPreviewArtifact } from '@/store/preview-status'
import { $sessionControlBySession, refreshSessionControl } from '@/store/session-control'
import { $threadScrolledUp } from '@/store/thread-scroll'
import { openSessionInNewWindow } from '@/store/windows'
import { PreviewStatusRow } from './preview-row'
import { SessionControlSections } from './session-control'
import { useSessionValue } from './session-control-utils'
import { StatusItemRow } from './status-row'
// Slow safety-net poll for silent exits (processes without notify_on_complete
@@ -74,6 +77,7 @@ const hasRunningTodo = (group: StatusGroup) =>
group.type === 'todo' && group.items.some(item => item.todoStatus === 'in_progress' && item.state === 'running')
interface ComposerStatusStackProps {
onSubmit?: (value: string, options?: SubmitTextOptions) => Promise<boolean> | boolean
/** The queue, built by the composer (it owns the queue's callbacks). Rendered
* as the last group so it stays fused to the composer like before. */
queue: ReactNode
@@ -85,7 +89,7 @@ interface ComposerStatusStackProps {
* every session-scoped status — subagents, background tasks, queue — grouped by
* type and separated by light dividers. Collapses to nothing when empty.
*/
export function ComposerStatusStack({ queue, sessionId }: ComposerStatusStackProps) {
export function ComposerStatusStack({ onSubmit, queue, sessionId }: ComposerStatusStackProps) {
const { t } = useI18n()
const navigate = useNavigate()
// Subscribe to THIS session's slice only. Both maps churn on other
@@ -96,10 +100,22 @@ export function ComposerStatusStack({ queue, sessionId }: ComposerStatusStackPro
// items actually changed.
const items = useSessionSlice($statusItemsBySession, sessionId)
const previews = useSessionSlice($previewStatusBySession, sessionId)
const controlEntry = useSessionValue($sessionControlBySession, sessionId)
const scrolledUp = useStore($threadScrolledUp)
const billing = useStore($billingBlock)
const groups = useMemo(() => groupStatusItems(items), [items])
const isStructuredSupported = controlEntry?.capability === 'supported'
const groups = useMemo(() => {
const raw = groupStatusItems(items)
if (isStructuredSupported) {
return raw.filter(g => g.type !== 'goal')
}
return raw
}, [items, isStructuredSupported])
// Seed from the registry on session open; event-driven refreshes (terminal /
// process tool completions) live in use-message-stream. This must NOT reset
@@ -112,7 +128,7 @@ export function ComposerStatusStack({ queue, sessionId }: ComposerStatusStackPro
useEffect(() => {
if (sessionId) {
void refreshBackgroundProcesses(sessionId)
void refreshSessionGoal(sessionId)
void refreshSessionControl(sessionId)
}
}, [sessionId])
@@ -160,6 +176,21 @@ export function ComposerStatusStack({ queue, sessionId }: ComposerStatusStackPro
sections.push({ key: 'billing', node: <BillingBanner sessionId={sessionId} /> })
}
const hasControlContent = Boolean(
controlEntry &&
(controlEntry.error ||
controlEntry.snapshot?.goal ||
controlEntry.snapshot?.loop ||
controlEntry.snapshot?.heartbeat)
)
if (sessionId && controlEntry && hasControlContent) {
sections.push({
key: 'session-control',
node: <SessionControlSections entry={controlEntry} onSubmit={onSubmit} sessionId={sessionId} />
})
}
for (const group of groups) {
sections.push({
key: group.type,
@@ -0,0 +1,606 @@
import { memo, useCallback, useState } from 'react'
import type { SubmitTextOptions } from '@/app/session/hooks/use-prompt-actions/utils'
import { StatusSection } from '@/components/chat/status-section'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger
} from '@/components/ui/context-menu'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tip } from '@/components/ui/tooltip'
import { useI18n } from '@/i18n'
import {
runSessionControlAction,
type SessionControlAction,
type SessionControlActionArgs,
type SessionControlGoal
} from '@/store/session-control'
import type { ConfirmState } from './session-control-utils'
interface GoalSectionProps {
goal: SessionControlGoal
sessionId: string
pendingAction: SessionControlAction | null
onSubmit?: (value: string, options?: SubmitTextOptions) => Promise<boolean> | boolean
onFeedback: (error: string | null, success: string | null) => void
}
export const SessionControlGoalSection = memo(function SessionControlGoalSection({
goal,
sessionId,
pendingAction,
onSubmit,
onFeedback
}: GoalSectionProps) {
const { t } = useI18n()
const s = t.statusStack
const ctrl = s.control
const [detailsOpen, setDetailsOpen] = useState(false)
const [addCriterionOpen, setAddCriterionOpen] = useState(false)
const [addCriterionError, setAddCriterionError] = useState<string | null>(null)
const [newCriterionText, setNewCriterionText] = useState('')
const [confirmState, setConfirmState] = useState<ConfirmState | null>(null)
const [menuOpen, setMenuOpen] = useState(false)
const isBusy = Boolean(pendingAction)
const handleAction = useCallback(
async (
action: SessionControlAction,
args?: SessionControlActionArgs,
onFailure?: (message: string) => void
): Promise<boolean> => {
onFeedback(null, null)
try {
const dispatch = await runSessionControlAction(sessionId, action, args)
if (dispatch.type === 'send') {
if (!dispatch.message || !onSubmit) {
onFeedback(ctrl.continuationFailed, null)
onFailure?.(ctrl.continuationFailed)
return false
}
const submitted = await onSubmit(dispatch.message, {
displayKind: 'hidden',
sessionId
})
if (!submitted) {
onFeedback(ctrl.continuationFailed, null)
onFailure?.(ctrl.continuationFailed)
return false
}
}
onFeedback(null, ctrl.actionSucceeded)
return true
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
const failure = ctrl.actionFailed(msg)
onFeedback(failure, null)
onFailure?.(failure)
return false
}
},
[sessionId, onSubmit, onFeedback, ctrl]
)
const copyCriterionText = useCallback(
async (text: string) => {
try {
await navigator.clipboard.writeText(text)
onFeedback(null, ctrl.copySuccess)
} catch {
onFeedback(ctrl.copyFailure, null)
}
},
[onFeedback, ctrl]
)
const visibleState: 'waiting' | 'active' | 'paused' | 'done' = goal.wait_barrier
? 'waiting'
: goal.status === 'paused'
? 'paused'
: goal.status === 'done'
? 'done'
: 'active'
const stateLabel =
visibleState === 'waiting'
? s.goalWaiting
: visibleState === 'paused'
? s.goalPaused
: visibleState === 'done'
? s.goalDone
: s.goalActive
const headerLabel =
visibleState === 'done'
? `${stateLabel} · ${ctrl.goalDoneTurns(goal.turns_used)}`
: goal.max_turns > 0
? `${stateLabel} · ${ctrl.goalActiveTurns(goal.turns_used, goal.max_turns)}`
: goal.turns_used > 0
? `${stateLabel} · ${ctrl.goalTurn(goal.turns_used)}`
: stateLabel
const confirmClearGoal = () => {
setConfirmState({
title: ctrl.clearGoalConfirmTitle,
description: ctrl.clearGoalConfirmBody,
destructive: true,
confirmLabel: ctrl.clearGoal,
onConfirm: async () => {
await handleAction('goal.clear')
}
})
}
const confirmRemoveCriterion = (index: number) => {
setConfirmState({
title: ctrl.removeCriterionConfirmTitle(index),
description: ctrl.removeCriterionConfirmBody(index),
destructive: true,
confirmLabel: ctrl.removeCriterion(index),
onConfirm: async () => {
await handleAction('subgoal.remove', { index })
}
})
}
const confirmClearCriteria = () => {
setConfirmState({
title: ctrl.clearCriteriaConfirmTitle,
description: ctrl.clearCriteriaConfirmBody,
destructive: true,
confirmLabel: ctrl.clearCriteria,
onConfirm: async () => {
await handleAction('subgoal.clear')
}
})
}
const openAddCriterion = () => {
setAddCriterionError(null)
setAddCriterionOpen(true)
}
const renderMenuItems = (isContext = false) => {
const Item = isContext ? ContextMenuItem : DropdownMenuItem
const Sep = isContext ? ContextMenuSeparator : DropdownMenuSeparator
return (
<>
{hasDetails && (
<Item disabled={isBusy} onSelect={() => setDetailsOpen(true)}>
<Codicon name="eye" size="0.8rem" />
<span>{ctrl.viewDetails}</span>
</Item>
)}
{visibleState !== 'done' && (
<Item disabled={isBusy} onSelect={openAddCriterion}>
<Codicon name="add" size="0.8rem" />
<span>{ctrl.addCriterion}</span>
</Item>
)}
{(hasDetails || visibleState !== 'done') && <Sep />}
{visibleState === 'active' && (
<Item disabled={isBusy} onSelect={() => void handleAction('goal.pause')}>
<Codicon name="debug-pause" size="0.8rem" />
<span>{ctrl.pauseGoal}</span>
</Item>
)}
{visibleState === 'paused' && (
<Item disabled={isBusy} onSelect={() => void handleAction('goal.resume')}>
<Codicon name="play" size="0.8rem" />
<span>{ctrl.resumeGoal}</span>
</Item>
)}
{visibleState === 'waiting' && (
<>
<Item disabled={isBusy} onSelect={() => void handleAction('goal.unwait')}>
<Codicon name="play" size="0.8rem" />
<span>{ctrl.resumeNow}</span>
</Item>
<Item disabled={isBusy} onSelect={() => void handleAction('goal.pause')}>
<Codicon name="debug-pause" size="0.8rem" />
<span>{ctrl.pauseGoal}</span>
</Item>
</>
)}
<Item disabled={isBusy} onSelect={confirmClearGoal} variant="destructive">
<Codicon name="trash" size="0.8rem" />
<span>{ctrl.clearGoal}</span>
</Item>
</>
)
}
const hasDetails = Boolean(
goal.contract.outcome ||
goal.contract.verification ||
goal.contract.constraints ||
goal.contract.boundaries ||
goal.contract.stop_when ||
goal.wait_barrier ||
goal.gates.length > 0
)
return (
<>
<ContextMenu>
<ContextMenuTrigger asChild>
<div data-slot="session-control-goal">
<StatusSection
accessory={
<DropdownMenu onOpenChange={setMenuOpen} open={menuOpen}>
<Tip label={ctrl.goalActions}>
<span className="inline-flex">
<DropdownMenuTrigger asChild>
<Button
aria-haspopup="menu"
aria-label={ctrl.goalActions}
className="size-6 rounded-md text-muted-foreground/70 hover:text-foreground/90"
disabled={isBusy}
onClick={event => {
// Radix opens pointer interactions from pointerdown. Keyboard,
// assistive-tech, and programmatic clicks have no pointer sequence.
if (event.detail === 0) {
setMenuOpen(true)
}
}}
onKeyDown={e => {
if (e.key === 'F10' && e.shiftKey) {
e.preventDefault()
setMenuOpen(true)
}
}}
size="icon-xs"
type="button"
variant="ghost"
>
<Codicon name="ellipsis" size="0.8rem" />
</Button>
</DropdownMenuTrigger>
</span>
</Tip>
<DropdownMenuContent align="end" className="w-44">
{renderMenuItems(false)}
</DropdownMenuContent>
</DropdownMenu>
}
defaultCollapsed={false}
icon={<Codicon className="text-muted-foreground/70" name="target" size="0.8rem" />}
label={headerLabel}
>
<div className="space-y-1.5 px-1 py-1">
{/* Full goal title */}
<div className="text-xs font-normal leading-relaxed text-foreground/92 break-words">{goal.title}</div>
{/* Optional reasons */}
{!detailsOpen && goal.wait_barrier && (
<div className="text-[0.7rem] italic text-muted-foreground/80">
{goal.wait_barrier.reason
? `${ctrl.waitBarrierTitle}: ${goal.wait_barrier.reason}`
: ctrl.waitBarrierTitle}
</div>
)}
{!goal.wait_barrier && goal.paused_reason && (
<div className="text-[0.7rem] italic text-muted-foreground/80">{goal.paused_reason}</div>
)}
{!goal.wait_barrier && !goal.paused_reason && goal.last_reason && (
<div className="text-[0.7rem] italic text-muted-foreground/80">{goal.last_reason}</div>
)}
{/* View details button */}
{hasDetails && (
<div>
<Button
className="text-[0.7rem] text-muted-foreground/75 hover:text-foreground/90"
onClick={() => setDetailsOpen(true)}
size="micro"
type="button"
variant="text"
>
{ctrl.viewDetails}
</Button>
</div>
)}
{/* Criteria subsection */}
<div className="mt-2 border-t border-(--ui-stroke-tertiary)/40 pt-1.5">
<div className="flex items-center justify-between pb-1 text-[0.68rem] font-medium text-muted-foreground/75">
<span>{ctrl.criteriaHeader(goal.subgoals.length)}</span>
<div className="flex items-center gap-1">
<Button
className="text-[0.68rem] text-muted-foreground/75 hover:text-foreground/90"
disabled={isBusy}
onClick={openAddCriterion}
size="micro"
type="button"
variant="text"
>
{ctrl.addCriterion}
</Button>
{goal.subgoals.length > 0 && (
<Button
className="text-[0.68rem] text-muted-foreground/60 hover:text-destructive"
disabled={isBusy}
onClick={confirmClearCriteria}
size="micro"
type="button"
variant="text"
>
{ctrl.clearCriteria}
</Button>
)}
</div>
</div>
{goal.subgoals.length > 0 ? (
<div className="space-y-1">
{goal.subgoals.map((subgoal, idx) => {
const index = idx + 1
return (
<div
className="group/criterion flex items-start justify-between gap-1.5 rounded px-1 py-0.5 text-xs text-foreground/85 hover:bg-(--ui-control-active-background)/40"
key={`${index}-${subgoal}`}
>
<div className="flex min-w-0 flex-1 items-start gap-1.5 leading-relaxed">
<span className="shrink-0 text-muted-foreground/60 tabular-nums">{index}.</span>
<span className="break-words">{subgoal}</span>
</div>
<div className="flex shrink-0 items-center gap-0.5 opacity-80 group-hover/criterion:opacity-100">
<Tip label={ctrl.copyCriterion(index)}>
<Button
aria-label={ctrl.copyCriterion(index)}
className="size-6 rounded text-muted-foreground/60 hover:text-foreground/90"
onClick={() => void copyCriterionText(subgoal)}
size="icon-xs"
type="button"
variant="ghost"
>
<Codicon name="copy" size="0.7rem" />
</Button>
</Tip>
<Tip label={ctrl.removeCriterion(index)}>
<Button
aria-label={ctrl.removeCriterion(index)}
className="size-6 rounded text-muted-foreground/60 hover:text-destructive"
disabled={isBusy}
onClick={() => confirmRemoveCriterion(index)}
size="icon-xs"
type="button"
variant="ghost"
>
<Codicon name="close" size="0.7rem" />
</Button>
</Tip>
</div>
</div>
)
})}
</div>
) : null}
</div>
</div>
</StatusSection>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-44">{renderMenuItems(true)}</ContextMenuContent>
</ContextMenu>
{/* Add Criterion Dialog */}
<Dialog
onOpenChange={open => {
if (!open) {
setAddCriterionOpen(false)
setAddCriterionError(null)
}
}}
open={addCriterionOpen}
>
<DialogContent className="max-w-md">
<form
onSubmit={async e => {
e.preventDefault()
const text = newCriterionText.trim()
if (!text || isBusy) {
return
}
setAddCriterionError(null)
const ok = await handleAction('subgoal.add', { text }, setAddCriterionError)
if (ok) {
setNewCriterionText('')
setAddCriterionOpen(false)
}
}}
>
<DialogHeader>
<DialogTitle>{ctrl.addCriterionDialogTitle}</DialogTitle>
<DialogDescription>{ctrl.addCriterionPlaceholder}</DialogDescription>
</DialogHeader>
<div className="py-2">
<textarea
aria-label={ctrl.criterionLabel}
autoFocus
className="w-full rounded-md border border-(--ui-stroke-secondary) bg-transparent p-2 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-primary"
disabled={isBusy}
onChange={e => setNewCriterionText(e.target.value)}
placeholder={ctrl.addCriterionPlaceholder}
rows={3}
value={newCriterionText}
/>
</div>
{addCriterionError && (
<div className="text-xs text-destructive" role="alert">
{addCriterionError}
</div>
)}
<DialogFooter>
<Button disabled={isBusy} onClick={() => setAddCriterionOpen(false)} type="button" variant="ghost">
{t.common.cancel}
</Button>
<Button disabled={isBusy || !newCriterionText.trim()} type="submit">
{ctrl.add}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Goal Details Dialog */}
<Dialog onOpenChange={open => !open && setDetailsOpen(false)} open={detailsOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{ctrl.goalDetailsTitle}</DialogTitle>
</DialogHeader>
<div className="space-y-3 py-2 text-xs">
<div>
<div className="font-semibold text-muted-foreground/75 uppercase tracking-wider text-[0.68rem]">
{ctrl.objectiveLabel}
</div>
<div className="mt-0.5 text-foreground leading-relaxed break-words">{goal.title}</div>
</div>
{/* Contract fields */}
{goal.contract.outcome && (
<div>
<div className="font-semibold text-muted-foreground/75 uppercase tracking-wider text-[0.68rem]">
{ctrl.contractOutcome}
</div>
<div className="mt-0.5 text-foreground/90 leading-relaxed">{goal.contract.outcome}</div>
</div>
)}
{goal.contract.verification && (
<div>
<div className="font-semibold text-muted-foreground/75 uppercase tracking-wider text-[0.68rem]">
{ctrl.contractVerification}
</div>
<div className="mt-0.5 text-foreground/90 leading-relaxed">{goal.contract.verification}</div>
</div>
)}
{goal.contract.constraints && (
<div>
<div className="font-semibold text-muted-foreground/75 uppercase tracking-wider text-[0.68rem]">
{ctrl.contractConstraints}
</div>
<div className="mt-0.5 text-foreground/90 leading-relaxed">{goal.contract.constraints}</div>
</div>
)}
{goal.contract.boundaries && (
<div>
<div className="font-semibold text-muted-foreground/75 uppercase tracking-wider text-[0.68rem]">
{ctrl.contractBoundaries}
</div>
<div className="mt-0.5 text-foreground/90 leading-relaxed">{goal.contract.boundaries}</div>
</div>
)}
{goal.contract.stop_when && (
<div>
<div className="font-semibold text-muted-foreground/75 uppercase tracking-wider text-[0.68rem]">
{ctrl.contractStopWhen}
</div>
<div className="mt-0.5 text-foreground/90 leading-relaxed">{goal.contract.stop_when}</div>
</div>
)}
{/* Wait Barrier */}
{goal.wait_barrier && (
<div>
<div className="font-semibold text-muted-foreground/75 uppercase tracking-wider text-[0.68rem]">
{ctrl.waitBarrierTitle}
</div>
<div className="mt-0.5 text-foreground/90 leading-relaxed">
{goal.wait_barrier.type === 'until' &&
ctrl.waitUntil(new Date(goal.wait_barrier.until_at * 1000).toLocaleTimeString())}
{goal.wait_barrier.type === 'session' && ctrl.waitSession(goal.wait_barrier.target)}
{goal.wait_barrier.type === 'pid' && ctrl.waitPid(goal.wait_barrier.target)}
{goal.wait_barrier.reason && (
<div className="italic text-muted-foreground/80 mt-0.5">{goal.wait_barrier.reason}</div>
)}
</div>
</div>
)}
{/* Quality Gates */}
{goal.gates.length > 0 && (
<div>
<div className="font-semibold text-muted-foreground/75 uppercase tracking-wider text-[0.68rem]">
{ctrl.qualityGatesTitle}
</div>
<div className="mt-1 space-y-1.5">
{goal.gates.map((gate, i) => (
<div
className="rounded border border-(--ui-stroke-tertiary)/60 bg-(--ui-control-active-background)/20 p-2"
key={`${i}-${gate.command}`}
>
<div className="font-mono text-[0.7rem] text-foreground/95">{gate.command}</div>
<div className="mt-1 flex items-center gap-2 text-[0.68rem] text-muted-foreground/80">
<span>{ctrl.gateAttempts(gate.attempts, gate.max_retries)}</span>
<span className="text-muted-foreground/40">·</span>
<span>{ctrl.gateTimeout(gate.timeout_seconds)}</span>
<span className="text-muted-foreground/40">·</span>
<span>{ctrl.gateLastExit(gate.last_exit_code)}</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
<DialogFooter>
<Button onClick={() => setDetailsOpen(false)} type="button">
{t.common.close}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Confirmation Dialog */}
{confirmState && (
<ConfirmDialog
cancelLabel={t.common.cancel}
confirmLabel={confirmState.confirmLabel}
description={confirmState.description}
destructive={confirmState.destructive}
onClose={() => setConfirmState(null)}
onConfirm={confirmState.onConfirm}
open={Boolean(confirmState)}
title={confirmState.title}
/>
)}
</>
)
})
@@ -0,0 +1,195 @@
import { memo, useCallback, useState } from 'react'
import { StatusSection } from '@/components/chat/status-section'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger
} from '@/components/ui/context-menu'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tip } from '@/components/ui/tooltip'
import { useI18n } from '@/i18n'
import {
runSessionControlAction,
type SessionControlAction,
type SessionControlActionArgs,
type SessionControlHeartbeat
} from '@/store/session-control'
import { type ConfirmState, formatApproximateTime, formatHeartbeatInterval } from './session-control-utils'
interface HeartbeatSectionProps {
heartbeat: SessionControlHeartbeat
sessionId: string
pendingAction: SessionControlAction | null
onFeedback: (error: string | null, success: string | null) => void
}
export const SessionControlHeartbeatSection = memo(function SessionControlHeartbeatSection({
heartbeat,
sessionId,
pendingAction,
onFeedback
}: HeartbeatSectionProps) {
const { t } = useI18n()
const s = t.statusStack
const ctrl = s.control
const [confirmState, setConfirmState] = useState<ConfirmState | null>(null)
const [menuOpen, setMenuOpen] = useState(false)
const isBusy = Boolean(pendingAction)
const handleAction = useCallback(
async (action: SessionControlAction, args?: SessionControlActionArgs): Promise<boolean> => {
onFeedback(null, null)
try {
await runSessionControlAction(sessionId, action, args)
onFeedback(null, ctrl.actionSucceeded)
return true
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
onFeedback(ctrl.actionFailed(msg), null)
return false
}
},
[sessionId, onFeedback, ctrl]
)
const stateLabel = heartbeat.status === 'paused' ? ctrl.heartbeatPaused : ctrl.heartbeatActive
const intervalLabel = formatHeartbeatInterval(heartbeat.interval_seconds, t)
const nextDueTimestamp = (heartbeat.last_fired_at || heartbeat.created_at) + heartbeat.interval_seconds
const nextRunLabel =
heartbeat.status === 'active' ? ` · ${ctrl.heartbeatNext(formatApproximateTime(nextDueTimestamp))}` : ''
const headerLabel = `${stateLabel} · ${intervalLabel}${nextRunLabel}`
const confirmClearHeartbeat = () => {
setConfirmState({
title: ctrl.clearHeartbeatConfirmTitle,
description: ctrl.clearHeartbeatConfirmBody,
destructive: true,
confirmLabel: ctrl.clearHeartbeat,
onConfirm: async () => {
await handleAction('heartbeat.clear')
}
})
}
const renderMenuItems = (isContext = false) => {
const Item = isContext ? ContextMenuItem : DropdownMenuItem
const Sep = isContext ? ContextMenuSeparator : DropdownMenuSeparator
return (
<>
{heartbeat.status === 'active' && (
<Item disabled={isBusy} onSelect={() => void handleAction('heartbeat.pause')}>
<Codicon name="debug-pause" size="0.8rem" />
<span>{ctrl.pauseHeartbeat}</span>
</Item>
)}
{heartbeat.status === 'paused' && (
<Item disabled={isBusy} onSelect={() => void handleAction('heartbeat.resume')}>
<Codicon name="play" size="0.8rem" />
<span>{ctrl.resumeHeartbeat}</span>
</Item>
)}
<Sep />
<Item disabled={isBusy} onSelect={confirmClearHeartbeat} variant="destructive">
<Codicon name="trash" size="0.8rem" />
<span>{ctrl.clearHeartbeat}</span>
</Item>
</>
)
}
return (
<>
<ContextMenu>
<ContextMenuTrigger asChild>
<div data-slot="session-control-heartbeat">
<StatusSection
accessory={
<DropdownMenu onOpenChange={setMenuOpen} open={menuOpen}>
<Tip label={ctrl.heartbeatActions}>
<span className="inline-flex">
<DropdownMenuTrigger asChild>
<Button
aria-haspopup="menu"
aria-label={ctrl.heartbeatActions}
className="size-6 rounded-md text-muted-foreground/70 hover:text-foreground/90"
disabled={isBusy}
onClick={event => {
// Radix opens pointer interactions from pointerdown. Keyboard,
// assistive-tech, and programmatic clicks have no pointer sequence.
if (event.detail === 0) {
setMenuOpen(true)
}
}}
onKeyDown={e => {
if (e.key === 'F10' && e.shiftKey) {
e.preventDefault()
setMenuOpen(true)
}
}}
size="icon-xs"
type="button"
variant="ghost"
>
<Codicon name="ellipsis" size="0.8rem" />
</Button>
</DropdownMenuTrigger>
</span>
</Tip>
<DropdownMenuContent align="end" className="w-44">
{renderMenuItems(false)}
</DropdownMenuContent>
</DropdownMenu>
}
defaultCollapsed={true}
icon={<Codicon className="text-muted-foreground/70" name="pulse" size="0.8rem" />}
label={headerLabel}
>
<div className="space-y-1 px-1 py-1 text-xs">
<div className="text-foreground/92 leading-relaxed break-words">{heartbeat.prompt}</div>
<div className="text-[0.7rem] text-muted-foreground/80">
<span>{ctrl.heartbeatFiredCount(heartbeat.fire_count)}</span>
</div>
</div>
</StatusSection>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-44">{renderMenuItems(true)}</ContextMenuContent>
</ContextMenu>
{confirmState && (
<ConfirmDialog
cancelLabel={t.common.cancel}
confirmLabel={confirmState.confirmLabel}
description={confirmState.description}
destructive={confirmState.destructive}
onClose={() => setConfirmState(null)}
onConfirm={confirmState.onConfirm}
open={Boolean(confirmState)}
title={confirmState.title}
/>
)}
</>
)
})
@@ -0,0 +1,239 @@
import { memo, useCallback, useState } from 'react'
import { StatusSection } from '@/components/chat/status-section'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger
} from '@/components/ui/context-menu'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tip } from '@/components/ui/tooltip'
import { useI18n } from '@/i18n'
import {
runSessionControlAction,
type SessionControlAction,
type SessionControlActionArgs,
type SessionControlLoop
} from '@/store/session-control'
import { type ConfirmState, formatApproximateTime, formatInterval } from './session-control-utils'
interface LoopSectionProps {
loop: SessionControlLoop
sessionId: string
pendingAction: SessionControlAction | null
onFeedback: (error: string | null, success: string | null) => void
}
export const SessionControlLoopSection = memo(function SessionControlLoopSection({
loop,
sessionId,
pendingAction,
onFeedback
}: LoopSectionProps) {
const { t } = useI18n()
const s = t.statusStack
const ctrl = s.control
const [confirmState, setConfirmState] = useState<ConfirmState | null>(null)
const [menuOpen, setMenuOpen] = useState(false)
const isBusy = Boolean(pendingAction)
const handleAction = useCallback(
async (action: SessionControlAction, args?: SessionControlActionArgs): Promise<boolean> => {
onFeedback(null, null)
try {
await runSessionControlAction(sessionId, action, args)
onFeedback(null, ctrl.actionSucceeded)
return true
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
onFeedback(ctrl.actionFailed(msg), null)
return false
}
},
[sessionId, onFeedback, ctrl]
)
const stateLabel = loop.deferred_by_goal
? ctrl.loopDeferred
: loop.status === 'paused'
? ctrl.loopPaused
: loop.status === 'done'
? ctrl.loopFinished
: loop.awaiting_response
? `${ctrl.loopActive} · ${ctrl.loopAwaitingResponse}`
: ctrl.loopActive
const runCountLabel =
loop.times > 0 ? ctrl.loopRunCount(loop.ticks_fired, loop.times) : ctrl.loopRuns(loop.ticks_fired)
const nextRunLabel =
loop.status === 'active' && !loop.deferred_by_goal && loop.next_due_at
? ` · ${ctrl.loopNext(formatApproximateTime(loop.next_due_at))}`
: ''
const headerLabel = `${stateLabel} · ${runCountLabel}${nextRunLabel}`
const confirmStopLoop = () => {
if (loop.status === 'done') {
void handleAction('loop.stop')
return
}
setConfirmState({
title: ctrl.stopLoopConfirmTitle,
description: ctrl.stopLoopConfirmBody,
destructive: true,
confirmLabel: ctrl.stopLoop,
onConfirm: async () => {
await handleAction('loop.stop')
}
})
}
const renderMenuItems = (isContext = false) => {
const Item = isContext ? ContextMenuItem : DropdownMenuItem
const Sep = isContext ? ContextMenuSeparator : DropdownMenuSeparator
return (
<>
{loop.status === 'active' && (
<Item disabled={isBusy} onSelect={() => void handleAction('loop.pause')}>
<Codicon name="debug-pause" size="0.8rem" />
<span>{ctrl.pauseLoop}</span>
</Item>
)}
{loop.status === 'paused' && (
<Item disabled={isBusy} onSelect={() => void handleAction('loop.resume')}>
<Codicon name="play" size="0.8rem" />
<span>{ctrl.resumeLoop}</span>
</Item>
)}
{loop.status !== 'done' && <Sep />}
{loop.status === 'done' ? (
<Item disabled={isBusy} onSelect={() => void handleAction('loop.stop')}>
<Codicon name="close" size="0.8rem" />
<span>{ctrl.dismissLoop}</span>
</Item>
) : (
<Item disabled={isBusy} onSelect={confirmStopLoop} variant="destructive">
<Codicon name="stop-circle" size="0.8rem" />
<span>{ctrl.stopLoop}</span>
</Item>
)}
</>
)
}
return (
<>
<ContextMenu>
<ContextMenuTrigger asChild>
<div data-slot="session-control-loop">
<StatusSection
accessory={
<DropdownMenu onOpenChange={setMenuOpen} open={menuOpen}>
<Tip label={ctrl.loopActions}>
<span className="inline-flex">
<DropdownMenuTrigger asChild>
<Button
aria-haspopup="menu"
aria-label={ctrl.loopActions}
className="size-6 rounded-md text-muted-foreground/70 hover:text-foreground/90"
disabled={isBusy}
onClick={event => {
// Radix opens pointer interactions from pointerdown. Keyboard,
// assistive-tech, and programmatic clicks have no pointer sequence.
if (event.detail === 0) {
setMenuOpen(true)
}
}}
onKeyDown={e => {
if (e.key === 'F10' && e.shiftKey) {
e.preventDefault()
setMenuOpen(true)
}
}}
size="icon-xs"
type="button"
variant="ghost"
>
<Codicon name="ellipsis" size="0.8rem" />
</Button>
</DropdownMenuTrigger>
</span>
</Tip>
<DropdownMenuContent align="end" className="w-40">
{renderMenuItems(false)}
</DropdownMenuContent>
</DropdownMenu>
}
defaultCollapsed={true}
icon={<Codicon className="text-muted-foreground/70" name="sync" size="0.8rem" />}
label={headerLabel}
>
<div className="space-y-1 px-1 py-1 text-xs">
<div className="text-foreground/92 leading-relaxed break-words">{loop.prompt}</div>
<div className="text-[0.7rem] text-muted-foreground/80">
<span>
{ctrl.loopCadenceLabel}:{' '}
{loop.mode === 'self_paced' ? ctrl.loopSelfPaced : formatInterval(loop.interval_seconds, t)}
</span>
</div>
{loop.until && (
<div className="text-[0.7rem] text-muted-foreground/80">
<span>
{ctrl.loopUntilLabel}: {loop.until}
</span>
</div>
)}
{loop.deferred_by_goal && (
<div className="text-[0.7rem] italic text-muted-foreground/80">{ctrl.loopDeferredNotice}</div>
)}
{loop.awaiting_response && (
<div className="text-[0.7rem] italic text-muted-foreground/80">{ctrl.loopAwaitingResponse}</div>
)}
{loop.paused_reason && (
<div className="text-[0.7rem] italic text-muted-foreground/80">{loop.paused_reason}</div>
)}
{loop.last_stop_reason && (
<div className="text-[0.7rem] italic text-muted-foreground/80">{loop.last_stop_reason}</div>
)}
</div>
</StatusSection>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-40">{renderMenuItems(true)}</ContextMenuContent>
</ContextMenu>
{confirmState && (
<ConfirmDialog
cancelLabel={t.common.cancel}
confirmLabel={confirmState.confirmLabel}
description={confirmState.description}
destructive={confirmState.destructive}
onClose={() => setConfirmState(null)}
onConfirm={confirmState.onConfirm}
open={Boolean(confirmState)}
title={confirmState.title}
/>
)}
</>
)
})
@@ -0,0 +1,82 @@
import { useCallback, useSyncExternalStore } from 'react'
import type { Translations } from '@/i18n'
interface SessionSliceStore<T> {
get(): Record<string, T | undefined>
listen(listener: () => void): () => void
}
/** Subscribe to one stable per-session store entry without reacting to other sessions' writes. */
export function useSessionValue<T>(store: SessionSliceStore<T>, sessionId: string | null): T | undefined {
const subscribe = useCallback((onChange: () => void) => store.listen(onChange), [store])
return useSyncExternalStore(subscribe, () => (sessionId ? store.get()[sessionId] : undefined))
}
export interface ConfirmState {
title: string
description: string
destructive?: boolean
confirmLabel?: string
onConfirm: () => Promise<void> | void
}
export function formatApproximateTime(targetTimestamp: number): string {
if (!targetTimestamp) {
return ''
}
const targetMs = targetTimestamp > 1e11 ? targetTimestamp : targetTimestamp * 1000
const diffMs = targetMs - Date.now()
if (diffMs <= 0) {
return '~0s'
}
const seconds = Math.round(diffMs / 1000)
if (seconds < 60) {
return `~${seconds}s`
}
const minutes = Math.round(seconds / 60)
if (minutes < 60) {
return `~${minutes}m`
}
const hours = Math.round(minutes / 60)
if (hours < 24) {
return `~${hours}h`
}
const days = Math.round(hours / 24)
return `~${days}d`
}
export function formatInterval(seconds: number, t: Translations): string {
if (seconds % 3600 === 0) {
return t.statusStack.control.loopEveryHours(seconds / 3600)
}
if (seconds % 60 === 0) {
return t.statusStack.control.loopEveryMinutes(seconds / 60)
}
return t.statusStack.control.loopEverySeconds(seconds)
}
export function formatHeartbeatInterval(seconds: number, t: Translations): string {
if (seconds % 3600 === 0) {
return t.statusStack.control.heartbeatEveryHours(seconds / 3600)
}
if (seconds % 60 === 0) {
return t.statusStack.control.heartbeatEveryMinutes(seconds / 60)
}
return t.statusStack.control.heartbeatEverySeconds(seconds)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
import { memo, useCallback, useState } from 'react'
import type { SubmitTextOptions } from '@/app/session/hooks/use-prompt-actions/utils'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { useI18n } from '@/i18n'
import type { SessionControlEntry } from '@/store/session-control'
import { SessionControlGoalSection } from './session-control-goal'
import { SessionControlHeartbeatSection } from './session-control-heartbeat'
import { SessionControlLoopSection } from './session-control-loop'
export interface SessionControlSectionsProps {
entry: SessionControlEntry
onSubmit?: (value: string, options?: SubmitTextOptions) => Promise<boolean> | boolean
sessionId: string
}
export const SessionControlSections = memo(function SessionControlSections({
entry,
onSubmit,
sessionId
}: SessionControlSectionsProps) {
const { t } = useI18n()
const ctrl = t.statusStack.control
const [feedbackError, setFeedbackError] = useState<string | null>(null)
const [feedbackSuccess, setFeedbackSuccess] = useState<string | null>(null)
const [dismissedStoreError, setDismissedStoreError] = useState<string | null>(null)
const handleFeedback = useCallback((error: string | null, success: string | null) => {
setFeedbackError(error)
setFeedbackSuccess(success)
}, [])
const storeError = entry.error ? ctrl.actionFailed(entry.error) : null
const displayError = feedbackError ?? (entry.error === dismissedStoreError ? null : storeError)
const handleDismissError = useCallback(() => {
if (feedbackError) {
setFeedbackError(null)
} else {
setDismissedStoreError(entry.error)
}
}, [entry.error, feedbackError])
const snapshot = entry.snapshot
if (!snapshot && !displayError) {
return null
}
return (
<>
{displayError && (
<div
className="flex items-center justify-between gap-2 border-b border-destructive/20 bg-destructive/10 px-3 py-1.5 text-xs text-destructive"
role="alert"
>
<div className="flex items-center gap-1.5 truncate">
<Codicon className="shrink-0" name="error" size="0.85rem" />
<span className="truncate">{displayError}</span>
</div>
<Button
aria-label={ctrl.dismissError}
className="size-6 shrink-0 p-0 text-destructive hover:bg-destructive/20"
onClick={handleDismissError}
size="icon-xs"
type="button"
variant="ghost"
>
<Codicon name="close" size="0.75rem" />
</Button>
</div>
)}
{feedbackSuccess && (
<div aria-live="polite" className="sr-only">
{feedbackSuccess}
</div>
)}
{snapshot?.goal && (
<SessionControlGoalSection
goal={snapshot.goal}
onFeedback={handleFeedback}
onSubmit={onSubmit}
pendingAction={entry.pendingAction}
sessionId={sessionId}
/>
)}
{snapshot?.loop && (
<SessionControlLoopSection
loop={snapshot.loop}
onFeedback={handleFeedback}
pendingAction={entry.pendingAction}
sessionId={sessionId}
/>
)}
{snapshot?.heartbeat && (
<SessionControlHeartbeatSection
heartbeat={snapshot.heartbeat}
onFeedback={handleFeedback}
pendingAction={entry.pendingAction}
sessionId={sessionId}
/>
)}
</>
)
})
+85
View File
@@ -1991,6 +1991,91 @@ export const ar = defineLocale({
stop: 'إيقاف',
dismiss: 'تجاهل',
exit: code => `خروج ${code}`,
control: {
goalActiveTurns: (turn, maxTurns) => `الجولة ${turn}/${maxTurns}`,
goalDoneTurns: turns => `${turns} جولات`,
goalTurn: turn => `الجولة ${turn}`,
goalActions: 'إجراءات الهدف',
viewDetails: 'عرض التفاصيل',
addCriterion: 'إضافة معيار',
addCriterionDialogTitle: 'إضافة معيار',
addCriterionPlaceholder: 'أدخل نص المعيار...',
criterionLabel: 'المعيار',
pauseGoal: 'إيقاف الهدف مؤقتاً',
resumeGoal: 'استئناف الهدف',
resumeNow: 'استئناف الآن',
clearGoal: 'مسح الهدف',
clearGoalConfirmTitle: 'مسح الهدف؟',
clearGoalConfirmBody: 'هل أنت متأكد من مسح الهدف النشط؟ لا يمكن التراجع عن هذا الإجراء.',
copyCriterion: index => `نسخ المعيار ${index}`,
removeCriterion: index => `إزالة المعيار ${index}`,
removeCriterionConfirmTitle: index => `إزالة المعيار ${index}؟`,
removeCriterionConfirmBody: index => `هل أنت متأكد من إزالة المعيار ${index}؟`,
clearCriteria: 'مسح جميع المعايير',
clearCriteriaConfirmTitle: 'مسح جميع المعايير؟',
clearCriteriaConfirmBody: 'هل أنت متأكد من رغبتك في إزالة جميع المعايير من هذا الهدف؟',
criteriaHeader: count => `المعايير · ${count}`,
noCriteria: 'لا توجد معايير',
goalDetailsTitle: 'تفاصيل الهدف',
objectiveLabel: 'الهدف',
contractOutcome: 'النتيجة',
contractVerification: 'التحقق',
contractConstraints: 'القيود',
contractBoundaries: 'الحدود',
contractStopWhen: 'التوقف عند',
waitBarrierTitle: 'شرط الانتظار',
waitUntil: target => `في الانتظار حتى ${target}`,
waitSession: target => `في انتظار الجلسة ${target}`,
waitPid: pid => `في انتظار العملية ${pid}`,
qualityGatesTitle: 'بوابات الجودة',
gateCommand: 'الأمر',
gateAttempts: (attempts, max) => `${attempts}/${max} محاولات`,
gateTimeout: seconds => `مهلة ${seconds} ثانية`,
gateLastExit: code => (code === null ? 'قيد الانتظار' : `رمز الخروج: ${code}`),
loopActive: 'التكرار نشط',
loopPaused: 'التكرار متوقف مؤقتاً',
loopDeferred: 'التكرار مؤجل',
loopFinished: 'التكرار منتهي',
loopRuns: runs => `${runs} مرات تشغيل`,
loopRunCount: (current, total) => `تشغيل ${current}/${total}`,
loopNext: time => `التالي ${time}`,
loopEverySeconds: seconds => `كل ${seconds} ث`,
loopEveryMinutes: minutes => `كل ${minutes} د`,
loopEveryHours: hours => `كل ${hours} س`,
loopSelfPaced: 'ذاتي الإيقاع',
loopActions: 'إجراءات التكرار',
pauseLoop: 'إيقاف التكرار مؤقتاً',
resumeLoop: 'استئناف التكرار',
stopLoop: 'إيقاف التكرار',
stopLoopConfirmTitle: 'إيقاف التكرار؟',
stopLoopConfirmBody: 'هل أنت متأكد من رغبتك في إيقاف هذا التكرار؟',
dismissLoop: 'تجاهل التكرار',
loopPromptLabel: 'الموجه',
loopCadenceLabel: 'الإيقاع',
loopUntilLabel: 'شرط الانتهاء',
loopDeferredNotice: 'الهدف النشط يتحكم بالجلسة حالياً.',
loopAwaitingResponse: 'في انتظار الاستجابة',
heartbeatActive: 'النبض نشط',
heartbeatPaused: 'النبض متوقف مؤقتاً',
heartbeatEveryMinutes: minutes => `كل ${minutes} د`,
heartbeatEveryHours: hours => `كل ${hours} س`,
heartbeatEverySeconds: seconds => `كل ${seconds} ث`,
heartbeatNext: time => `التالي ${time}`,
heartbeatActions: 'إجراءات النبض',
pauseHeartbeat: 'إيقاف النبض مؤقتاً',
resumeHeartbeat: 'استئناف النبض',
clearHeartbeat: 'مسح النبض',
clearHeartbeatConfirmTitle: 'مسح النبض؟',
clearHeartbeatConfirmBody: 'هل أنت متأكد من رغبتك في مسح هذا النبض؟',
heartbeatFiredCount: count => `انطلق ${count} مرة`,
actionFailed: msg => `فشل الإجراء: ${msg}`,
actionSucceeded: 'تم الإجراء بنجاح',
copySuccess: 'تم نسخ المعيار إلى الحافظة',
copyFailure: 'فشل نسخ المعيار إلى الحافظة',
continuationFailed: 'فشل إرسال متابعة الهدف',
dismissError: 'تجاهل الخطأ',
add: 'إضافة'
},
coding: {
title: 'شجرة العمل',
noBranch: 'لا يوجد فرع',
+85
View File
@@ -2709,6 +2709,91 @@ export const en: Translations = {
stop: 'Stop',
dismiss: 'Dismiss',
exit: code => `exit ${code}`,
control: {
goalActiveTurns: (turn, maxTurns) => `Turn ${turn}/${maxTurns}`,
goalDoneTurns: turns => `${turns} turn${turns === 1 ? '' : 's'}`,
goalTurn: turn => `Turn ${turn}`,
goalActions: 'Goal actions',
viewDetails: 'View details',
addCriterion: 'Add criterion',
addCriterionDialogTitle: 'Add criterion',
addCriterionPlaceholder: 'Enter criterion text...',
criterionLabel: 'Criterion',
pauseGoal: 'Pause goal',
resumeGoal: 'Resume goal',
resumeNow: 'Resume now',
clearGoal: 'Clear goal',
clearGoalConfirmTitle: 'Clear goal?',
clearGoalConfirmBody: 'Are you sure you want to clear the active goal? This cannot be undone.',
copyCriterion: index => `Copy criterion ${index}`,
removeCriterion: index => `Remove criterion ${index}`,
removeCriterionConfirmTitle: index => `Remove criterion ${index}?`,
removeCriterionConfirmBody: index => `Are you sure you want to remove criterion ${index}?`,
clearCriteria: 'Clear all criteria',
clearCriteriaConfirmTitle: 'Clear all criteria?',
clearCriteriaConfirmBody: 'Are you sure you want to remove all criteria from this goal?',
criteriaHeader: count => `Criteria · ${count}`,
noCriteria: 'No criteria',
goalDetailsTitle: 'Goal details',
objectiveLabel: 'Objective',
contractOutcome: 'Outcome',
contractVerification: 'Verification',
contractConstraints: 'Constraints',
contractBoundaries: 'Boundaries',
contractStopWhen: 'Stop when',
waitBarrierTitle: 'Wait condition',
waitUntil: target => `Waiting until ${target}`,
waitSession: target => `Waiting on session ${target}`,
waitPid: pid => `Waiting on process ${pid}`,
qualityGatesTitle: 'Quality gates',
gateCommand: 'Command',
gateAttempts: (attempts, max) => `${attempts}/${max} attempts`,
gateTimeout: seconds => `${seconds}s timeout`,
gateLastExit: code => (code === null ? 'Pending' : `Exit code: ${code}`),
loopActive: 'Loop active',
loopPaused: 'Loop paused',
loopDeferred: 'Loop deferred',
loopFinished: 'Loop finished',
loopRuns: runs => `${runs} run${runs === 1 ? '' : 's'}`,
loopRunCount: (current, total) => `Run ${current}/${total}`,
loopNext: time => `next ${time}`,
loopEverySeconds: seconds => `every ${seconds}s`,
loopEveryMinutes: minutes => `every ${minutes}m`,
loopEveryHours: hours => `every ${hours}h`,
loopSelfPaced: 'self-paced',
loopActions: 'Loop actions',
pauseLoop: 'Pause loop',
resumeLoop: 'Resume loop',
stopLoop: 'Stop loop',
stopLoopConfirmTitle: 'Stop loop?',
stopLoopConfirmBody: 'Are you sure you want to stop this loop?',
dismissLoop: 'Dismiss loop',
loopPromptLabel: 'Prompt',
loopCadenceLabel: 'Cadence',
loopUntilLabel: 'Until condition',
loopDeferredNotice: 'An active goal currently controls the session.',
loopAwaitingResponse: 'Awaiting response',
heartbeatActive: 'Heartbeat active',
heartbeatPaused: 'Heartbeat paused',
heartbeatEveryMinutes: minutes => `every ${minutes}m`,
heartbeatEveryHours: hours => `every ${hours}h`,
heartbeatEverySeconds: seconds => `every ${seconds}s`,
heartbeatNext: time => `next ${time}`,
heartbeatActions: 'Heartbeat actions',
pauseHeartbeat: 'Pause heartbeat',
resumeHeartbeat: 'Resume heartbeat',
clearHeartbeat: 'Clear heartbeat',
clearHeartbeatConfirmTitle: 'Clear heartbeat?',
clearHeartbeatConfirmBody: 'Are you sure you want to clear this heartbeat?',
heartbeatFiredCount: count => `Fired ${count} time${count === 1 ? '' : 's'}`,
actionFailed: msg => `Action failed: ${msg}`,
actionSucceeded: 'Action succeeded',
copySuccess: 'Criterion copied to clipboard',
copyFailure: 'Failed to copy criterion to clipboard',
continuationFailed: 'Failed to submit goal continuation',
dismissError: 'Dismiss error',
add: 'Add'
},
coding: {
title: 'Working tree',
noBranch: 'No branch',
+85
View File
@@ -2334,6 +2334,91 @@ export const ja = defineLocale({
stop: '停止',
dismiss: '閉じる',
exit: code => `終了コード ${code}`,
control: {
goalActiveTurns: (turn, maxTurns) => `ターン ${turn}/${maxTurns}`,
goalDoneTurns: turns => `${turns} ターン`,
goalTurn: turn => `ターン ${turn}`,
goalActions: 'ゴールのアクション',
viewDetails: '詳細を表示',
addCriterion: '基準を追加',
addCriterionDialogTitle: '基準の追加',
addCriterionPlaceholder: '基準を入力...',
criterionLabel: '基準',
pauseGoal: 'ゴールを一時停止',
resumeGoal: 'ゴールを再開',
resumeNow: '今すぐ再開',
clearGoal: 'ゴールをクリア',
clearGoalConfirmTitle: 'ゴールをクリアしますか?',
clearGoalConfirmBody: 'アクティブなゴールをクリアしてもよろしいですか?この操作は取り消せません。',
copyCriterion: index => `基準 ${index} をコピー`,
removeCriterion: index => `基準 ${index} を削除`,
removeCriterionConfirmTitle: index => `基準 ${index} を削除しますか?`,
removeCriterionConfirmBody: index => `基準 ${index} を削除してもよろしいですか?`,
clearCriteria: 'すべての基準をクリア',
clearCriteriaConfirmTitle: 'すべての基準をクリアしますか?',
clearCriteriaConfirmBody: 'このゴールからすべての基準を削除してもよろしいですか?',
criteriaHeader: count => `基準 · ${count}`,
noCriteria: '基準なし',
goalDetailsTitle: 'ゴールの詳細',
objectiveLabel: '目標',
contractOutcome: '成果',
contractVerification: '検証',
contractConstraints: '制約',
contractBoundaries: '境界',
contractStopWhen: '終了条件',
waitBarrierTitle: '待機条件',
waitUntil: target => `${target} まで待機`,
waitSession: target => `セッション ${target} を待機中`,
waitPid: pid => `プロセス ${pid} を待機中`,
qualityGatesTitle: '品質ゲート',
gateCommand: 'コマンド',
gateAttempts: (attempts, max) => `試行 ${attempts}/${max}`,
gateTimeout: seconds => `タイムアウト ${seconds}秒`,
gateLastExit: code => (code === null ? '保留中' : `終了コード: ${code}`),
loopActive: 'ループ実行中',
loopPaused: 'ループ一時停止',
loopDeferred: 'ループ保留中',
loopFinished: 'ループ終了',
loopRuns: runs => `${runs} 回実行`,
loopRunCount: (current, total) => `実行 ${current}/${total}`,
loopNext: time => `次回 ${time}`,
loopEverySeconds: seconds => `${seconds}秒ごと`,
loopEveryMinutes: minutes => `${minutes}分ごと`,
loopEveryHours: hours => `${hours}時間ごと`,
loopSelfPaced: 'セルフペース',
loopActions: 'ループのアクション',
pauseLoop: 'ループを一時停止',
resumeLoop: 'ループを再開',
stopLoop: 'ループを停止',
stopLoopConfirmTitle: 'ループを停止しますか?',
stopLoopConfirmBody: 'このループを停止してもよろしいですか?',
dismissLoop: 'ループを閉じる',
loopPromptLabel: 'プロンプト',
loopCadenceLabel: 'ペース',
loopUntilLabel: '終了条件',
loopDeferredNotice: '現在アクティブなゴールがセッションを制御しています。',
loopAwaitingResponse: '応答待ち',
heartbeatActive: 'ハートビート実行中',
heartbeatPaused: 'ハートビート一時停止',
heartbeatEveryMinutes: minutes => `${minutes}分ごと`,
heartbeatEveryHours: hours => `${hours}時間ごと`,
heartbeatEverySeconds: seconds => `${seconds}秒ごと`,
heartbeatNext: time => `次回 ${time}`,
heartbeatActions: 'ハートビートのアクション',
pauseHeartbeat: 'ハートビートを一時停止',
resumeHeartbeat: 'ハートビートを再開',
clearHeartbeat: 'ハートビートをクリア',
clearHeartbeatConfirmTitle: 'ハートビートをクリアしますか?',
clearHeartbeatConfirmBody: 'このハートビートをクリアしてもよろしいですか?',
heartbeatFiredCount: count => `${count} 回発火`,
actionFailed: msg => `アクションに失敗しました: ${msg}`,
actionSucceeded: 'アクションが完了しました',
copySuccess: '基準をクリップボードにコピーしました',
copyFailure: '基準のコピーに失敗しました',
continuationFailed: 'ゴールの継続送信に失敗しました',
dismissError: 'エラーを閉じる',
add: '追加'
},
coding: {
title: 'ワークツリー',
noBranch: 'ブランチなし',
+85
View File
@@ -2745,6 +2745,91 @@ export const ru = defineLocale({
stop: 'Стоп',
dismiss: 'Скрыть',
exit: code => `exit ${code}`,
control: {
goalActiveTurns: (turn, maxTurns) => `Ход ${turn}/${maxTurns}`,
goalDoneTurns: turns => `${turns} ходов`,
goalTurn: turn => `Ход ${turn}`,
goalActions: 'Действия с целью',
viewDetails: 'Подробнее',
addCriterion: 'Добавить критерий',
addCriterionDialogTitle: 'Добавить критерий',
addCriterionPlaceholder: 'Введите текст критерия...',
criterionLabel: 'Критерий',
pauseGoal: 'Приостановить цель',
resumeGoal: 'Возобновить цель',
resumeNow: 'Возобновить сейчас',
clearGoal: 'Очистить цель',
clearGoalConfirmTitle: 'Очистить цель?',
clearGoalConfirmBody: 'Вы уверены, что хотите очистить активную цель? Это действие необратимо.',
copyCriterion: index => `Скопировать критерий ${index}`,
removeCriterion: index => `Удалить критерий ${index}`,
removeCriterionConfirmTitle: index => `Удалить критерий ${index}?`,
removeCriterionConfirmBody: index => `Вы уверены, что хотите удалить критерий ${index}?`,
clearCriteria: 'Очистить все критерии',
clearCriteriaConfirmTitle: 'Очистить все критерии?',
clearCriteriaConfirmBody: 'Вы уверены, что хотите удалить все критерии этой цели?',
criteriaHeader: count => `Критерии · ${count}`,
noCriteria: 'Нет критериев',
goalDetailsTitle: 'Детали цели',
objectiveLabel: 'Задача',
contractOutcome: 'Результат',
contractVerification: 'Проверка',
contractConstraints: 'Ограничения',
contractBoundaries: 'Границы',
contractStopWhen: 'Условие остановки',
waitBarrierTitle: 'Условие ожидания',
waitUntil: target => `Ожидание до ${target}`,
waitSession: target => `Ожидание сессии ${target}`,
waitPid: pid => `Ожидание процесса ${pid}`,
qualityGatesTitle: 'Контроли качества',
gateCommand: 'Команда',
gateAttempts: (attempts, max) => `${attempts}/${max} попыток`,
gateTimeout: seconds => `таймаут ${seconds}с`,
gateLastExit: code => (code === null ? 'В ожидании' : `Код возврата: ${code}`),
loopActive: 'Цикл активен',
loopPaused: 'Цикл приостановлен',
loopDeferred: 'Цикл отложен',
loopFinished: 'Цикл завершен',
loopRuns: runs => `${runs} запусков`,
loopRunCount: (current, total) => `Запуск ${current}/${total}`,
loopNext: time => `следующий ${time}`,
loopEverySeconds: seconds => `каждые ${seconds}с`,
loopEveryMinutes: minutes => `каждые ${minutes}м`,
loopEveryHours: hours => `каждые ${hours}ч`,
loopSelfPaced: 'автономный',
loopActions: 'Действия цикла',
pauseLoop: 'Приостановить цикл',
resumeLoop: 'Возобновить цикл',
stopLoop: 'Остановить цикл',
stopLoopConfirmTitle: 'Остановить цикл?',
stopLoopConfirmBody: 'Вы уверены, что хотите остановить этот цикл?',
dismissLoop: 'Закрыть цикл',
loopPromptLabel: 'Промпт',
loopCadenceLabel: 'Интервал',
loopUntilLabel: 'Условие окончания',
loopDeferredNotice: 'Активная цель в данный момент управляет сессией.',
loopAwaitingResponse: 'Ожидание ответа',
heartbeatActive: 'Пульс активен',
heartbeatPaused: 'Пульс приостановлен',
heartbeatEveryMinutes: minutes => `каждые ${minutes}м`,
heartbeatEveryHours: hours => `каждые ${hours}ч`,
heartbeatEverySeconds: seconds => `каждые ${seconds}с`,
heartbeatNext: time => `следующий ${time}`,
heartbeatActions: 'Действия пульса',
pauseHeartbeat: 'Приостановить пульс',
resumeHeartbeat: 'Возобновить пульс',
clearHeartbeat: 'Очистить пульс',
clearHeartbeatConfirmTitle: 'Очистить пульс?',
clearHeartbeatConfirmBody: 'Вы уверены, что хотите очистить этот пульс?',
heartbeatFiredCount: count => `Сработал ${count} раз`,
actionFailed: msg => `Ошибка действия: ${msg}`,
actionSucceeded: 'Действие выполнено успешно',
copySuccess: 'Критерий скопирован в буфер обмена',
copyFailure: 'Не удалось скопировать критерий в буфер обмена',
continuationFailed: 'Не удалось отправить продолжение цели',
dismissError: 'Скрыть ошибку',
add: 'Добавить'
},
coding: {
title: 'Рабочее дерево',
noBranch: 'Без ветки',
+85
View File
@@ -2303,6 +2303,91 @@ export interface Translations {
stop: string
dismiss: string
exit: (code: number) => string
control: {
goalActiveTurns: (turn: number, maxTurns: number) => string
goalDoneTurns: (turns: number) => string
goalTurn: (turn: number) => string
goalActions: string
viewDetails: string
addCriterion: string
addCriterionDialogTitle: string
addCriterionPlaceholder: string
criterionLabel: string
pauseGoal: string
resumeGoal: string
resumeNow: string
clearGoal: string
clearGoalConfirmTitle: string
clearGoalConfirmBody: string
copyCriterion: (index: number) => string
removeCriterion: (index: number) => string
removeCriterionConfirmTitle: (index: number) => string
removeCriterionConfirmBody: (index: number) => string
clearCriteria: string
clearCriteriaConfirmTitle: string
clearCriteriaConfirmBody: string
criteriaHeader: (count: number) => string
noCriteria: string
goalDetailsTitle: string
objectiveLabel: string
contractOutcome: string
contractVerification: string
contractConstraints: string
contractBoundaries: string
contractStopWhen: string
waitBarrierTitle: string
waitUntil: (target: string) => string
waitSession: (target: string) => string
waitPid: (pid: number) => string
qualityGatesTitle: string
gateCommand: string
gateAttempts: (attempts: number, max: number) => string
gateTimeout: (seconds: number) => string
gateLastExit: (code: number | null) => string
loopActive: string
loopPaused: string
loopDeferred: string
loopFinished: string
loopRuns: (runs: number) => string
loopRunCount: (current: number, total: number) => string
loopNext: (time: string) => string
loopEverySeconds: (seconds: number) => string
loopEveryMinutes: (minutes: number) => string
loopEveryHours: (hours: number) => string
loopSelfPaced: string
loopActions: string
pauseLoop: string
resumeLoop: string
stopLoop: string
stopLoopConfirmTitle: string
stopLoopConfirmBody: string
dismissLoop: string
loopPromptLabel: string
loopCadenceLabel: string
loopUntilLabel: string
loopDeferredNotice: string
loopAwaitingResponse: string
heartbeatActive: string
heartbeatPaused: string
heartbeatEveryMinutes: (minutes: number) => string
heartbeatEveryHours: (hours: number) => string
heartbeatEverySeconds: (seconds: number) => string
heartbeatNext: (time: string) => string
heartbeatActions: string
pauseHeartbeat: string
resumeHeartbeat: string
clearHeartbeat: string
clearHeartbeatConfirmTitle: string
clearHeartbeatConfirmBody: string
heartbeatFiredCount: (count: number) => string
actionFailed: (msg: string) => string
actionSucceeded: string
copySuccess: string
copyFailure: string
continuationFailed: string
dismissError: string
add: string
}
coding: {
title: string
noBranch: string
+85
View File
@@ -2254,6 +2254,91 @@ export const zhHant = defineLocale({
stop: '停止',
dismiss: '關閉',
exit: code => `結束碼 ${code}`,
control: {
goalActiveTurns: (turn, maxTurns) => `第 ${turn}/${maxTurns} 輪`,
goalDoneTurns: turns => `共 ${turns} 輪`,
goalTurn: turn => `第 ${turn} 輪`,
goalActions: '目標操作',
viewDetails: '檢視詳細資訊',
addCriterion: '新增條件',
addCriterionDialogTitle: '新增條件',
addCriterionPlaceholder: '輸入條件內容...',
criterionLabel: '條件',
pauseGoal: '暫停目標',
resumeGoal: '繼續目標',
resumeNow: '立即繼續',
clearGoal: '清除目標',
clearGoalConfirmTitle: '清除目標?',
clearGoalConfirmBody: '確定要清除目前活躍的目標嗎?此動作無法復原。',
copyCriterion: index => `複製條件 ${index}`,
removeCriterion: index => `移除條件 ${index}`,
removeCriterionConfirmTitle: index => `移除條件 ${index}?`,
removeCriterionConfirmBody: index => `確定要移除條件 ${index} 嗎?`,
clearCriteria: '清除全部條件',
clearCriteriaConfirmTitle: '清除全部條件?',
clearCriteriaConfirmBody: '確定要移除此目標下的所有條件嗎?',
criteriaHeader: count => `條件 · ${count}`,
noCriteria: '無條件',
goalDetailsTitle: '目標詳細資訊',
objectiveLabel: '目標內容',
contractOutcome: '預期成果',
contractVerification: '驗證方式',
contractConstraints: '限制條件',
contractBoundaries: '邊界',
contractStopWhen: '停止條件',
waitBarrierTitle: '等待條件',
waitUntil: target => `等待至 ${target}`,
waitSession: target => `等待工作階段 ${target}`,
waitPid: pid => `等待處理程序 ${pid}`,
qualityGatesTitle: '品質關卡',
gateCommand: '命令',
gateAttempts: (attempts, max) => `嘗試 ${attempts}/${max}`,
gateTimeout: seconds => `逾時 ${seconds} 秒`,
gateLastExit: code => (code === null ? '擱置中' : `結束代碼: ${code}`),
loopActive: '循環進行中',
loopPaused: '循環已暫停',
loopDeferred: '循環已延後',
loopFinished: '循環已完成',
loopRuns: runs => `執行 ${runs} 次`,
loopRunCount: (current, total) => `執行 ${current}/${total}`,
loopNext: time => `下次 ${time}`,
loopEverySeconds: seconds => `每 ${seconds} 秒`,
loopEveryMinutes: minutes => `每 ${minutes} 分鐘`,
loopEveryHours: hours => `每 ${hours} 小時`,
loopSelfPaced: '自訂步調',
loopActions: '循環操作',
pauseLoop: '暫停循環',
resumeLoop: '繼續循環',
stopLoop: '停止循環',
stopLoopConfirmTitle: '停止循環?',
stopLoopConfirmBody: '確定要停止此循環嗎?',
dismissLoop: '關閉循環',
loopPromptLabel: '提示詞',
loopCadenceLabel: '頻率',
loopUntilLabel: '截止條件',
loopDeferredNotice: '目前由進行中的目標控制工作階段。',
loopAwaitingResponse: '等待回應中',
heartbeatActive: '活動訊號進行中',
heartbeatPaused: '活動訊號已暫停',
heartbeatEveryMinutes: minutes => `每 ${minutes} 分鐘`,
heartbeatEveryHours: hours => `每 ${hours} 小時`,
heartbeatEverySeconds: seconds => `每 ${seconds} 秒`,
heartbeatNext: time => `下次 ${time}`,
heartbeatActions: '活動訊號操作',
pauseHeartbeat: '暫停活動訊號',
resumeHeartbeat: '繼續活動訊號',
clearHeartbeat: '清除活動訊號',
clearHeartbeatConfirmTitle: '清除活動訊號?',
clearHeartbeatConfirmBody: '確定要清除此活動訊號嗎?',
heartbeatFiredCount: count => `已觸發 ${count} 次`,
actionFailed: msg => `操作失敗: ${msg}`,
actionSucceeded: '操作成功',
copySuccess: '已將條件複製到剪貼板',
copyFailure: '複製條件到剪貼板失敗',
continuationFailed: '提交目標延續內容失敗',
dismissError: '關閉錯誤',
add: '新增'
},
coding: {
title: '工作區',
noBranch: '無分支',
+85
View File
@@ -2873,6 +2873,91 @@ export const zh: Translations = {
stop: '停止',
dismiss: '关闭',
exit: code => `退出码 ${code}`,
control: {
goalActiveTurns: (turn, maxTurns) => `第 ${turn}/${maxTurns} 轮`,
goalDoneTurns: turns => `共 ${turns} 轮`,
goalTurn: turn => `第 ${turn} 轮`,
goalActions: '目标操作',
viewDetails: '查看详情',
addCriterion: '添加条件',
addCriterionDialogTitle: '添加条件',
addCriterionPlaceholder: '输入条件内容...',
criterionLabel: '条件',
pauseGoal: '暂停目标',
resumeGoal: '恢复目标',
resumeNow: '立即恢复',
clearGoal: '清除目标',
clearGoalConfirmTitle: '清除目标?',
clearGoalConfirmBody: '确定要清除当前活跃的目标吗?此操作无法撤销。',
copyCriterion: index => `复制条件 ${index}`,
removeCriterion: index => `移除条件 ${index}`,
removeCriterionConfirmTitle: index => `移除条件 ${index}?`,
removeCriterionConfirmBody: index => `确定要移除条件 ${index} 吗?`,
clearCriteria: '清除全部条件',
clearCriteriaConfirmTitle: '清除全部条件?',
clearCriteriaConfirmBody: '确定要移除此目标下的所有条件吗?',
criteriaHeader: count => `标准 · ${count}`,
noCriteria: '无条件',
goalDetailsTitle: '目标详情',
objectiveLabel: '目标内容',
contractOutcome: '预期结果',
contractVerification: '验证方式',
contractConstraints: '约束条件',
contractBoundaries: '边界条件',
contractStopWhen: '停止条件',
waitBarrierTitle: '等待状态',
waitUntil: target => `等待至 ${target}`,
waitSession: target => `等待会话 ${target}`,
waitPid: pid => `等待进程 ${pid}`,
qualityGatesTitle: '质量关卡',
gateCommand: '命令',
gateAttempts: (attempts, max) => `尝试 ${attempts}/${max}`,
gateTimeout: seconds => `超时 ${seconds} 秒`,
gateLastExit: code => (code === null ? '等待中' : `退出代码: ${code}`),
loopActive: '循环活跃',
loopPaused: '循环暂停',
loopDeferred: '循环已推迟',
loopFinished: '循环已完成',
loopRuns: runs => `运行 ${runs} 次`,
loopRunCount: (current, total) => `运行 ${current}/${total}`,
loopNext: time => `下次 ${time}`,
loopEverySeconds: seconds => `每 ${seconds} 秒`,
loopEveryMinutes: minutes => `每 ${minutes} 分钟`,
loopEveryHours: hours => `每 ${hours} 小时`,
loopSelfPaced: '自节奏',
loopActions: '循环操作',
pauseLoop: '暂停循环',
resumeLoop: '恢复循环',
stopLoop: '停止循环',
stopLoopConfirmTitle: '停止循环?',
stopLoopConfirmBody: '确定要停止此循环吗?',
dismissLoop: '关闭循环',
loopPromptLabel: '提示词',
loopCadenceLabel: '节奏',
loopUntilLabel: '终止条件',
loopDeferredNotice: '活跃目标当前正在控制会话。',
loopAwaitingResponse: '等待响应中',
heartbeatActive: '心跳活跃',
heartbeatPaused: '心跳暂停',
heartbeatEveryMinutes: minutes => `每 ${minutes} 分钟`,
heartbeatEveryHours: hours => `每 ${hours} 小时`,
heartbeatEverySeconds: seconds => `每 ${seconds} 秒`,
heartbeatNext: time => `下次 ${time}`,
heartbeatActions: '心跳操作',
pauseHeartbeat: '暂停心跳',
resumeHeartbeat: '恢复心跳',
clearHeartbeat: '清除心跳',
clearHeartbeatConfirmTitle: '清除心跳?',
clearHeartbeatConfirmBody: '确定要清除该心跳吗?',
heartbeatFiredCount: count => `已触发 ${count} 次`,
actionFailed: msg => `操作失败: ${msg}`,
actionSucceeded: '操作成功',
copySuccess: '已将条件复制到剪贴板',
copyFailure: '复制条件到剪贴板失败',
continuationFailed: '提交目标后续内容失败',
dismissError: '关闭错误',
add: '添加'
},
coding: {
title: '工作区',
noBranch: '无分支',