Spaces:
Running
Running
Agent Manager commited on
Commit ·
d7ec16b
1
Parent(s): 126e54c
Make input request signals advisory
Browse files
docs/tui-input-required.md
CHANGED
|
@@ -9,12 +9,12 @@ such as `Allow` in terminal text. Those heuristics cannot distinguish a dialog
|
|
| 9 |
from thinking, a completed turn, documentation, or agent-produced output; a
|
| 10 |
false "blocked" badge would be worse than a missed prompt.
|
| 11 |
|
| 12 |
-
The pane reader and Overview card
|
| 13 |
-
|
| 14 |
-
also identify the condition.
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
|
| 19 |
The reader does not answer the dialog in this version. The CLIs have different
|
| 20 |
choice identifiers, validation, queueing, "allow once/always" semantics, and
|
|
@@ -80,14 +80,20 @@ removed at process exit.
|
|
| 80 |
|
| 81 |
OpenCode's paired event remains authoritative through menu-navigation input and
|
| 82 |
closes immediately when the final queued request closes. Every marker, including
|
| 83 |
-
OpenCode's, also has a 30-minute ceiling so a lost close event cannot
|
| 84 |
-
|
| 85 |
native completion event can clear the signal sooner; process exit clears every
|
| 86 |
kind. Automatic terminal query replies do not count as operator input. The
|
| 87 |
ceiling can create a false negative for a dialog left open longer than 30
|
| 88 |
minutes; that is intentional because an indefinitely stale warning is the more
|
| 89 |
damaging failure mode.
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
## Verification
|
| 92 |
|
| 93 |
Automated coverage includes stale/mismatched launch markers, unrelated terminal
|
|
|
|
| 9 |
from thinking, a completed turn, documentation, or agent-produced output; a
|
| 10 |
false "blocked" badge would be worse than a missed prompt.
|
| 11 |
|
| 12 |
+
The pane reader and Overview card show a **Needs input** banner and an **open
|
| 13 |
+
terminal** action above their normal reply composer. The session tile and
|
| 14 |
+
sidebar also identify the condition. Detection is advisory: the composer,
|
| 15 |
+
prompt API, and attachment delivery stay available while a signal is active.
|
| 16 |
+
A false-positive warning is recoverable; turning it into a write denial would
|
| 17 |
+
make detector error an availability bug.
|
| 18 |
|
| 19 |
The reader does not answer the dialog in this version. The CLIs have different
|
| 20 |
choice identifiers, validation, queueing, "allow once/always" semantics, and
|
|
|
|
| 80 |
|
| 81 |
OpenCode's paired event remains authoritative through menu-navigation input and
|
| 82 |
closes immediately when the final queued request closes. Every marker, including
|
| 83 |
+
OpenCode's, also has a 30-minute ceiling so a lost close event cannot leave a
|
| 84 |
+
stale warning indefinitely. For unpaired CLIs, an actual operator key or a
|
| 85 |
native completion event can clear the signal sooner; process exit clears every
|
| 86 |
kind. Automatic terminal query replies do not count as operator input. The
|
| 87 |
ceiling can create a false negative for a dialog left open longer than 30
|
| 88 |
minutes; that is intentional because an indefinitely stale warning is the more
|
| 89 |
damaging failure mode.
|
| 90 |
|
| 91 |
+
Enforcement remains a possible later step per adapter, after every covered
|
| 92 |
+
dialog and subtype has been observed end-to-end in the deployed environment.
|
| 93 |
+
It should not be enabled globally: a notification such as Claude's
|
| 94 |
+
`agent_needs_input` may describe a teammate that needs attention rather than a
|
| 95 |
+
modal dialog owning the main pane's input.
|
| 96 |
+
|
| 97 |
## Verification
|
| 98 |
|
| 99 |
Automated coverage includes stale/mismatched launch markers, unrelated terminal
|
server/src/runner.js
CHANGED
|
@@ -2095,11 +2095,6 @@ export function attach(session, cols, rows) {
|
|
| 2095 |
export async function sendInput(id, text, { confirmEcho = false } = {}) {
|
| 2096 |
const host = hosts.get(id);
|
| 2097 |
if (!host || stopping.has(id)) throw new Error('session is not running');
|
| 2098 |
-
if (host.inputRequired.get()) {
|
| 2099 |
-
const error = new Error('session needs input in its terminal — a normal prompt was not sent into the open dialog');
|
| 2100 |
-
error.statusCode = 409;
|
| 2101 |
-
throw error;
|
| 2102 |
-
}
|
| 2103 |
host.inputRequired.observeInput();
|
| 2104 |
// Multi-line prompts go in as a bracketed paste so the CLI's composer treats
|
| 2105 |
// the inner newlines as soft line breaks instead of submitting early.
|
|
@@ -2150,11 +2145,6 @@ export async function sendInput(id, text, { confirmEcho = false } = {}) {
|
|
| 2150 |
export function pasteInput(id, text) {
|
| 2151 |
const host = hosts.get(id);
|
| 2152 |
if (!host || stopping.has(id)) throw new Error('session is not running');
|
| 2153 |
-
if (host.inputRequired.get()) {
|
| 2154 |
-
const error = new Error('session needs input in its terminal — text was not pasted into the open dialog');
|
| 2155 |
-
error.statusCode = 409;
|
| 2156 |
-
throw error;
|
| 2157 |
-
}
|
| 2158 |
const value = String(text || '');
|
| 2159 |
if (!value) return;
|
| 2160 |
host.inputRequired.observeInput();
|
|
|
|
| 2095 |
export async function sendInput(id, text, { confirmEcho = false } = {}) {
|
| 2096 |
const host = hosts.get(id);
|
| 2097 |
if (!host || stopping.has(id)) throw new Error('session is not running');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2098 |
host.inputRequired.observeInput();
|
| 2099 |
// Multi-line prompts go in as a bracketed paste so the CLI's composer treats
|
| 2100 |
// the inner newlines as soft line breaks instead of submitting early.
|
|
|
|
| 2145 |
export function pasteInput(id, text) {
|
| 2146 |
const host = hosts.get(id);
|
| 2147 |
if (!host || stopping.has(id)) throw new Error('session is not running');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2148 |
const value = String(text || '');
|
| 2149 |
if (!value) return;
|
| 2150 |
host.inputRequired.observeInput();
|
web/src/components/Overview.tsx
CHANGED
|
@@ -367,30 +367,29 @@ export function Card({ s, color, group, pending, isMobile, onOpen, onClose }: {
|
|
| 367 |
)}
|
| 368 |
</div>
|
| 369 |
|
| 370 |
-
{s.inputRequired
|
| 371 |
<InputRequiredNotice
|
| 372 |
input={s.inputRequired}
|
| 373 |
onOpenTerminal={() => { writePaneMode('terminal'); onOpen(s.id); }}
|
| 374 |
/>
|
| 375 |
-
) : (
|
| 376 |
-
<Composer
|
| 377 |
-
draft={draft}
|
| 378 |
-
sending={sending}
|
| 379 |
-
isMobile={isMobile}
|
| 380 |
-
inputRef={inputRef}
|
| 381 |
-
canSend={!!draft.trim() || images.length > 0}
|
| 382 |
-
above={<Attachments
|
| 383 |
-
attachments={images}
|
| 384 |
-
disabled={sending || !allowAttachments}
|
| 385 |
-
disabledReason={!allowAttachments ? 'Files are not available for remote agents yet — that agent cannot read files stored on this Space.' : undefined}
|
| 386 |
-
onFiles={addImages}
|
| 387 |
-
onRemove={removeImage}
|
| 388 |
-
/>}
|
| 389 |
-
onChange={setDraft}
|
| 390 |
-
onSend={send}
|
| 391 |
-
onPasteFiles={allowAttachments ? addImages : undefined}
|
| 392 |
-
/>
|
| 393 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
{(imageError || failed) && <div className="ov-note" role="alert">{imageError || failed}</div>}
|
| 395 |
</div>
|
| 396 |
);
|
|
|
|
| 367 |
)}
|
| 368 |
</div>
|
| 369 |
|
| 370 |
+
{s.inputRequired && (
|
| 371 |
<InputRequiredNotice
|
| 372 |
input={s.inputRequired}
|
| 373 |
onOpenTerminal={() => { writePaneMode('terminal'); onOpen(s.id); }}
|
| 374 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
)}
|
| 376 |
+
<Composer
|
| 377 |
+
draft={draft}
|
| 378 |
+
sending={sending}
|
| 379 |
+
isMobile={isMobile}
|
| 380 |
+
inputRef={inputRef}
|
| 381 |
+
canSend={!!draft.trim() || images.length > 0}
|
| 382 |
+
above={<Attachments
|
| 383 |
+
attachments={images}
|
| 384 |
+
disabled={sending || !allowAttachments}
|
| 385 |
+
disabledReason={!allowAttachments ? 'Files are not available for remote agents yet — that agent cannot read files stored on this Space.' : undefined}
|
| 386 |
+
onFiles={addImages}
|
| 387 |
+
onRemove={removeImage}
|
| 388 |
+
/>}
|
| 389 |
+
onChange={setDraft}
|
| 390 |
+
onSend={send}
|
| 391 |
+
onPasteFiles={allowAttachments ? addImages : undefined}
|
| 392 |
+
/>
|
| 393 |
{(imageError || failed) && <div className="ov-note" role="alert">{imageError || failed}</div>}
|
| 394 |
</div>
|
| 395 |
);
|
web/src/components/Sidebar.tsx
CHANGED
|
@@ -346,7 +346,10 @@ export default function Sidebar({
|
|
| 346 |
>
|
| 347 |
{/* The same three lights, but for a remote agent they mean connection,
|
| 348 |
not process: working / listening / not connected. */}
|
| 349 |
-
<span
|
|
|
|
|
|
|
|
|
|
| 350 |
<Logo cli={s.cli} size={12} tint={colorOf[s.cli]} />
|
| 351 |
{editing ? (
|
| 352 |
<input
|
|
|
|
| 346 |
>
|
| 347 |
{/* The same three lights, but for a remote agent they mean connection,
|
| 348 |
not process: working / listening / not connected. */}
|
| 349 |
+
<span
|
| 350 |
+
className={`status ${s.inputRequired ? 'needs-input' : s.state}`}
|
| 351 |
+
title={s.inputRequired ? 'needs input in terminal' : (isRemote(s.cli) ? REMOTE_STATE_LABEL : STATE_LABEL)[s.state]}
|
| 352 |
+
/>
|
| 353 |
<Logo cli={s.cli} size={12} tint={colorOf[s.cli]} />
|
| 354 |
{editing ? (
|
| 355 |
<input
|
web/src/components/conversation/ConversationView.tsx
CHANGED
|
@@ -559,7 +559,7 @@ export default function ConversationView({
|
|
| 559 |
{!readOnly && session.inputRequired && (
|
| 560 |
<InputRequiredNotice input={session.inputRequired} onOpenTerminal={() => writePaneMode('terminal')} />
|
| 561 |
)}
|
| 562 |
-
{!readOnly &&
|
| 563 |
<Composer
|
| 564 |
className="cxv-live"
|
| 565 |
containerClassName="cxv-composer"
|
|
|
|
| 559 |
{!readOnly && session.inputRequired && (
|
| 560 |
<InputRequiredNotice input={session.inputRequired} onOpenTerminal={() => writePaneMode('terminal')} />
|
| 561 |
)}
|
| 562 |
+
{!readOnly && (
|
| 563 |
<Composer
|
| 564 |
className="cxv-live"
|
| 565 |
containerClassName="cxv-composer"
|
web/src/conversation.css
CHANGED
|
@@ -401,6 +401,7 @@ mark.cx-hit.on { background: var(--accent); color: var(--panel); }
|
|
| 401 |
padding: 4px 8px; background: var(--panel-2); color: var(--text); font: inherit; cursor: pointer;
|
| 402 |
}
|
| 403 |
.input-required button:hover { border-color: var(--accent); }
|
|
|
|
| 404 |
@media (max-width: 720px) and (pointer: coarse) {
|
| 405 |
/* Restating styles.css's iOS guard: 16px is what stops zoom-on-focus, and the
|
| 406 |
rule above is later in the cascade, so it would otherwise win. A phone
|
|
|
|
| 401 |
padding: 4px 8px; background: var(--panel-2); color: var(--text); font: inherit; cursor: pointer;
|
| 402 |
}
|
| 403 |
.input-required button:hover { border-color: var(--accent); }
|
| 404 |
+
.input-required + .ov-composer { border-top: 0; }
|
| 405 |
@media (max-width: 720px) and (pointer: coarse) {
|
| 406 |
/* Restating styles.css's iOS guard: 16px is what stops zoom-on-focus, and the
|
| 407 |
rule above is later in the cascade, so it would otherwise win. A phone
|