duration is one checkbox, because two of the three options were the same thing
"This session" meant nothing. The backend deliberately holds the agent process alive while a background job is running, so closing the browser does not end one — "this session" and "until stopped" were one behaviour wearing two labels, and offering both implied a difference that does not exist. So there is one real choice, and it is a yes/no: bound it by time, or run until stopped. [ ] Stop after [60] minutes Unchecked is the default and the common case. Checked exits when the time is up whether or not it fired, which is the only thing a clock can usefully mean here. This also removes a note that had become false. The dialog used to say "all of these end if the session does" — written from the process tree, where the job is a descendant of the agent process. True as far as it went, and wrong in practice because of the keep-alive. It now says what each choice actually does instead of what the ancestry suggests. Still true, and unstated because it belongs somewhere else: `pm2 restart officer-claude-code` ends every running job. That is a deploy-time fact, not something to put in a composer. tsgo clean, frontend builds, 851 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,17 +18,22 @@ export type BackgroundJobSpec = {
|
||||
condition: string;
|
||||
/** Whether firing ends the job. Ignored without a condition. */
|
||||
stopOnFire: boolean;
|
||||
/** `session` = until this chat ends. A number = minutes. `until-stopped` = until told otherwise. */
|
||||
duration: 'session' | 'until-stopped' | number;
|
||||
/**
|
||||
* Minutes to run for, or `null` to run until stopped.
|
||||
*
|
||||
* There used to be a "this session" option. It meant nothing: the backend deliberately holds the agent
|
||||
* process alive while a job is running, so closing the browser does not end one — "this session" and
|
||||
* "until stopped" were the same thing wearing two labels.
|
||||
*/
|
||||
duration: number | null;
|
||||
/** Where it runs. The panel's own directory, so nothing has to be typed. */
|
||||
cwd: string;
|
||||
};
|
||||
|
||||
const durationLine = (d: BackgroundJobSpec['duration']): string => {
|
||||
if (d === 'session') return 'Run until this session ends. Do not bound the loop by time.';
|
||||
if (d === 'until-stopped') return 'Run until I stop it. Do not bound the loop by time.';
|
||||
return `Run for about ${d} minute${d === 1 ? '' : 's'}, then exit 1 to say it timed out.`;
|
||||
};
|
||||
const durationLine = (d: BackgroundJobSpec['duration']): string =>
|
||||
d === null
|
||||
? 'Run until I stop it. Do not bound the loop by time.'
|
||||
: `Run for about ${d} minute${d === 1 ? '' : 's'}, then exit 1 to say it timed out.`;
|
||||
|
||||
export function buildBackgroundJobPrompt(spec: BackgroundJobSpec): string {
|
||||
const watching = spec.condition.trim().length > 0;
|
||||
|
||||
@@ -34,15 +34,15 @@ type BackgroundJobDialogProps = {
|
||||
onSubmit: (intent: string, preamble: string) => void;
|
||||
};
|
||||
|
||||
type Duration = 'session' | 'until-stopped' | 'timed';
|
||||
|
||||
export const BackgroundJobDialog = ({ open, onOpenChange, cwd, onSubmit }: BackgroundJobDialogProps) => {
|
||||
const [mode, setMode] = useState<'command' | 'prompt'>('command');
|
||||
const [body, setBody] = useState('');
|
||||
const [watching, setWatching] = useState(false);
|
||||
const [condition, setCondition] = useState('');
|
||||
const [stopOnFire, setStopOnFire] = useState(true);
|
||||
const [duration, setDuration] = useState<Duration>('session');
|
||||
// Unchecked means no time bound — the job runs until stopped. There is no third option: the backend
|
||||
// keeps the agent alive while a job runs, so "this session" would have meant the same thing.
|
||||
const [timed, setTimed] = useState(false);
|
||||
const [minutes, setMinutes] = useState(60);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,7 +52,7 @@ export const BackgroundJobDialog = ({ open, onOpenChange, cwd, onSubmit }: Backg
|
||||
setWatching(false);
|
||||
setCondition('');
|
||||
setStopOnFire(true);
|
||||
setDuration('session');
|
||||
setTimed(false);
|
||||
setMinutes(60);
|
||||
}, [open]);
|
||||
|
||||
@@ -74,7 +74,7 @@ export const BackgroundJobDialog = ({ open, onOpenChange, cwd, onSubmit }: Backg
|
||||
body,
|
||||
condition: watching ? condition : '',
|
||||
stopOnFire,
|
||||
duration: duration === 'timed' ? minutes : duration,
|
||||
duration: timed ? minutes : null,
|
||||
cwd,
|
||||
};
|
||||
// The intent is what the transcript shows; the preamble rides underneath it.
|
||||
@@ -151,36 +151,21 @@ export const BackgroundJobDialog = ({ open, onOpenChange, cwd, onSubmit }: Backg
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs uppercase tracking-wide text-duck-dark/40">Duration</Label>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input type="radio" checked={duration === 'session'} onChange={() => setDuration('session')} />
|
||||
This session
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input type="radio" checked={duration === 'timed'} onChange={() => setDuration('timed')} />
|
||||
For
|
||||
<input type="checkbox" checked={timed} onChange={(ev) => setTimed(ev.target.checked)} />
|
||||
Stop after
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={minutes}
|
||||
onFocus={() => setDuration('timed')}
|
||||
disabled={!timed}
|
||||
onChange={(ev) => setMinutes(Math.max(1, Number(ev.target.value) || 1))}
|
||||
className="w-16 rounded border border-duck-dark/20 bg-transparent px-1.5 py-0.5 text-sm"
|
||||
className="w-16 rounded border border-duck-dark/20 bg-transparent px-1.5 py-0.5 text-sm disabled:opacity-40"
|
||||
/>
|
||||
minutes
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
checked={duration === 'until-stopped'}
|
||||
onChange={() => setDuration('until-stopped')}
|
||||
/>
|
||||
Until stopped
|
||||
</label>
|
||||
{/* Said plainly rather than promised. The job is a descendant of this session's process, so
|
||||
nothing here outlives the session — including "until stopped". */}
|
||||
<p className="pt-1 text-xs text-duck-dark/40">
|
||||
All of these end if the session does — the job runs underneath it.
|
||||
{timed ? 'Exits when the time is up, whether or not it fired.' : 'Runs until you stop it.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user