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 replace their reply composer with a **Needs
13
- input** banner and an **open terminal** action. The session tile and sidebar
14
- also identify the condition. The normal composer is hidden because submitting
15
- text while a choice menu owns stdin can select the wrong option. The prompt and
16
- attachment APIs refuse the same operation with HTTP 409 while the signal is
17
- active; direct terminal input remains available.
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 disable
84
- reader prompting 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
  ## 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 className={`status ${s.state}`} title={s.inputRequired ? 'needs input in terminal' : (isRemote(s.cli) ? REMOTE_STATE_LABEL : STATE_LABEL)[s.state]} />
 
 
 
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 && !session.inputRequired && (
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