@multiclawcli/dashboard 0.6.3 → 0.7.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.
package/dist/tui.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  Orchestrator,
13
13
  validateRuntimeQuestionContent
14
14
  } from "@multiclawcli/core";
15
- import { Box as Box5, Text as Text5, useApp, useInput, useWindowSize } from "ink";
15
+ import { Box as Box6, Text as Text6, useApp, useInput, useWindowSize } from "ink";
16
16
  import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
17
17
 
18
18
  // src/tui/atomic.ts
@@ -127,7 +127,7 @@ var COMMANDS = [
127
127
  {
128
128
  name: "respond",
129
129
  args: "<question id> <JSON object>",
130
- description: "Respond to a structured tool question",
130
+ description: "Answer a question in one line of JSON",
131
131
  availability: "always"
132
132
  },
133
133
  {
@@ -144,14 +144,14 @@ var COMMANDS = [
144
144
  },
145
145
  {
146
146
  name: "approve",
147
- args: "<request id> <option number>",
148
- description: "Choose an offered permission option explicitly",
147
+ args: "[id] <number>",
148
+ description: "Approve the request on screen, or a named one",
149
149
  availability: "always"
150
150
  },
151
151
  {
152
152
  name: "deny",
153
- args: "<request id>",
154
- description: "Deny a pending permission request",
153
+ args: "[id]",
154
+ description: "Deny the request on screen, or a named one",
155
155
  availability: "always"
156
156
  },
157
157
  { name: "help", description: "Show the key map", availability: "always" },
@@ -371,6 +371,8 @@ var RAIL_MIN_ROWS = 16;
371
371
  var MAX_RAIL_ROWS = 2;
372
372
  var MAX_MENU_ROWS = 7;
373
373
  var MAX_QUEUE_ROWS = 3;
374
+ var MAX_INTERACTION_ROWS = 10;
375
+ var MIN_INTERACTION_ROWS = 4;
374
376
  var MIN_BODY_ROWS = 3;
375
377
  function measureRailRows(cells, columns) {
376
378
  if (!cells || cells.length === 0) return 0;
@@ -396,14 +398,17 @@ function computeLayout(input) {
396
398
  const showRail = !welcome && !input.overlay && rows >= RAIL_MIN_ROWS && input.hasRail;
397
399
  const menuRows = Math.min(Math.max(0, input.menuRows), MAX_MENU_ROWS);
398
400
  const queueRows = Math.min(Math.max(0, input.queueRows ?? 0), MAX_QUEUE_ROWS);
401
+ const promptRows = Math.max(1, input.promptRows ?? 1);
399
402
  const railRows = showRail ? Math.min(MAX_RAIL_ROWS, Math.max(1, measureRailRows(input.railCells, columns))) : 0;
400
- const reserved = 1 + // header
403
+ const chrome = 1 + // header
401
404
  1 + // rule under the header
402
405
  (railRows > 0 ? railRows + 1 : 0) + // pipeline rail plus the gap above it
403
406
  1 + // gap before the body
404
407
  (input.notice ? 1 : 0) + (input.viewing ? 1 : 0) + // "viewing turn N" banner
405
- queueRows + menuRows + 1 + // prompt
406
- 1;
408
+ queueRows + menuRows + promptRows + 1;
409
+ const barRoom = Math.max(0, rows - chrome - MIN_BODY_ROWS - 1);
410
+ const interactionRows2 = barRoom < MIN_INTERACTION_ROWS ? 0 : Math.min(Math.max(0, input.interactionRows ?? 0), MAX_INTERACTION_ROWS, barRoom);
411
+ const reserved = chrome + (interactionRows2 > 0 ? interactionRows2 + 1 : 0);
407
412
  const bodyHeight = Math.max(MIN_BODY_ROWS, rows - reserved);
408
413
  const showTeam = !compact;
409
414
  const teamWidth = Math.min(30, Math.max(18, Math.floor(columns * 0.24)));
@@ -423,7 +428,8 @@ function computeLayout(input) {
423
428
  teamWidth,
424
429
  streamWidth,
425
430
  menuRows,
426
- queueRows
431
+ queueRows,
432
+ interactionRows: interactionRows2
427
433
  };
428
434
  }
429
435
 
@@ -582,8 +588,8 @@ function QueueStrip({ items, max = MAX_QUEUE_ROWS }) {
582
588
  )) });
583
589
  }
584
590
 
585
- // src/tui/components/overlays.tsx
586
- import { Box as Box2, Text as Text2 } from "ink";
591
+ // src/tui/components/interaction.tsx
592
+ import { Box as Box3, Text as Text3 } from "ink";
587
593
 
588
594
  // src/tui/state.ts
589
595
  var MAX_LOG_CHARS = 8e3;
@@ -1077,14 +1083,281 @@ function visibleLog(turn, viewport, width) {
1077
1083
  };
1078
1084
  }
1079
1085
 
