@crouton-kit/humanloop 0.3.38 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/api.d.ts +3 -2
  2. package/dist/api.js +2 -2
  3. package/dist/browser/server.d.ts +15 -5
  4. package/dist/browser/server.js +110 -14
  5. package/dist/cli.js +0 -0
  6. package/dist/editor/roundtrip.d.ts +5 -0
  7. package/dist/editor/roundtrip.js +36 -0
  8. package/dist/inbox/completion.js +1 -27
  9. package/dist/inbox/controller.d.ts +32 -3
  10. package/dist/inbox/controller.js +427 -35
  11. package/dist/inbox/convention.d.ts +9 -2
  12. package/dist/inbox/convention.js +51 -3
  13. package/dist/inbox/deck-adapter.d.ts +10 -2
  14. package/dist/inbox/deck-adapter.js +23 -12
  15. package/dist/inbox/deck-schema.d.ts +2 -0
  16. package/dist/inbox/deck-schema.js +4 -4
  17. package/dist/inbox/followup.d.ts +57 -0
  18. package/dist/inbox/followup.js +117 -0
  19. package/dist/inbox/registry.d.ts +4 -0
  20. package/dist/inbox/registry.js +9 -4
  21. package/dist/inbox/tickets.d.ts +5 -0
  22. package/dist/inbox/tickets.js +25 -32
  23. package/dist/inbox/tui.d.ts +1 -1
  24. package/dist/inbox/tui.js +58 -25
  25. package/dist/inbox/visual.d.ts +130 -0
  26. package/dist/inbox/visual.js +747 -0
  27. package/dist/index.d.ts +5 -3
  28. package/dist/index.js +2 -1
  29. package/dist/tui/app.d.ts +3 -6
  30. package/dist/tui/app.js +130 -97
  31. package/dist/tui/input.d.ts +5 -1
  32. package/dist/tui/input.js +41 -15
  33. package/dist/tui/log.d.ts +2 -0
  34. package/dist/tui/log.js +53 -0
  35. package/dist/tui/render.js +44 -27
  36. package/dist/tui/terminal.d.ts +1 -0
  37. package/dist/tui/terminal.js +5 -0
  38. package/dist/tui/tmux.js +44 -22
  39. package/dist/types.d.ts +71 -7
  40. package/dist/web/assets/{index-DhbBiRqS.js → index-YFwgZZMg.js} +2 -2
  41. package/dist/web/index.html +1 -1
  42. package/package.json +6 -3
  43. package/dist/conversation/reader.d.ts +0 -6
  44. package/dist/conversation/reader.js +0 -58
  45. package/dist/visuals/generate.d.ts +0 -9
  46. package/dist/visuals/generate.js +0 -79
