feat: session goals - server-driven goal loop with independent small-model audit (#2148)
Arm the target button in the composer and the next prompt becomes a goal: the server keeps the session working toward it (idle tick -> small-model audit -> continuation) until the objective is verifiably complete, blocked, or out of budget — even with the UI closed. Server (packages/web/server/lib/session-goal): - event-driven loop on the global SSE hub; goal state lives in session.metadata.openchamber.goal (merge-safe patches, stale-write guard by goal id), so it survives restarts and syncs to every client for free - the small-model audit (objective + last assistant turn only, language pinned to the objective) is the sole termination authority; blocked needs 3 consecutive verdicts, audit outages tolerate one unaudited continuation then stop the goal as resumable-blocked - hard stops: optional token budget, auto-continuation cap (Resume grants a fresh allowance), turn errors; user abort pauses the goal instead of blocking it, and resuming over an aborted tail nudges immediately - token accounting as a snapshot of the latest turn (input + cache.read + output), goal-relative via a creation baseline and segmented across compactions; a compaction summary skips the audit and continues - continuations reuse the session's own provider/model/agent/variant UI: - three-mode target button (arm / disarm / manage dialog), informational goal strip with inline pause/resume and an Evaluating indicator, sidebar state glyph, objective length counter (2000-char server clamp), read-only completed goals - goal entry points: composer (sessions and drafts), start-new-session- from-answer dialog, plan implement dialog (plan content becomes the objective), scheduled tasks (Run as goal + budget) - Settings -> Chat -> Goal: feature toggle + default token budget with three-layer parity (web server, client persistence, VS Code bridge); VS Code renders goal state but hides the entry points (the loop runs in the web server only) Notifications: per-turn "ready" notifications are suppressed while a goal is active; settling sends one final notification (desktop, web-push, APNs generic titles with the session name as body) honoring the completion toggle. Error/question/permission notifications are untouched. Docs: user guide (session-goals) in all 9 locales + sidebar entry, scheduled-tasks cross-reference, server module DOCUMENTATION.md.
This commit is contained in:
committed by
GitHub
parent
82c039117a
commit
bb45164ae8
@@ -100,6 +100,8 @@ import {
|
||||
} from './attachmentCitations';
|
||||
import { getFileMentionAutocompleteQuery, type FileMentionAutocompleteInputSource } from './fileMentionAutocompleteState';
|
||||
import { SessionSuggestionChip } from '@/components/chat/SessionSuggestionChip';
|
||||
import { SessionGoalRow } from '@/components/chat/SessionGoalRow';
|
||||
import { SessionGoalButton, SessionGoalObjectiveCounter } from '@/components/chat/SessionGoalButton';
|
||||
import type { Part } from '@opencode-ai/sdk/v2/client';
|
||||
|
||||
const MAX_VISIBLE_TEXTAREA_LINES = 8;
|
||||
@@ -4904,6 +4906,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
>
|
||||
{isMobile && !mobileComposerExpanded ? (
|
||||
<div className="flex flex-col">
|
||||
<SessionGoalRow
|
||||
sessionId={currentSessionId}
|
||||
directory={currentSessionDirectoryForSync ?? currentDirectory}
|
||||
className="mb-1.5"
|
||||
/>
|
||||
<SessionSuggestionChip
|
||||
sessionId={currentSessionId}
|
||||
directory={currentSessionDirectoryForSync ?? currentDirectory}
|
||||
@@ -4986,6 +4993,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<SessionGoalRow
|
||||
sessionId={currentSessionId}
|
||||
directory={currentSessionDirectoryForSync ?? currentDirectory}
|
||||
className="mb-1.5"
|
||||
/>
|
||||
<SessionSuggestionChip
|
||||
sessionId={currentSessionId}
|
||||
directory={currentSessionDirectoryForSync ?? currentDirectory}
|
||||
@@ -5313,6 +5325,14 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
permissionAutoAcceptEnabled={permissionAutoAcceptEnabled}
|
||||
handlePermissionAutoAcceptToggle={handlePermissionAutoAcceptToggle}
|
||||
/>
|
||||
<SessionGoalButton
|
||||
sessionId={currentSessionId}
|
||||
directory={currentSessionDirectoryForSync ?? currentDirectory}
|
||||
draftOpen={newSessionDraftOpen}
|
||||
footerIconButtonClass={footerIconButtonClass}
|
||||
iconSizeClass={iconSizeClass}
|
||||
/>
|
||||
<SessionGoalObjectiveCounter length={message.length} />
|
||||
</div>
|
||||
<div className="flex items-center min-w-0 gap-x-1 justify-end">
|
||||
<div className="flex items-center gap-x-1 flex-shrink-0">
|
||||
@@ -5382,6 +5402,15 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
handlePermissionAutoAcceptToggle={handlePermissionAutoAcceptToggle}
|
||||
withTooltip
|
||||
/>
|
||||
<SessionGoalButton
|
||||
sessionId={currentSessionId}
|
||||
directory={currentSessionDirectoryForSync ?? currentDirectory}
|
||||
draftOpen={newSessionDraftOpen}
|
||||
footerIconButtonClass={footerIconButtonClass}
|
||||
iconSizeClass={iconSizeClass}
|
||||
withTooltip
|
||||
/>
|
||||
<SessionGoalObjectiveCounter length={message.length} />
|
||||
</div>
|
||||
<div className={cn('flex items-center flex-1 justify-end', footerGapClass, 'md:gap-x-3')}>
|
||||
<MemoModelControls className={cn('flex-1 min-w-0 justify-end')} />
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useSessionGoal } from '@/hooks/useSessionGoal';
|
||||
import { useSessionGoalArmStore } from '@/stores/useSessionGoalArmStore';
|
||||
import { SESSION_GOAL_OBJECTIVE_CHAR_LIMIT } from '@/lib/sessionGoalMetadata';
|
||||
import { SessionGoalDialog } from '@/components/chat/SessionGoalDialog';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SessionGoalButtonProps {
|
||||
sessionId: string | null;
|
||||
directory?: string;
|
||||
/** Session draft is open — the goal arms for the session the draft creates. */
|
||||
draftOpen?: boolean;
|
||||
footerIconButtonClass: string;
|
||||
iconSizeClass: string;
|
||||
withTooltip?: boolean;
|
||||
}
|
||||
|
||||
// Composer target button — the goal switch. With no live goal one tap arms
|
||||
// goal mode (the next sent prompt becomes the objective; works on drafts
|
||||
// too) and a second tap disarms. While a goal is live the target stays lit
|
||||
// (info while running, success when complete, error when blocked / out of
|
||||
// budget) and tapping opens the manage dialog.
|
||||
export const SessionGoalButton: React.FC<SessionGoalButtonProps> = React.memo(({
|
||||
sessionId,
|
||||
directory,
|
||||
draftOpen = false,
|
||||
footerIconButtonClass,
|
||||
iconSizeClass,
|
||||
withTooltip = false,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { goal, enabled } = useSessionGoal(sessionId ?? '', directory);
|
||||
const armed = useSessionGoalArmStore((state) => state.armed);
|
||||
const setArmed = useSessionGoalArmStore((state) => state.setArmed);
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
|
||||
// The goal loop runs in the web server; the VS Code extension only renders
|
||||
// goal state. Arming a goal there would create one nothing drives, so the
|
||||
// entry point is hidden entirely.
|
||||
if (isVSCodeRuntime() || !enabled || (!sessionId && !draftOpen)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// A settled goal no longer drives the loop — the button goes back to being
|
||||
// an arm switch, while still tinting with the outcome color.
|
||||
const liveGoal = goal && goal.status !== 'complete' ? goal : null;
|
||||
const isEngaged = armed || Boolean(liveGoal);
|
||||
|
||||
const colorClass = (() => {
|
||||
if (goal?.status === 'complete') return 'text-[var(--status-success)]';
|
||||
if (goal?.status === 'blocked' || goal?.status === 'budgetLimited') return 'text-[var(--status-error)]';
|
||||
if (armed || goal?.status === 'active' || goal?.status === 'paused') return 'text-[var(--status-info)]';
|
||||
return '';
|
||||
})();
|
||||
|
||||
const label = goal
|
||||
? t('chat.goal.button.manageAria')
|
||||
: (armed ? t('chat.goal.button.disarmAria') : t('chat.goal.button.armAria'));
|
||||
|
||||
// Any existing goal (live or completed) opens the manage dialog — a
|
||||
// completed goal must be removed there before a new one can be armed.
|
||||
const handleClick = () => {
|
||||
if (goal) {
|
||||
setDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
setArmed(!armed);
|
||||
};
|
||||
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(footerIconButtonClass, colorClass)}
|
||||
onClick={handleClick}
|
||||
aria-label={label}
|
||||
aria-pressed={isEngaged}
|
||||
{...(withTooltip ? {} : { title: label })}
|
||||
>
|
||||
{isEngaged || goal ? (
|
||||
<Icon name="target-fill" className={cn(iconSizeClass, 'text-current')} aria-hidden="true" />
|
||||
) : (
|
||||
<Icon name="target" className={cn(iconSizeClass, 'text-current')} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{withTooltip ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6}>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : button}
|
||||
{sessionId ? (
|
||||
<SessionGoalDialog open={dialogOpen} onOpenChange={setDialogOpen} sessionId={sessionId} directory={directory} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
SessionGoalButton.displayName = 'SessionGoalButton';
|
||||
|
||||
interface SessionGoalObjectiveCounterProps {
|
||||
/** Current composer text length — the armed message becomes the objective. */
|
||||
length: number;
|
||||
}
|
||||
|
||||
// Tiny hot-path leaf next to the target button: while goal mode is armed the
|
||||
// typed message becomes the objective, which the server clamps to 2000
|
||||
// chars — surface that limit during typing instead of truncating silently.
|
||||
// Renders null when not armed, so normal typing shows nothing.
|
||||
export const SessionGoalObjectiveCounter: React.FC<SessionGoalObjectiveCounterProps> = React.memo(({ length }) => {
|
||||
const { t } = useI18n();
|
||||
const armed = useSessionGoalArmStore((state) => state.armed);
|
||||
|
||||
if (!armed || length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const over = length > SESSION_GOAL_OBJECTIVE_CHAR_LIMIT;
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'flex-shrink-0 self-center typography-micro tabular-nums',
|
||||
over ? 'text-[var(--status-error)]' : 'text-muted-foreground/70',
|
||||
)}
|
||||
aria-label={t('chat.goal.counter.aria')}
|
||||
title={t('chat.goal.counter.aria')}
|
||||
>
|
||||
{length}/{SESSION_GOAL_OBJECTIVE_CHAR_LIMIT}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
SessionGoalObjectiveCounter.displayName = 'SessionGoalObjectiveCounter';
|
||||
@@ -0,0 +1,189 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { toast } from '@/components/ui';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useSessionGoal } from '@/hooks/useSessionGoal';
|
||||
import {
|
||||
formatGoalTokens,
|
||||
SESSION_GOAL_OBJECTIVE_CHAR_LIMIT,
|
||||
} from '@/lib/sessionGoalMetadata';
|
||||
import { sessionGoalStatusColor, sessionGoalStatusLabelKey } from '@/lib/sessionGoalPresentation';
|
||||
import { clearSessionGoal, setSessionGoal } from '@/lib/sessionGoalActions';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
interface SessionGoalDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
sessionId: string;
|
||||
directory?: string;
|
||||
}
|
||||
|
||||
// Create/manage dialog for the session goal: objective + optional token
|
||||
// budget on creation; status, usage, latest audit note and lifecycle actions
|
||||
// (pause/resume/complete/clear) once a goal exists.
|
||||
export function SessionGoalDialog({ open, onOpenChange, sessionId, directory }: SessionGoalDialogProps) {
|
||||
const { t } = useI18n();
|
||||
const { goal } = useSessionGoal(sessionId, directory);
|
||||
|
||||
const [objective, setObjective] = React.useState('');
|
||||
const [budgetEnabled, setBudgetEnabled] = React.useState(false);
|
||||
const [tokenBudget, setTokenBudget] = React.useState<number>(200_000);
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
setObjective(goal?.objective ?? '');
|
||||
setBudgetEnabled(Boolean(goal?.tokenBudget));
|
||||
setTokenBudget(goal?.tokenBudget ?? 200_000);
|
||||
// Seed the form only when the dialog opens; live goal updates while it is
|
||||
// open must not clobber the user's edits.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const run = React.useCallback(async (action: () => Promise<void>, closeAfter: boolean) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await action();
|
||||
if (closeAfter) onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.warn('[session-goal] action failed:', error);
|
||||
toast.error(t('chat.goal.toast.actionFailed'));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [onOpenChange, t]);
|
||||
|
||||
const trimmedObjective = objective.trim();
|
||||
const objectiveChanged = trimmedObjective !== (goal?.objective ?? '');
|
||||
const budgetValue = budgetEnabled ? tokenBudget : null;
|
||||
const budgetChanged = budgetValue !== (goal?.tokenBudget ?? null);
|
||||
// A completed goal is read-only: remove it and arm a new one instead of
|
||||
// "saving" over the outcome (re-saving used to spawn a fresh active goal
|
||||
// that the auditor instantly re-completed — a confusing status flash).
|
||||
const isCompleted = goal?.status === 'complete';
|
||||
const canSave = !isCompleted && trimmedObjective.length > 0 && (!goal || objectiveChanged || budgetChanged);
|
||||
|
||||
const handleSave = () => run(
|
||||
() => setSessionGoal(sessionId, directory, { objective: trimmedObjective, tokenBudget: budgetValue }, goal),
|
||||
true,
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{goal ? t('chat.goal.dialog.titleManage') : t('chat.goal.dialog.titleCreate')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3">
|
||||
{goal && (
|
||||
<div className="space-y-1 p-2 rounded-lg" style={{ backgroundColor: 'var(--surface-elevated)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-2 w-2 rounded-full flex-shrink-0" style={{ backgroundColor: sessionGoalStatusColor[goal.status] }} aria-hidden="true" />
|
||||
<span className="typography-ui-label text-foreground">{t(sessionGoalStatusLabelKey[goal.status] as never)}</span>
|
||||
<span className="typography-meta text-muted-foreground tabular-nums">
|
||||
{goal.tokenBudget
|
||||
? t('chat.goal.usage.tokensWithBudget', {
|
||||
used: formatGoalTokens(goal.tokensUsed),
|
||||
budget: formatGoalTokens(goal.tokenBudget),
|
||||
})
|
||||
: t('chat.goal.usage.tokens', { used: formatGoalTokens(goal.tokensUsed) })}
|
||||
{' · '}
|
||||
{t('chat.goal.usage.turns', { turns: goal.turnsUsed })}
|
||||
</span>
|
||||
</div>
|
||||
{goal.note ? (
|
||||
<p className="typography-meta text-muted-foreground">{goal.note}</p>
|
||||
) : null}
|
||||
{/* Only failure states carry a reason worth reading; outcomes
|
||||
like "verified by audit" are noise next to the status dot. */}
|
||||
{goal.statusReason && (goal.status === 'blocked' || goal.status === 'budgetLimited') ? (
|
||||
<p className="typography-meta text-muted-foreground/70">{goal.statusReason}</p>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCompleted ? (
|
||||
<p className="max-h-48 overflow-y-auto whitespace-pre-wrap break-words typography-meta text-muted-foreground">{goal.objective}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="typography-ui-label text-foreground">{t('chat.goal.dialog.objectiveLabel')}</span>
|
||||
<span className="typography-micro tabular-nums text-muted-foreground/70" aria-label={t('chat.goal.counter.aria')}>
|
||||
{objective.length}/{SESSION_GOAL_OBJECTIVE_CHAR_LIMIT}
|
||||
</span>
|
||||
</div>
|
||||
<Textarea
|
||||
value={objective}
|
||||
onChange={(event) => setObjective(event.target.value)}
|
||||
placeholder={t('chat.goal.dialog.objectivePlaceholder')}
|
||||
maxLength={SESSION_GOAL_OBJECTIVE_CHAR_LIMIT}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8">
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={budgetEnabled}
|
||||
onClick={() => setBudgetEnabled((value) => !value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
setBudgetEnabled((value) => !value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={budgetEnabled}
|
||||
onChange={setBudgetEnabled}
|
||||
ariaLabel={t('chat.goal.dialog.budgetLabel')}
|
||||
/>
|
||||
<span className="typography-ui-label text-foreground">{t('chat.goal.dialog.budgetLabel')}</span>
|
||||
</div>
|
||||
{budgetEnabled && (
|
||||
<NumberInput
|
||||
value={tokenBudget}
|
||||
onValueChange={(value) => setTokenBudget(typeof value === 'number' && value > 0 ? Math.floor(value) : 1000)}
|
||||
min={1000}
|
||||
max={100_000_000}
|
||||
step={50_000}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
{goal && (
|
||||
<Button variant="destructive" size="sm" disabled={busy} onClick={() => run(() => clearSessionGoal(sessionId, directory), true)}>
|
||||
{t('chat.goal.action.clear')}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex flex-1 items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="sm" disabled={busy} onClick={() => onOpenChange(false)}>
|
||||
{t('chat.goal.action.cancel')}
|
||||
</Button>
|
||||
{!isCompleted && (
|
||||
<Button size="sm" disabled={busy || !canSave} onClick={handleSave}>
|
||||
{goal ? t('chat.goal.action.save') : t('chat.goal.action.start')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
import { useSessionStatus } from '@/sync/sync-context';
|
||||
import { useSessionGoal } from '@/hooks/useSessionGoal';
|
||||
import { formatGoalTokens } from '@/lib/sessionGoalMetadata';
|
||||
import { sessionGoalStatusColor, sessionGoalStatusLabelKey } from '@/lib/sessionGoalPresentation';
|
||||
import { setSessionGoalStatus } from '@/lib/sessionGoalActions';
|
||||
import { toast } from '@/components/ui';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SessionGoalRowProps {
|
||||
sessionId: string | null;
|
||||
directory?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Compact goal strip near the composer: informational only — status dot,
|
||||
// objective (or the latest audit note), token usage — plus an inline
|
||||
// pause/resume action. The manage dialog opens from the composer target
|
||||
// button, not from here.
|
||||
export const SessionGoalRow: React.FC<SessionGoalRowProps> = React.memo(({ sessionId, directory, className }) => {
|
||||
const { t } = useI18n();
|
||||
const { goal, enabled } = useSessionGoal(sessionId ?? '', directory);
|
||||
const sessionStatus = useSessionStatus(sessionId ?? '', directory);
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
|
||||
const handleToggleStatus = React.useCallback(async (nextStatus: 'active' | 'paused') => {
|
||||
if (!sessionId || busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await setSessionGoalStatus(sessionId, directory, nextStatus);
|
||||
} catch (error) {
|
||||
console.warn('[session-goal] status change failed:', error);
|
||||
toast.error(t('chat.goal.toast.actionFailed'));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [sessionId, directory, busy, t]);
|
||||
|
||||
if (!sessionId || !enabled || !goal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Accounting only lands on idle ticks — hide the counter until there is a
|
||||
// real number (or a budget worth tracking against) instead of showing "0".
|
||||
const usage = goal.tokenBudget
|
||||
? t('chat.goal.usage.tokensWithBudget', {
|
||||
used: formatGoalTokens(goal.tokensUsed),
|
||||
budget: formatGoalTokens(goal.tokenBudget),
|
||||
})
|
||||
: (goal.tokensUsed > 0 ? t('chat.goal.usage.tokens', { used: formatGoalTokens(goal.tokensUsed) }) : null);
|
||||
|
||||
const pauseResume = goal.status === 'active'
|
||||
? { icon: 'pause' as const, labelKey: 'chat.goal.action.pause' as const, next: 'paused' as const }
|
||||
: (goal.status === 'paused' || goal.status === 'blocked' || goal.status === 'budgetLimited'
|
||||
? { icon: 'play' as const, labelKey: 'chat.goal.action.resume' as const, next: 'active' as const }
|
||||
: null);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full min-w-0 items-center gap-2 rounded-lg border px-2 py-1',
|
||||
'border-[var(--interactive-border)]',
|
||||
className,
|
||||
)}
|
||||
aria-label={t('chat.goal.row.aria')}
|
||||
title={goal.objective}
|
||||
>
|
||||
<Icon name="target" className="h-3.5 w-3.5 flex-shrink-0" style={{ color: sessionGoalStatusColor[goal.status] }} aria-hidden="true" />
|
||||
<span className="min-w-0 flex-1 truncate typography-meta text-foreground">
|
||||
{goal.note || goal.objective}
|
||||
</span>
|
||||
{goal.status === 'active' && (!sessionStatus || sessionStatus.type === 'idle') ? (
|
||||
// The agent stopped but the goal is still active: the server is
|
||||
// sitting out the quiet window and running the audit — show that
|
||||
// instead of a static "Active" that looks stuck.
|
||||
<span className="flex flex-shrink-0 items-center gap-1 typography-meta text-muted-foreground">
|
||||
<Icon name="loader-4" className="h-3 w-3 animate-spin" aria-hidden="true" />
|
||||
{t('chat.goal.status.evaluating')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex-shrink-0 typography-meta text-muted-foreground">
|
||||
{t(sessionGoalStatusLabelKey[goal.status] as never)}
|
||||
</span>
|
||||
)}
|
||||
{usage ? (
|
||||
<span className="flex-shrink-0 typography-meta tabular-nums text-muted-foreground/70">
|
||||
{usage}
|
||||
</span>
|
||||
) : null}
|
||||
{pauseResume ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleToggleStatus(pauseResume.next)}
|
||||
disabled={busy}
|
||||
className="flex flex-shrink-0 cursor-pointer items-center gap-1 rounded px-1 py-0.5 typography-meta text-muted-foreground hover:bg-[var(--interactive-hover)] hover:text-foreground disabled:opacity-50"
|
||||
aria-label={t(pauseResume.labelKey)}
|
||||
>
|
||||
<Icon name={pauseResume.icon} className="h-3 w-3" aria-hidden="true" />
|
||||
<span>{t(pauseResume.labelKey)}</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
SessionGoalRow.displayName = 'SessionGoalRow';
|
||||
@@ -76,7 +76,6 @@ export const iconSpriteData = {
|
||||
"download": `<path d="M3 19H21V21H3V19ZM13 13.1716L19.0711 7.1005L20.4853 8.51472L12 17L3.51472 8.51472L4.92893 7.1005L11 13.1716V2H13V13.1716Z" fill="currentColor"/>`,
|
||||
"drag-move-2": `<path d="M11 11V5.82843L9.17157 7.65685L7.75736 6.24264L12 2L16.2426 6.24264L14.8284 7.65685L13 5.82843V11H18.1716L16.3431 9.17157L17.7574 7.75736L22 12L17.7574 16.2426L16.3431 14.8284L18.1716 13H13V18.1716L14.8284 16.3431L16.2426 17.7574L12 22L7.75736 17.7574L9.17157 16.3431L11 18.1716V13H5.82843L7.65685 14.8284L6.24264 16.2426L2 12L6.24264 7.75736L7.65685 9.17157L5.82843 11H11Z" fill="currentColor"/>`,
|
||||
"draggable": `<path d="M8.5 7C9.32843 7 10 6.32843 10 5.5C10 4.67157 9.32843 4 8.5 4C7.67157 4 7 4.67157 7 5.5C7 6.32843 7.67157 7 8.5 7ZM8.5 13.5C9.32843 13.5 10 12.8284 10 12C10 11.1716 9.32843 10.5 8.5 10.5C7.67157 10.5 7 11.1716 7 12C7 12.8284 7.67157 13.5 8.5 13.5ZM10 18.5C10 19.3284 9.32843 20 8.5 20C7.67157 20 7 19.3284 7 18.5C7 17.6716 7.67157 17 8.5 17C9.32843 17 10 17.6716 10 18.5ZM15.5 7C16.3284 7 17 6.32843 17 5.5C17 4.67157 16.3284 4 15.5 4C14.6716 4 14 4.67157 14 5.5C14 6.32843 14.6716 7 15.5 7ZM17 12C17 12.8284 16.3284 13.5 15.5 13.5C14.6716 13.5 14 12.8284 14 12C14 11.1716 14.6716 10.5 15.5 10.5C16.3284 10.5 17 11.1716 17 12ZM15.5 20C16.3284 20 17 19.3284 17 18.5C17 17.6716 16.3284 17 15.5 17C14.6716 17 14 17.6716 14 18.5C14 19.3284 14.6716 20 15.5 20Z" fill="currentColor"/>`,
|
||||
"earth": `<path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z" fill="currentColor"/>`,
|
||||
"edit-2": `<path d="M5 18.89H6.41421L15.7279 9.57627L14.3137 8.16206L5 17.4758V18.89ZM21 20.89H3V16.6473L16.435 3.21231C16.8256 2.82179 17.4587 2.82179 17.8492 3.21231L20.6777 6.04074C21.0682 6.43126 21.0682 7.06443 20.6777 7.45495L9.24264 18.89H21V20.89ZM15.7279 6.74785L17.1421 8.16206L18.5563 6.74785L17.1421 5.33363L15.7279 6.74785Z" fill="currentColor"/>`,
|
||||
"edit": `<path d="M6.41421 15.89L16.5563 5.74785L15.1421 4.33363L5 14.4758V15.89H6.41421ZM7.24264 17.89H3V13.6473L14.435 2.21231C14.8256 1.82179 15.4587 1.82179 15.8492 2.21231L18.6777 5.04074C19.0682 5.43126 19.0682 6.06443 18.6777 6.45495L7.24264 17.89ZM3 19.89H21V21.89H3V19.89Z" fill="currentColor"/>`,
|
||||
"emotion-happy": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM7 13H9C9 14.6569 10.3431 16 12 16C13.6569 16 15 14.6569 15 13H17C17 15.7614 14.7614 18 12 18C9.23858 18 7 15.7614 7 13ZM8 11C7.17157 11 6.5 10.3284 6.5 9.5C6.5 8.67157 7.17157 8 8 8C8.82843 8 9.5 8.67157 9.5 9.5C9.5 10.3284 8.82843 11 8 11ZM16 11C15.1716 11 14.5 10.3284 14.5 9.5C14.5 8.67157 15.1716 8 16 8C16.8284 8 17.5 8.67157 17.5 9.5C17.5 10.3284 16.8284 11 16 11Z" fill="currentColor"/>`,
|
||||
@@ -167,6 +166,7 @@ export const iconSpriteData = {
|
||||
"node-tree": `<path d="M10 2C10.5523 2 11 2.44772 11 3V7C11 7.55228 10.5523 8 10 8H8V10H13V9C13 8.44772 13.4477 8 14 8H20C20.5523 8 21 8.44772 21 9V13C21 13.5523 20.5523 14 20 14H14C13.4477 14 13 13.5523 13 13V12H8V18H13V17C13 16.4477 13.4477 16 14 16H20C20.5523 16 21 16.4477 21 17V21C21 21.5523 20.5523 22 20 22H14C13.4477 22 13 21.5523 13 21V20H7C6.44772 20 6 19.5523 6 19V8H4C3.44772 8 3 7.55228 3 7V3C3 2.44772 3.44772 2 4 2H10ZM19 18H15V20H19V18ZM19 10H15V12H19V10ZM9 4H5V6H9V4Z" fill="currentColor"/>`,
|
||||
"notification-3": `<path d="M20 17H22V19H2V17H4V10C4 5.58172 7.58172 2 12 2C16.4183 2 20 5.58172 20 10V17ZM18 17V10C18 6.68629 15.3137 4 12 4C8.68629 4 6 6.68629 6 10V17H18ZM9 21H15V23H9V21Z" fill="currentColor"/>`,
|
||||
"palette": `<path d="M12 2C17.5222 2 22 5.97778 22 10.8889C22 13.9556 19.5111 16.4444 16.4444 16.4444H14.4778C13.5556 16.4444 12.8111 17.1889 12.8111 18.1111C12.8111 18.5333 12.9778 18.9222 13.2333 19.2111C13.5 19.5111 13.6667 19.9 13.6667 20.3333C13.6667 21.2556 12.9 22 12 22C6.47778 22 2 17.5222 2 12C2 6.47778 6.47778 2 12 2ZM10.8111 18.1111C10.8111 16.0843 12.451 14.4444 14.4778 14.4444H16.4444C18.4065 14.4444 20 12.851 20 10.8889C20 7.1392 16.4677 4 12 4C7.58235 4 4 7.58235 4 12C4 16.19 7.2226 19.6285 11.324 19.9718C10.9948 19.4168 10.8111 18.7761 10.8111 18.1111ZM7.5 12C6.67157 12 6 11.3284 6 10.5C6 9.67157 6.67157 9 7.5 9C8.32843 9 9 9.67157 9 10.5C9 11.3284 8.32843 12 7.5 12ZM16.5 12C15.6716 12 15 11.3284 15 10.5C15 9.67157 15.6716 9 16.5 9C17.3284 9 18 9.67157 18 10.5C18 11.3284 17.3284 12 16.5 12ZM12 9C11.1716 9 10.5 8.32843 10.5 7.5C10.5 6.67157 11.1716 6 12 6C12.8284 6 13.5 6.67157 13.5 7.5C13.5 8.32843 12.8284 9 12 9Z" fill="currentColor"/>`,
|
||||
"pause": `<path d="M6 5H8V19H6V5ZM16 5H18V19H16V5Z" fill="currentColor"/>`,
|
||||
"pencil-ai-2": `<path d="M18.5293 15.3193C18.7058 14.8934 19.2942 14.8934 19.4707 15.3193L19.7236 15.9307C20.1556 16.9735 20.9615 17.8062 21.9746 18.2568L22.6914 18.5762C23.1022 18.7589 23.1022 19.3564 22.6914 19.5391L21.9326 19.877C20.9449 20.3163 20.1534 21.1194 19.7139 22.1279L19.4668 22.6934C19.2863 23.1075 18.7136 23.1075 18.5332 22.6934L18.2861 22.1279C17.8466 21.1194 17.0551 20.3163 16.0674 19.877L15.3076 19.5391C14.8974 19.3562 14.8974 18.759 15.3076 18.5762L16.0254 18.2568C17.0385 17.8062 17.8444 16.9735 18.2764 15.9307L18.5293 15.3193ZM16.4346 3.21193C16.8251 2.82141 17.4591 2.82141 17.8496 3.21193L20.6777 6.04103C21.0681 6.43157 21.0682 7.06464 20.6777 7.45509L7.24219 20.8897H3V16.6475L16.4346 3.21193ZM5 17.4756V18.8897H6.41406L15.7275 9.57618L14.3135 8.16212L5 17.4756ZM15.7275 6.74806L17.1426 8.16212L18.5566 6.74806L17.1426 5.334L15.7275 6.74806Z" fill="currentColor"/>`,
|
||||
"pencil-ai": `<path d="M16.4356 3.21188C16.8261 2.82185 17.4592 2.82157 17.8496 3.21188L20.6777 6.04099C21.0681 6.43152 21.0682 7.06457 20.6777 7.45505L7.2422 20.8896H3.00001V16.6475L16.4356 3.21188ZM5.00001 17.4756V18.8896H6.41407L15.7276 9.57615L14.3135 8.16208L5.00001 17.4756ZM4.5293 1.3193C4.70583 0.893505 5.29418 0.893508 5.47071 1.3193L5.72364 1.93063C6.15555 2.97342 6.96155 3.80613 7.97462 4.2568L8.69239 4.57614C9.10267 4.75896 9.10262 5.35616 8.69239 5.53903L7.93263 5.87692C6.94497 6.3162 6.15339 7.11943 5.71387 8.1279L5.4668 8.69334C5.28636 9.10747 4.71366 9.10747 4.53321 8.69334L4.28614 8.1279C3.84661 7.11943 3.05506 6.3162 2.06739 5.87692L1.30762 5.53903C0.897483 5.35617 0.897435 4.75896 1.30762 4.57614L2.0254 4.2568C3.03845 3.80614 3.84446 2.97344 4.27637 1.93063L4.5293 1.3193ZM15.7276 6.74802L17.1426 8.16208L18.5567 6.74802L17.1426 5.33395L15.7276 6.74802Z" fill="currentColor"/>`,
|
||||
"pencil": `<path d="M15.7279 9.57627L14.3137 8.16206L5 17.4758V18.89H6.41421L15.7279 9.57627ZM17.1421 8.16206L18.5563 6.74785L17.1421 5.33363L15.7279 6.74785L17.1421 8.16206ZM7.24264 20.89H3V16.6473L16.435 3.21231C16.8256 2.82179 17.4587 2.82179 17.8492 3.21231L20.6777 6.04074C21.0682 6.43126 21.0682 7.06443 20.6777 7.45495L7.24264 20.89Z" fill="currentColor"/>`,
|
||||
@@ -214,6 +214,8 @@ export const iconSpriteData = {
|
||||
"stop": `<path d="M7 7V17H17V7H7ZM6 5H18C18.5523 5 19 5.44772 19 6V18C19 18.5523 18.5523 19 18 19H6C5.44772 19 5 18.5523 5 18V6C5 5.44772 5.44772 5 6 5Z" fill="currentColor"/>`,
|
||||
"subtract": `<path d="M5 11V13H19V11H5Z" fill="currentColor"/>`,
|
||||
"survey": `<path d="M17 2V4H20.0066C20.5552 4 21 4.44495 21 4.9934V21.0066C21 21.5552 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5551 3 21.0066V4.9934C3 4.44476 3.44495 4 3.9934 4H7V2H17ZM7 6H5V20H19V6H17V8H7V6ZM9 16V18H7V16H9ZM9 13V15H7V13H9ZM9 10V12H7V10H9ZM15 4H9V6H15V4Z" fill="currentColor"/>`,
|
||||
"target-fill": `<path d="M12 2C12.5523 2 13 2.44772 13 3C13 3.55228 12.5523 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 11.4477 20.4477 11 21 11C21.5523 11 22 11.4477 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 6C12.5523 6 13 6.44772 13 7C13 7.55228 12.5523 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 11.4477 16.4477 11 17 11C17.5523 11 18 11.4477 18 12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6ZM18.5713 2.10059C18.8474 2.1006 19.0712 2.32449 19.0713 2.60059V4.42969C19.0716 4.70553 19.2954 4.92866 19.5713 4.92871H21.3994C21.6754 4.92871 21.8992 5.15275 21.8994 5.42871V6.34375L20.0107 8.23242C19.6358 8.60719 19.1268 8.81824 18.5967 8.81836H16.5967L12.707 12.707C12.3165 13.0974 11.6835 13.0975 11.293 12.707C10.9027 12.3165 10.9026 11.6834 11.293 11.293L15.1826 7.4043V5.4043C15.1826 4.87411 15.3928 4.36526 15.7676 3.99023L17.6572 2.10059H18.5713Z" fill="currentColor"/>`,
|
||||
"target": `<path d="M12 1.99999C12.5523 1.99999 13 2.4477 13 2.99999C12.9999 3.55224 12.5522 3.99999 12 3.99999C7.58172 3.99999 4 7.58171 4 12C4.00004 16.4182 7.58174 20 12 20C16.4182 20 19.9999 16.4182 20 12C20 11.4477 20.4477 11 21 11C21.5523 11 22 11.4477 22 12C21.9999 17.5228 17.5228 22 12 22C6.47717 22 2.00004 17.5228 2 12C2 6.47714 6.47715 1.99999 12 1.99999ZM12 5.99999C12.5523 5.99999 13 6.4477 13 6.99999C12.9999 7.55224 12.5522 7.99999 12 7.99999C9.79085 7.99999 7.99999 9.79085 7.99999 12C8.00004 14.2091 9.79088 16 12 16C14.2091 16 15.9999 14.2091 16 12C16 11.4477 16.4477 11 17 11C17.5523 11 18 11.4477 18 12C17.9999 15.3137 15.3137 18 12 18C8.68631 18 6.00004 15.3137 6 12C6 8.68628 8.68629 5.99999 12 5.99999ZM17.6562 2.10057C18.0468 1.71005 18.6807 1.71005 19.0713 2.10057C19.4614 2.49105 19.4615 3.12419 19.0713 3.51463L18.3633 4.22069L18.3642 4.22167C17.9737 4.61219 17.9737 5.2452 18.3642 5.63573C18.7548 6.02612 19.3878 6.02621 19.7783 5.63573L20.4853 4.9287C20.8759 4.53839 21.5089 4.53826 21.8994 4.9287C22.2899 5.31915 22.2897 5.95222 21.8994 6.34276L19.7783 8.46483C19.5909 8.65223 19.3363 8.75671 19.0713 8.75682H16.6572L12.707 12.707C12.3165 13.0974 11.6834 13.0974 11.293 12.707C10.9025 12.3165 10.9026 11.6835 11.293 11.293L15.2422 7.34374V4.9287C15.2422 4.66356 15.3477 4.40916 15.5351 4.22167L17.6562 2.10057Z" fill="currentColor"/>`,
|
||||
"task": `<path d="M19 4H5V20H19V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H19.9997C20.5519 2 20.9996 2.44772 20.9997 3L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11.2929 13.1213L15.5355 8.87868L16.9497 10.2929L11.2929 15.9497L7.40381 12.0607L8.81802 10.6464L11.2929 13.1213Z" fill="currentColor"/>`,
|
||||
"terminal-box": `<path d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM4 5V19H20V5H4ZM12 15H18V17H12V15ZM8.66685 12L5.83842 9.17157L7.25264 7.75736L11.4953 12L7.25264 16.2426L5.83842 14.8284L8.66685 12Z" fill="currentColor"/>`,
|
||||
"terminal": `<path d="M10.9999 12L3.92886 19.0711L2.51465 17.6569L8.1715 12L2.51465 6.34317L3.92886 4.92896L10.9999 12ZM10.9999 19H20.9999V21H10.9999V19Z" fill="currentColor"/>`,
|
||||
|
||||
@@ -145,7 +145,7 @@ const VisualSectionContent: React.FC = () => {
|
||||
|
||||
// Chat section: User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Follow-up behavior, Persist draft
|
||||
const ChatSectionContent: React.FC = () => {
|
||||
return <OpenChamberVisualSettings visibleSettings={['sessionAssist', 'chatRenderMode', 'messageTransport', 'activityRenderMode', 'userMessageRendering', 'mermaidRendering', 'reasoning', 'showToolFileIcons', 'showTurnChangedFiles', 'expandedTools', 'collapsibleUserMessages', 'stickyUserHeader', 'wideChatLayout', 'codeBlockLineWrap', 'splitAssistantMessageActions', 'diffLayout', 'dotfiles', 'fileViewerPreview', 'followUpBehavior', 'persistDraft', 'inputSpellcheck']} />;
|
||||
return <OpenChamberVisualSettings visibleSettings={['sessionGoal', 'sessionAssist', 'chatRenderMode', 'messageTransport', 'activityRenderMode', 'userMessageRendering', 'mermaidRendering', 'reasoning', 'showToolFileIcons', 'showTurnChangedFiles', 'expandedTools', 'collapsibleUserMessages', 'stickyUserHeader', 'wideChatLayout', 'codeBlockLineWrap', 'splitAssistantMessageActions', 'diffLayout', 'dotfiles', 'fileViewerPreview', 'followUpBehavior', 'persistDraft', 'inputSpellcheck']} />;
|
||||
};
|
||||
|
||||
// Sessions section: Default model & agent, Session retention
|
||||
|
||||
@@ -245,7 +245,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
|
||||
return mode === 'markdown' ? 'markdown' : 'plain';
|
||||
};
|
||||
|
||||
type VisibleSetting = 'sessionAssist' | 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
|
||||
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
|
||||
|
||||
interface OpenChamberVisualSettingsProps {
|
||||
/** Which settings to show. If undefined, shows all. */
|
||||
@@ -263,6 +263,12 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const sessionSuggestionEnabled = useUIStore(state => state.sessionSuggestionEnabled);
|
||||
const setSessionRecapEnabled = useUIStore(state => state.setSessionRecapEnabled);
|
||||
const setSessionSuggestionEnabled = useUIStore(state => state.setSessionSuggestionEnabled);
|
||||
const sessionGoalEnabled = useUIStore(state => state.sessionGoalEnabled);
|
||||
const setSessionGoalEnabled = useUIStore(state => state.setSessionGoalEnabled);
|
||||
const sessionGoalDefaultBudgetEnabled = useUIStore(state => state.sessionGoalDefaultBudgetEnabled);
|
||||
const setSessionGoalDefaultBudgetEnabled = useUIStore(state => state.setSessionGoalDefaultBudgetEnabled);
|
||||
const sessionGoalDefaultBudget = useUIStore(state => state.sessionGoalDefaultBudget);
|
||||
const setSessionGoalDefaultBudget = useUIStore(state => state.setSessionGoalDefaultBudget);
|
||||
const setShowReasoningTraces = useUIStore(state => state.setShowReasoningTraces);
|
||||
const collapsibleThinkingBlocks = useUIStore(state => state.collapsibleThinkingBlocks);
|
||||
const setCollapsibleThinkingBlocks = useUIStore(state => state.setCollapsibleThinkingBlocks);
|
||||
@@ -1813,6 +1819,81 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* The goal loop runs in the web server — VS Code only renders
|
||||
goal state, so the settings section is hidden there too. */}
|
||||
{shouldShow('sessionGoal') && !isVSCode && (
|
||||
<div className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.visual.goal.sectionTitle')}</h3>
|
||||
</div>
|
||||
<section className="p-2 space-y-0.5">
|
||||
<div
|
||||
data-settings-item="chat.session-goal"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={sessionGoalEnabled}
|
||||
onClick={() => setSessionGoalEnabled(!sessionGoalEnabled)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
setSessionGoalEnabled(!sessionGoalEnabled);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={sessionGoalEnabled}
|
||||
onChange={setSessionGoalEnabled}
|
||||
ariaLabel={t('settings.openchamber.visual.field.sessionGoalAria')}
|
||||
/>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.sessionGoal')}</span>
|
||||
</div>
|
||||
<div
|
||||
data-settings-item="chat.session-goal-budget"
|
||||
className="flex items-center gap-2 py-0.5"
|
||||
>
|
||||
<div
|
||||
className={cn('flex items-center gap-2', sessionGoalEnabled ? 'cursor-pointer' : 'opacity-50')}
|
||||
role="button"
|
||||
tabIndex={sessionGoalEnabled ? 0 : -1}
|
||||
aria-pressed={sessionGoalDefaultBudgetEnabled}
|
||||
onClick={() => {
|
||||
if (sessionGoalEnabled) setSessionGoalDefaultBudgetEnabled(!sessionGoalDefaultBudgetEnabled);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (sessionGoalEnabled && (event.key === ' ' || event.key === 'Enter')) {
|
||||
event.preventDefault();
|
||||
setSessionGoalDefaultBudgetEnabled(!sessionGoalDefaultBudgetEnabled);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={sessionGoalDefaultBudgetEnabled}
|
||||
onChange={setSessionGoalDefaultBudgetEnabled}
|
||||
disabled={!sessionGoalEnabled}
|
||||
ariaLabel={t('settings.openchamber.visual.goal.budgetAria')}
|
||||
/>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.goal.budgetLabel')}</span>
|
||||
</div>
|
||||
{sessionGoalEnabled && sessionGoalDefaultBudgetEnabled ? (
|
||||
<NumberInput
|
||||
value={sessionGoalDefaultBudget}
|
||||
onValueChange={(value) => {
|
||||
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
||||
setSessionGoalDefaultBudget(Math.floor(value));
|
||||
}
|
||||
}}
|
||||
min={1000}
|
||||
max={100000000}
|
||||
step={50000}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="typography-meta text-muted-foreground/70 px-0.5 pt-0.5">{t('settings.openchamber.visual.goal.description')}</p>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(shouldShow('sessionAssist') || shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('codeBlockLineWrap') || shouldShow('splitAssistantMessageActions') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && (
|
||||
<section className="p-2 space-y-0.5">
|
||||
{shouldShow('sessionAssist') && (
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ThinkingPill } from '@/components/session/ThinkingPill';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useAgentsStore } from '@/stores/useAgentsStore';
|
||||
import { isPrimaryMode } from '@/components/chat/mobileControlsUtils';
|
||||
import { EXECUTION_FORK_DEFAULT_INSTRUCTIONS } from '@/lib/messages/executionMeta';
|
||||
import { EXECUTION_FORK_DEFAULT_INSTRUCTIONS, EXECUTION_FORK_GOAL_INSTRUCTIONS } from '@/lib/messages/executionMeta';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
|
||||
@@ -25,6 +25,7 @@ export type ForkSessionExecution = {
|
||||
agent: string;
|
||||
instructions: string;
|
||||
createWorktree?: boolean;
|
||||
runAsGoal?: boolean;
|
||||
};
|
||||
|
||||
type ForkSessionDialogProps = {
|
||||
@@ -54,7 +55,22 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
|
||||
const [agent, setAgent] = React.useState(currentAgentName);
|
||||
const [instructions, setInstructions] = React.useState(EXECUTION_FORK_DEFAULT_INSTRUCTIONS);
|
||||
const [createWorktree, setCreateWorktree] = React.useState(false);
|
||||
const [runAsGoal, setRunAsGoal] = React.useState(false);
|
||||
const showCreateWorktree = React.useMemo(() => !isVSCodeRuntime(), []);
|
||||
// The goal loop lives in the web server; VS Code only renders goal state.
|
||||
const showRunAsGoal = React.useMemo(() => !isVSCodeRuntime(), []);
|
||||
|
||||
// Toggling goal mode swaps the prefilled instructions between the
|
||||
// report-back default and the assertive execute-to-completion variant —
|
||||
// but never clobbers text the user has edited.
|
||||
const handleToggleRunAsGoal = React.useCallback((next: boolean) => {
|
||||
setRunAsGoal(next);
|
||||
setInstructions((current) => {
|
||||
if (next && current === EXECUTION_FORK_DEFAULT_INSTRUCTIONS) return EXECUTION_FORK_GOAL_INSTRUCTIONS;
|
||||
if (!next && current === EXECUTION_FORK_GOAL_INSTRUCTIONS) return EXECUTION_FORK_DEFAULT_INSTRUCTIONS;
|
||||
return current;
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -75,6 +91,7 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
|
||||
setAgent(config.currentAgentName || '');
|
||||
setInstructions(EXECUTION_FORK_DEFAULT_INSTRUCTIONS);
|
||||
setCreateWorktree(false);
|
||||
setRunAsGoal(false);
|
||||
}, [open]);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -119,8 +136,9 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
|
||||
agent,
|
||||
instructions,
|
||||
createWorktree: showCreateWorktree && createWorktree,
|
||||
runAsGoal: showRunAsGoal && runAsGoal,
|
||||
});
|
||||
}, [canConfirm, submitting, onConfirm, providerID, modelID, variant, agent, instructions, showCreateWorktree, createWorktree]);
|
||||
}, [canConfirm, submitting, onConfirm, providerID, modelID, variant, agent, instructions, showCreateWorktree, createWorktree, showRunAsGoal, runAsGoal]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -186,25 +204,45 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center gap-3 ${showCreateWorktree ? 'justify-between' : 'justify-end'}`}>
|
||||
{showCreateWorktree ? (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Checkbox
|
||||
checked={createWorktree}
|
||||
onChange={setCreateWorktree}
|
||||
disabled={submitting}
|
||||
ariaLabel={t('chat.messageBody.forkDialog.createWorktree')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="truncate typography-ui-label text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={submitting}
|
||||
onClick={() => setCreateWorktree((value) => !value)}
|
||||
>
|
||||
{t('chat.messageBody.forkDialog.createWorktree')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={`flex items-center gap-3 ${showCreateWorktree || showRunAsGoal ? 'justify-between' : 'justify-end'}`}>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1">
|
||||
{showCreateWorktree ? (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Checkbox
|
||||
checked={createWorktree}
|
||||
onChange={setCreateWorktree}
|
||||
disabled={submitting}
|
||||
ariaLabel={t('chat.messageBody.forkDialog.createWorktree')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="truncate typography-ui-label text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={submitting}
|
||||
onClick={() => setCreateWorktree((value) => !value)}
|
||||
>
|
||||
{t('chat.messageBody.forkDialog.createWorktree')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{showRunAsGoal ? (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Checkbox
|
||||
checked={runAsGoal}
|
||||
onChange={handleToggleRunAsGoal}
|
||||
disabled={submitting}
|
||||
ariaLabel={t('sessions.scheduledTasks.editor.goal.aria')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="truncate typography-ui-label text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={submitting}
|
||||
onClick={() => handleToggleRunAsGoal(!runAsGoal)}
|
||||
>
|
||||
{t('sessions.scheduledTasks.editor.goal.label')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)} disabled={submitting}>
|
||||
{t('rightSidebar.contextNotesTodo.sendDialog.actions.cancel')}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
||||
import { toast } from '@/components/ui';
|
||||
@@ -466,6 +467,8 @@ type ScheduledTaskDraft = {
|
||||
modelID: string;
|
||||
variant: string;
|
||||
agent: string;
|
||||
goalEnabled: boolean;
|
||||
goalTokenBudget: number | null;
|
||||
};
|
||||
state?: ScheduledTask['state'];
|
||||
};
|
||||
@@ -515,6 +518,8 @@ const toDraft = (
|
||||
modelID: defaults.modelID,
|
||||
variant: defaults.variant,
|
||||
agent: defaults.agent,
|
||||
goalEnabled: false,
|
||||
goalTokenBudget: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -548,6 +553,10 @@ const toDraft = (
|
||||
modelID: task.execution.modelID,
|
||||
variant: task.execution.variant || '',
|
||||
agent: task.execution.agent || '',
|
||||
goalEnabled: task.execution.goalEnabled === true,
|
||||
goalTokenBudget: typeof task.execution.goalTokenBudget === 'number' && task.execution.goalTokenBudget > 0
|
||||
? task.execution.goalTokenBudget
|
||||
: null,
|
||||
},
|
||||
state: task.state,
|
||||
};
|
||||
@@ -1153,6 +1162,10 @@ export function ScheduledTaskEditorDialog(props: {
|
||||
modelID: draft.execution.modelID,
|
||||
...(draft.execution.variant.trim() ? { variant: draft.execution.variant.trim() } : {}),
|
||||
...(draft.execution.agent.trim() ? { agent: draft.execution.agent.trim() } : {}),
|
||||
...(draft.execution.goalEnabled ? { goalEnabled: true } : {}),
|
||||
...(draft.execution.goalEnabled && draft.execution.goalTokenBudget
|
||||
? { goalTokenBudget: draft.execution.goalTokenBudget }
|
||||
: {}),
|
||||
},
|
||||
...(draft.state ? { state: draft.state } : {}),
|
||||
};
|
||||
@@ -1608,6 +1621,49 @@ export function ScheduledTaskEditorDialog(props: {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-8 gap-y-2">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2">
|
||||
<Checkbox
|
||||
checked={draft.execution.goalEnabled}
|
||||
onChange={(goalEnabled) => setDraft((prev) => ({
|
||||
...prev,
|
||||
execution: { ...prev.execution, goalEnabled },
|
||||
}))}
|
||||
ariaLabel={t('sessions.scheduledTasks.editor.goal.aria')}
|
||||
/>
|
||||
<span className="typography-meta">{t('sessions.scheduledTasks.editor.goal.label')}</span>
|
||||
</label>
|
||||
{draft.execution.goalEnabled ? (
|
||||
<label className="inline-flex cursor-pointer items-center gap-2">
|
||||
<Checkbox
|
||||
checked={draft.execution.goalTokenBudget !== null}
|
||||
onChange={(hasBudget) => setDraft((prev) => ({
|
||||
...prev,
|
||||
execution: { ...prev.execution, goalTokenBudget: hasBudget ? 200_000 : null },
|
||||
}))}
|
||||
ariaLabel={t('sessions.scheduledTasks.editor.goal.budgetAria')}
|
||||
/>
|
||||
<span className="typography-meta">{t('sessions.scheduledTasks.editor.goal.budgetLabel')}</span>
|
||||
</label>
|
||||
) : null}
|
||||
{draft.execution.goalEnabled && draft.execution.goalTokenBudget !== null ? (
|
||||
<NumberInput
|
||||
value={draft.execution.goalTokenBudget}
|
||||
onValueChange={(value) => {
|
||||
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
||||
setDraft((prev) => ({
|
||||
...prev,
|
||||
execution: { ...prev.execution, goalTokenBudget: Math.floor(value) },
|
||||
}));
|
||||
}
|
||||
}}
|
||||
min={1000}
|
||||
max={100000000}
|
||||
step={50000}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
|
||||
import { AgentSelector } from '@/components/sections/commands/AgentSelector';
|
||||
import { ThinkingPill } from '@/components/session/ThinkingPill';
|
||||
@@ -21,6 +23,7 @@ export type TodoSendExecution = {
|
||||
modelID: string;
|
||||
variant: string;
|
||||
agent: string;
|
||||
runAsGoal?: boolean;
|
||||
};
|
||||
|
||||
type TodoSendDialogProps = {
|
||||
@@ -29,6 +32,8 @@ type TodoSendDialogProps = {
|
||||
target: TodoSendTarget;
|
||||
projectDirectory: string | null;
|
||||
submitting?: boolean;
|
||||
/** Offer a "Run as goal" checkbox (hidden in VS Code, where the loop does not run). */
|
||||
allowRunAsGoal?: boolean;
|
||||
onConfirm: (execution: TodoSendExecution) => Promise<void> | void;
|
||||
};
|
||||
|
||||
@@ -46,7 +51,8 @@ const getInitialExecution = (params: {
|
||||
|
||||
export function TodoSendDialog(props: TodoSendDialogProps) {
|
||||
const { t } = useI18n();
|
||||
const { open, onOpenChange, target, projectDirectory, submitting = false, onConfirm } = props;
|
||||
const { open, onOpenChange, target, projectDirectory, submitting = false, allowRunAsGoal = false, onConfirm } = props;
|
||||
const showRunAsGoal = allowRunAsGoal && !isVSCodeRuntime();
|
||||
|
||||
const loadProviders = useConfigStore((state) => state.loadProviders);
|
||||
const loadConfigAgents = useConfigStore((state) => state.loadAgents);
|
||||
@@ -177,7 +183,26 @@ export function TodoSendDialog(props: TodoSendDialogProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className={`flex items-center gap-3 ${showRunAsGoal ? 'justify-between' : 'justify-end'}`}>
|
||||
{showRunAsGoal ? (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Checkbox
|
||||
checked={execution.runAsGoal === true}
|
||||
onChange={(runAsGoal: boolean) => setExecution((prev) => ({ ...prev, runAsGoal }))}
|
||||
disabled={submitting}
|
||||
ariaLabel={t('sessions.scheduledTasks.editor.goal.aria')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="truncate typography-ui-label text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={submitting}
|
||||
onClick={() => setExecution((prev) => ({ ...prev, runAsGoal: prev.runAsGoal !== true }))}
|
||||
>
|
||||
{t('sessions.scheduledTasks.editor.goal.label')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)} disabled={submitting}>
|
||||
{t('rightSidebar.contextNotesTodo.sendDialog.actions.cancel')}
|
||||
</Button>
|
||||
@@ -186,6 +211,7 @@ export function TodoSendDialog(props: TodoSendDialogProps) {
|
||||
? t('rightSidebar.contextNotesTodo.sendDialog.actions.sending')
|
||||
: t('rightSidebar.contextNotesTodo.sendDialog.actions.send')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -34,6 +34,8 @@ import { useSessionUnseenCount } from '@/sync/notification-store';
|
||||
import { useSessionMultiSelectStore } from '@/stores/useSessionMultiSelectStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { useShiftKeyHeld } from '@/hooks/useShiftKeyHeld';
|
||||
import { getSessionGoal } from '@/lib/sessionGoalMetadata';
|
||||
import { sessionGoalStatusColor, sessionGoalStatusLabelKey } from '@/lib/sessionGoalPresentation';
|
||||
import { getRuntimeBearerTokenSync } from '@/lib/runtime-auth';
|
||||
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
|
||||
import { parseMultiRunSessionTitle } from '@/lib/multirun/title';
|
||||
@@ -353,6 +355,16 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
);
|
||||
const sessionStatus = useGlobalSessionStatus(session.id);
|
||||
const sessionPermissions = useSessionPermissions(session.id, sessionDirectory ?? undefined);
|
||||
const sessionGoal = getSessionGoal(resolvedSession);
|
||||
const sessionGoalGlyph = sessionGoal ? (
|
||||
<span
|
||||
className="inline-flex flex-shrink-0 items-center"
|
||||
title={t(sessionGoalStatusLabelKey[sessionGoal.status] as never)}
|
||||
aria-label={t(sessionGoalStatusLabelKey[sessionGoal.status] as never)}
|
||||
>
|
||||
<Icon name="target" className="h-3 w-3" style={{ color: sessionGoalStatusColor[sessionGoal.status] }} />
|
||||
</span>
|
||||
) : null;
|
||||
const isActive = currentSessionId === session.id;
|
||||
const sessionTitle = resolvedSession.title || t('sessions.sidebar.session.untitled');
|
||||
const hasChildren = node.children.length > 0;
|
||||
@@ -1030,15 +1042,21 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
>
|
||||
<div className={cn('flex w-full items-center min-w-0 flex-1 overflow-hidden', isMinimalMode ? 'gap-1' : 'gap-1')}>
|
||||
<div className={cn('block min-w-0 flex-1 truncate typography-ui-label font-normal', isActive ? 'text-primary' : 'text-foreground')}>{renderHighlightedText(sessionTitle, normalizedSessionSearchQuery)}</div>
|
||||
{alwaysShowActions ? <span className="ml-2 flex-shrink-0 text-[0.72rem] text-muted-foreground/75">{sessionCompactUpdatedLabel}</span> : null}
|
||||
{alwaysShowActions ? (
|
||||
<span className="ml-2 inline-flex flex-shrink-0 items-center gap-1 text-[0.72rem] text-muted-foreground/75">
|
||||
{sessionGoalGlyph}
|
||||
{sessionCompactUpdatedLabel}
|
||||
</span>
|
||||
) : null}
|
||||
{!alwaysShowActions ? (
|
||||
<div className="relative ml-1 flex h-4 min-w-4 flex-shrink-0 items-center justify-end">
|
||||
<span className={cn(
|
||||
'whitespace-nowrap text-right text-[0.72rem] text-muted-foreground/75 transition-opacity duration-150',
|
||||
'inline-flex items-center gap-1 whitespace-nowrap text-right text-[0.72rem] text-muted-foreground/75 transition-opacity duration-150',
|
||||
isSessionMenuOpen
|
||||
? 'opacity-0'
|
||||
: hideOnHoverClass,
|
||||
)}>
|
||||
{sessionGoalGlyph}
|
||||
{sessionCompactUpdatedLabel}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1105,6 +1123,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
{!isMinimalMode ? (
|
||||
<div className="flex items-center justify-between gap-3 text-muted-foreground/60 min-w-0 overflow-hidden leading-tight" style={{ fontSize: 'calc(var(--text-ui-label) * 0.85)' }}>
|
||||
<div className={cn('flex min-w-0 items-center gap-1.5 overflow-hidden', metadataSubsessionChevron && hasChildren ? 'pl-4' : '')}>
|
||||
{sessionGoalGlyph}
|
||||
<span className="flex-shrink-0">{sessionUpdatedLabel}</span>
|
||||
{hasSecondaryProjectLabel ? <span className="truncate">{secondaryMeta?.projectLabel}</span> : null}
|
||||
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useSelectionStore } from '@/sync/selection-store';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useSessionGoalArmStore } from '@/stores/useSessionGoalArmStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useGitStore } from '@/stores/useGitStore';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
@@ -594,6 +595,19 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null }) => {
|
||||
}
|
||||
|
||||
setCurrentSession(sessionId, directoryHint);
|
||||
// "Run as goal" rides the same arm mechanism as the composer target
|
||||
// button; set explicitly either way so a stray armed flag cannot
|
||||
// leak into a non-goal plan send. The objective override carries the
|
||||
// actual plan content — "Implement this plan: X" alone would give
|
||||
// the progress audit nothing to judge against.
|
||||
const goalObjective = execution.runAsGoal === true
|
||||
? [
|
||||
`Implement the plan "${sendPromptTitle}" end-to-end${resolvedPath ? ` (plan file: ${resolvedPath})` : ''}.`,
|
||||
'',
|
||||
content,
|
||||
].join('\n')
|
||||
: null;
|
||||
useSessionGoalArmStore.getState().setArmed(execution.runAsGoal === true, goalObjective);
|
||||
await sendMessage(
|
||||
visiblePrompt,
|
||||
execution.providerID,
|
||||
@@ -610,7 +624,7 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null }) => {
|
||||
setIsPlanSendSubmitting(false);
|
||||
}
|
||||
},
|
||||
[canCreateWorktree, createSession, currentProjectRef, initializeNewOpenChamberSession, pendingPlanSend, resolvedPath, routeToChat, sendMessage, sendPromptTitle, setCurrentSession]
|
||||
[canCreateWorktree, content, createSession, currentProjectRef, initializeNewOpenChamberSession, pendingPlanSend, resolvedPath, routeToChat, sendMessage, sendPromptTitle, setCurrentSession]
|
||||
);
|
||||
|
||||
const blockWidgets = React.useMemo(() => {
|
||||
@@ -778,6 +792,7 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null }) => {
|
||||
target={pendingPlanSend?.target ?? 'session'}
|
||||
projectDirectory={currentProjectRef?.path ?? null}
|
||||
submitting={isPlanSendSubmitting}
|
||||
allowRunAsGoal
|
||||
onConfirm={handleConfirmPlanSend}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useSession } from '@/sync/sync-context';
|
||||
import { getSessionGoal, type SessionGoalPayload } from '@/lib/sessionGoalMetadata';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
|
||||
export interface SessionGoalState {
|
||||
/** Parsed goal payload, or null when the session has no goal. */
|
||||
goal: SessionGoalPayload | null;
|
||||
/** The Settings → Chat toggle; when off, goal UI stays hidden. */
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// Live goal state: the payload rides session.updated, so subscribing to the
|
||||
// session record is all the plumbing needed.
|
||||
export function useSessionGoal(sessionId: string, directory?: string): SessionGoalState {
|
||||
const session = useSession(sessionId, directory);
|
||||
const enabled = useUIStore((state) => state.sessionGoalEnabled);
|
||||
return {
|
||||
goal: getSessionGoal(session),
|
||||
enabled,
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,9 @@ type AppearanceSlice = {
|
||||
showReasoningTraces: boolean;
|
||||
sessionRecapEnabled: boolean;
|
||||
sessionSuggestionEnabled: boolean;
|
||||
sessionGoalEnabled: boolean;
|
||||
sessionGoalDefaultBudgetEnabled: boolean;
|
||||
sessionGoalDefaultBudget: number;
|
||||
collapsibleThinkingBlocks: boolean;
|
||||
showDeletionDialog: boolean;
|
||||
nativeNotificationsEnabled: boolean;
|
||||
@@ -55,6 +58,9 @@ export const startAppearanceAutoSave = (): void => {
|
||||
showReasoningTraces: useUIStore.getState().showReasoningTraces,
|
||||
sessionRecapEnabled: useUIStore.getState().sessionRecapEnabled,
|
||||
sessionSuggestionEnabled: useUIStore.getState().sessionSuggestionEnabled,
|
||||
sessionGoalEnabled: useUIStore.getState().sessionGoalEnabled,
|
||||
sessionGoalDefaultBudgetEnabled: useUIStore.getState().sessionGoalDefaultBudgetEnabled,
|
||||
sessionGoalDefaultBudget: useUIStore.getState().sessionGoalDefaultBudget,
|
||||
collapsibleThinkingBlocks: useUIStore.getState().collapsibleThinkingBlocks,
|
||||
showDeletionDialog: useUIStore.getState().showDeletionDialog,
|
||||
nativeNotificationsEnabled: useUIStore.getState().nativeNotificationsEnabled,
|
||||
@@ -109,6 +115,9 @@ export const startAppearanceAutoSave = (): void => {
|
||||
showReasoningTraces: state.showReasoningTraces,
|
||||
sessionRecapEnabled: state.sessionRecapEnabled,
|
||||
sessionSuggestionEnabled: state.sessionSuggestionEnabled,
|
||||
sessionGoalEnabled: state.sessionGoalEnabled,
|
||||
sessionGoalDefaultBudgetEnabled: state.sessionGoalDefaultBudgetEnabled,
|
||||
sessionGoalDefaultBudget: state.sessionGoalDefaultBudget,
|
||||
collapsibleThinkingBlocks: state.collapsibleThinkingBlocks,
|
||||
showDeletionDialog: state.showDeletionDialog,
|
||||
nativeNotificationsEnabled: state.nativeNotificationsEnabled,
|
||||
@@ -149,6 +158,15 @@ export const startAppearanceAutoSave = (): void => {
|
||||
if (current.sessionSuggestionEnabled !== previous.sessionSuggestionEnabled) {
|
||||
diff.sessionSuggestionEnabled = current.sessionSuggestionEnabled;
|
||||
}
|
||||
if (current.sessionGoalEnabled !== previous.sessionGoalEnabled) {
|
||||
diff.sessionGoalEnabled = current.sessionGoalEnabled;
|
||||
}
|
||||
if (current.sessionGoalDefaultBudgetEnabled !== previous.sessionGoalDefaultBudgetEnabled) {
|
||||
diff.sessionGoalDefaultBudgetEnabled = current.sessionGoalDefaultBudgetEnabled;
|
||||
}
|
||||
if (current.sessionGoalDefaultBudget !== previous.sessionGoalDefaultBudget) {
|
||||
diff.sessionGoalDefaultBudget = current.sessionGoalDefaultBudget;
|
||||
}
|
||||
if (current.collapsibleThinkingBlocks !== previous.collapsibleThinkingBlocks) {
|
||||
diff.collapsibleThinkingBlocks = current.collapsibleThinkingBlocks;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,9 @@ export type DesktopSettings = {
|
||||
smallModelUseDefault?: boolean;
|
||||
sessionRecapEnabled?: boolean;
|
||||
sessionSuggestionEnabled?: boolean;
|
||||
sessionGoalEnabled?: boolean;
|
||||
sessionGoalDefaultBudgetEnabled?: boolean;
|
||||
sessionGoalDefaultBudget?: number;
|
||||
smallModelOverride?: string; // format: "provider/model"
|
||||
defaultGitIdentityId?: string; // ''/undefined = unset, 'global' or profile id
|
||||
openInAppId?: string;
|
||||
|
||||
@@ -1760,6 +1760,12 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.sessionRecapAria': 'Generate a recap after the agent finishes',
|
||||
'settings.openchamber.visual.field.sessionSuggestion': 'Generate Next User Message Suggestion',
|
||||
'settings.openchamber.visual.field.sessionSuggestionAria': 'Generate a suggested next user message after the agent finishes',
|
||||
'settings.openchamber.visual.field.sessionGoal': 'Enable Session Goals',
|
||||
'settings.openchamber.visual.field.sessionGoalAria': 'Keep the session working toward a goal automatically',
|
||||
'settings.openchamber.visual.goal.sectionTitle': 'Goal',
|
||||
'settings.openchamber.visual.goal.budgetLabel': 'Default token budget',
|
||||
'settings.openchamber.visual.goal.budgetAria': 'Apply a default token budget to new goals',
|
||||
'settings.openchamber.visual.goal.description': 'Arm the target button in the composer and the next message becomes a goal: the agent keeps working toward it automatically, audited by the small model, even while you are away.',
|
||||
'settings.openchamber.visual.field.showReasoningTracesAria': 'Show reasoning traces',
|
||||
'settings.openchamber.visual.field.showReasoningTraces': 'Show Reasoning Traces',
|
||||
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Enable collapsible reasoning blocks',
|
||||
|
||||
@@ -292,6 +292,10 @@ export const dict = {
|
||||
'sessions.scheduledTasks.editor.prompt.placeholder': 'Summarize open tasks and propose next actions',
|
||||
'sessions.scheduledTasks.editor.enabled.aria': 'Enable task',
|
||||
'sessions.scheduledTasks.editor.enabled.label': 'Enabled',
|
||||
'sessions.scheduledTasks.editor.goal.label': 'Run as goal',
|
||||
'sessions.scheduledTasks.editor.goal.aria': 'Run this task as a goal the agent pursues to completion',
|
||||
'sessions.scheduledTasks.editor.goal.budgetLabel': 'Token budget',
|
||||
'sessions.scheduledTasks.editor.goal.budgetAria': 'Limit the goal to a token budget',
|
||||
'sessions.scheduledTasks.editor.actions.closeAria': 'Close',
|
||||
'sessions.scheduledTasks.editor.actions.cancel': 'Cancel',
|
||||
'sessions.scheduledTasks.editor.actions.save': 'Save',
|
||||
@@ -1411,6 +1415,39 @@ export const dict = {
|
||||
'header.actions.terminalPanelWithShortcut': 'Terminal panel ({shortcut})',
|
||||
'chat.recap.aria': 'Session recap',
|
||||
'chat.recap.label': 'Recap:',
|
||||
'chat.goal.dialog.titleCreate': 'Set Session Goal',
|
||||
'chat.goal.dialog.titleManage': 'Session Goal',
|
||||
'chat.goal.dialog.objectiveLabel': 'Objective',
|
||||
'chat.goal.dialog.objectivePlaceholder': 'Describe the end state the agent should reach and verify…',
|
||||
'chat.goal.dialog.budgetLabel': 'Token budget',
|
||||
'chat.goal.status.active': 'Active',
|
||||
'chat.goal.status.evaluating': 'Evaluating…',
|
||||
'chat.goal.status.paused': 'Paused',
|
||||
'chat.goal.status.blocked': 'Blocked',
|
||||
'chat.goal.status.budgetLimited': 'Budget reached',
|
||||
'chat.goal.status.complete': 'Complete',
|
||||
'chat.goal.usage.tokens': '{used} tokens',
|
||||
'chat.goal.usage.tokensWithBudget': '{used}/{budget} tokens',
|
||||
'chat.goal.usage.turns': '{turns} continuations',
|
||||
'chat.goal.action.pause': 'Pause',
|
||||
'chat.goal.action.resume': 'Resume',
|
||||
'chat.goal.action.markComplete': 'Mark complete',
|
||||
'chat.goal.action.clear': 'Remove goal',
|
||||
'chat.goal.action.cancel': 'Cancel',
|
||||
'chat.goal.action.save': 'Save goal',
|
||||
'chat.goal.action.start': 'Start goal',
|
||||
'chat.goal.toast.actionFailed': 'Goal update failed',
|
||||
'chat.goal.row.aria': 'Session goal — open details',
|
||||
'chat.goal.button.createAria': 'Set a session goal',
|
||||
'chat.goal.button.manageAria': 'Manage session goal',
|
||||
'chat.goal.button.armAria': 'Start a goal with the next message',
|
||||
'chat.goal.counter.aria': 'Goal objective length limit',
|
||||
'chat.goal.button.disarmAria': 'Goal armed — tap to disarm',
|
||||
'chat.goal.button.cancelAria': 'Goal running — tap to cancel it',
|
||||
'chat.goal.cancelDialog.title': 'Cancel this goal?',
|
||||
'chat.goal.cancelDialog.description': 'The agent will stop working toward this goal automatically.',
|
||||
'chat.goal.cancelDialog.keep': 'Keep goal',
|
||||
'chat.goal.cancelDialog.confirm': 'Cancel goal',
|
||||
'chat.suggestion.applyAria': 'Use suggested message',
|
||||
'chat.suggestion.dismissAria': 'Dismiss suggestion',
|
||||
'header.actions.toggleTerminalPanelAria': 'Toggle terminal panel',
|
||||
|
||||
@@ -1727,6 +1727,12 @@ export const settingsDict = {
|
||||
"settings.openchamber.visual.field.sessionRecapAria": "Generar un resumen cuando el agente termina",
|
||||
"settings.openchamber.visual.field.sessionSuggestion": "Generar sugerencia del próximo mensaje del usuario",
|
||||
"settings.openchamber.visual.field.sessionSuggestionAria": "Generar un próximo mensaje sugerido del usuario cuando el agente termina",
|
||||
"settings.openchamber.visual.field.sessionGoal": "Habilitar objetivos de sesión",
|
||||
"settings.openchamber.visual.field.sessionGoalAria": "Mantener la sesión trabajando automáticamente hacia un objetivo",
|
||||
"settings.openchamber.visual.goal.sectionTitle": "Objetivo",
|
||||
"settings.openchamber.visual.goal.budgetLabel": "Presupuesto de tokens predeterminado",
|
||||
"settings.openchamber.visual.goal.budgetAria": "Aplicar un presupuesto de tokens predeterminado a los nuevos objetivos",
|
||||
"settings.openchamber.visual.goal.description": "Activa el botón de diana en el compositor y el próximo mensaje se convierte en un objetivo: el agente sigue trabajando hacia él automáticamente, auditado por el modelo pequeño, incluso mientras no estás.",
|
||||
"settings.openchamber.visual.field.showReasoningTracesAria": "Mostrar rastros de razonamiento",
|
||||
"settings.openchamber.visual.field.showReasoningTraces": "Mostrar trazas de razonamiento",
|
||||
"settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar bloques de razonamiento colapsables",
|
||||
|
||||
@@ -293,6 +293,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.scheduledTasks.editor.prompt.placeholder": "Resumir tareas abiertas y proponer próximas acciones",
|
||||
"sessions.scheduledTasks.editor.enabled.aria": "Habilitar tarea",
|
||||
"sessions.scheduledTasks.editor.enabled.label": "Habilitado",
|
||||
"sessions.scheduledTasks.editor.goal.label": "Ejecutar como objetivo",
|
||||
"sessions.scheduledTasks.editor.goal.aria": "Ejecutar esta tarea como un objetivo que el agente persigue hasta completarlo",
|
||||
"sessions.scheduledTasks.editor.goal.budgetLabel": "Presupuesto de tokens",
|
||||
"sessions.scheduledTasks.editor.goal.budgetAria": "Limitar el objetivo a un presupuesto de tokens",
|
||||
"sessions.scheduledTasks.editor.actions.closeAria": "Cerrar",
|
||||
"sessions.scheduledTasks.editor.actions.cancel": "Cancelar",
|
||||
"sessions.scheduledTasks.editor.actions.save": "Guardar",
|
||||
@@ -1389,6 +1393,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
"header.actions.terminalPanelWithShortcut": "Panel de terminal ({shortcut})",
|
||||
"chat.recap.aria": "Resumen de la sesión",
|
||||
"chat.recap.label": "Resumen:",
|
||||
"chat.goal.dialog.titleCreate": "Definir objetivo de sesión",
|
||||
"chat.goal.dialog.titleManage": "Objetivo de sesión",
|
||||
"chat.goal.dialog.objectiveLabel": "Objetivo",
|
||||
"chat.goal.dialog.objectivePlaceholder": "Describe el estado final que el agente debe alcanzar y verificar…",
|
||||
"chat.goal.dialog.budgetLabel": "Presupuesto de tokens",
|
||||
"chat.goal.status.active": "Activo",
|
||||
"chat.goal.status.evaluating": "Evaluando…",
|
||||
"chat.goal.status.paused": "En pausa",
|
||||
"chat.goal.status.blocked": "Bloqueado",
|
||||
"chat.goal.status.budgetLimited": "Presupuesto alcanzado",
|
||||
"chat.goal.status.complete": "Completado",
|
||||
"chat.goal.usage.tokens": "{used} tokens",
|
||||
"chat.goal.usage.tokensWithBudget": "{used}/{budget} tokens",
|
||||
"chat.goal.usage.turns": "{turns} continuaciones",
|
||||
"chat.goal.action.pause": "Pausar",
|
||||
"chat.goal.action.resume": "Reanudar",
|
||||
"chat.goal.action.markComplete": "Marcar como completado",
|
||||
"chat.goal.action.clear": "Eliminar objetivo",
|
||||
"chat.goal.action.cancel": "Cancelar",
|
||||
"chat.goal.action.save": "Guardar objetivo",
|
||||
"chat.goal.action.start": "Iniciar objetivo",
|
||||
"chat.goal.toast.actionFailed": "No se pudo actualizar el objetivo",
|
||||
"chat.goal.row.aria": "Objetivo de sesión — abrir detalles",
|
||||
"chat.goal.button.createAria": "Definir un objetivo de sesión",
|
||||
"chat.goal.button.manageAria": "Gestionar el objetivo de sesión",
|
||||
"chat.goal.button.armAria": "Iniciar un objetivo con el próximo mensaje",
|
||||
"chat.goal.counter.aria": "Límite de longitud del objetivo",
|
||||
"chat.goal.button.disarmAria": "Objetivo armado — toca para desactivarlo",
|
||||
"chat.goal.button.cancelAria": "Objetivo en curso — toca para cancelarlo",
|
||||
"chat.goal.cancelDialog.title": "¿Cancelar este objetivo?",
|
||||
"chat.goal.cancelDialog.description": "El agente dejará de trabajar automáticamente hacia este objetivo.",
|
||||
"chat.goal.cancelDialog.keep": "Mantener objetivo",
|
||||
"chat.goal.cancelDialog.confirm": "Cancelar objetivo",
|
||||
"chat.suggestion.applyAria": "Usar mensaje sugerido",
|
||||
"chat.suggestion.dismissAria": "Descartar sugerencia",
|
||||
"header.actions.toggleTerminalPanelAria": "Mostrar u ocultar panel de terminal",
|
||||
|
||||
@@ -1639,6 +1639,12 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.sessionRecapAria': "Générer un récapitulatif quand l'agent termine",
|
||||
'settings.openchamber.visual.field.sessionSuggestion': 'Générer une suggestion de prochain message utilisateur',
|
||||
'settings.openchamber.visual.field.sessionSuggestionAria': "Générer un prochain message utilisateur suggéré quand l'agent termine",
|
||||
'settings.openchamber.visual.field.sessionGoal': 'Activer les objectifs de session',
|
||||
'settings.openchamber.visual.field.sessionGoalAria': "Faire travailler la session automatiquement vers un objectif",
|
||||
'settings.openchamber.visual.goal.sectionTitle': 'Objectif',
|
||||
'settings.openchamber.visual.goal.budgetLabel': 'Budget de tokens par défaut',
|
||||
'settings.openchamber.visual.goal.budgetAria': 'Appliquer un budget de tokens par défaut aux nouveaux objectifs',
|
||||
'settings.openchamber.visual.goal.description': 'Armez le bouton cible du composeur et le prochain message devient un objectif : l\'agent continue d\'y travailler automatiquement, audité par le petit modèle, même en votre absence.',
|
||||
'settings.openchamber.visual.field.showReasoningTracesAria': 'Afficher les traces de raisonnement',
|
||||
'settings.openchamber.visual.field.showReasoningTraces': 'Afficher les traces de raisonnement',
|
||||
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Activer les blocs de raisonnement pliables',
|
||||
|
||||
@@ -137,6 +137,10 @@ export const dict = {
|
||||
'sessions.scheduledTasks.editor.prompt.placeholder': 'Résumer les tâches ouvertes et proposer les prochaines actions',
|
||||
'sessions.scheduledTasks.editor.enabled.aria': 'Activer la tâche',
|
||||
'sessions.scheduledTasks.editor.enabled.label': 'Activé',
|
||||
'sessions.scheduledTasks.editor.goal.label': 'Exécuter comme objectif',
|
||||
'sessions.scheduledTasks.editor.goal.aria': 'Exécuter cette tâche comme un objectif que l\'agent poursuit jusqu\'au bout',
|
||||
'sessions.scheduledTasks.editor.goal.budgetLabel': 'Budget de tokens',
|
||||
'sessions.scheduledTasks.editor.goal.budgetAria': 'Limiter l’objectif à un budget de tokens',
|
||||
'sessions.scheduledTasks.editor.actions.closeAria': 'Fermer',
|
||||
'sessions.scheduledTasks.editor.actions.cancel': 'Annuler',
|
||||
'sessions.scheduledTasks.editor.actions.save': 'Sauvegarder',
|
||||
@@ -1232,6 +1236,39 @@ export const dict = {
|
||||
'header.actions.terminalPanelWithShortcut': 'Panneau à bornes ({shortcut})',
|
||||
'chat.recap.aria': 'Récapitulatif de la session',
|
||||
'chat.recap.label': 'Récap :',
|
||||
'chat.goal.dialog.titleCreate': 'Définir un objectif de session',
|
||||
'chat.goal.dialog.titleManage': 'Objectif de session',
|
||||
'chat.goal.dialog.objectiveLabel': 'Objectif',
|
||||
'chat.goal.dialog.objectivePlaceholder': 'Décrivez l\'état final que l\'agent doit atteindre et vérifier…',
|
||||
'chat.goal.dialog.budgetLabel': 'Budget de tokens',
|
||||
'chat.goal.status.active': 'Actif',
|
||||
'chat.goal.status.evaluating': 'Évaluation…',
|
||||
'chat.goal.status.paused': 'En pause',
|
||||
'chat.goal.status.blocked': 'Bloqué',
|
||||
'chat.goal.status.budgetLimited': 'Budget atteint',
|
||||
'chat.goal.status.complete': 'Terminé',
|
||||
'chat.goal.usage.tokens': '{used} tokens',
|
||||
'chat.goal.usage.tokensWithBudget': '{used}/{budget} tokens',
|
||||
'chat.goal.usage.turns': '{turns} continuations',
|
||||
'chat.goal.action.pause': 'Mettre en pause',
|
||||
'chat.goal.action.resume': 'Reprendre',
|
||||
'chat.goal.action.markComplete': 'Marquer comme terminé',
|
||||
'chat.goal.action.clear': 'Supprimer l\'objectif',
|
||||
'chat.goal.action.cancel': 'Annuler',
|
||||
'chat.goal.action.save': 'Enregistrer l\'objectif',
|
||||
'chat.goal.action.start': 'Lancer l\'objectif',
|
||||
'chat.goal.toast.actionFailed': 'Échec de la mise à jour de l\'objectif',
|
||||
'chat.goal.row.aria': 'Objectif de session — ouvrir les détails',
|
||||
'chat.goal.button.createAria': 'Définir un objectif de session',
|
||||
'chat.goal.button.manageAria': 'Gérer l\'objectif de session',
|
||||
'chat.goal.button.armAria': 'Démarrer un objectif avec le prochain message',
|
||||
"chat.goal.counter.aria": "Limite de longueur de l'objectif",
|
||||
'chat.goal.button.disarmAria': 'Objectif armé — touchez pour désarmer',
|
||||
'chat.goal.button.cancelAria': 'Objectif en cours — touchez pour l’annuler',
|
||||
'chat.goal.cancelDialog.title': 'Annuler cet objectif ?',
|
||||
'chat.goal.cancelDialog.description': 'L\'agent cessera de travailler automatiquement vers cet objectif.',
|
||||
'chat.goal.cancelDialog.keep': 'Garder l\'objectif',
|
||||
'chat.goal.cancelDialog.confirm': 'Annuler l\'objectif',
|
||||
'chat.suggestion.applyAria': 'Utiliser le message suggéré',
|
||||
'chat.suggestion.dismissAria': 'Ignorer la suggestion',
|
||||
'header.actions.toggleTerminalPanelAria': 'Basculer le panneau à bornes',
|
||||
|
||||
@@ -1760,6 +1760,12 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.sessionRecapAria': 'エージェントの完了後に要約を生成します',
|
||||
'settings.openchamber.visual.field.sessionSuggestion': '次のユーザーメッセージの提案を生成',
|
||||
'settings.openchamber.visual.field.sessionSuggestionAria': 'エージェントの完了後に次のユーザーメッセージの提案を生成します',
|
||||
'settings.openchamber.visual.field.sessionGoal': 'セッションゴールを有効化',
|
||||
'settings.openchamber.visual.field.sessionGoalAria': 'セッションが自動的にゴールに向かって作業を続けます',
|
||||
'settings.openchamber.visual.goal.sectionTitle': 'ゴール',
|
||||
'settings.openchamber.visual.goal.budgetLabel': 'デフォルトのトークン予算',
|
||||
'settings.openchamber.visual.goal.budgetAria': '新しいゴールにデフォルトのトークン予算を適用',
|
||||
'settings.openchamber.visual.goal.description': 'コンポーザーのターゲットボタンを有効にすると、次のメッセージがゴールになります。エージェントは小型モデルの監査のもと、離席中でも自動的に作業を続けます。',
|
||||
'settings.openchamber.visual.field.showReasoningTracesAria': '推論トレースを表示',
|
||||
'settings.openchamber.visual.field.showReasoningTraces': '推論トレースを表示',
|
||||
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '折りたたみ可能な推論ブロックを有効化',
|
||||
|
||||
@@ -293,6 +293,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.scheduledTasks.editor.prompt.placeholder': '未完了のタスクを要約し次のアクションを提案',
|
||||
'sessions.scheduledTasks.editor.enabled.aria': 'タスクを有効にする',
|
||||
'sessions.scheduledTasks.editor.enabled.label': '有効',
|
||||
'sessions.scheduledTasks.editor.goal.label': 'ゴールとして実行',
|
||||
'sessions.scheduledTasks.editor.goal.aria': 'このタスクをエージェントが完了まで追求するゴールとして実行します',
|
||||
'sessions.scheduledTasks.editor.goal.budgetLabel': 'トークン予算',
|
||||
'sessions.scheduledTasks.editor.goal.budgetAria': 'ゴールをトークン予算で制限します',
|
||||
'sessions.scheduledTasks.editor.actions.closeAria': '閉じる',
|
||||
'sessions.scheduledTasks.editor.actions.cancel': 'キャンセル',
|
||||
'sessions.scheduledTasks.editor.actions.save': '保存',
|
||||
@@ -1407,6 +1411,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.terminalPanelWithShortcut': 'ターミナルパネル({shortcut})',
|
||||
'chat.recap.aria': 'セッションの要約',
|
||||
'chat.recap.label': '要約:',
|
||||
'chat.goal.dialog.titleCreate': 'セッションゴールを設定',
|
||||
'chat.goal.dialog.titleManage': 'セッションゴール',
|
||||
'chat.goal.dialog.objectiveLabel': '目標',
|
||||
'chat.goal.dialog.objectivePlaceholder': 'エージェントが到達して検証すべき最終状態を記述してください…',
|
||||
'chat.goal.dialog.budgetLabel': 'トークン予算',
|
||||
'chat.goal.status.active': '進行中',
|
||||
'chat.goal.status.evaluating': '評価中…',
|
||||
'chat.goal.status.paused': '一時停止',
|
||||
'chat.goal.status.blocked': 'ブロック',
|
||||
'chat.goal.status.budgetLimited': '予算上限に到達',
|
||||
'chat.goal.status.complete': '完了',
|
||||
'chat.goal.usage.tokens': '{used} トークン',
|
||||
'chat.goal.usage.tokensWithBudget': '{used}/{budget} トークン',
|
||||
'chat.goal.usage.turns': '継続 {turns} 回',
|
||||
'chat.goal.action.pause': '一時停止',
|
||||
'chat.goal.action.resume': '再開',
|
||||
'chat.goal.action.markComplete': '完了にする',
|
||||
'chat.goal.action.clear': 'ゴールを削除',
|
||||
'chat.goal.action.cancel': 'キャンセル',
|
||||
'chat.goal.action.save': 'ゴールを保存',
|
||||
'chat.goal.action.start': 'ゴールを開始',
|
||||
'chat.goal.toast.actionFailed': 'ゴールの更新に失敗しました',
|
||||
'chat.goal.row.aria': 'セッションゴール — 詳細を開く',
|
||||
'chat.goal.button.createAria': 'セッションゴールを設定',
|
||||
'chat.goal.button.manageAria': 'セッションゴールを管理',
|
||||
'chat.goal.button.armAria': '次のメッセージでゴールを開始',
|
||||
'chat.goal.counter.aria': 'ゴール目標の文字数上限',
|
||||
'chat.goal.button.disarmAria': 'ゴール待機中 — タップで解除',
|
||||
'chat.goal.button.cancelAria': 'ゴール実行中 — タップでキャンセル',
|
||||
'chat.goal.cancelDialog.title': 'このゴールをキャンセルしますか?',
|
||||
'chat.goal.cancelDialog.description': 'エージェントはこのゴールへの自動作業を停止します。',
|
||||
'chat.goal.cancelDialog.keep': 'ゴールを維持',
|
||||
'chat.goal.cancelDialog.confirm': 'ゴールをキャンセル',
|
||||
'chat.suggestion.applyAria': '提案されたメッセージを使用',
|
||||
'chat.suggestion.dismissAria': '提案を閉じる',
|
||||
'header.actions.toggleTerminalPanelAria': 'ターミナルパネルの切り替え',
|
||||
|
||||
@@ -1727,6 +1727,12 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.sessionRecapAria': '에이전트가 완료되면 요약을 생성합니다',
|
||||
'settings.openchamber.visual.field.sessionSuggestion': '다음 사용자 메시지 제안 생성',
|
||||
'settings.openchamber.visual.field.sessionSuggestionAria': '에이전트가 완료되면 다음 사용자 메시지 제안을 생성합니다',
|
||||
'settings.openchamber.visual.field.sessionGoal': '세션 목표 사용',
|
||||
'settings.openchamber.visual.field.sessionGoalAria': '세션이 목표를 향해 자동으로 계속 작업하도록 합니다',
|
||||
'settings.openchamber.visual.goal.sectionTitle': '목표',
|
||||
'settings.openchamber.visual.goal.budgetLabel': '기본 토큰 예산',
|
||||
'settings.openchamber.visual.goal.budgetAria': '새 목표에 기본 토큰 예산 적용',
|
||||
'settings.openchamber.visual.goal.description': '컴포저의 타깃 버튼을 켜면 다음 메시지가 목표가 됩니다. 에이전트는 소형 모델의 감사를 받으며 자리를 비운 동안에도 자동으로 계속 작업합니다.',
|
||||
'settings.openchamber.visual.field.showReasoningTracesAria': 'Reasoning trace 표시',
|
||||
'settings.openchamber.visual.field.showReasoningTraces': 'Reasoning Trace 표시',
|
||||
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '접을 수 있는 추론 블록 활성화',
|
||||
|
||||
@@ -293,6 +293,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.scheduledTasks.editor.prompt.placeholder': '열린 작업을 요약하고 다음 액션을 제안하세요',
|
||||
'sessions.scheduledTasks.editor.enabled.aria': '작업 활성화',
|
||||
'sessions.scheduledTasks.editor.enabled.label': '활성화됨',
|
||||
'sessions.scheduledTasks.editor.goal.label': '목표로 실행',
|
||||
'sessions.scheduledTasks.editor.goal.aria': '이 작업을 에이전트가 완료까지 추진하는 목표로 실행합니다',
|
||||
'sessions.scheduledTasks.editor.goal.budgetLabel': '토큰 예산',
|
||||
'sessions.scheduledTasks.editor.goal.budgetAria': '목표를 토큰 예산으로 제한합니다',
|
||||
'sessions.scheduledTasks.editor.actions.closeAria': '닫기',
|
||||
'sessions.scheduledTasks.editor.actions.cancel': '취소',
|
||||
'sessions.scheduledTasks.editor.actions.save': '저장',
|
||||
@@ -1413,6 +1417,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.terminalPanelWithShortcut': '터미널 패널 ({shortcut})',
|
||||
'chat.recap.aria': '세션 요약',
|
||||
'chat.recap.label': '요약:',
|
||||
'chat.goal.dialog.titleCreate': '세션 목표 설정',
|
||||
'chat.goal.dialog.titleManage': '세션 목표',
|
||||
'chat.goal.dialog.objectiveLabel': '목표',
|
||||
'chat.goal.dialog.objectivePlaceholder': '에이전트가 도달하고 검증해야 할 최종 상태를 설명하세요…',
|
||||
'chat.goal.dialog.budgetLabel': '토큰 예산',
|
||||
'chat.goal.status.active': '진행 중',
|
||||
'chat.goal.status.evaluating': '평가 중…',
|
||||
'chat.goal.status.paused': '일시 중지됨',
|
||||
'chat.goal.status.blocked': '차단됨',
|
||||
'chat.goal.status.budgetLimited': '예산 도달',
|
||||
'chat.goal.status.complete': '완료됨',
|
||||
'chat.goal.usage.tokens': '{used} 토큰',
|
||||
'chat.goal.usage.tokensWithBudget': '{used}/{budget} 토큰',
|
||||
'chat.goal.usage.turns': '{turns}회 계속',
|
||||
'chat.goal.action.pause': '일시 중지',
|
||||
'chat.goal.action.resume': '재개',
|
||||
'chat.goal.action.markComplete': '완료로 표시',
|
||||
'chat.goal.action.clear': '목표 제거',
|
||||
'chat.goal.action.cancel': '취소',
|
||||
'chat.goal.action.save': '목표 저장',
|
||||
'chat.goal.action.start': '목표 시작',
|
||||
'chat.goal.toast.actionFailed': '목표 업데이트에 실패했습니다',
|
||||
'chat.goal.row.aria': '세션 목표 — 세부 정보 열기',
|
||||
'chat.goal.button.createAria': '세션 목표 설정',
|
||||
'chat.goal.button.manageAria': '세션 목표 관리',
|
||||
'chat.goal.button.armAria': '다음 메시지로 목표 시작',
|
||||
'chat.goal.counter.aria': '목표 길이 제한',
|
||||
'chat.goal.button.disarmAria': '목표 대기 중 — 탭하여 해제',
|
||||
'chat.goal.button.cancelAria': '목표 진행 중 — 탭하여 취소',
|
||||
'chat.goal.cancelDialog.title': '이 목표를 취소할까요?',
|
||||
'chat.goal.cancelDialog.description': '에이전트가 이 목표를 향한 자동 작업을 중단합니다.',
|
||||
'chat.goal.cancelDialog.keep': '목표 유지',
|
||||
'chat.goal.cancelDialog.confirm': '목표 취소',
|
||||
'chat.suggestion.applyAria': '제안된 메시지 사용',
|
||||
'chat.suggestion.dismissAria': '제안 닫기',
|
||||
'header.actions.toggleTerminalPanelAria': '토글 터미널 패널',
|
||||
|
||||
@@ -984,6 +984,12 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.sessionRecapAria': 'Generuj podsumowanie po zakończeniu pracy agenta',
|
||||
'settings.openchamber.visual.field.sessionSuggestion': 'Generuj sugestię następnej wiadomości użytkownika',
|
||||
'settings.openchamber.visual.field.sessionSuggestionAria': 'Generuj sugerowaną następną wiadomość użytkownika po zakończeniu pracy agenta',
|
||||
'settings.openchamber.visual.field.sessionGoal': 'Włącz cele sesji',
|
||||
'settings.openchamber.visual.field.sessionGoalAria': 'Utrzymuj automatyczną pracę sesji w kierunku celu',
|
||||
'settings.openchamber.visual.goal.sectionTitle': 'Cel',
|
||||
'settings.openchamber.visual.goal.budgetLabel': 'Domyślny budżet tokenów',
|
||||
'settings.openchamber.visual.goal.budgetAria': 'Stosuj domyślny budżet tokenów do nowych celów',
|
||||
'settings.openchamber.visual.goal.description': 'Uzbrój przycisk celu w kompozytorze, a następna wiadomość stanie się celem: agent będzie nad nim automatycznie pracować, audytowany przez mały model, nawet pod twoją nieobecność.',
|
||||
'settings.openchamber.visual.field.showReasoningTraces': 'Pokaż ślady rozumowania',
|
||||
'settings.openchamber.visual.field.showReasoningTracesAria': 'Pokaż ślady rozumowania',
|
||||
'settings.openchamber.visual.field.collapsibleThinkingBlocks': 'Włącz zwijalne bloki rozumowania',
|
||||
|
||||
@@ -381,6 +381,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.scheduledTasks.editor.prompt.placeholder': 'Podsumuj otwarte zadania i zaproponuj następne działania',
|
||||
'sessions.scheduledTasks.editor.enabled.aria': 'Włącz zadanie',
|
||||
'sessions.scheduledTasks.editor.enabled.label': 'Włączone',
|
||||
'sessions.scheduledTasks.editor.goal.label': 'Uruchom jako cel',
|
||||
'sessions.scheduledTasks.editor.goal.aria': 'Uruchom to zadanie jako cel, do którego agent dąży aż do ukończenia',
|
||||
'sessions.scheduledTasks.editor.goal.budgetLabel': 'Budżet tokenów',
|
||||
'sessions.scheduledTasks.editor.goal.budgetAria': 'Ogranicz cel budżetem tokenów',
|
||||
'sessions.scheduledTasks.editor.actions.closeAria': 'Zamknij',
|
||||
'sessions.scheduledTasks.editor.actions.cancel': 'Anuluj',
|
||||
'sessions.scheduledTasks.editor.actions.save': 'Zapisz',
|
||||
@@ -2093,6 +2097,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.terminalPanelWithShortcut': 'Panel terminala ({shortcut})',
|
||||
'chat.recap.aria': 'Podsumowanie sesji',
|
||||
'chat.recap.label': 'Podsumowanie:',
|
||||
'chat.goal.dialog.titleCreate': 'Ustaw cel sesji',
|
||||
'chat.goal.dialog.titleManage': 'Cel sesji',
|
||||
'chat.goal.dialog.objectiveLabel': 'Cel',
|
||||
'chat.goal.dialog.objectivePlaceholder': 'Opisz stan końcowy, który agent ma osiągnąć i zweryfikować…',
|
||||
'chat.goal.dialog.budgetLabel': 'Budżet tokenów',
|
||||
'chat.goal.status.active': 'Aktywny',
|
||||
'chat.goal.status.evaluating': 'Ocenianie…',
|
||||
'chat.goal.status.paused': 'Wstrzymany',
|
||||
'chat.goal.status.blocked': 'Zablokowany',
|
||||
'chat.goal.status.budgetLimited': 'Budżet wyczerpany',
|
||||
'chat.goal.status.complete': 'Ukończony',
|
||||
'chat.goal.usage.tokens': '{used} tokenów',
|
||||
'chat.goal.usage.tokensWithBudget': '{used}/{budget} tokenów',
|
||||
'chat.goal.usage.turns': 'kontynuacje: {turns}',
|
||||
'chat.goal.action.pause': 'Wstrzymaj',
|
||||
'chat.goal.action.resume': 'Wznów',
|
||||
'chat.goal.action.markComplete': 'Oznacz jako ukończony',
|
||||
'chat.goal.action.clear': 'Usuń cel',
|
||||
'chat.goal.action.cancel': 'Anuluj',
|
||||
'chat.goal.action.save': 'Zapisz cel',
|
||||
'chat.goal.action.start': 'Rozpocznij cel',
|
||||
'chat.goal.toast.actionFailed': 'Nie udało się zaktualizować celu',
|
||||
'chat.goal.row.aria': 'Cel sesji — otwórz szczegóły',
|
||||
'chat.goal.button.createAria': 'Ustaw cel sesji',
|
||||
'chat.goal.button.manageAria': 'Zarządzaj celem sesji',
|
||||
'chat.goal.button.armAria': 'Rozpocznij cel następną wiadomością',
|
||||
'chat.goal.counter.aria': 'Limit długości celu',
|
||||
'chat.goal.button.disarmAria': 'Cel uzbrojony — dotknij, aby rozbroić',
|
||||
'chat.goal.button.cancelAria': 'Cel w toku — dotknij, aby anulować',
|
||||
'chat.goal.cancelDialog.title': 'Anulować ten cel?',
|
||||
'chat.goal.cancelDialog.description': 'Agent przestanie automatycznie pracować nad tym celem.',
|
||||
'chat.goal.cancelDialog.keep': 'Zachowaj cel',
|
||||
'chat.goal.cancelDialog.confirm': 'Anuluj cel',
|
||||
'chat.suggestion.applyAria': 'Użyj sugerowanej wiadomości',
|
||||
'chat.suggestion.dismissAria': 'Odrzuć sugestię',
|
||||
'header.actions.toggleRightSidebarAria': 'Przełącz prawy panel boczny',
|
||||
|
||||
@@ -1727,6 +1727,12 @@ export const settingsDict = {
|
||||
"settings.openchamber.visual.field.sessionRecapAria": "Gerar um resumo quando o agente termina",
|
||||
"settings.openchamber.visual.field.sessionSuggestion": "Gerar sugestão da próxima mensagem do usuário",
|
||||
"settings.openchamber.visual.field.sessionSuggestionAria": "Gerar uma próxima mensagem sugerida do usuário quando o agente termina",
|
||||
"settings.openchamber.visual.field.sessionGoal": "Habilitar objetivos de sessão",
|
||||
"settings.openchamber.visual.field.sessionGoalAria": "Manter a sessão trabalhando automaticamente em direção a um objetivo",
|
||||
"settings.openchamber.visual.goal.sectionTitle": "Objetivo",
|
||||
"settings.openchamber.visual.goal.budgetLabel": "Orçamento de tokens padrão",
|
||||
"settings.openchamber.visual.goal.budgetAria": "Aplicar um orçamento de tokens padrão a novos objetivos",
|
||||
"settings.openchamber.visual.goal.description": "Arme o botão de alvo no compositor e a próxima mensagem vira um objetivo: o agente continua trabalhando nele automaticamente, auditado pelo modelo pequeno, mesmo enquanto você está ausente.",
|
||||
"settings.openchamber.visual.field.showReasoningTracesAria": "Mostrar rastros de raciocínio",
|
||||
"settings.openchamber.visual.field.showReasoningTraces": "Mostrar rastros de raciocínio",
|
||||
"settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar blocos de raciocínio recolhíveis",
|
||||
|
||||
@@ -293,6 +293,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.scheduledTasks.editor.prompt.placeholder": "Resumir tarefas abertas e propor próximas ações",
|
||||
"sessions.scheduledTasks.editor.enabled.aria": "Ativar tarefa",
|
||||
"sessions.scheduledTasks.editor.enabled.label": "Ativado",
|
||||
"sessions.scheduledTasks.editor.goal.label": "Executar como objetivo",
|
||||
"sessions.scheduledTasks.editor.goal.aria": "Executar esta tarefa como um objetivo que o agente persegue até concluir",
|
||||
"sessions.scheduledTasks.editor.goal.budgetLabel": "Orçamento de tokens",
|
||||
"sessions.scheduledTasks.editor.goal.budgetAria": "Limitar o objetivo a um orçamento de tokens",
|
||||
"sessions.scheduledTasks.editor.actions.closeAria": "Fechar",
|
||||
"sessions.scheduledTasks.editor.actions.cancel": "Cancelar",
|
||||
"sessions.scheduledTasks.editor.actions.save": "Salvar",
|
||||
@@ -1389,6 +1393,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
"header.actions.terminalPanelWithShortcut": "Painel de terminal ({shortcut})",
|
||||
"chat.recap.aria": "Resumo da sessão",
|
||||
"chat.recap.label": "Resumo:",
|
||||
"chat.goal.dialog.titleCreate": "Definir objetivo da sessão",
|
||||
"chat.goal.dialog.titleManage": "Objetivo da sessão",
|
||||
"chat.goal.dialog.objectiveLabel": "Objetivo",
|
||||
"chat.goal.dialog.objectivePlaceholder": "Descreva o estado final que o agente deve alcançar e verificar…",
|
||||
"chat.goal.dialog.budgetLabel": "Orçamento de tokens",
|
||||
"chat.goal.status.active": "Ativo",
|
||||
"chat.goal.status.evaluating": "Avaliando…",
|
||||
"chat.goal.status.paused": "Pausado",
|
||||
"chat.goal.status.blocked": "Bloqueado",
|
||||
"chat.goal.status.budgetLimited": "Orçamento atingido",
|
||||
"chat.goal.status.complete": "Concluído",
|
||||
"chat.goal.usage.tokens": "{used} tokens",
|
||||
"chat.goal.usage.tokensWithBudget": "{used}/{budget} tokens",
|
||||
"chat.goal.usage.turns": "{turns} continuações",
|
||||
"chat.goal.action.pause": "Pausar",
|
||||
"chat.goal.action.resume": "Retomar",
|
||||
"chat.goal.action.markComplete": "Marcar como concluído",
|
||||
"chat.goal.action.clear": "Remover objetivo",
|
||||
"chat.goal.action.cancel": "Cancelar",
|
||||
"chat.goal.action.save": "Salvar objetivo",
|
||||
"chat.goal.action.start": "Iniciar objetivo",
|
||||
"chat.goal.toast.actionFailed": "Falha ao atualizar o objetivo",
|
||||
"chat.goal.row.aria": "Objetivo da sessão — abrir detalhes",
|
||||
"chat.goal.button.createAria": "Definir um objetivo da sessão",
|
||||
"chat.goal.button.manageAria": "Gerenciar objetivo da sessão",
|
||||
"chat.goal.button.armAria": "Iniciar um objetivo com a próxima mensagem",
|
||||
"chat.goal.counter.aria": "Limite de comprimento do objetivo",
|
||||
"chat.goal.button.disarmAria": "Objetivo armado — toque para desarmar",
|
||||
"chat.goal.button.cancelAria": "Objetivo em andamento — toque para cancelar",
|
||||
"chat.goal.cancelDialog.title": "Cancelar este objetivo?",
|
||||
"chat.goal.cancelDialog.description": "O agente deixará de trabalhar automaticamente neste objetivo.",
|
||||
"chat.goal.cancelDialog.keep": "Manter objetivo",
|
||||
"chat.goal.cancelDialog.confirm": "Cancelar objetivo",
|
||||
"chat.suggestion.applyAria": "Usar mensagem sugerida",
|
||||
"chat.suggestion.dismissAria": "Dispensar sugestão",
|
||||
"header.actions.toggleTerminalPanelAria": "Mostrar ou ocultar painel de terminal",
|
||||
|
||||
@@ -1727,6 +1727,12 @@ export const settingsDict = {
|
||||
"settings.openchamber.visual.field.sessionRecapAria": "Генерувати підсумок після завершення роботи агента",
|
||||
"settings.openchamber.visual.field.sessionSuggestion": "Генерувати пропозицію наступного повідомлення користувача",
|
||||
"settings.openchamber.visual.field.sessionSuggestionAria": "Генерувати запропоноване наступне повідомлення користувача після завершення роботи агента",
|
||||
"settings.openchamber.visual.field.sessionGoal": "Увімкнути цілі сесії",
|
||||
"settings.openchamber.visual.field.sessionGoalAria": "Автоматично продовжувати роботу сесії до досягнення цілі",
|
||||
"settings.openchamber.visual.goal.sectionTitle": "Ціль",
|
||||
"settings.openchamber.visual.goal.budgetLabel": "Типовий бюджет токенів",
|
||||
"settings.openchamber.visual.goal.budgetAria": "Застосовувати типовий бюджет токенів до нових цілей",
|
||||
"settings.openchamber.visual.goal.description": "Увімкніть кнопку-мішень у полі вводу — і наступне повідомлення стане ціллю: агент автоматично працюватиме над нею під наглядом малої моделі, навіть поки вас немає поруч.",
|
||||
"settings.openchamber.visual.field.showReasoningTracesAria": "Показати сліди міркувань",
|
||||
"settings.openchamber.visual.field.showReasoningTraces": "Показати сліди міркувань",
|
||||
"settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Увімкнути згортальні блоки міркувань",
|
||||
|
||||
@@ -293,6 +293,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.scheduledTasks.editor.prompt.placeholder": "Підсумуйте відкриті завдання й запропонуйте наступні дії",
|
||||
"sessions.scheduledTasks.editor.enabled.aria": "Увімкнути завдання",
|
||||
"sessions.scheduledTasks.editor.enabled.label": "Увімкнено",
|
||||
"sessions.scheduledTasks.editor.goal.label": "Виконати як ціль",
|
||||
"sessions.scheduledTasks.editor.goal.aria": "Виконати це завдання як ціль, яку агент веде до завершення",
|
||||
"sessions.scheduledTasks.editor.goal.budgetLabel": "Бюджет токенів",
|
||||
"sessions.scheduledTasks.editor.goal.budgetAria": "Обмежити ціль бюджетом токенів",
|
||||
"sessions.scheduledTasks.editor.actions.closeAria": "Закрити",
|
||||
"sessions.scheduledTasks.editor.actions.cancel": "Скасувати",
|
||||
"sessions.scheduledTasks.editor.actions.save": "Зберегти",
|
||||
@@ -1389,6 +1393,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
"header.actions.terminalPanelWithShortcut": "Термінальна панель ({shortcut})",
|
||||
"chat.recap.aria": "Підсумок сесії",
|
||||
"chat.recap.label": "Підсумок:",
|
||||
"chat.goal.dialog.titleCreate": "Встановити ціль сесії",
|
||||
"chat.goal.dialog.titleManage": "Ціль сесії",
|
||||
"chat.goal.dialog.objectiveLabel": "Ціль",
|
||||
"chat.goal.dialog.objectivePlaceholder": "Опишіть кінцевий стан, якого агент має досягти та перевірити…",
|
||||
"chat.goal.dialog.budgetLabel": "Бюджет токенів",
|
||||
"chat.goal.status.active": "Активна",
|
||||
"chat.goal.status.evaluating": "Оцінювання…",
|
||||
"chat.goal.status.paused": "Призупинена",
|
||||
"chat.goal.status.blocked": "Заблокована",
|
||||
"chat.goal.status.budgetLimited": "Бюджет вичерпано",
|
||||
"chat.goal.status.complete": "Завершена",
|
||||
"chat.goal.usage.tokens": "{used} токенів",
|
||||
"chat.goal.usage.tokensWithBudget": "{used}/{budget} токенів",
|
||||
"chat.goal.usage.turns": "продовжень: {turns}",
|
||||
"chat.goal.action.pause": "Призупинити",
|
||||
"chat.goal.action.resume": "Відновити",
|
||||
"chat.goal.action.markComplete": "Позначити завершеною",
|
||||
"chat.goal.action.clear": "Видалити ціль",
|
||||
"chat.goal.action.cancel": "Скасувати",
|
||||
"chat.goal.action.save": "Зберегти ціль",
|
||||
"chat.goal.action.start": "Розпочати ціль",
|
||||
"chat.goal.toast.actionFailed": "Не вдалося оновити ціль",
|
||||
"chat.goal.row.aria": "Ціль сесії — відкрити деталі",
|
||||
"chat.goal.button.createAria": "Встановити ціль сесії",
|
||||
"chat.goal.button.manageAria": "Керувати ціллю сесії",
|
||||
"chat.goal.button.armAria": "Розпочати ціль наступним повідомленням",
|
||||
"chat.goal.counter.aria": "Ліміт довжини цілі",
|
||||
"chat.goal.button.disarmAria": "Ціль увімкнена — торкніться, щоб вимкнути",
|
||||
"chat.goal.button.cancelAria": "Ціль виконується — торкніться, щоб скасувати",
|
||||
"chat.goal.cancelDialog.title": "Скасувати цю ціль?",
|
||||
"chat.goal.cancelDialog.description": "Агент припинить автоматично працювати над цією ціллю.",
|
||||
"chat.goal.cancelDialog.keep": "Залишити ціль",
|
||||
"chat.goal.cancelDialog.confirm": "Скасувати ціль",
|
||||
"chat.suggestion.applyAria": "Використати запропоноване повідомлення",
|
||||
"chat.suggestion.dismissAria": "Прибрати пропозицію",
|
||||
"header.actions.toggleTerminalPanelAria": "Перемкнути панель терміналу",
|
||||
|
||||
@@ -1727,6 +1727,12 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.sessionRecapAria': '代理完成后生成回顾',
|
||||
'settings.openchamber.visual.field.sessionSuggestion': '生成下一条用户消息建议',
|
||||
'settings.openchamber.visual.field.sessionSuggestionAria': '代理完成后生成下一条用户消息建议',
|
||||
'settings.openchamber.visual.field.sessionGoal': '启用会话目标',
|
||||
'settings.openchamber.visual.field.sessionGoalAria': '让会话自动朝着目标持续工作',
|
||||
'settings.openchamber.visual.goal.sectionTitle': '目标',
|
||||
'settings.openchamber.visual.goal.budgetLabel': '默认令牌预算',
|
||||
'settings.openchamber.visual.goal.budgetAria': '为新目标应用默认令牌预算',
|
||||
'settings.openchamber.visual.goal.description': '在输入框中启用靶心按钮,下一条消息即成为目标:代理将在小模型的审核下自动持续工作,即使你不在电脑前。',
|
||||
'settings.openchamber.visual.field.showReasoningTracesAria': '显示推理轨迹',
|
||||
'settings.openchamber.visual.field.showReasoningTraces': '显示推理轨迹',
|
||||
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '启用可折叠推理块',
|
||||
|
||||
@@ -293,6 +293,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.scheduledTasks.editor.prompt.placeholder': '总结未完成任务并给出下一步建议',
|
||||
'sessions.scheduledTasks.editor.enabled.aria': '启用任务',
|
||||
'sessions.scheduledTasks.editor.enabled.label': '已启用',
|
||||
'sessions.scheduledTasks.editor.goal.label': '作为目标运行',
|
||||
'sessions.scheduledTasks.editor.goal.aria': '将此任务作为代理持续推进直至完成的目标运行',
|
||||
'sessions.scheduledTasks.editor.goal.budgetLabel': '令牌预算',
|
||||
'sessions.scheduledTasks.editor.goal.budgetAria': '用令牌预算限制该目标',
|
||||
'sessions.scheduledTasks.editor.actions.closeAria': '关闭',
|
||||
'sessions.scheduledTasks.editor.actions.cancel': '取消',
|
||||
'sessions.scheduledTasks.editor.actions.save': '保存',
|
||||
@@ -1377,6 +1381,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.terminalPanelWithShortcut': '终端面板({shortcut})',
|
||||
'chat.recap.aria': '会话回顾',
|
||||
'chat.recap.label': '回顾:',
|
||||
'chat.goal.dialog.titleCreate': '设置会话目标',
|
||||
'chat.goal.dialog.titleManage': '会话目标',
|
||||
'chat.goal.dialog.objectiveLabel': '目标',
|
||||
'chat.goal.dialog.objectivePlaceholder': '描述代理应达到并验证的最终状态…',
|
||||
'chat.goal.dialog.budgetLabel': '令牌预算',
|
||||
'chat.goal.status.active': '进行中',
|
||||
'chat.goal.status.evaluating': '评估中…',
|
||||
'chat.goal.status.paused': '已暂停',
|
||||
'chat.goal.status.blocked': '已阻塞',
|
||||
'chat.goal.status.budgetLimited': '已达预算',
|
||||
'chat.goal.status.complete': '已完成',
|
||||
'chat.goal.usage.tokens': '{used} 令牌',
|
||||
'chat.goal.usage.tokensWithBudget': '{used}/{budget} 令牌',
|
||||
'chat.goal.usage.turns': '{turns} 次续跑',
|
||||
'chat.goal.action.pause': '暂停',
|
||||
'chat.goal.action.resume': '继续',
|
||||
'chat.goal.action.markComplete': '标记为完成',
|
||||
'chat.goal.action.clear': '移除目标',
|
||||
'chat.goal.action.cancel': '取消',
|
||||
'chat.goal.action.save': '保存目标',
|
||||
'chat.goal.action.start': '启动目标',
|
||||
'chat.goal.toast.actionFailed': '目标更新失败',
|
||||
'chat.goal.row.aria': '会话目标 — 打开详情',
|
||||
'chat.goal.button.createAria': '设置会话目标',
|
||||
'chat.goal.button.manageAria': '管理会话目标',
|
||||
'chat.goal.button.armAria': '用下一条消息启动目标',
|
||||
'chat.goal.counter.aria': '目标长度限制',
|
||||
'chat.goal.button.disarmAria': '目标已就绪 — 点按取消就绪',
|
||||
'chat.goal.button.cancelAria': '目标进行中 — 点按取消',
|
||||
'chat.goal.cancelDialog.title': '取消此目标?',
|
||||
'chat.goal.cancelDialog.description': '代理将停止自动朝该目标工作。',
|
||||
'chat.goal.cancelDialog.keep': '保留目标',
|
||||
'chat.goal.cancelDialog.confirm': '取消目标',
|
||||
'chat.suggestion.applyAria': '使用建议的消息',
|
||||
'chat.suggestion.dismissAria': '关闭建议',
|
||||
'header.actions.toggleTerminalPanelAria': '切换终端面板',
|
||||
|
||||
@@ -1638,6 +1638,12 @@
|
||||
'settings.openchamber.visual.field.sessionRecapAria': '代理完成後產生回顧',
|
||||
'settings.openchamber.visual.field.sessionSuggestion': '產生下一則使用者訊息建議',
|
||||
'settings.openchamber.visual.field.sessionSuggestionAria': '代理完成後產生下一則使用者訊息建議',
|
||||
'settings.openchamber.visual.field.sessionGoal': '啟用工作階段目標',
|
||||
'settings.openchamber.visual.field.sessionGoalAria': '讓工作階段自動朝目標持續工作',
|
||||
'settings.openchamber.visual.goal.sectionTitle': '目標',
|
||||
'settings.openchamber.visual.goal.budgetLabel': '預設權杖預算',
|
||||
'settings.openchamber.visual.goal.budgetAria': '為新目標套用預設權杖預算',
|
||||
'settings.openchamber.visual.goal.description': '在輸入框中啟用靶心按鈕,下一則訊息即成為目標:代理將在小型模型的稽核下自動持續工作,即使你不在電腦前。',
|
||||
'settings.openchamber.visual.field.showReasoningTracesAria': '顯示推理軌跡',
|
||||
'settings.openchamber.visual.field.showReasoningTraces': '顯示推理軌跡',
|
||||
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '啟用可摺疊推理區塊',
|
||||
|
||||
@@ -306,6 +306,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.scheduledTasks.editor.prompt.placeholder': '總結未完成任務並給出下一步建議',
|
||||
'sessions.scheduledTasks.editor.enabled.aria': '啟用任務',
|
||||
'sessions.scheduledTasks.editor.enabled.label': '已啟用',
|
||||
'sessions.scheduledTasks.editor.goal.label': '作為目標執行',
|
||||
'sessions.scheduledTasks.editor.goal.aria': '將此任務作為代理持續推進直至完成的目標執行',
|
||||
'sessions.scheduledTasks.editor.goal.budgetLabel': '權杖預算',
|
||||
'sessions.scheduledTasks.editor.goal.budgetAria': '以權杖預算限制該目標',
|
||||
'sessions.scheduledTasks.editor.actions.closeAria': '關閉',
|
||||
'sessions.scheduledTasks.editor.actions.cancel': '取消',
|
||||
'sessions.scheduledTasks.editor.actions.save': '儲存',
|
||||
@@ -1381,6 +1385,39 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.terminalPanelWithShortcut': '終端機面板({shortcut})',
|
||||
'chat.recap.aria': '工作階段回顧',
|
||||
'chat.recap.label': '回顧:',
|
||||
'chat.goal.dialog.titleCreate': '設定工作階段目標',
|
||||
'chat.goal.dialog.titleManage': '工作階段目標',
|
||||
'chat.goal.dialog.objectiveLabel': '目標',
|
||||
'chat.goal.dialog.objectivePlaceholder': '描述代理應達成並驗證的最終狀態…',
|
||||
'chat.goal.dialog.budgetLabel': '權杖預算',
|
||||
'chat.goal.status.active': '進行中',
|
||||
'chat.goal.status.evaluating': '評估中…',
|
||||
'chat.goal.status.paused': '已暫停',
|
||||
'chat.goal.status.blocked': '已受阻',
|
||||
'chat.goal.status.budgetLimited': '已達預算',
|
||||
'chat.goal.status.complete': '已完成',
|
||||
'chat.goal.usage.tokens': '{used} 權杖',
|
||||
'chat.goal.usage.tokensWithBudget': '{used}/{budget} 權杖',
|
||||
'chat.goal.usage.turns': '{turns} 次續跑',
|
||||
'chat.goal.action.pause': '暫停',
|
||||
'chat.goal.action.resume': '繼續',
|
||||
'chat.goal.action.markComplete': '標記為完成',
|
||||
'chat.goal.action.clear': '移除目標',
|
||||
'chat.goal.action.cancel': '取消',
|
||||
'chat.goal.action.save': '儲存目標',
|
||||
'chat.goal.action.start': '啟動目標',
|
||||
'chat.goal.toast.actionFailed': '目標更新失敗',
|
||||
'chat.goal.row.aria': '工作階段目標 — 開啟詳細資訊',
|
||||
'chat.goal.button.createAria': '設定工作階段目標',
|
||||
'chat.goal.button.manageAria': '管理工作階段目標',
|
||||
'chat.goal.button.armAria': '用下一則訊息啟動目標',
|
||||
'chat.goal.counter.aria': '目標長度限制',
|
||||
'chat.goal.button.disarmAria': '目標已就緒 — 點按取消就緒',
|
||||
'chat.goal.button.cancelAria': '目標進行中 — 點按取消',
|
||||
'chat.goal.cancelDialog.title': '取消此目標?',
|
||||
'chat.goal.cancelDialog.description': '代理將停止自動朝此目標工作。',
|
||||
'chat.goal.cancelDialog.keep': '保留目標',
|
||||
'chat.goal.cancelDialog.confirm': '取消目標',
|
||||
'chat.suggestion.applyAria': '使用建議的訊息',
|
||||
'chat.suggestion.dismissAria': '關閉建議',
|
||||
'header.actions.toggleTerminalPanelAria': '切換終端機面板',
|
||||
|
||||
@@ -15,6 +15,17 @@ export const EXECUTION_FORK_DEFAULT_INSTRUCTIONS =
|
||||
"if it is a conclusion or summary, your task is to verify it, explain whether you agree or disagree, and correct it if needed. " +
|
||||
"Always clearly state what you understand your task to be, and wait for the user's approval of your conclusions before taking any further actions.";
|
||||
|
||||
// Assertive variant prefilled when "Run as goal" is checked: the new session
|
||||
// must treat the forked message as an assignment and execute it to completion
|
||||
// (the goal loop audits progress and keeps it going), not report back and wait.
|
||||
export const EXECUTION_FORK_GOAL_INSTRUCTIONS =
|
||||
"The message I share is an assignment handed over from another AI agent. Extract the concrete task from it and start executing immediately: " +
|
||||
"if it is an implementation plan, implement that plan; " +
|
||||
"if it is a conclusion or summary, verify it against the actual current state of the code and correct it if needed; " +
|
||||
"if it is a bug description, find the root cause and fix it. " +
|
||||
"Do not stop at restating your understanding and do not wait for approval — keep working until the task is verifiably complete, " +
|
||||
"and end every turn with a factual statement of what is done, what was verified, and what remains.";
|
||||
|
||||
// Fixed connective that opens the forked assistant content. Not editable by the
|
||||
// user — it sits between the user's instructions and the assistant message.
|
||||
const EXECUTION_FORK_CONTENT_PREFACE =
|
||||
|
||||
@@ -429,6 +429,15 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
|
||||
if (typeof settings.sessionSuggestionEnabled === 'boolean' && settings.sessionSuggestionEnabled !== store.sessionSuggestionEnabled) {
|
||||
store.setSessionSuggestionEnabled(settings.sessionSuggestionEnabled);
|
||||
}
|
||||
if (typeof settings.sessionGoalEnabled === 'boolean' && settings.sessionGoalEnabled !== store.sessionGoalEnabled) {
|
||||
store.setSessionGoalEnabled(settings.sessionGoalEnabled);
|
||||
}
|
||||
if (typeof settings.sessionGoalDefaultBudgetEnabled === 'boolean' && settings.sessionGoalDefaultBudgetEnabled !== store.sessionGoalDefaultBudgetEnabled) {
|
||||
store.setSessionGoalDefaultBudgetEnabled(settings.sessionGoalDefaultBudgetEnabled);
|
||||
}
|
||||
if (typeof settings.sessionGoalDefaultBudget === 'number' && Number.isFinite(settings.sessionGoalDefaultBudget) && settings.sessionGoalDefaultBudget !== store.sessionGoalDefaultBudget) {
|
||||
store.setSessionGoalDefaultBudget(settings.sessionGoalDefaultBudget);
|
||||
}
|
||||
if (typeof settings.collapsibleThinkingBlocks === 'boolean' && settings.collapsibleThinkingBlocks !== store.collapsibleThinkingBlocks) {
|
||||
store.setCollapsibleThinkingBlocks(settings.collapsibleThinkingBlocks);
|
||||
}
|
||||
@@ -789,6 +798,15 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
||||
if (typeof candidate.sessionSuggestionEnabled === 'boolean') {
|
||||
result.sessionSuggestionEnabled = candidate.sessionSuggestionEnabled;
|
||||
}
|
||||
if (typeof candidate.sessionGoalEnabled === 'boolean') {
|
||||
result.sessionGoalEnabled = candidate.sessionGoalEnabled;
|
||||
}
|
||||
if (typeof candidate.sessionGoalDefaultBudgetEnabled === 'boolean') {
|
||||
result.sessionGoalDefaultBudgetEnabled = candidate.sessionGoalDefaultBudgetEnabled;
|
||||
}
|
||||
if (typeof candidate.sessionGoalDefaultBudget === 'number' && Number.isFinite(candidate.sessionGoalDefaultBudget) && candidate.sessionGoalDefaultBudget > 0) {
|
||||
result.sessionGoalDefaultBudget = Math.floor(candidate.sessionGoalDefaultBudget);
|
||||
}
|
||||
if (typeof candidate.collapsibleThinkingBlocks === 'boolean') {
|
||||
result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export type ScheduledTask = {
|
||||
modelID: string;
|
||||
variant?: string;
|
||||
agent?: string;
|
||||
goalEnabled?: boolean;
|
||||
goalTokenBudget?: number;
|
||||
};
|
||||
state: {
|
||||
createdAt: number;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { abortCurrentOperation, patchSessionMetadata } from '@/sync/session-actions';
|
||||
import {
|
||||
SESSION_GOAL_OBJECTIVE_CHAR_LIMIT,
|
||||
type SessionGoalPayload,
|
||||
type SessionGoalStatus,
|
||||
} from '@/lib/sessionGoalMetadata';
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const createGoalId = (): string =>
|
||||
`${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const writeGoal = (
|
||||
sessionId: string,
|
||||
directory: string | undefined,
|
||||
update: (currentGoal: Record<string, unknown> | null) => Record<string, unknown> | null,
|
||||
) =>
|
||||
patchSessionMetadata(sessionId, directory, (metadata) => {
|
||||
const namespace = isRecord(metadata.openchamber) ? metadata.openchamber : {};
|
||||
const currentGoal = isRecord(namespace.goal) ? namespace.goal : null;
|
||||
const nextGoal = update(currentGoal);
|
||||
const nextNamespace = { ...namespace };
|
||||
if (nextGoal) {
|
||||
nextNamespace.goal = nextGoal;
|
||||
} else {
|
||||
delete nextNamespace.goal;
|
||||
}
|
||||
return { ...metadata, openchamber: nextNamespace };
|
||||
});
|
||||
|
||||
export interface SetSessionGoalInput {
|
||||
objective: string;
|
||||
tokenBudget: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new goal (fresh id resets accounting) or edit the existing one
|
||||
* (id and usage counters preserved).
|
||||
*/
|
||||
export async function setSessionGoal(
|
||||
sessionId: string,
|
||||
directory: string | undefined,
|
||||
input: SetSessionGoalInput,
|
||||
existing: SessionGoalPayload | null,
|
||||
): Promise<void> {
|
||||
const objective = input.objective.trim().slice(0, SESSION_GOAL_OBJECTIVE_CHAR_LIMIT);
|
||||
if (!objective) {
|
||||
throw new Error('Goal objective must not be empty');
|
||||
}
|
||||
const tokenBudget = typeof input.tokenBudget === 'number' && Number.isFinite(input.tokenBudget) && input.tokenBudget > 0
|
||||
? Math.floor(input.tokenBudget)
|
||||
: null;
|
||||
const now = Date.now();
|
||||
await writeGoal(sessionId, directory, (currentGoal) => {
|
||||
if (existing && currentGoal && currentGoal.id === existing.id && existing.status !== 'complete') {
|
||||
// Edit in place: keep accounting, reactivate, clear stale audit state.
|
||||
return {
|
||||
...currentGoal,
|
||||
objective,
|
||||
tokenBudget,
|
||||
status: 'active',
|
||||
statusReason: 'resumed',
|
||||
blockedStreak: 0,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: createGoalId(),
|
||||
objective,
|
||||
status: 'active',
|
||||
tokenBudget,
|
||||
tokensUsed: 0,
|
||||
turnsUsed: 0,
|
||||
blockedStreak: 0,
|
||||
note: '',
|
||||
statusReason: '',
|
||||
lastAccountedMessageID: '',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function setSessionGoalStatus(
|
||||
sessionId: string,
|
||||
directory: string | undefined,
|
||||
status: Extract<SessionGoalStatus, 'active' | 'paused' | 'complete'>,
|
||||
): Promise<void> {
|
||||
// Pausing a goal also stops the agent's current turn — same mental model
|
||||
// as the stop button, expressed through goal control. A no-op when the
|
||||
// session is already idle.
|
||||
if (status === 'paused') {
|
||||
void abortCurrentOperation(sessionId);
|
||||
}
|
||||
await writeGoal(sessionId, directory, (currentGoal) => {
|
||||
if (!currentGoal) return null;
|
||||
return {
|
||||
...currentGoal,
|
||||
status,
|
||||
// 'resumed' is the server's kickoff signal for an already-idle session.
|
||||
statusReason: status === 'active' ? 'resumed' : (status === 'complete' ? 'marked by user' : ''),
|
||||
blockedStreak: 0,
|
||||
// An explicit resume grants a fresh auto-continuation allowance —
|
||||
// otherwise a goal blocked on the turn cap would re-block on the very
|
||||
// next tick and Resume would be a dead end.
|
||||
...(status === 'active' ? { turnsUsed: 0 } : {}),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearSessionGoal(sessionId: string, directory: string | undefined): Promise<void> {
|
||||
let wasActive = false;
|
||||
await writeGoal(sessionId, directory, (currentGoal) => {
|
||||
wasActive = currentGoal?.status === 'active';
|
||||
return null;
|
||||
});
|
||||
// Removing a running goal is a "stop" too — abort the current turn like
|
||||
// pause does. A no-op when the session is idle.
|
||||
if (wasActive) {
|
||||
void abortCurrentOperation(sessionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
|
||||
// Session goal driven by the server's session-goal runtime, stored under
|
||||
// session.metadata.openchamber.goal. The UI writes goals (create/edit/
|
||||
// pause/resume/clear) by patching this metadata; the server loop accounts
|
||||
// usage, audits progress with the small model, and auto-continues the
|
||||
// session until the goal settles.
|
||||
export type SessionGoalStatus = 'active' | 'paused' | 'blocked' | 'budgetLimited' | 'complete';
|
||||
|
||||
const SESSION_GOAL_STATUSES: SessionGoalStatus[] = ['active', 'paused', 'blocked', 'budgetLimited', 'complete'];
|
||||
|
||||
export const SESSION_GOAL_OBJECTIVE_CHAR_LIMIT = 2000;
|
||||
|
||||
export interface SessionGoalPayload {
|
||||
id: string;
|
||||
objective: string;
|
||||
status: SessionGoalStatus;
|
||||
tokenBudget: number | null;
|
||||
tokensUsed: number;
|
||||
turnsUsed: number;
|
||||
blockedStreak: number;
|
||||
note: string;
|
||||
statusReason: string;
|
||||
lastAccountedMessageID: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const isGoalStatus = (value: unknown): value is SessionGoalStatus =>
|
||||
typeof value === 'string' && (SESSION_GOAL_STATUSES as string[]).includes(value);
|
||||
|
||||
export function getSessionGoal(session: Session | null | undefined): SessionGoalPayload | null {
|
||||
const metadata = (session as { metadata?: unknown } | null | undefined)?.metadata;
|
||||
if (!isRecord(metadata)) return null;
|
||||
const namespace = metadata.openchamber;
|
||||
if (!isRecord(namespace)) return null;
|
||||
const goal = namespace.goal;
|
||||
if (!isRecord(goal)) return null;
|
||||
|
||||
const id = typeof goal.id === 'string' ? goal.id : '';
|
||||
const objective = typeof goal.objective === 'string' ? goal.objective.trim() : '';
|
||||
if (!id || !objective || !isGoalStatus(goal.status)) return null;
|
||||
|
||||
const tokenBudget = typeof goal.tokenBudget === 'number' && Number.isFinite(goal.tokenBudget) && goal.tokenBudget > 0
|
||||
? Math.floor(goal.tokenBudget)
|
||||
: null;
|
||||
const asCount = (value: unknown): number =>
|
||||
typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
||||
|
||||
return {
|
||||
id,
|
||||
objective: objective.slice(0, SESSION_GOAL_OBJECTIVE_CHAR_LIMIT),
|
||||
status: goal.status,
|
||||
tokenBudget,
|
||||
tokensUsed: asCount(goal.tokensUsed),
|
||||
turnsUsed: asCount(goal.turnsUsed),
|
||||
blockedStreak: asCount(goal.blockedStreak),
|
||||
note: typeof goal.note === 'string' ? goal.note : '',
|
||||
statusReason: typeof goal.statusReason === 'string' ? goal.statusReason : '',
|
||||
lastAccountedMessageID: typeof goal.lastAccountedMessageID === 'string' ? goal.lastAccountedMessageID : '',
|
||||
createdAt: typeof goal.createdAt === 'number' ? goal.createdAt : 0,
|
||||
updatedAt: typeof goal.updatedAt === 'number' ? goal.updatedAt : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatGoalTokens(count: number): string {
|
||||
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
||||
if (count >= 10_000) return `${Math.round(count / 1000)}K`;
|
||||
if (count >= 1_000) return `${(count / 1000).toFixed(1)}K`;
|
||||
return String(count);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { SessionGoalStatus } from '@/lib/sessionGoalMetadata';
|
||||
|
||||
// Shared presentation mapping for the goal status across chat, sidebar and
|
||||
// mobile surfaces. Colors are theme tokens; labels resolve through i18n at
|
||||
// the call site.
|
||||
export const sessionGoalStatusColor: Record<SessionGoalStatus, string> = {
|
||||
active: 'var(--status-info)',
|
||||
paused: 'var(--surface-muted-foreground)',
|
||||
blocked: 'var(--status-warning)',
|
||||
budgetLimited: 'var(--status-warning)',
|
||||
complete: 'var(--status-success)',
|
||||
};
|
||||
|
||||
export const sessionGoalStatusLabelKey: Record<SessionGoalStatus, string> = {
|
||||
active: 'chat.goal.status.active',
|
||||
paused: 'chat.goal.status.paused',
|
||||
blocked: 'chat.goal.status.blocked',
|
||||
budgetLimited: 'chat.goal.status.budgetLimited',
|
||||
complete: 'chat.goal.status.complete',
|
||||
};
|
||||
@@ -186,6 +186,20 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
|
||||
titleKey: 'settings.openchamber.visual.field.sessionSuggestion',
|
||||
keywords: ['suggestion', 'assist', 'small model', 'follow up'],
|
||||
},
|
||||
{
|
||||
id: 'chat.session-goal',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.sessionGoal',
|
||||
keywords: ['goal', 'objective', 'auto continue', 'small model'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'chat.session-goal-budget',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.goal.budgetLabel',
|
||||
keywords: ['goal', 'budget', 'tokens', 'limit'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'chat.reasoning-traces',
|
||||
page: 'chat',
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
// Narrow store for the "next message starts a goal" flag. Armed by the
|
||||
// composer target button (works for existing sessions AND session drafts)
|
||||
// and by the run-as-goal flows (fork dialog, plan send); consumed by
|
||||
// sendMessage in session-ui-store, which turns the sent prompt into the
|
||||
// goal objective — unless the arming flow supplied a richer objective
|
||||
// override (e.g. the plan content instead of "Implement this plan: X").
|
||||
interface SessionGoalArmStore {
|
||||
armed: boolean;
|
||||
objectiveOverride: string | null;
|
||||
setArmed: (armed: boolean, objectiveOverride?: string | null) => void;
|
||||
/** Read-and-clear in one step at send time. */
|
||||
consume: () => { armed: boolean; objectiveOverride: string | null };
|
||||
}
|
||||
|
||||
export const useSessionGoalArmStore = create<SessionGoalArmStore>((set, get) => ({
|
||||
armed: false,
|
||||
objectiveOverride: null,
|
||||
setArmed: (armed, objectiveOverride = null) => set({
|
||||
armed,
|
||||
objectiveOverride: armed ? objectiveOverride : null,
|
||||
}),
|
||||
consume: () => {
|
||||
const { armed, objectiveOverride } = get();
|
||||
if (armed) set({ armed: false, objectiveOverride: null });
|
||||
return { armed, objectiveOverride };
|
||||
},
|
||||
}));
|
||||
@@ -574,6 +574,9 @@ interface UIStore {
|
||||
showReasoningTraces: boolean;
|
||||
sessionRecapEnabled: boolean;
|
||||
sessionSuggestionEnabled: boolean;
|
||||
sessionGoalEnabled: boolean;
|
||||
sessionGoalDefaultBudgetEnabled: boolean;
|
||||
sessionGoalDefaultBudget: number;
|
||||
collapsibleThinkingBlocks: boolean;
|
||||
chatRenderMode: ChatRenderMode;
|
||||
activityRenderMode: ActivityRenderMode;
|
||||
@@ -725,6 +728,9 @@ interface UIStore {
|
||||
setShowReasoningTraces: (value: boolean) => void;
|
||||
setSessionRecapEnabled: (value: boolean) => void;
|
||||
setSessionSuggestionEnabled: (value: boolean) => void;
|
||||
setSessionGoalEnabled: (value: boolean) => void;
|
||||
setSessionGoalDefaultBudgetEnabled: (value: boolean) => void;
|
||||
setSessionGoalDefaultBudget: (value: number) => void;
|
||||
setCollapsibleThinkingBlocks: (value: boolean) => void;
|
||||
setChatRenderMode: (value: ChatRenderMode) => void;
|
||||
setActivityRenderMode: (value: ActivityRenderMode) => void;
|
||||
@@ -873,6 +879,9 @@ export const useUIStore = create<UIStore>()(
|
||||
showReasoningTraces: true,
|
||||
sessionRecapEnabled: true,
|
||||
sessionSuggestionEnabled: true,
|
||||
sessionGoalEnabled: true,
|
||||
sessionGoalDefaultBudgetEnabled: false,
|
||||
sessionGoalDefaultBudget: 200_000,
|
||||
collapsibleThinkingBlocks: true,
|
||||
chatRenderMode: 'live',
|
||||
activityRenderMode: 'summary',
|
||||
@@ -1581,6 +1590,18 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ sessionSuggestionEnabled: value });
|
||||
},
|
||||
|
||||
setSessionGoalEnabled: (value) => {
|
||||
set({ sessionGoalEnabled: value });
|
||||
},
|
||||
|
||||
setSessionGoalDefaultBudgetEnabled: (value) => {
|
||||
set({ sessionGoalDefaultBudgetEnabled: value });
|
||||
},
|
||||
|
||||
setSessionGoalDefaultBudget: (value) => {
|
||||
set({ sessionGoalDefaultBudget: value });
|
||||
},
|
||||
|
||||
setCollapsibleThinkingBlocks: (value) => {
|
||||
set({ collapsibleThinkingBlocks: value });
|
||||
},
|
||||
@@ -2276,6 +2297,9 @@ export const useUIStore = create<UIStore>()(
|
||||
showReasoningTraces: state.showReasoningTraces,
|
||||
sessionRecapEnabled: state.sessionRecapEnabled,
|
||||
sessionSuggestionEnabled: state.sessionSuggestionEnabled,
|
||||
sessionGoalEnabled: state.sessionGoalEnabled,
|
||||
sessionGoalDefaultBudgetEnabled: state.sessionGoalDefaultBudgetEnabled,
|
||||
sessionGoalDefaultBudget: state.sessionGoalDefaultBudget,
|
||||
collapsibleThinkingBlocks: state.collapsibleThinkingBlocks,
|
||||
chatRenderMode: state.chatRenderMode,
|
||||
activityRenderMode: state.activityRenderMode,
|
||||
|
||||
@@ -57,6 +57,10 @@ import {
|
||||
fetchMessagesForSession,
|
||||
} from "./session-actions"
|
||||
import { useInputStore, type SyntheticContextPart } from "./input-store"
|
||||
import { useSessionGoalArmStore } from "@/stores/useSessionGoalArmStore"
|
||||
import { setSessionGoal } from "@/lib/sessionGoalActions"
|
||||
import { wrapSystemReminder } from "@/lib/systemReminder"
|
||||
import { useUIStore } from "@/stores/useUIStore"
|
||||
import { useSelectionStore } from "./selection-store"
|
||||
import { getViewportSessionMemory, useViewportStore, viewportSessionKey } from "./viewport-store"
|
||||
import { useSessionWorktreeStore } from "./session-worktree-store"
|
||||
@@ -177,6 +181,7 @@ type AssistantMessageSessionExecution = {
|
||||
agent: string
|
||||
instructions: string
|
||||
createWorktree?: boolean
|
||||
runAsGoal?: boolean
|
||||
}
|
||||
|
||||
function notifyMessageSent(sessionId: string): void {
|
||||
@@ -1003,6 +1008,9 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
// ---------------------------------------------------------------------------
|
||||
// sendMessage — calls SDK, reads domain data from sync
|
||||
// ---------------------------------------------------------------------------
|
||||
// Armed goal (composer target button): the sent prompt becomes the goal
|
||||
// objective; budget comes from the global default setting. Fire-and-forget —
|
||||
// a failed metadata patch must not fail the send.
|
||||
sendMessage: async (
|
||||
content: string,
|
||||
providerID: string,
|
||||
@@ -1026,6 +1034,36 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
const draft = get().newSessionDraft
|
||||
const trimmedAgent = typeof agent === "string" && agent.trim().length > 0 ? agent.trim() : undefined
|
||||
|
||||
const goalArm = inputMode !== "shell" && content.trim().length > 0
|
||||
? useSessionGoalArmStore.getState().consume()
|
||||
: { armed: false, objectiveOverride: null }
|
||||
const goalArmed = goalArm.armed
|
||||
if (goalArmed) {
|
||||
// Teach the agent the goal protocol from turn one — without this it
|
||||
// only learns about goal mode from the first server continuation.
|
||||
const uiState = useUIStore.getState()
|
||||
const budgetLine = uiState.sessionGoalDefaultBudgetEnabled
|
||||
? ` A token budget of ${uiState.sessionGoalDefaultBudget} tokens applies to this goal.`
|
||||
: ""
|
||||
const goalIntro = wrapSystemReminder(
|
||||
"Goal mode is active for this session. The user message above defines the goal objective. "
|
||||
+ "Work toward it across turns; whenever you stop before the objective is verifiably complete, the system will automatically prompt you to continue. "
|
||||
+ "Progress is evaluated independently after each turn, so end every turn with a clear, factual statement of what is done, what was verified, and what remains."
|
||||
+ budgetLine,
|
||||
)
|
||||
additionalParts = [...(additionalParts ?? []), { text: goalIntro, synthetic: true }]
|
||||
}
|
||||
const applyArmedGoal = (goalSessionId: string, goalDirectory: string | null | undefined) => {
|
||||
if (!goalArmed) return
|
||||
const uiState = useUIStore.getState()
|
||||
const tokenBudget = uiState.sessionGoalDefaultBudgetEnabled ? uiState.sessionGoalDefaultBudget : null
|
||||
const objective = goalArm.objectiveOverride?.trim() || content
|
||||
void setSessionGoal(goalSessionId, goalDirectory ?? undefined, { objective, tokenBudget }, null)
|
||||
.catch((error) => {
|
||||
console.warn("[session-ui-store] failed to set goal from armed send", error)
|
||||
})
|
||||
}
|
||||
|
||||
// ---- New session from draft ----
|
||||
if (!options?.sessionId && draft?.open) {
|
||||
const createdDraftSession = await materializeOpenDraftSession({
|
||||
@@ -1074,6 +1112,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
})),
|
||||
})),
|
||||
})
|
||||
applyArmedGoal(createdDraftSession.sessionId, createdDraftSession.directory)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1153,6 +1192,9 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
})),
|
||||
})),
|
||||
})
|
||||
if (targetSessionId) {
|
||||
applyArmedGoal(targetSessionId, currentSessionDirectory)
|
||||
}
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1437,6 +1479,13 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
useDirectoryStore.getState().setDirectory(createdWorktree.path, { showOverlay: false })
|
||||
}
|
||||
|
||||
// "Run as goal" rides the same arm mechanism as the composer target
|
||||
// button: sendMessage consumes the flag, stamps the goal (objective =
|
||||
// the composed fork message) and attaches the goal-mode intro part.
|
||||
// Set explicitly either way so a stray armed flag cannot leak into a
|
||||
// non-goal fork.
|
||||
useSessionGoalArmStore.getState().setArmed(execution.runAsGoal === true)
|
||||
|
||||
await get().sendMessage(
|
||||
composeForkSessionMessage(execution.instructions, assistantPlanText),
|
||||
pID,
|
||||
|
||||
Reference in New Issue
Block a user