1086
+ // src/tui/interaction.ts
1087
+ function clean(value) {
1088
+ return stripAnsi(value).replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").trim();
1089
+ }
1090
+ var KIND_LABEL = {
1091
+ allow_once: "allow once",
1092
+ allow_always: "always",
1093
+ reject_once: "deny",
1094
+ reject_always: "deny always"
1095
+ };
1096
+ function permissionChoices(request) {
1097
+ return request.options.slice(0, 9).map((option, index) => ({
1098
+ key: String(index + 1),
1099
+ optionId: option.optionId,
1100
+ label: clean(option.name) || KIND_LABEL[option.kind],
1101
+ // The one option with an effect that outlives this request.
1102
+ ...option.kind === "allow_always" || option.kind === "reject_always" ? { note: "this session" } : {}
1103
+ }));
1104
+ }
1105
+ var MAX_IMPACT_ROWS = 2;
1106
+ function isRecord(value) {
1107
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1108
+ }
1109
+ function lineCount(text) {
1110
+ return text.split("\n").length;
1111
+ }
1112
+ function describeImpact(request, width) {
1113
+ const input = request.input;
1114
+ if (!isRecord(input)) return [];
1115
+ const room = Math.max(8, width);
1116
+ const command = typeof input.command === "string" ? input.command : void 0;
1117
+ if (command !== void 0) {
1118
+ const total = lineCount(command);
1119
+ return total > 1 ? [truncate(`${total} lines`, room)] : [];
1120
+ }
1121
+ const file = ["path", "file_path", "file", "filename"].map((key) => input[key]).find((value) => typeof value === "string");
1122
+ if (file === void 0) return [];
1123
+ const before = typeof input.old_string === "string" ? lineCount(input.old_string) : void 0;
1124
+ const after = typeof input.new_string === "string" ? lineCount(input.new_string) : void 0;
1125
+ if (before !== void 0 && after !== void 0) {
1126
+ return [truncate(`replaces ${before} line${before === 1 ? "" : "s"} with ${after}`, room)];
1127
+ }
1128
+ if (typeof input.content === "string") {
1129
+ const written = lineCount(input.content);
1130
+ return [truncate(`writes ${written} line${written === 1 ? "" : "s"}`, room)];
1131
+ }
1132
+ return [];
1133
+ }
1134
+ function moveCursor(cursor, delta, total) {
1135
+ if (total <= 0) return void 0;
1136
+ if (cursor === void 0) return delta > 0 ? 0 : total - 1;
1137
+ return (cursor + delta + total) % total;
1138
+ }
1139
+ function jumpCursor(digit, total) {
1140
+ if (!/^[1-9]$/.test(digit)) return void 0;
1141
+ const index = Number(digit) - 1;
1142
+ return index < total ? index : void 0;
1143
+ }
1144
+ function waitingLine(rest) {
1145
+ if (rest.length === 0) return void 0;
1146
+ const named = rest.slice(0, 3).map(
1147
+ (request) => `${clean(request.agentId)} (${clean(request.toolName ?? request.kind ?? "tool")})`
1148
+ );
1149
+ const more = rest.length - named.length;
1150
+ return `also waiting \xB7 ${named.join(", ")}${more > 0 ? ` \xB7 +${more} more` : ""}`;
1151
+ }
1152
+ function permissionBarRows(request, width, rest = []) {
1153
+ const impact = Math.min(MAX_IMPACT_ROWS, describeImpact(request, width).length);
1154
+ return 1 + 1 + impact + permissionChoices(request).length + (rest.length > 0 ? 1 : 0) + 1;
1155
+ }
1156
+ function choiceFrom(value) {
1157
+ if (typeof value === "string") return { value, title: value };
1158
+ if (!isRecord(value) || typeof value.const !== "string") return void 0;
1159
+ const title = typeof value.title === "string" && value.title !== "" ? clean(value.title) : value.const;
1160
+ const description = typeof value.description === "string" ? clean(value.description) : void 0;
1161
+ return { value: value.const, title, ...description ? { description } : {} };
1162
+ }
1163
+ function fieldChoices(field) {
1164
+ const items = isRecord(field.items) ? field.items : void 0;
1165
+ const raw = field.oneOf ?? field.enum ?? items?.anyOf ?? items?.enum;
1166
+ if (!Array.isArray(raw)) return [];
1167
+ return raw.map(choiceFrom).filter((choice) => choice !== void 0);
1168
+ }
1169
+ function bound(value) {
1170
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1171
+ }
1172
+ function fieldBounds(field) {
1173
+ return {
1174
+ min: bound(field.minimum),
1175
+ max: bound(field.maximum),
1176
+ minItems: bound(field.minItems),
1177
+ maxItems: bound(field.maxItems),
1178
+ minLength: bound(field.minLength),
1179
+ maxLength: bound(field.maxLength)
1180
+ };
1181
+ }
1182
+ function formFields(request) {
1183
+ const schema = request.requestedSchema;
1184
+ const required = schema.required ?? [];
1185
+ return Object.entries(schema.properties ?? {}).map(([key, raw]) => {
1186
+ const field = isRecord(raw) ? raw : {};
1187
+ const choices = fieldChoices(field);
1188
+ const type = field.type;
1189
+ const kind = type === "boolean" ? "boolean" : type === "number" || type === "integer" ? "number" : type === "array" ? "multi" : choices.length > 0 ? "choice" : "text";
1190
+ const title = typeof field.title === "string" && field.title !== "" ? clean(field.title) : key;
1191
+ const description = typeof field.description === "string" ? clean(field.description) : void 0;
1192
+ return {
1193
+ key,
1194
+ title,
1195
+ required: required.includes(key),
1196
+ kind,
1197
+ choices,
1198
+ bounds: fieldBounds(field),
1199
+ ...description === void 0 ? {} : { description }
1200
+ };
1201
+ });
1202
+ }
1203
+ function boundsText(field) {
1204
+ const span = (low, high, unit = "") => {
1205
+ if (low !== void 0 && high !== void 0) return `${low}\u2013${high}${unit}`;
1206
+ if (low !== void 0) return `min ${low}${unit}`;
1207
+ if (high !== void 0) return `max ${high}${unit}`;
1208
+ return void 0;
1209
+ };
1210
+ const { min, max, minItems, maxItems, minLength, maxLength } = field.bounds;
1211
+ if (field.kind === "multi") return span(minItems, maxItems);
1212
+ if (field.kind === "number") return span(min, max);
1213
+ if (field.kind === "text" || field.kind === "choice") return span(minLength, maxLength, " chars");
1214
+ return void 0;
1215
+ }
1216
+ function initialForm() {
1217
+ return { field: 0, picks: {}, values: {} };
1218
+ }
1219
+ function formValueText(field, state) {
1220
+ const value = state.values[field.key];
1221
+ if (value === void 0) return "";
1222
+ if (field.kind === "boolean") return value === true ? "yes" : "no";
1223
+ const title = (entry) => field.choices.find((choice) => choice.value === entry)?.title ?? entry;
1224
+ if (field.kind === "multi") {
1225
+ const picked = Array.isArray(value) ? value : [];
1226
+ return picked.map(title).join(", ");
1227
+ }
1228
+ const text = typeof value === "string" ? value : "";
1229
+ return field.kind === "choice" ? title(text) : text;
1230
+ }
1231
+ function pickFor(state, field) {
1232
+ const pick = state.picks[field.key] ?? 0;
1233
+ return Math.min(Math.max(0, pick), Math.max(0, field.choices.length - 1));
1234
+ }
1235
+ function movePick(state, field, delta) {
1236
+ const total = field.choices.length;
1237
+ if (total === 0) return state;
1238
+ const next = (pickFor(state, field) + delta + total) % total;
1239
+ return { ...state, picks: { ...state.picks, [field.key]: next } };
1240
+ }
1241
+ function jumpPick(state, field, digit) {
1242
+ if (!/^[1-9]$/.test(digit)) return state;
1243
+ const index = Number(digit) - 1;
1244
+ if (index >= field.choices.length) return state;
1245
+ return { ...state, picks: { ...state.picks, [field.key]: index } };
1246
+ }
1247
+ function toggleChoice(state, field) {
1248
+ if (field.kind !== "multi" || field.choices.length === 0) return state;
1249
+ const value = field.choices[pickFor(state, field)].value;
1250
+ const current = Array.isArray(state.values[field.key]) ? state.values[field.key] : [];
1251
+ const next = current.includes(value) ? current.filter((entry) => entry !== value) : [...current, value];
1252
+ return { ...state, values: { ...state.values, [field.key]: next } };
1253
+ }
1254
+ function toggleBoolean(state, field) {
1255
+ if (field.kind !== "boolean") return state;
1256
+ return {
1257
+ ...state,
1258
+ values: { ...state.values, [field.key]: state.values[field.key] !== true }
1259
+ };
1260
+ }
1261
+ function withDraft(fields, state, draft) {
1262
+ const field = fields[state.field];
1263
+ if (!field || field.kind !== "text" && field.kind !== "number") return state;
1264
+ return { ...state, values: { ...state.values, [field.key]: draft } };
1265
+ }
1266
+ function focusField(fields, state, index) {
1267
+ if (fields.length === 0) return { state, draft: "" };
1268
+ const next = Math.min(Math.max(0, index), fields.length - 1);
1269
+ const field = fields[next];
1270
+ const value = state.values[field.key];
1271
+ return {
1272
+ state: { ...state, field: next },
1273
+ draft: typeof value === "string" ? value : ""
1274
+ };
1275
+ }
1276
+ function commitField(fields, state) {
1277
+ const field = fields[state.field];
1278
+ if (field?.kind !== "choice" || field.choices.length === 0) return state;
1279
+ const value = field.choices[pickFor(state, field)].value;
1280
+ return { ...state, values: { ...state.values, [field.key]: value } };
1281
+ }
1282
+ function formContent(fields, state) {
1283
+ const content = {};
1284
+ for (const field of fields) {
1285
+ const value = state.values[field.key];
1286
+ if (value === void 0) continue;
1287
+ if (field.kind === "boolean") {
1288
+ content[field.key] = value === true;
1289
+ continue;
1290
+ }
1291
+ if (field.kind === "multi") {
1292
+ content[field.key] = Array.isArray(value) ? value : [];
1293
+ continue;
1294
+ }
1295
+ const text = typeof value === "string" ? value : "";
1296
+ if (text.trim() === "") continue;
1297
+ if (field.kind === "number") {
1298
+ const parsed = Number(text.trim());
1299
+ if (Number.isFinite(parsed)) content[field.key] = parsed;
1300
+ continue;
1301
+ }
1302
+ content[field.key] = text;
1303
+ }
1304
+ return content;
1305
+ }
1306
+ function fieldComplete(field, state) {
1307
+ const value = state.values[field.key];
1308
+ if (field.kind === "boolean") return typeof value === "boolean";
1309
+ if (field.kind === "multi") {
1310
+ const picked = Array.isArray(value) ? value : [];
1311
+ if (value === void 0) return !field.required;
1312
+ if (field.required && picked.length === 0) return false;
1313
+ const { minItems, maxItems } = field.bounds;
1314
+ if (minItems !== void 0 && picked.length < minItems) return false;
1315
+ if (maxItems !== void 0 && picked.length > maxItems) return false;
1316
+ return true;
1317
+ }
1318
+ if (field.kind === "number") {
1319
+ const text2 = typeof value === "string" ? value.trim() : "";
1320
+ if (text2 === "") return !field.required;
1321
+ const parsed = Number(text2);
1322
+ if (!Number.isFinite(parsed)) return false;
1323
+ const { min, max } = field.bounds;
1324
+ if (min !== void 0 && parsed < min) return false;
1325
+ if (max !== void 0 && parsed > max) return false;
1326
+ return true;
1327
+ }
1328
+ const text = typeof value === "string" ? value : "";
1329
+ if (text.trim() === "") return !field.required;
1330
+ const { minLength, maxLength } = field.bounds;
1331
+ if (minLength !== void 0 && text.length < minLength) return false;
1332
+ if (maxLength !== void 0 && text.length > maxLength) return false;
1333
+ return true;
1334
+ }
1335
+ function formComplete(fields, state) {
1336
+ return fields.every((field) => fieldComplete(field, state));
1337
+ }
1338
+ var MAX_FORM_FIELDS = 6;
1339
+ function questionBarRows(fields) {
1340
+ return 2 + Math.min(fields.length, MAX_FORM_FIELDS) + 1 + 1;
1341
+ }
1342
+ function interactionRows(interactions, width) {
1343
+ const front = interactions[0];
1344
+ if (!front) return 0;
1345
+ if (front.kind === "permission") {
1346
+ const rest = interactions.slice(1).map((entry) => entry.request).filter((request) => "options" in request);
1347
+ return permissionBarRows(front.request, width, rest);
1348
+ }
1349
+ return questionBarRows(formFields(front.request));
1350
+ }
1351
+
1080
1352
  // src/tui/components/overlays.tsx
1353
+ import { Box as Box2, Text as Text2 } from "ink";
1081
1354
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1082
1355
  var HELP_GROUPS = [
1083
1356
  {
1084
1357
  title: "prompt",
1085
1358
  rows: [
1086
1359
  ["enter", "Run the requirement"],
1087
- ["enter (busy)", "Queue only; /approve tools, /respond questions"],
1360
+ ["enter (busy)", "Queue only; the bar above decides tools and questions"],
1088
1361
  ["esc", "Clear the line, close an overlay"],
1089
1362
  ["\u2190 \u2192 home end", "Move the caret"]
1090
1363
  ]
@@ -1139,95 +1412,6 @@ function HelpOverlay({ width, maxRows }) {
1139
1412
  /* @__PURE__ */ jsx2(Box2, { marginTop: gap, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "esc or ? to close" }) })
1140
1413
  ] });
1141
1414
  }