@@ -134,30 +134,46 @@ function buildItemReviewLayout(state, cols, rows) {
134
134
  // and live in the scrollable region so long content never overflows the fixed
135
135
  // header — agents put rich prose in either field, so both must render markdown.
136
136
  const bodyLines = [];
137
- if (interaction.subtitle) {
138
- bodyLines.push('');
139
- for (const line of renderMarkdown(interaction.subtitle, maxW)) {
140
- bodyLines.push(` ${line}`);
137
+ if (state.bodyMode === 'question') {
138
+ if (interaction.subtitle) {
139
+ bodyLines.push('');
140
+ for (const line of renderMarkdown(interaction.subtitle, maxW)) {
141
+ bodyLines.push(` ${line}`);
142
+ }
141
143
  }
142
- }
143
- if (interaction.body) {
144
- bodyLines.push('');
145
- for (const line of renderMarkdown(interaction.body, maxW)) {
146
- bodyLines.push(` ${line}`);
144
+ if (interaction.body) {
145
+ bodyLines.push('');
146
+ for (const line of renderMarkdown(interaction.body, maxW)) {
147
+ bodyLines.push(` ${line}`);
148
+ }
147
149
  }
148
150
  }
149
- if (visual && visual.status === 'ready' && state.detailExpanded) {
150
- bodyLines.push('');
151
- bodyLines.push(` ${DIM}── context ${hline(maxW - 12)}${RESET}`);
152
- for (const vl of visual.content.split('\n')) {
153
- bodyLines.push(` ${vl}`);
151
+ if (state.bodyMode === 'visual') {
152
+ if (visual?.status === 'ready') {
153
+ bodyLines.push('');
154
+ bodyLines.push(` ${DIM}── context ${hline(maxW - 12)}${RESET}`);
155
+ for (const vl of visual.content.split('\n')) {
156
+ bodyLines.push(` ${vl}`);
157
+ }
158
+ bodyLines.push(` ${DIM}${hline(maxW)}${RESET}`);
159
+ }
160
+ if (state.followUp !== undefined && state.followUp.status !== 'idle') {
161
+ bodyLines.push('');
162
+ bodyLines.push(` ${DIM}── follow-up ${hline(Math.max(0, maxW - 14))}${RESET}`);
163
+ if (state.followUp.status === 'running')
164
+ bodyLines.push(` ${DIM}consulting…${RESET}`);
165
+ else if (state.followUp.status === 'ready') {
166
+ for (const line of renderMarkdown(state.followUp.markdown, maxW))
167
+ bodyLines.push(` ${line}`);
168
+ }
169
+ else
170
+ bodyLines.push(` ${YELLOW}${state.followUp.error}${RESET}`);
154
171
  }
155
- bodyLines.push(` ${DIM}${hline(maxW)}${RESET}`);
156
172
  }
157
173
  // Post-body: visual status hint, input buffer or actions, footer (always visible)
158
174
  const postLines = [];
159
175
  postLines.push('');
160
- if (visual) {
176
+ if (state.bodyMode === 'visual' && visual) {
161
177
  if (visual.status === 'loading') {
162
178
  postLines.push(` ${DIM}loading context...${RESET}`);
163
179
  postLines.push('');
@@ -166,10 +182,6 @@ function buildItemReviewLayout(state, cols, rows) {
166
182
  postLines.push(` ${YELLOW}visual context unavailable${RESET}`);
167
183
  postLines.push('');
168
184
  }
169
- else if (!state.detailExpanded) {
170
- postLines.push(` ${DIM}[space] expand context${RESET}`);
171
- postLines.push('');
172
- }
173
185
  }
174
186
  if (state.inputMode) {
175
187
  postLines.push(` ${DIM}${hline(maxW)}${RESET}`);
@@ -204,10 +216,10 @@ function buildItemReviewLayout(state, cols, rows) {
204
216
  postLines.push(attachedLine);
205
217
  }
206
218
  postLines.push('');
207
- postLines.push(` ${DIM}enter${RESET} submit ${DIM}^J/⌥⏎${RESET} newline ${DIM}^O${RESET} editor ${DIM}esc${RESET} cancel`);
219
+ postLines.push(` ${DIM}enter${RESET} submit ${DIM}^J/⌥⏎${RESET} newline${state.editorAvailable ? ` ${DIM}^O${RESET} editor` : ''} ${DIM}esc${RESET} cancel`);
208
220
  }
209
221
  else {
210
- postLines.push(...renderActions(interaction, state.selectedAction, maxW, response));
222
+ postLines.push(...renderActions(interaction, state.selectedAction, maxW, response, state.followUp?.status !== 'running'));
211
223
  }
212
224
  // Transient hint (e.g. an empty multi-select Enter that was rejected). Sits
213
225
  // just above the footer; cleared on the next keypress.
@@ -236,6 +248,7 @@ function buildItemReviewLayout(state, cols, rows) {
236
248
  export function clampItemReviewScroll(state, cols, rows) {
237
249
  const { maxScroll } = buildItemReviewLayout(state, cols, rows);
238
250
  state.scrollOffset = Math.max(0, Math.min(state.scrollOffset || 0, maxScroll));
251
+ state.bodyScrollOffsets[state.bodyMode] = state.scrollOffset;
239
252
  }
240
253
  export function renderItemReview(state, cols, rows) {
241
254
  const { interaction, preLines, bodyLines, postLines, maxW, bodyHeight, maxScroll, overflows } = buildItemReviewLayout(state, cols, rows);
@@ -262,18 +275,22 @@ export function renderItemReview(state, cols, rows) {
262
275
  `${DIM}n/p${RESET} prev/next`,
263
276
  `${DIM}space${RESET} toggle`,
264
277
  `${DIM}enter${RESET} confirm`,
278
+ `${DIM}shift-tab${RESET} ${state.bodyMode === 'question' ? 'visual' : 'question'}`,
265
279
  `${DIM}q${RESET} overview`,
266
280
  ]
267
281
  : [
268
282
  `${DIM}n/p${RESET} prev/next`,
269
- `${DIM}space${RESET} expand`,
283
+ `${DIM}shift-tab${RESET} ${state.bodyMode === 'question' ? 'visual' : 'question'}`,
270
284
  `${DIM}q${RESET} overview`,
271
285
  ];
272
286
  if (overflows) {
273
287
  footerParts.unshift(state.inputMode ? `${DIM}pgup/pgdn${RESET} scroll` : `${DIM}u/d${RESET} scroll`);
274
288
  }
275
- if (state.inputMode === null)
289
+ if (state.inputMode === null) {
290
+ if (state.followUpAvailable)
291
+ footerParts.push(`${DIM}?${RESET} follow-up`);
276
292
  footerParts.push(`${DIM}w${RESET} browser`);
293
+ }
277
294
  const footer = ` ${footerParts.join(' ')}`;
278
295
  // Assemble — pad to fill rows so post-body sits at the bottom
279
296
  const lines = [...preLines, ...visibleBody, ...postLines];
@@ -288,7 +305,7 @@ export function renderItemReview(state, cols, rows) {
288
305
  // whole block when the terminal is wider than that.
289
306
  return centerHorizontal(clamped, cols, maxW + 2);
290
307
  }
291
- function renderActions(interaction, selectedAction, maxW, existing) {
308
+ function renderActions(interaction, selectedAction, maxW, existing, showFocus = true) {
292
309
  const lines = [];
293
310
  const opts = interaction.options;
294
311
  // Prefix on first row: " X [s] " — 2 + 1 (cursor) + 1 + 3 ([s]) + 1 = 8 visible cols.
@@ -301,7 +318,7 @@ function renderActions(interaction, selectedAction, maxW, existing) {
301
318
  const optionComments = existing !== undefined ? existing.optionComments : undefined;
302
319
  for (let i = 0; i < opts.length; i++) {
303
320
  const o = opts[i];
304
- const cursor = i === selectedAction ? `${CYAN}▸${RESET}` : ' ';
321
+ const cursor = showFocus && i === selectedAction ? `${CYAN}▸${RESET}` : ' ';
305
322
  const sc = o.shortcut === undefined ? ' ' : o.shortcut;
306
323
  const keyBadge = `${DIM}[${sc}]${RESET}`;
307
324
  const box = multi
@@ -329,7 +346,7 @@ function renderActions(interaction, selectedAction, maxW, existing) {
329
346
  }
330
347
  }
331
348
  if (interaction.allowFreetext && opts.length > 0) {
332
- const cursor = opts.length === selectedAction ? `${CYAN}▸${RESET}` : ' ';
349
+ const cursor = showFocus && opts.length === selectedAction ? `${CYAN}▸${RESET}` : ' ';
333
350
  let label;
334
351
  if (interaction.freetextLabel !== undefined)
335
352
  label = interaction.freetextLabel;
@@ -19,6 +19,7 @@ export interface Key {
19
19
  ctrl: boolean;
20
20
  meta: boolean;
21
21
  tab: boolean;
22
+ backTab: boolean;
22
23
  backspace: boolean;
23
24
  }
24
25
  export type KeypressHandler = (input: string, key: Key) => void;
@@ -17,6 +17,7 @@ function emptyKey() {
17
17
  ctrl: false,
18
18
  meta: false,
19
19
  tab: false,
20
+ backTab: false,
20
21
  backspace: false,
21
22
  };
22
23
  }
@@ -69,6 +70,10 @@ export function parseKeypress(data) {
69
70
  key.del = true;
70
71
  return { input: '', key };
71
72
  }
73
+ if (str === '\x1b[Z') {
74
+ key.backTab = true;
75
+ return { input: '', key };
76
+ }
72
77
  // Alt+Enter inserts a newline in freetext (distinct from Enter=submit). Must
73
78
  // precede the bare-ESC and meta-backspace checks so the two-byte sequence
74
79
  // isn't swallowed as a lone escape.
package/dist/tui/tmux.js CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { existsSync, mkdirSync, rmSync } from 'node:fs';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
+ import { logPopupEvent } from './log.js';
7
8
  const POPUP_TITLE = 'humanloop · inbox';
8
9
  const POPUP_STYLE = 'bg=#20242d';
9
10
  const POPUP_BORDER_STYLE = 'fg=#5c6370';
@@ -54,33 +55,42 @@ function acquireStartupLock(path) {
54
55
  }
55
56
  export async function toggleInboxPopup(target) {
56
57
  const socket = target?.socket ?? tmuxSocketFromEnvironment();
57
- if (socket === undefined)
58
+ if (socket === undefined) {
59
+ logPopupEvent('toggle.rejected', { reason: 'not_in_tmux' });
58
60
  return 'not_in_tmux';
61
+ }
59
62
  const inferred = target?.client ?? inferTmuxClient(socket, target?.targetPane);
60
- if (inferred === 'ambiguous')
61
- return 'ambiguous_client';
62
- if (inferred === undefined)
63
+ if (inferred === 'ambiguous' || inferred === undefined) {
64
+ logPopupEvent('toggle.rejected', { reason: 'ambiguous_client', socket, targetPane: target?.targetPane });
63
65
  return 'ambiguous_client';
66
+ }
64
67
  const resolved = { socket, client: inferred, targetPane: target?.targetPane };
65
68
  const paths = popupPaths(resolved);
69
+ logPopupEvent('toggle.requested', { ...resolved, controlSocket: paths.controlSocket });
66
70
  mkdirSync(join(paths.controlSocket, '..'), { recursive: true, mode: 0o700 });
67
- if (await requestPopupClose(paths.controlSocket))
71
+ if (await requestPopupClose(paths.controlSocket)) {
72
+ logPopupEvent('toggle.closed', { ...resolved, controlSocket: paths.controlSocket });
68
73
  return 'closed';
69
- // A concurrent toggle for this same client won the startup lock and is opening the popup.
70
- // This gesture must not launch a second one; report a benign no-op close rather than a
71
- // generic failure, so exactly one popup exists and the loser never exits with an error.
72
- if (!acquireStartupLock(paths.startupLock))
74
+ }
75
+ // A concurrent toggle for this same client owns popup startup. This gesture
76
+ // leaves that startup as the sole owner and reports a benign close result.
77
+ if (!acquireStartupLock(paths.startupLock)) {
78
+ logPopupEvent('toggle.coalesced', { ...resolved, controlSocket: paths.controlSocket });
73
79
  return 'closed';
80
+ }
74
81
  try {
75
- // Re-probe under the lock: between the first probe and acquiring the lock a concurrent
76
- // toggle may have brought a live popup up. Deleting its control socket now would orphan
77
- // that popup, so close it instead. The finally below releases the lock on every path.
78
- if (await requestPopupClose(paths.controlSocket))
82
+ // Re-probe under the lock so a popup that became live during lock acquisition
83
+ // is closed through its controller rather than orphaned.
84
+ if (await requestPopupClose(paths.controlSocket)) {
85
+ logPopupEvent('toggle.closed', { ...resolved, controlSocket: paths.controlSocket, phase: 'locked-reprobe' });
79
86
  return 'closed';
87
+ }
80
88
  if (existsSync(paths.controlSocket))
81
89
  rmSync(paths.controlSocket, { force: true });
82
90
  const command = `${quote(process.execPath)} ${quote(fileURLToPath(new URL('../cli.js', import.meta.url)))} inbox open --control-socket ${quote(paths.controlSocket)}`;
83
- return await launchPopup(socket, resolved, paths.controlSocket, command);
91
+ const result = await launchPopup(socket, resolved, paths.controlSocket, command);
92
+ logPopupEvent('toggle.completed', { ...resolved, controlSocket: paths.controlSocket, result });
93
+ return result;
84
94
  }
85
95
  finally {
86
96
  rmSync(paths.startupLock, { recursive: true, force: true });
@@ -100,17 +110,27 @@ async function requestPopupClose(controlSocket) {
100
110
  /** Launch one popup and report `opened` only once its controller owns the control socket. */
101
111
  async function launchPopup(socket, target, controlSocket, command) {
102
112
  const args = ['-S', socket, 'display-popup', '-E', '-c', target.client, ...inboxPopupFlags(), ...(target.targetPane === undefined ? [] : ['-t', target.targetPane]), command];
113
+ const startedAt = Date.now();
103
114
  const child = spawn('tmux', args, { stdio: ['ignore', 'ignore', 'pipe'], detached: true });
115
+ logPopupEvent('popup.spawned', { ...target, controlSocket, pid: child.pid, args });
104
116
  let stderr = '';
105
- child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
117
+ child.stderr.on('data', (chunk) => {
118
+ stderr += chunk.toString('utf8');
119
+ if (stderr.length > 8192)
120
+ stderr = stderr.slice(-8192);
121
+ });
106
122
  const exited = new Promise((resolve) => {
107
- // `display-popup -E` blocks until our popup closes, so a successful open keeps the child
108
- // alive while the control socket comes up (detected below). An early exit before that means
109
- // the popup command never ran: tmux 3.x silently declines to stack a popup on a client that
110
- // already shows one (exit 0, command dropped), which is exactly the foreign-popup case the
111
- // design must report as `other_popup` without disturbing the existing popup or its process.
112
- child.once('exit', (code) => resolve(code === 0 || /popup/i.test(stderr) ? 'other_popup' : 'failed'));
113
- child.once('error', () => resolve('failed'));
123
+ // `display-popup -E` blocks until the popup closes. An exit before the
124
+ // controller socket is live means tmux declined or failed the launch.
125
+ child.once('exit', (code, signal) => {
126
+ const result = code === 0 || /popup/i.test(stderr) ? 'other_popup' : 'failed';
127
+ logPopupEvent('popup.exited', { ...target, controlSocket, pid: child.pid, code, signal, result, stderr: stderr.trim(), elapsedMs: Date.now() - startedAt });
128
+ resolve(result);
129
+ });
130
+ child.once('error', (error) => {
131
+ logPopupEvent('popup.error', { ...target, controlSocket, pid: child.pid, error: error.message, stderr: stderr.trim(), elapsedMs: Date.now() - startedAt });
132
+ resolve('failed');
133
+ });
114
134
  });
115
135
  const { Socket } = await import('node:net');
116
136
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -125,12 +145,14 @@ async function launchPopup(socket, target, controlSocket, command) {
125
145
  }),
126
146
  ]);
127
147
  if (outcome === 'opened') {
148
+ logPopupEvent('popup.opened', { ...target, controlSocket, pid: child.pid, elapsedMs: Date.now() - startedAt });
128
149
  child.unref();
129
150
  return 'opened';
130
151
  }
131
152
  if (outcome !== 'pending')
132
153
  return outcome;
133
154
  }
155
+ logPopupEvent('popup.startup_timeout', { ...target, controlSocket, pid: child.pid, stderr: stderr.trim(), elapsedMs: Date.now() - startedAt });
134
156
  return 'failed';
135
157
  }
136
158
  /** The static tmux `display-popup` geometry and style flags; the client and target pane are added per-invocation. */
package/dist/types.d.ts CHANGED
@@ -68,6 +68,8 @@ export interface DeckSource {
68
68
  * a crouter canvas node. Lets per-node attention scoping attribute the ask
69
69
  * to the node that raised it rather than every sibling sharing the cwd. */
70
70
  nodeId?: string;
71
+ /** Host-owned durable Visual capability marker. A registered handler is also required. */
72
+ visual?: 'humanloop.visual/v1';
71
73
  }
72
74
  export interface Deck {
73
75
  title?: string;
@@ -103,8 +105,21 @@ export interface FeedbackResult {
103
105
  export interface VisualBlock {
104
106
  questionId: string;
105
107
  content: string;
108
+ /** Original model output, retained so resize can re-render locally. */
109
+ markdown?: string;
106
110
  status: 'loading' | 'ready' | 'error';
107
111
  }
112
+ export type FollowUpState = {
113
+ status: 'idle';
114
+ } | {
115
+ status: 'running';
116
+ } | {
117
+ status: 'ready';
118
+ markdown: string;
119
+ } | {
120
+ status: 'error';
121
+ error: string;
122
+ };
108
123
  export type Phase = 'overview' | 'item-review' | 'final';
109
124
  export type InputMode = null | {
110
125
  kind: 'comment';
@@ -115,6 +130,10 @@ export type InputMode = null | {
115
130
  kind: 'freetext';
116
131
  buffer: string;
117
132
  cursor: number;
133
+ } | {
134
+ kind: 'follow-up';
135
+ buffer: string;
136
+ cursor: number;
118
137
  };
119
138
  export interface TuiState {
120
139
  phase: Phase;
@@ -128,8 +147,16 @@ export interface TuiState {
128
147
  preAnsweredIds: Set<string>;
129
148
  inputMode: InputMode;
130
149
  selectedAction: number;
131
- detailExpanded: boolean;
150
+ bodyMode: 'question' | 'visual';
132
151
  scrollOffset: number;
152
+ bodyScrollOffsets: {
153
+ question: number;
154
+ visual: number;
155
+ };
156
+ /** The mounting host provided an active Ctrl+O editor callback. */
157
+ editorAvailable: boolean;
158
+ followUpAvailable: boolean;
159
+ followUp?: FollowUpState;
133
160
  /** Transient one-line notice shown in item-review (e.g. an empty multi-select
134
161
  * Enter that was rejected). Cleared on the next keypress. */
135
162
  hint?: string;
@@ -228,18 +255,52 @@ export interface DisplayOpts {
228
255
  /** Pane budget per window before `'auto'` opens a new window. Default 3. */
229
256
  maxPanes?: number;
230
257
  }
231
- export type GenerateVisual = (interaction: Interaction) => Promise<{
232
- ok: true;
233
- ansi: string;
258
+ /** Width-free Interaction snapshot shared by the panel and durable Visual protocol. */
259
+ export interface CanonicalInteractionOption {
260
+ id: string;
261
+ label: string;
262
+ description?: string;
263
+ }
264
+ export interface CanonicalInteraction {
265
+ id: string;
266
+ title: string;
267
+ subtitle?: string;
268
+ body?: string;
269
+ options: CanonicalInteractionOption[];
270
+ multiSelect?: boolean;
271
+ allowFreetext?: boolean;
272
+ freetextLabel?: string;
273
+ kind?: InteractionKind;
274
+ preAnswered?: InteractionPreAnswer;
275
+ }
276
+ export interface VisualRequest {
277
+ requestId: string;
278
+ generationId: string;
279
+ interaction: CanonicalInteraction;
280
+ }
281
+ export type VisualResult = {
282
+ status: 'ready';
234
283
  markdown: string;
235
284
  } | {
236
- ok: false;
285
+ status: 'error';
237
286
  error: string;
238
- }>;
287
+ };
288
+ export interface VisualHandle {
289
+ result: Promise<VisualResult>;
290
+ /** Synchronous and idempotent. A canceled handle has no renderable result. */
291
+ cancel(): void;
292
+ }
293
+ /** Host-injected, Markdown-only Visual capability. Rendering width belongs to the panel. */
294
+ export type VisualProvider = (request: VisualRequest) => VisualHandle;
239
295
  export interface MountedPanelOpts {
240
296
  deck: Deck;
241
297
  progressPath?: string;
242
- generateVisual?: GenerateVisual;
298
+ visualProvider?: VisualProvider;
299
+ /** Host callback for Ctrl+O while a comment/freetext buffer is active. */
300
+ onEditorRequest?: () => void;
301
+ followUpAvailable?: boolean;
302
+ onFollowUpRequest?: (question: string) => void;
303
+ onFollowUpCancel?: () => void;
243
304
  cols: number;
244
305
  rows: number;
245
306
  onProgress?: (responses: InteractionResponse[]) => void;
@@ -258,7 +319,10 @@ export interface MountedPanel {
258
319
  unmount(): void;
259
320
  loadDeck(deck: Deck, opts?: {
260
321
  progressPath?: string;
322
+ visualProvider?: VisualProvider;
261
323
  }): void;
324
+ setFollowUpHandlers(available: boolean, onRequest?: (question: string) => void, onCancel?: () => void): void;
325
+ setFollowUpState(state: FollowUpState): void;
262
326
  canAcceptHostKeys(): boolean;
263
327
  /**
264
328
  * True when the deck is at its top level: overview phase with no active