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;
|
condition: string;
|
||||||
/** Whether firing ends the job. Ignored without a condition. */
|
/** Whether firing ends the job. Ignored without a condition. */
|
||||||
stopOnFire: boolean;
|
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. */
|
/** Where it runs. The panel's own directory, so nothing has to be typed. */
|
||||||
cwd: string;
|
cwd: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const durationLine = (d: BackgroundJobSpec['duration']): string => {
|
const durationLine = (d: BackgroundJobSpec['duration']): string =>
|
||||||
if (d === 'session') return 'Run until this session ends. Do not bound the loop by time.';
|
d === null
|
||||||
if (d === 'until-stopped') return 'Run until I stop it. Do not bound the loop by time.';
|
? '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.`;
|
: `Run for about ${d} minute${d === 1 ? '' : 's'}, then exit 1 to say it timed out.`;
|
||||||
};
|
|
||||||
|
|
||||||
export function buildBackgroundJobPrompt(spec: BackgroundJobSpec): string {
|
export function buildBackgroundJobPrompt(spec: BackgroundJobSpec): string {
|
||||||
const watching = spec.condition.trim().length > 0;
|
const watching = spec.condition.trim().length > 0;
|
||||||
|
|||||||
@@ -34,15 +34,15 @@ type BackgroundJobDialogProps = {
|
|||||||
onSubmit: (intent: string, preamble: string) => void;
|
onSubmit: (intent: string, preamble: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Duration = 'session' | 'until-stopped' | 'timed';
|
|
||||||
|
|
||||||
export const BackgroundJobDialog = ({ open, onOpenChange, cwd, onSubmit }: BackgroundJobDialogProps) => {
|
export const BackgroundJobDialog = ({ open, onOpenChange, cwd, onSubmit }: BackgroundJobDialogProps) => {
|
||||||
const [mode, setMode] = useState<'command' | 'prompt'>('command');
|
const [mode, setMode] = useState<'command' | 'prompt'>('command');
|
||||||
const [body, setBody] = useState('');
|
const [body, setBody] = useState('');
|
||||||
const [watching, setWatching] = useState(false);
|
const [watching, setWatching] = useState(false);
|
||||||
const [condition, setCondition] = useState('');
|
const [condition, setCondition] = useState('');
|
||||||
const [stopOnFire, setStopOnFire] = useState(true);
|
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);
|
const [minutes, setMinutes] = useState(60);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,7 +52,7 @@ export const BackgroundJobDialog = ({ open, onOpenChange, cwd, onSubmit }: Backg
|
|||||||
setWatching(false);
|
setWatching(false);
|
||||||
setCondition('');
|
setCondition('');
|
||||||
setStopOnFire(true);
|
setStopOnFire(true);
|
||||||
setDuration('session');
|
setTimed(false);
|
||||||
setMinutes(60);
|
setMinutes(60);
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ export const BackgroundJobDialog = ({ open, onOpenChange, cwd, onSubmit }: Backg
|
|||||||
body,
|
body,
|
||||||
condition: watching ? condition : '',
|
condition: watching ? condition : '',
|
||||||
stopOnFire,
|
stopOnFire,
|
||||||
duration: duration === 'timed' ? minutes : duration,
|
duration: timed ? minutes : null,
|
||||||
cwd,
|
cwd,
|
||||||
};
|
};
|
||||||
// The intent is what the transcript shows; the preamble rides underneath it.
|
// 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>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<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">
|
<label className="flex items-center gap-2 text-sm">
|
||||||
<input type="radio" checked={duration === 'session'} onChange={() => setDuration('session')} />
|
<input type="checkbox" checked={timed} onChange={(ev) => setTimed(ev.target.checked)} />
|
||||||
This session
|
Stop after
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input type="radio" checked={duration === 'timed'} onChange={() => setDuration('timed')} />
|
|
||||||
For
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
value={minutes}
|
value={minutes}
|
||||||
onFocus={() => setDuration('timed')}
|
disabled={!timed}
|
||||||
onChange={(ev) => setMinutes(Math.max(1, Number(ev.target.value) || 1))}
|
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
|
minutes
|
||||||
</label>
|
</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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user