1142
- function PermissionOverlay({
1143
- request,
1144
- shortId,
1145
- queued,
1146
- width,
1147
- maxRows
1148
- }) {
1149
- const clean = (value) => stripAnsi(value).replace(/\p{Cc}/gu, " ");
1150
- const input = request.input === void 0 ? "(no input)" : JSON.stringify(request.input) ?? "(no input)";
1151
- const inputRows = Math.max(1, Math.min(3, maxRows - request.options.length - 7));
1152
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, width, children: [
1153
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: COLOR.warn, wrap: "truncate-end", children: `PERMISSION ${shortId} \xB7 ${queued} pending \xB7 ${clean(request.agentId)}` }),
1154
- /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", children: clean(request.title) }),
1155
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: `request ${clean(request.requestId)} \xB7 ${clean(request.toolName ?? request.kind ?? request.toolCallId)}` }),
1156
- /* @__PURE__ */ jsx2(Text2, { color: COLOR.accent, wrap: "truncate-end", children: `/approve ${shortId} <number> \xB7 /deny ${shortId}` }),
1157
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: "Ordinary text queues a follow-up; it never grants permission." }),
1158
- /* @__PURE__ */ jsx2(Box2, { height: inputRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx2(Text2, { children: `input: ${clean(input)}` }) }),
1159
- request.options.map((option, index) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", children: `${index + 1}. ${clean(option.name)} (${option.kind})` }, option.optionId))
1160
- ] });
1161
- }
1162
- function questionLines(request, width) {
1163
- const lines = [request.message, `request ${request.requestId}`];
1164
- for (const [key, field] of Object.entries(request.requestedSchema.properties ?? {})) {
1165
- const schema = field;
1166
- const items = schema.items;
1167
- const choices = schema.oneOf ?? schema.enum ?? items?.anyOf ?? items?.enum;
1168
- lines.push(
1169
- `${key}${request.requestedSchema.required?.includes(key) ? " *" : ""}: ${typeof schema.title === "string" ? schema.title : key} (${field.type})`
1170
- );
1171
- if (typeof schema.description === "string") lines.push(schema.description);
1172
- const limits = [
1173
- "minimum",
1174
- "maximum",
1175
- "exclusiveMinimum",
1176
- "exclusiveMaximum",
1177
- "minLength",
1178
- "maxLength",
1179
- "minItems",
1180
- "maxItems",
1181
- "uniqueItems"
1182
- ].filter((limit) => schema[limit] !== void 0 && schema[limit] !== null).map((limit) => `${limit}=${schema[limit]}`);
1183
- if (limits.length) lines.push(limits.join(" \xB7 "));
1184
- for (const choice of choices ?? []) {
1185
- lines.push(
1186
- typeof choice === "string" ? JSON.stringify(choice) : `${JSON.stringify(choice.const)} \u2014 ${choice.title ?? choice.const}${choice.description ? `: ${choice.description}` : ""}`
1187
- );
1188
- }
1189
- }
1190
- return lines.flatMap(
1191
- (line) => wrapLine(stripAnsi(line).replace(/\p{Cc}/gu, " "), Math.max(1, width - 2))
1192
- );
1193
- }
1194
- function QuestionOverlay({
1195
- request,
1196
- shortId,
1197
- queued,
1198
- width,
1199
- maxRows,
1200
- offset
1201
- }) {
1202
- const lines = questionLines(request, width);
1203
- const capacity = Math.max(1, maxRows - 4);
1204
- const start = Math.min(offset, Math.max(0, lines.length - capacity));
1205
- const visible = lines.slice(start, start + capacity);
1206
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, width, children: [
1207
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: COLOR.warn, wrap: "truncate-end", children: `QUESTION ${shortId} \xB7 ${queued} pending \xB7 ${stripAnsi(request.agentId)}` }),
1208
- /* @__PURE__ */ jsx2(Text2, { color: COLOR.accent, wrap: "truncate-end", children: `/respond ${shortId} <JSON object>` }),
1209
- /* @__PURE__ */ jsx2(
1210
- Text2,
1211
- {
1212
- dimColor: true,
1213
- wrap: "truncate-end",
1214
- children: `/skip ${shortId} declines \xB7 /dismiss ${shortId} cancels`
1215
- }
1216
- ),
1217
- visible.map((line, index) => (
1218
- // biome-ignore lint/suspicious/noArrayIndexKey: immutable text rows are identified by their absolute form offset.
1219
- /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", children: line }, `${start + index}-${line}`)
1220
- )),
1221
- /* @__PURE__ */ jsx2(
1222
- Text2,
1223
- {
1224
- dimColor: true,
1225
- wrap: "truncate-end",
1226
- children: `\u2191\u2193 / pgup pgdn form \xB7 ${start + 1}\u2013${start + visible.length}/${lines.length} \xB7 ordinary text queues`
1227
- }
1228
- )
1229
- ] });
1230
- }
1231
1415
  function outcome(turn) {
1232
1416
  if (turn.reply) {
1233
1417
  return turn.reply.mode === "ask" ? { text: `${GLYPH.ask} needs input`, color: COLOR.warn } : { text: `${STATUS.success.glyph} answered`, color: COLOR.ok };
@@ -1304,9 +1488,110 @@ function HistoryOverlay({
1304
1488
  ] });
1305
1489
  }
1306
1490
 
1491
+ // src/tui/components/interaction.tsx
1492
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1493
+ function PermissionBar({
1494
+ request,
1495
+ others,
1496
+ cursor,
1497
+ released,
1498
+ width
1499
+ }) {
1500
+ const choices = permissionChoices(request);
1501
+ const impact = describeImpact(request, Math.max(8, width - 6));
1502
+ const also = waitingLine(others);
1503
+ const tool = clean(request.toolName ?? request.kind ?? "tool");
1504
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, width, children: [
1505
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLOR.warn, wrap: "truncate-end", children: `permission \xB7 ${clean(request.agentId)} \xB7 ${tool}${others.length > 0 ? ` \xB7 ${others.length + 1} waiting` : ""}` }),
1506
+ /* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: ` ${truncate(clean(request.title), Math.max(8, width - 4))}` }),
1507
+ impact.map((line) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` ${line}` }, line)),
1508
+ choices.map((choice, index) => {
1509
+ const active = index === cursor;
1510
+ return /* @__PURE__ */ jsx3(
1511
+ Text3,
1512
+ {
1513
+ color: active ? COLOR.accent : void 0,
1514
+ dimColor: !active,
1515
+ wrap: "truncate-end",
1516
+ children: `${active ? GLYPH.caret : " "} ${choice.key} ${choice.label}${choice.note ? ` \xB7 ${choice.note}` : ""}`
1517
+ },
1518
+ choice.optionId
1519
+ );
1520
+ }),
1521
+ also === void 0 ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` ${also}` }),
1522
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: released ? " typing \xB7 /approve 1 or /deny still reach it" : " \u2191\u2193 or 1-9 choose \xB7 enter confirm \xB7 esc to type instead" })
1523
+ ] });
1524
+ }
1525
+ function QuestionForm({
1526
+ request,
1527
+ others,
1528
+ fields,
1529
+ form,
1530
+ draft,
1531
+ width
1532
+ }) {
1533
+ const focused = fields[form.field];
1534
+ const titleWidth = Math.min(
1535
+ 24,
1536
+ Math.max(4, ...fields.map((field) => field.title.length + (field.required ? 2 : 0)))
1537
+ );
1538
+ const capacity = Math.min(fields.length, MAX_FORM_FIELDS);
1539
+ const start = listWindowStart(form.field, fields.length, Math.max(1, capacity));
1540
+ const visible = fields.slice(start, start + capacity);
1541
+ const range = fields.length > capacity ? ` \xB7 ${start + 1}\u2013${start + visible.length}/${fields.length}` : "";
1542
+ const detail = focused === void 0 ? [] : detailLines(focused, width);
1543
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, width, children: [
1544
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLOR.warn, wrap: "truncate-end", children: `question \xB7 ${clean(request.agentId)}${others.length > 0 ? ` \xB7 ${others.length + 1} waiting` : ""}` }),
1545
+ /* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: clean(request.message) }),
1546
+ visible.map((field, offset) => {
1547
+ const index = start + offset;
1548
+ const active = index === form.field;
1549
+ const marker = field.required ? " *" : "";
1550
+ const value = fieldValue(field, form, draft, index === form.field);
1551
+ return /* @__PURE__ */ jsx3(
1552
+ Text3,
1553
+ {
1554
+ color: active ? COLOR.accent : void 0,
1555
+ dimColor: !active,
1556
+ wrap: "truncate-end",
1557
+ children: `${active ? GLYPH.caret : " "} ${`${field.title}${marker}`.padEnd(titleWidth)} ${value}`
1558
+ },
1559
+ field.key
1560
+ );
1561
+ }),
1562
+ detail.map((line) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` ${line}` }, line)),
1563
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` tab next \xB7 \u2191\u2193 pick \xB7 space toggle \xB7 enter next${range} \xB7 esc clear` })
1564
+ ] });
1565
+ }
1566
+ function fieldValue(field, form, draft, focused) {
1567
+ if (focused && field.kind === "choice" && field.choices.length > 0) {
1568
+ return field.choices[pickFor(form, field)].title;
1569
+ }
1570
+ const editing = focused && (field.kind === "text" || field.kind === "number");
1571
+ const text = editing ? draft : formValueText(field, form);
1572
+ if (text !== "") return text;
1573
+ if (field.kind === "boolean") return "(unset)";
1574
+ return "(empty)";
1575
+ }
1576
+ function detailLines(field, width) {
1577
+ const room = Math.max(8, width - 6);
1578
+ const limits = boundsText(field);
1579
+ const suffix = limits === void 0 ? "" : ` \xB7 ${limits}`;
1580
+ if (field.choices.length > 0) {
1581
+ const listed = field.choices.map(
1582
+ (choice, index) => `${index + 1} ${choice.title}${choice.description ? ` \u2014 ${choice.description}` : ""}`
1583
+ ).join(" ");
1584
+ return [truncate(`${listed}${suffix}`, room)];
1585
+ }
1586
+ const editing = field.kind === "text" || field.kind === "number";
1587
+ const hint = field.description ?? (editing ? "type below, enter to continue" : "");
1588
+ if (hint === "") return limits === void 0 ? [] : [`${limits}`];
1589
+ return [truncate(`${hint}${suffix}`, room)];
1590
+ }
1591
+
1307
1592
  // src/tui/components/panels.tsx
1308
- import { Box as Box3, Text as Text3 } from "ink";
1309
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1593
+ import { Box as Box4, Text as Text4 } from "ink";
1594
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1310
1595
  function rightColumn(agent, now) {
1311
1596
  if (agent.status === "retrying" && agent.retry) {
1312
1597
  return { text: `retry ${agent.retry.attempt}/${agent.retry.max}`, color: COLOR.warn };
@@ -1325,9 +1610,9 @@ function TeamRow({
1325
1610
  const style = STATUS[agent.status];
1326
1611
  const glyph = agent.status === "running" ? spinner : style.glyph;
1327
1612
  const right = rightColumn(agent, now);
1328
- return /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
1329
- /* @__PURE__ */ jsx3(Box3, { flexShrink: 1, children: /* @__PURE__ */ jsxs3(
1330
- Text3,
1613
+ return /* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
1614
+ /* @__PURE__ */ jsx4(Box4, { flexShrink: 1, children: /* @__PURE__ */ jsxs4(
1615
+ Text4,
1331
1616
  {
1332
1617
  color: focused ? COLOR.accent : style.color,
1333
1618
  dimColor: !focused && style.dim,
@@ -1339,14 +1624,14 @@ function TeamRow({
1339
1624
  " ",
1340
1625
  agent.icon ? `${agent.icon} ` : "",
1341
1626
  agent.name,
1342
- agent.taskTitle ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1627
+ agent.taskTitle ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1343
1628
  " ",
1344
1629
  agent.taskTitle
1345
1630
  ] }) : null
1346
1631
  ]
1347
1632
  }
1348
1633
  ) }),
1349
- /* @__PURE__ */ jsx3(Box3, { flexShrink: 0, children: /* @__PURE__ */ jsx3(Text3, { color: right.color, dimColor: right.color === void 0, children: right.text }) })
1634
+ /* @__PURE__ */ jsx4(Box4, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { color: right.color, dimColor: right.color === void 0, children: right.text }) })
1350
1635
  ] });
1351
1636
  }
1352
1637
  function TeamPanel({
@@ -1357,16 +1642,16 @@ function TeamPanel({
1357
1642
  }) {
1358
1643
  const ids = Object.keys(turn.agents);
1359
1644
  const { done, total } = progress(turn);
1360
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, flexShrink: 0, paddingRight: 1, children: [
1361
- /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
1362
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "TEAM" }),
1363
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1645
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, flexShrink: 0, paddingRight: 1, children: [
1646
+ /* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
1647
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "TEAM" }),
1648
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1364
1649
  done,
1365
1650
  "/",
1366
1651
  total
1367
1652
  ] })
1368
1653
  ] }),
1369
- ids.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "no agents configured" }) : ids.map((id) => /* @__PURE__ */ jsx3(
1654
+ ids.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "no agents configured" }) : ids.map((id) => /* @__PURE__ */ jsx4(
1370
1655
  TeamRow,
1371
1656
  {
1372
1657
  agent: turn.agents[id],
@@ -1379,8 +1664,8 @@ function TeamPanel({
1379
1664
  ] });
1380
1665
  }
1381
1666
  function Divider() {
1382
- return /* @__PURE__ */ jsx3(
1383
- Box3,
1667
+ return /* @__PURE__ */ jsx4(
1668
+ Box4,
1384
1669
  {
1385
1670
  flexShrink: 0,
1386
1671
  borderStyle: "single",
@@ -1404,43 +1689,43 @@ function StreamPanel({
1404
1689
  const window = visibleLog(turn, viewport, width);
1405
1690
  const style = agent ? STATUS[agent.status] : void 0;
1406
1691
  const elapsed = agent ? agentElapsed(agent, now) : void 0;
1407
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexGrow: 1, height, paddingLeft: 1, children: [
1408
- /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
1409
- /* @__PURE__ */ jsx3(Box3, { flexShrink: 1, children: /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate-end", children: [
1410
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: agent?.name ?? "\u2014" }),
1411
- agent?.model ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1692
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexGrow: 1, height, paddingLeft: 1, children: [
1693
+ /* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
1694
+ /* @__PURE__ */ jsx4(Box4, { flexShrink: 1, children: /* @__PURE__ */ jsxs4(Text4, { wrap: "truncate-end", children: [
1695
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: agent?.name ?? "\u2014" }),
1696
+ agent?.model ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1412
1697
  " \xB7 ",
1413
1698
  agent.model
1414
1699
  ] }) : null,
1415
- agent && style ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1416
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " \xB7 " }),
1417
- /* @__PURE__ */ jsx3(Text3, { color: style.color, dimColor: style.dim, children: agent.status === "running" ? `${spinner} ${style.label}` : style.label })
1700
+ agent && style ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1701
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " \xB7 " }),
1702
+ /* @__PURE__ */ jsx4(Text4, { color: style.color, dimColor: style.dim, children: agent.status === "running" ? `${spinner} ${style.label}` : style.label })
1418
1703
  ] }) : null,
1419
- agent?.taskTitle ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1704
+ agent?.taskTitle ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1420
1705
  " \xB7 ",
1421
1706
  agent.taskTitle
1422
1707
  ] }) : null,
1423
- agent?.usage?.contextTokens !== void 0 && agent.usage.contextSize !== void 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1708
+ agent?.usage?.contextTokens !== void 0 && agent.usage.contextSize !== void 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1424
1709
  " ",
1425
1710
  "\xB7 ctx ",
1426
1711
  formatTokens(agent.usage.contextTokens),
1427
1712
  "/",
1428
1713
  formatTokens(agent.usage.contextSize)
1429
1714
  ] }) : null,
1430
- agent?.costUsd !== void 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1715
+ agent?.costUsd !== void 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1431
1716
  " \xB7 ",
1432
1717
  formatCost(agent.costUsd)
1433
1718
  ] }) : null
1434
1719
  ] }) }),
1435
- /* @__PURE__ */ jsxs3(Box3, { flexShrink: 0, children: [
1436
- elapsed === void 0 ? null : /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1720
+ /* @__PURE__ */ jsxs4(Box4, { flexShrink: 0, children: [
1721
+ elapsed === void 0 ? null : /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1437
1722
  formatDuration(elapsed),
1438
1723
  " "
1439
1724
  ] }),
1440
- /* @__PURE__ */ jsx3(Text3, { color: window.following ? COLOR.accent : COLOR.warn, children: window.following ? GLYPH.live : `${GLYPH.paused} ${window.scrolledBack}\u2191` })
1725
+ /* @__PURE__ */ jsx4(Text4, { color: window.following ? COLOR.accent : COLOR.warn, children: window.following ? GLYPH.live : `${GLYPH.paused} ${window.scrolledBack}\u2191` })
1441
1726
  ] })
1442
1727
  ] }),
1443
- window.rows.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: turn.phase === "running" ? "waiting for output\u2026" : "no output yet" }) : (
1728
+ window.rows.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: turn.phase === "running" ? "waiting for output\u2026" : "no output yet" }) : (
1444
1729
  // The tail, already wrapped to `width` by `visibleLog` — the pane draws
1445
1730
  // rows, and one log line is several of them. `visibleLog` also picks
1446
1731
  // which rows, so the content here is exactly the pane's height and the
@@ -1456,10 +1741,10 @@ function StreamPanel({
1456
1741
  // `flexShrink={0}` stops Yoga compressing the rows into each other,
1457
1742
  // and `overflow="hidden"` is the backstop if any of the above is ever
1458
1743
  // wrong.
1459
- /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", flexShrink: 0, height: viewport, overflow: "hidden", children: window.rows.map((row, index) => (
1744
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", flexShrink: 0, height: viewport, overflow: "hidden", children: window.rows.map((row, index) => (
1460
1745
  // Rows are an append-only tail; position is the identity.
1461
1746
  // biome-ignore lint/suspicious/noArrayIndexKey: positional log tail
1462
- /* @__PURE__ */ jsx3(Box3, { flexShrink: 0, children: /* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: row }) }, index)
1747
+ /* @__PURE__ */ jsx4(Box4, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { wrap: "truncate-end", children: row }) }, index)
1463
1748
  )) })
1464
1749
  )
1465
1750
  ] });
@@ -1470,14 +1755,14 @@ function ReplyPanel({ reply, width }) {
1470
1755
  // `flexShrink={0}`: the body must clip this, not compress it. A shrunken
1471
1756
  // column box folds its own text rows into each other before the body ever
1472
1757
  // gets a chance to hide the overflow.
1473
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1474
- /* @__PURE__ */ jsxs3(Text3, { color: asking ? COLOR.warn : COLOR.accent, bold: true, children: [
1758
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1759
+ /* @__PURE__ */ jsxs4(Text4, { color: asking ? COLOR.warn : COLOR.accent, bold: true, children: [
1475
1760
  asking ? GLYPH.ask : GLYPH.leader,
1476
1761
  " ",
1477
1762
  asking ? "the team needs a decision" : "answered"
1478
1763
  ] }),
1479
- /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { children: reply.message }) }),
1480
- /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: asking ? "Answer below and press enter." : "Type a requirement and press enter." }) })
1764
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { children: reply.message }) }),
1765
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: asking ? "Answer below and press enter." : "Type a requirement and press enter." }) })
1481
1766
  ] })
1482
1767
  );
1483
1768
  }
@@ -1485,12 +1770,12 @@ function ErrorPanel({ message, width }) {
1485
1770
  return (
1486
1771
  // `flexShrink={0}` — see ReplyPanel. A compressed column box folds its own
1487
1772
  // text rows into each other before the body gets a chance to clip them.
1488
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1489
- /* @__PURE__ */ jsxs3(Text3, { color: COLOR.error, bold: true, children: [
1773
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1774
+ /* @__PURE__ */ jsxs4(Text4, { color: COLOR.error, bold: true, children: [
1490
1775
  GLYPH.warn,
1491
1776
  " the run stopped"
1492
1777
  ] }),
1493
- /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: COLOR.error, children: message }) })
1778
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: COLOR.error, children: message }) })
1494
1779
  ] })
1495
1780
  );
1496
1781
  }
@@ -1501,62 +1786,108 @@ function WelcomePanel({ turn, width }) {
1501
1786
  return (
1502
1787
  // `flexShrink={0}` — see ReplyPanel. Compressing the roster folds one agent's
1503
1788
  // description onto the next row's, which reads as a corrupted screen.
1504
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1505
- /* @__PURE__ */ jsxs3(Text3, { color: COLOR.accent, bold: true, children: [
1789
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1790
+ /* @__PURE__ */ jsxs4(Text4, { color: COLOR.accent, bold: true, children: [
1506
1791
  GLYPH.brand,
1507
1792
  " multiclaw"
1508
1793
  ] }),
1509
- /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "A team of agents, one prompt away." }) }),
1510
- /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: agents.map((agent) => {
1794
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "A team of agents, one prompt away." }) }),
1795
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: agents.map((agent) => {
1511
1796
  const focused = turn.focusId === agent.id;
1512
1797
  const leader = agent.id === leaderId;
1513
- return /* @__PURE__ */ jsxs3(Box3, { children: [
1514
- /* @__PURE__ */ jsxs3(Box3, { flexShrink: 0, children: [
1515
- /* @__PURE__ */ jsx3(Text3, { color: focused ? COLOR.accent : void 0, children: focused ? `${GLYPH.caret} ` : " " }),
1516
- /* @__PURE__ */ jsx3(Text3, { color: leader ? COLOR.accent : COLOR.muted, children: leader ? GLYPH.leader : GLYPH.agent }),
1517
- /* @__PURE__ */ jsxs3(Text3, { bold: focused, color: focused ? COLOR.accent : void 0, children: [
1798
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
1799
+ /* @__PURE__ */ jsxs4(Box4, { flexShrink: 0, children: [
1800
+ /* @__PURE__ */ jsx4(Text4, { color: focused ? COLOR.accent : void 0, children: focused ? `${GLYPH.caret} ` : " " }),
1801
+ /* @__PURE__ */ jsx4(Text4, { color: leader ? COLOR.accent : COLOR.muted, children: leader ? GLYPH.leader : GLYPH.agent }),
1802
+ /* @__PURE__ */ jsxs4(Text4, { bold: focused, color: focused ? COLOR.accent : void 0, children: [
1518
1803
  " ",
1519
1804
  padRight(truncate(agent.name, nameWidth - 2), nameWidth - 1)
1520
1805
  ] })
1521
1806
  ] }),
1522
- /* @__PURE__ */ jsx3(Box3, { flexShrink: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: agent.taskTitle ?? agent.description ?? "" }) })
1807
+ /* @__PURE__ */ jsx4(Box4, { flexShrink: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, wrap: "truncate-end", children: agent.taskTitle ?? agent.description ?? "" }) })
1523
1808
  ] }, agent.id);
1524
1809
  }) }),
1525
- /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Type a requirement and press enter." }) })
1810
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Type a requirement and press enter." }) })
1526
1811
  ] })
1527
1812
  );
1528
1813
  }
1529
1814
 
1530
1815
  // src/tui/components/prompt.tsx
1531
- import { Box as Box4, Text as Text4 } from "ink";
1532
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1816
+ import { Box as Box5, Text as Text5 } from "ink";
1817
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1818
+ var MARKER = `${GLYPH.caret} `;
1819
+ var MARKER_WIDTH = displayWidth(MARKER);
1820
+ var INDENT = " ".repeat(MARKER_WIDTH);
1821
+ var MAX_PROMPT_ROWS = 5;
1822
+ function promptLines(value, cursor, width) {
1823
+ const shown = value.replace(/\r/g, "").replace(/\n/g, GLYPH.newline);
1824
+ const limit = Math.max(1, width);
1825
+ const lines = [{ text: "" }];
1826
+ let col = 0;
1827
+ let offset = 0;
1828
+ const breakRow = () => {
1829
+ lines.push({ text: "" });
1830
+ col = 0;
1831
+ };
1832
+ for (const char of shown) {
1833
+ const charWidth = displayWidth(char);
1834
+ if (col > 0 && col + charWidth > limit) breakRow();
1835
+ const line = lines[lines.length - 1];
1836
+ if (offset === cursor) line.caret = line.text.length;
1837
+ line.text += char;
1838
+ col += charWidth;
1839
+ offset += char.length;
1840
+ }
1841
+ if (cursor >= offset) {
1842
+ if (col >= limit) breakRow();
1843
+ const line = lines[lines.length - 1];
1844
+ line.caret = line.text.length;
1845
+ }
1846
+ return lines;
1847
+ }
1848
+ function promptRowCount(value, cursor, width) {
1849
+ return Math.min(MAX_PROMPT_ROWS, promptLines(value, cursor, width).length);
1850
+ }
1533
1851
  function Prompt({
1534
1852
  value,
1535
1853
  cursor,
1536
1854
  caretVisible,
1537
- placeholder
1855
+ placeholder,
1856
+ width
1538
1857
  }) {
1539
- const shown = value.replace(/\r/g, "").replace(/\n/g, GLYPH.newline);
1858
+ const textWidth = Math.max(1, width - MARKER_WIDTH);
1859
+ const lines = promptLines(value, cursor, textWidth);
1540
1860
  const ghost = value === "";
1541
- const before = shown.slice(0, cursor);
1542
- const cell = ghost ? placeholder.slice(0, 1) : shown.slice(cursor, cursor + 1);
1543
- const tail2 = ghost ? placeholder.slice(1) : shown.slice(cursor + 1);
1544
- return /* @__PURE__ */ jsxs4(Box4, { flexShrink: 0, children: [
1545
- /* @__PURE__ */ jsxs4(Text4, { color: COLOR.accent, children: [
1546
- GLYPH.caret,
1547
- " "
1548
- ] }),
1549
- /* @__PURE__ */ jsx4(Text4, { dimColor: ghost, children: before }),
1550
- /* @__PURE__ */ jsx4(Text4, { inverse: caretVisible, dimColor: !caretVisible && ghost, children: cell || " " }),
1551
- /* @__PURE__ */ jsx4(Text4, { dimColor: ghost, children: tail2 })
1552
- ] });
1861
+ const hint = truncate(placeholder, textWidth);
1862
+ const caretRow = Math.max(
1863
+ 0,
1864
+ lines.findIndex((line) => line.caret !== void 0)
1865
+ );
1866
+ const start = lines.length <= MAX_PROMPT_ROWS ? 0 : Math.max(0, Math.min(caretRow - (MAX_PROMPT_ROWS - 1), lines.length - MAX_PROMPT_ROWS));
1867
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", flexShrink: 0, children: lines.slice(start, start + MAX_PROMPT_ROWS).map((line, index) => {
1868
+ const caret = line.caret;
1869
+ const rest = caret === void 0 ? "" : line.text.slice(caret);
1870
+ const cell = [...ghost ? hint : rest][0] ?? " ";
1871
+ const tail2 = ghost ? [...hint].slice(1).join("") : rest.slice(cell.length);
1872
+ return (
1873
+ // biome-ignore lint/suspicious/noArrayIndexKey: a text row has no identity beyond its position — the whole prompt is rebuilt on every keystroke anyway
1874
+ /* @__PURE__ */ jsxs5(Box5, { flexShrink: 0, children: [
1875
+ /* @__PURE__ */ jsx5(Text5, { color: COLOR.accent, children: start + index === 0 ? MARKER : INDENT }),
1876
+ caret === void 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: line.text }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
1877
+ /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: line.text.slice(0, caret) }),
1878
+ /* @__PURE__ */ jsx5(Text5, { inverse: caretVisible, dimColor: !caretVisible && ghost, children: cell }),
1879
+ /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: tail2 })
1880
+ ] })
1881
+ ] }, start + index)
1882
+ );
1883
+ }) });
1553
1884
  }
1554
1885
  function CommandMenu({ rows, selected }) {
1555
1886
  const labelWidth = Math.max(1, ...rows.map((row) => row.label.length));
1556
- return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", paddingLeft: 2, children: rows.map((row, index) => {
1887
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", paddingLeft: 2, children: rows.map((row, index) => {
1557
1888
  const active = index === selected;
1558
- return /* @__PURE__ */ jsxs4(
1559
- Text4,
1889
+ return /* @__PURE__ */ jsxs5(
1890
+ Text5,
1560
1891
  {
1561
1892
  color: active ? COLOR.accent : void 0,
1562
1893
  dimColor: !active,
@@ -1806,7 +2137,7 @@ function restoreConsole(workDir) {
1806
2137
  }
1807
2138
 
1808
2139
  // src/tui/App.tsx
1809
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2140
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1810
2141
  var FLUSH_INTERVAL_MS = 80;
1811
2142
  var SPINNER_INTERVAL_MS = 110;
1812
2143
  var BLINK_INTERVAL_MS = 530;
@@ -1838,19 +2169,19 @@ function StatusLine({
1838
2169
  }) {
1839
2170
  const { done, total } = progress(state);
1840
2171
  if (error)
1841
- return /* @__PURE__ */ jsx5(Text5, { color: COLOR.error, wrap: "truncate-end", children: error });
2172
+ return /* @__PURE__ */ jsx6(Text6, { color: COLOR.error, wrap: "truncate-end", children: error });
1842
2173
  if (state.warning && state.phase !== "complete")
1843
- return /* @__PURE__ */ jsx5(Text5, { color: COLOR.warn, wrap: "truncate-end", children: state.warning });
2174
+ return /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, wrap: "truncate-end", children: state.warning });
1844
2175
  if ((cancelled || state.cancelled) && !running)
1845
- return /* @__PURE__ */ jsx5(Text5, { color: COLOR.warn, children: "cancelled \xB7 workspace changes are retained" });
2176
+ return /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, children: "cancelled \xB7 workspace changes are retained" });
1846
2177
  if (state.reply) {
1847
2178
  const asking = state.reply.mode === "ask";
1848
- return /* @__PURE__ */ jsx5(Text5, { color: asking ? COLOR.warn : COLOR.ok, wrap: "truncate-end", children: asking ? `${GLYPH.ask} needs your input` : `${STATUS.success.glyph} answered` });
2179
+ return /* @__PURE__ */ jsx6(Text6, { color: asking ? COLOR.warn : COLOR.ok, wrap: "truncate-end", children: asking ? `${GLYPH.ask} needs your input` : `${STATUS.success.glyph} answered` });
1849
2180
  }
1850
2181
  if (running) {
1851
2182
  const elapsed = turnElapsed(state, now);
1852
2183
  const time = elapsed === void 0 ? "" : ` ${formatDuration(elapsed)}`;
1853
- return /* @__PURE__ */ jsxs5(Text5, { color: COLOR.accent, wrap: "truncate-end", children: [
2184
+ return /* @__PURE__ */ jsxs6(Text6, { color: COLOR.accent, wrap: "truncate-end", children: [
1854
2185
  spinner,
1855
2186
  " running",
1856
2187
  time,
@@ -1866,14 +2197,14 @@ function StatusLine({
1866
2197
  const time = state.totalDuration === void 0 ? "" : ` ${formatDuration(state.totalDuration)}`;
1867
2198
  const outcome2 = state.acceptance ? `${state.success ? STATUS.success.glyph : STATUS.failed.glyph} ${state.acceptance.status === "approved" ? "delivery approved" : state.acceptance.status === "changes_required" ? "changes required" : "acceptance failed"}${time}` : state.success ? `${STATUS.success.glyph} done${time}` : `${STATUS.failed.glyph} finished with failures${time}`;
1868
2199
  const cost = state.totalCostUsd === void 0 ? "" : ` \xB7 ${formatCost(state.totalCostUsd)}`;
1869
- return /* @__PURE__ */ jsxs5(Text5, { color: state.success ? COLOR.ok : COLOR.error, wrap: "truncate-end", children: [
2200
+ return /* @__PURE__ */ jsxs6(Text6, { color: state.success ? COLOR.ok : COLOR.error, wrap: "truncate-end", children: [
1870
2201
  outcome2,
1871
2202
  cost,
1872
2203
  state.warning ? ` \xB7 ${GLYPH.warn} ${state.warning}` : ""
1873
2204
  ] });
1874
2205
  }
1875
2206
  const stages = state.stages.length;
1876
- return /* @__PURE__ */ jsxs5(Text5, { dimColor: true, wrap: "truncate-end", children: [
2207
+ return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate-end", children: [
1877
2208
  "ready \xB7 ",
1878
2209
  total,
1879
2210
  " agents",
@@ -1930,7 +2261,11 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
1930
2261
  const mountedRef = useRef(true);
1931
2262
  const interactionsRef = useRef([]);
1932
2263
  const [interactions, setInteractions] = useState([]);
1933
- const [questionScroll, setQuestionScroll] = useState({ id: "", offset: 0 });
2264
+ const [permissionMenu, setPermissionMenu] = useState({ id: "", cursor: void 0, released: false });
2265
+ const [formEntry, setFormEntry] = useState({
2266
+ id: "",
2267
+ state: initialForm()
2268
+ });
1934
2269
  const requestPermission = useCallback((request, signal) => {
1935
2270
  if (closingRef.current || signal.aborted) return Promise.resolve(void 0);
1936
2271
  const shortId = `p${++permissionSerialRef.current}`;
@@ -2229,6 +2564,21 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2229
2564
  const menuOpen = !helpOpen && !historyOpen && query !== void 0 && matches.length > 0;
2230
2565
  const argOpen = !helpOpen && !historyOpen && arg !== void 0 && argRows.length > 0;
2231
2566
  const paletteOpen = menuOpen || argOpen;
2567
+ const front = interactions[0];
2568
+ const menu = front?.kind === "permission" && permissionMenu.id === front.shortId ? permissionMenu : { id: front?.shortId ?? "", cursor: void 0, released: false };
2569
+ const frontQuestion = front?.kind === "question" ? front : void 0;
2570
+ const questionFields = useMemo(
2571
+ () => frontQuestion ? formFields(frontQuestion.request) : [],
2572
+ [frontQuestion]
2573
+ );
2574
+ const form = frontQuestion !== void 0 && formEntry.id === frontQuestion.shortId ? formEntry.state : initialForm();
2575
+ const formField = frontQuestion === void 0 ? void 0 : questionFields[form.field];
2576
+ const editingField = formField !== void 0 && (formField.kind === "text" || formField.kind === "number") ? formField : void 0;
2577
+ const promptRows = promptRowCount(
2578
+ line.text,
2579
+ line.cursor,
2580
+ Math.max(MIN_COLUMNS, columns) - MARKER_WIDTH
2581
+ );
2232
2582
  const layout = computeLayout({
2233
2583
  columns,
2234
2584
  rows,
@@ -2236,6 +2586,8 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2236
2586
  overlay: helpOpen || historyOpen,
2237
2587
  menuRows: paletteOpen ? paletteTotal : 0,
2238
2588
  queueRows: queue.length,
2589
+ interactionRows: interactionRows(interactions, Math.max(MIN_COLUMNS, columns)),
2590
+ promptRows,
2239
2591
  notice: notice !== void 0 || queuePaused && queue.length > 0,
2240
2592
  viewing: state.view !== void 0,
2241
2593
  hasRail: shown.leaderIds.length > 0 || shown.stages.length > 0,
@@ -2319,23 +2671,40 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2319
2671
  }
2320
2672
  case "approve":
2321
2673
  case "deny": {
2322
- const [id, number] = args;
2323
- if (!id || (name === "approve" ? args.length !== 2 : args.length !== 1)) {
2674
+ const onScreen = interactionsRef.current[0];
2675
+ const byId = (id) => {
2676
+ const found = interactionsRef.current.filter(
2677
+ (item) => item.request.requestId === id || item.shortId === id
2678
+ );
2679
+ if (found.length !== 1) setNotice(`unknown or stale permission request: ${id}`);
2680
+ return found.length === 1 ? found[0] : void 0;
2681
+ };
2682
+ let entry;
2683
+ let number;
2684
+ if (name === "approve" && args.length === 1 && /^\d+$/.test(args[0])) {
2685
+ number = args[0];
2686
+ entry = onScreen;
2687
+ } else if (name === "approve" && args.length === 2) {
2688
+ number = args[1];
2689
+ entry = byId(args[0]);
2690
+ } else if (name === "deny" && args.length === 0) {
2691
+ entry = onScreen;
2692
+ } else if (name === "deny" && args.length === 1) {
2693
+ entry = byId(args[0]);
2694
+ } else {
2324
2695
  setNotice(
2325
- name === "approve" ? "usage: /approve <request id> <option number>" : "usage: /deny <request id>"
2696
+ name === "approve" ? "usage: /approve [id] <option number>" : "usage: /deny [id]"
2326
2697
  );
2327
2698
  return;
2328
2699
  }
2329
- const matches2 = interactionsRef.current.filter(
2330
- (entry2) => entry2.request.requestId === id || entry2.shortId === id
2331
- );
2332
- if (matches2.length !== 1) {
2333
- setNotice(`unknown or stale permission request: ${id}`);
2700
+ if (entry === void 0) {
2701
+ if (args.length === 0 || /^\d+$/.test(args[0] ?? "")) {
2702
+ setNotice("no permission request on screen");
2703
+ }
2334
2704
  return;
2335
2705
  }
2336
- const entry = matches2[0];
2337
2706
  if (entry.kind !== "permission") {
2338
- setNotice(`not a permission request: ${id}`);
2707
+ setNotice("the request on screen is a question, not a permission");
2339
2708
  return;
2340
2709
  }
2341
2710
  if (name === "deny") {
@@ -2351,7 +2720,7 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2351
2720
  return;
2352
2721
  }
2353
2722
  entry.settle(option.optionId);
2354
- setNotice(`selected ${option.name} for ${entry.shortId}`);
2723
+ setNotice(`selected ${clean(option.name)} for ${entry.shortId}`);
2355
2724
  return;
2356
2725
  }
2357
2726
  case "task": {
@@ -2676,7 +3045,8 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2676
3045
  setLine((l) => ({ ...l, cursor: l.text.length }));
2677
3046
  return;
2678
3047
  }
2679
- if (key.tab) {
3048
+ const formFront = front?.kind === "question" && !isCommandLine(line.text) ? front : void 0;
3049
+ if (key.tab && formFront === void 0) {
2680
3050
  dispatch({ type: "focus:cycle", delta: key.shift ? -1 : 1 });
2681
3051
  return;
2682
3052
  }
@@ -2699,19 +3069,111 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2699
3069
  return;
2700
3070
  }
2701
3071
  }
2702
- if (interactions[0]?.kind === "question" && (key.upArrow || key.downArrow || key.pageUp || key.pageDown)) {
2703
- const entry = interactions[0];
2704
- const capacity = Math.max(1, layout.bodyHeight - 4);
2705
- const maxOffset = Math.max(0, questionLines(entry.request, layout.columns).length - capacity);
2706
- const delta = key.pageDown ? capacity : key.pageUp ? -capacity : key.downArrow ? 1 : -1;
2707
- setQuestionScroll((scroll) => ({
2708
- id: entry.shortId,
2709
- offset: Math.min(
2710
- maxOffset,
2711
- Math.max(0, (scroll.id === entry.shortId ? scroll.offset : 0) + delta)
2712
- )
2713
- }));
2714
- return;
3072
+ if (front?.kind === "permission" && line.text === "" && !menu.released) {
3073
+ const total = permissionChoices(front.request).length;
3074
+ if (key.upArrow || key.downArrow) {
3075
+ setPermissionMenu({
3076
+ id: front.shortId,
3077
+ cursor: moveCursor(menu.cursor, key.upArrow ? -1 : 1, total),
3078
+ released: false
3079
+ });
3080
+ return;
3081
+ }
3082
+ if (/^[1-9]$/.test(char) && !key.ctrl && !key.meta) {
3083
+ const cursor = jumpCursor(char, total);
3084
+ if (cursor !== void 0) {
3085
+ setPermissionMenu({ id: front.shortId, cursor, released: false });
3086
+ return;
3087
+ }
3088
+ }
3089
+ if (key.return) {
3090
+ const choice = menu.cursor === void 0 ? void 0 : permissionChoices(front.request)[menu.cursor];
3091
+ if (choice !== void 0) {
3092
+ front.settle(choice.optionId);
3093
+ setNotice(`chose ${choice.label} \xB7 ${clean(front.request.agentId)}`);
3094
+ }
3095
+ return;
3096
+ }
3097
+ }
3098
+ if (formFront !== void 0) {
3099
+ const front2 = formFront;
3100
+ const total = questionFields.length;
3101
+ const field = questionFields[form.field];
3102
+ const committed = withDraft(questionFields, form, line.text);
3103
+ const save = (state2, draft) => {
3104
+ setFormEntry({ id: front2.shortId, state: state2 });
3105
+ if (draft !== void 0) setLine({ text: draft, cursor: draft.length });
3106
+ };
3107
+ const moveFocus = (index, state2 = committed) => {
3108
+ const next = focusField(questionFields, state2, index);
3109
+ save(next.state, next.draft);
3110
+ };
3111
+ const submit2 = (state2) => {
3112
+ if (!formComplete(questionFields, state2)) {
3113
+ const missing = questionFields.findIndex((item) => !formComplete([item], state2));
3114
+ if (missing >= 0) {
3115
+ setNotice(`${questionFields[missing].title} still needs a value`);
3116
+ if (missing !== form.field) moveFocus(missing, state2);
3117
+ }
3118
+ return;
3119
+ }
3120
+ const content = formContent(questionFields, state2);
3121
+ if (!validateRuntimeQuestionContent(front2.request.requestedSchema, content)) {
3122
+ setNotice("answer does not match the question schema");
3123
+ return;
3124
+ }
3125
+ front2.settle({ action: "accept", content });
3126
+ setFormEntry({ id: "", state: initialForm() });
3127
+ setLine(EMPTY_LINE);
3128
+ setNotice(`answered ${front2.shortId}`);
3129
+ };
3130
+ const advance = (state2) => {
3131
+ if (form.field >= total - 1) submit2(state2);
3132
+ else moveFocus(form.field + 1, state2);
3133
+ };
3134
+ if (field === void 0) {
3135
+ if (key.return) submit2(committed);
3136
+ return;
3137
+ }
3138
+ if (key.tab) {
3139
+ moveFocus(key.shift ? form.field - 1 : form.field + 1);
3140
+ return;
3141
+ }
3142
+ if (field.choices.length > 0 && (key.upArrow || key.downArrow)) {
3143
+ save(movePick(committed, field, key.upArrow ? -1 : 1));
3144
+ return;
3145
+ }
3146
+ if (field.choices.length > 0 && (key.leftArrow || key.rightArrow)) {
3147
+ save(movePick(committed, field, key.leftArrow ? -1 : 1));
3148
+ return;
3149
+ }
3150
+ if (key.upArrow || key.downArrow) {
3151
+ moveFocus(key.upArrow ? form.field - 1 : form.field + 1);
3152
+ return;
3153
+ }
3154
+ if (char === " " && field.kind === "multi") {
3155
+ save(toggleChoice(committed, field));
3156
+ return;
3157
+ }
3158
+ if (field.kind === "boolean" && (char === " " || key.leftArrow || key.rightArrow)) {
3159
+ save(toggleBoolean(committed, field));
3160
+ return;
3161
+ }
3162
+ if (field.choices.length > 0 && /^[1-9]$/.test(char) && !key.ctrl && !key.meta) {
3163
+ save(jumpPick(committed, field, char));
3164
+ return;
3165
+ }
3166
+ if (key.return) {
3167
+ advance(field.kind === "choice" ? commitField(questionFields, committed) : committed);
3168
+ return;
3169
+ }
3170
+ if (key.escape) {
3171
+ if (field.kind === "text" || field.kind === "number") {
3172
+ save({ ...committed, values: { ...committed.values, [field.key]: "" } });
3173
+ setLine(EMPTY_LINE);
3174
+ }
3175
+ return;
3176
+ }
2715
3177
  }
2716
3178
  if (key.upArrow) {
2717
3179
  dispatch({
@@ -2782,6 +3244,9 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2782
3244
  dispatch({ type: "view:close" });
2783
3245
  return;
2784
3246
  }
3247
+ if (front !== void 0) {
3248
+ setPermissionMenu({ id: front.shortId, cursor: void 0, released: true });
3249
+ }
2785
3250
  setLine(EMPTY_LINE);
2786
3251
  setNotice(void 0);
2787
3252
  return;
@@ -2799,33 +3264,37 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2799
3264
  const handlerRef = useRef(handleKey);
2800
3265
  handlerRef.current = handleKey;
2801
3266
  useInput((char, key) => handlerRef.current(char, key));
2802
- const keys = helpOpen ? "esc close" : historyOpen ? "\u2191\u2193 select \xB7 enter open \xB7 esc close" : argOpen ? `\u2191\u2193 select \xB7 enter ${arg?.command.name} \xB7 esc close` : menuOpen ? "\u2191\u2193 select \xB7 enter run \xB7 esc close" : running ? interactions.length > 0 ? interactions[0].kind === "question" ? "/respond <id> <JSON> \xB7 /skip <id> \xB7 /dismiss <id>" : "/approve <id> <number> \xB7 /deny <id> \xB7 ctrl+c cancel" : "enter queues \xB7 ctrl+c cancel" : state.view ? "esc live \xB7 tab agent \xB7 ? help" : state.reply ? "enter sends \xB7 / commands \xB7 ? help" : "tab switch \xB7 / commands \xB7 ? help";
2803
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: layout.rows, children: [
2804
- /* @__PURE__ */ jsx5(
3267
+ const keys = helpOpen ? "esc close" : historyOpen ? "\u2191\u2193 select \xB7 enter open \xB7 esc close" : argOpen ? `\u2191\u2193 select \xB7 enter ${arg?.command.name} \xB7 esc close` : menuOpen ? "\u2191\u2193 select \xB7 enter run \xB7 esc close" : running ? interactions.length > 0 ? layout.interactionRows === 0 ? (
3268
+ // Too short to dock the bar, so the commands are the only way in.
3269
+ "decide with /approve 1 \xB7 /deny \xB7 /respond"
3270
+ ) : interactions[0].kind === "question" ? "tab field \xB7 \u2191\u2193 pick \xB7 enter next \xB7 /dismiss" : menu.released ? "/approve 1 and /deny still reach it" : "\u2191\u2193 or 1-9 choose \xB7 enter confirm \xB7 esc to type" : "enter queues \xB7 ctrl+c cancel" : state.view ? "esc live \xB7 tab agent \xB7 ? help" : state.reply ? "enter sends \xB7 / commands \xB7 ? help" : "tab switch \xB7 / commands \xB7 ? help";
3271
+ const placeholder = frontQuestion !== void 0 ? formField === void 0 ? "enter to answer" : editingField !== void 0 ? `${editingField.title}${editingField.required ? " *" : ""} \u2014 enter for the next field` : `${formField.title} \u2014 \u2191\u2193 to pick \xB7 enter for the next field` : front?.kind === "permission" && !menu.released ? "choose above \xB7 esc to type here instead" : running ? RUNNING_PLACEHOLDER : awaitingAnswer ? "answer the leader\u2026 \xB7 /task starts a new task" : INPUT_PLACEHOLDER;
3272
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: layout.rows, children: [
3273
+ /* @__PURE__ */ jsx6(
2805
3274
  Header,
2806
3275
  {
2807
3276
  name: state.name || config.name,
2808
3277
  right: state.view ? `turn ${shown.turn} \xB7 viewing` : `turn ${shown.turn} \xB7 ${running ? formatDuration(turnElapsed(state, now) ?? 0) : state.phase === "complete" ? "complete" : "ready"}`
2809
3278
  }
2810
3279
  ),
2811
- /* @__PURE__ */ jsx5(Rule, { width: layout.columns }),
2812
- config.useLeader === false ? /* @__PURE__ */ jsx5(Text5, { color: COLOR.warn, dimColor: true, wrap: "truncate-end", children: " \u26A0 leader off \u2014 every message runs the full team" }) : null,
3280
+ /* @__PURE__ */ jsx6(Rule, { width: layout.columns }),
3281
+ config.useLeader === false ? /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, dimColor: true, wrap: "truncate-end", children: " \u26A0 leader off \u2014 every message runs the full team" }) : null,
2813
3282
  layout.showRail ? (
2814
3283
  // The rail wraps, so it can be taller than one row. Cap it at the height
2815
3284
  // the budget reserved and clip, rather than letting the extra rows push
2816
3285
  // the chrome below it off the screen.
2817
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx5(PipelineRail, { turn: shown, spinner }) })
3286
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx6(PipelineRail, { turn: shown, spinner }) })
2818
3287
  ) : null,
2819
- shown.reason ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "truncate-end", children: ` reason: ${shown.reason}` }) : null,
2820
- /* @__PURE__ */ jsx5(
2821
- Box5,
3288
+ shown.reason ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: ` reason: ${shown.reason}` }) : null,
3289
+ /* @__PURE__ */ jsx6(
3290
+ Box6,
2822
3291
  {
2823
3292
  marginTop: 1,
2824
3293
  height: layout.bodyHeight,
2825
3294
  flexShrink: 0,
2826
3295
  flexDirection: "column",
2827
3296
  overflow: "hidden",
2828
- children: helpOpen ? /* @__PURE__ */ jsx5(HelpOverlay, { width: layout.columns, maxRows: layout.bodyHeight }) : historyOpen ? /* @__PURE__ */ jsx5(
3297
+ children: helpOpen ? /* @__PURE__ */ jsx6(HelpOverlay, { width: layout.columns, maxRows: layout.bodyHeight }) : historyOpen ? /* @__PURE__ */ jsx6(
2829
3298
  HistoryOverlay,
2830
3299
  {
2831
3300
  turns: historyTurns,
@@ -2833,36 +3302,17 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2833
3302
  width: layout.columns,
2834
3303
  maxRows: layout.bodyHeight
2835
3304
  }
2836
- ) : interactions[0]?.kind === "question" ? /* @__PURE__ */ jsx5(
2837
- QuestionOverlay,
2838
- {
2839
- request: interactions[0].request,
2840
- shortId: interactions[0].shortId,
2841
- queued: interactions.length,
2842
- width: layout.columns,
2843
- maxRows: layout.bodyHeight,
2844
- offset: questionScroll.id === interactions[0].shortId ? questionScroll.offset : 0
2845
- }
2846
- ) : interactions[0]?.kind === "permission" ? /* @__PURE__ */ jsx5(
2847
- PermissionOverlay,
2848
- {
2849
- request: interactions[0].request,
2850
- shortId: interactions[0].shortId,
2851
- queued: interactions.length,
2852
- width: layout.columns,
2853
- maxRows: layout.bodyHeight
2854
- }
2855
- ) : layout.welcome ? /* @__PURE__ */ jsx5(WelcomePanel, { turn: state, width: layout.columns }) : error ? (
3305
+ ) : layout.welcome ? /* @__PURE__ */ jsx6(WelcomePanel, { turn: state, width: layout.columns }) : error ? (
2856
3306
  // The error wins over a reply: a run that stopped with an error is the
2857
3307
  // most urgent thing on screen, and a stale answer underneath it would
2858
3308
  // read as the outcome. The status line keeps a one-line version too.
2859
- /* @__PURE__ */ jsx5(ErrorPanel, { message: error, width: layout.columns })
2860
- ) : shown.reply ? /* @__PURE__ */ jsx5(ReplyPanel, { reply: shown.reply, width: layout.columns }) : /* @__PURE__ */ jsxs5(Box5, { height: layout.bodyHeight, flexShrink: 0, flexDirection: "row", children: [
2861
- layout.showTeam ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
2862
- /* @__PURE__ */ jsx5(TeamPanel, { turn: shown, width: layout.teamWidth, now, spinner }),
2863
- /* @__PURE__ */ jsx5(Divider, {})
3309
+ /* @__PURE__ */ jsx6(ErrorPanel, { message: error, width: layout.columns })
3310
+ ) : shown.reply ? /* @__PURE__ */ jsx6(ReplyPanel, { reply: shown.reply, width: layout.columns }) : /* @__PURE__ */ jsxs6(Box6, { height: layout.bodyHeight, flexShrink: 0, flexDirection: "row", children: [
3311
+ layout.showTeam ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
3312
+ /* @__PURE__ */ jsx6(TeamPanel, { turn: shown, width: layout.teamWidth, now, spinner }),
3313
+ /* @__PURE__ */ jsx6(Divider, {})
2864
3314
  ] }) : null,
2865
- /* @__PURE__ */ jsx5(
3315
+ /* @__PURE__ */ jsx6(
2866
3316
  StreamPanel,
2867
3317
  {
2868
3318
  turn: shown,
@@ -2875,26 +3325,59 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2875
3325
  ] })
2876
3326
  }
2877
3327
  ),
2878
- state.view ? /* @__PURE__ */ jsx5(Text5, { color: COLOR.warn, wrap: "truncate-end", children: ` viewing turn ${shown.turn} \xB7 ${shown.requirement ?? "(no requirement)"} \xB7 esc to return` }) : null,
2879
- notice || queuePaused && queue.length > 0 ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, wrap: "truncate-end", children: [
3328
+ state.view ? /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, wrap: "truncate-end", children: ` viewing turn ${shown.turn} \xB7 ${shown.requirement ?? "(no requirement)"} \xB7 esc to return` }) : null,
3329
+ notice || queuePaused && queue.length > 0 ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate-end", children: [
2880
3330
  " ",
2881
3331
  notice ?? (awaitingAnswer ? "queue paused \u2014 answer the leader first" : "queue paused \u2014 /continue resumes it")
2882
3332
  ] }) : null,
2883
- /* @__PURE__ */ jsx5(QueueStrip, { items: queue, max: layout.queueRows }),
2884
- paletteOpen ? /* @__PURE__ */ jsx5(CommandMenu, { rows: paletteWindow, selected: paletteSelected }) : null,
2885
- /* @__PURE__ */ jsx5(
3333
+ /* @__PURE__ */ jsx6(QueueStrip, { items: queue, max: layout.queueRows }),
3334
+ front === void 0 || layout.interactionRows === 0 ? null : /* @__PURE__ */ jsxs6(Fragment3, { children: [
3335
+ /* @__PURE__ */ jsx6(Rule, { width: layout.columns }),
3336
+ /* @__PURE__ */ jsx6(
3337
+ Box6,
3338
+ {
3339
+ height: layout.interactionRows,
3340
+ overflow: "hidden",
3341
+ flexShrink: 0,
3342
+ flexDirection: "column",
3343
+ children: front.kind === "permission" ? /* @__PURE__ */ jsx6(
3344
+ PermissionBar,
3345
+ {
3346
+ request: front.request,
3347
+ others: interactions.slice(1).flatMap((entry) => entry.kind === "permission" ? [entry.request] : []),
3348
+ cursor: menu.cursor,
3349
+ released: menu.released,
3350
+ width: layout.columns
3351
+ }
3352
+ ) : /* @__PURE__ */ jsx6(
3353
+ QuestionForm,
3354
+ {
3355
+ request: front.request,
3356
+ others: interactions.slice(1).flatMap((entry) => entry.kind === "question" ? [entry.request] : []),
3357
+ fields: questionFields,
3358
+ form,
3359
+ draft: line.text,
3360
+ width: layout.columns
3361
+ }
3362
+ )
3363
+ }
3364
+ )
3365
+ ] }),
3366
+ paletteOpen ? /* @__PURE__ */ jsx6(CommandMenu, { rows: paletteWindow, selected: paletteSelected }) : null,
3367
+ /* @__PURE__ */ jsx6(
2886
3368
  Prompt,
2887
3369
  {
2888
3370
  value: line.text,
2889
3371
  cursor: line.cursor,
2890
3372
  caretVisible: caretOn,
2891
- placeholder: running ? RUNNING_PLACEHOLDER : awaitingAnswer ? "answer the leader\u2026 \xB7 /task starts a new task" : INPUT_PLACEHOLDER
3373
+ width: layout.columns,
3374
+ placeholder
2892
3375
  }
2893
3376
  ),
2894
- /* @__PURE__ */ jsx5(
3377
+ /* @__PURE__ */ jsx6(
2895
3378
  HintBar,
2896
3379
  {
2897
- status: /* @__PURE__ */ jsx5(
3380
+ status: /* @__PURE__ */ jsx6(
2898
3381
  StatusLine,
2899
3382
  {
2900
3383
  state,
@@ -2945,11 +3428,13 @@ async function startTui(options) {
2945
3428
  export {
2946
3429
  COMPACT_COLUMNS,
2947
3430
  MAX_HISTORY,
3431
+ MAX_INTERACTION_ROWS,
2948
3432
  MAX_LOG_CHARS,
2949
3433
  MAX_MENU_ROWS,
2950
3434
  MAX_QUEUE_ROWS,
2951
3435
  MAX_RAIL_ROWS,
2952
3436
  MIN_COLUMNS,
3437
+ MIN_INTERACTION_ROWS,
2953
3438
  MIN_ROWS,
2954
3439
  RAIL_MIN_ROWS,
2955
3440
  agentElapsed,