@multiclawcli/dashboard 0.11.0 → 0.13.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
@@ -10,6 +10,7 @@ import {
10
10
  describeResumePlan,
11
11
  isAbortError,
12
12
  Orchestrator,
13
+ resolvePermissionMode,
13
14
  validateRuntimeQuestionContent
14
15
  } from "@multiclawcli/core";
15
16
  import { Box as Box6, Text as Text6, useApp, useInput, useWindowSize } from "ink";
@@ -57,154 +58,95 @@ function appendLines(target, lines) {
57
58
 
58
59
  // src/tui/commands.ts
59
60
  var COMMANDS = [
60
- { name: "agents", description: "List the configured team", availability: "always" },
61
+ { name: "new", description: "Start an empty session", availability: "idle" },
62
+ { name: "sessions", description: "Switch or delete saved sessions", availability: "idle" },
61
63
  {
62
64
  name: "focus",
63
- args: "<id>",
64
- description: "Focus an agent by id",
65
+ args: "[id]",
66
+ description: "Choose the focused agent",
65
67
  availability: "always",
66
68
  completes: "agent"
67
69
  },
68
- { name: "clear", description: "Clear the focused agent's log", availability: "always" },
69
70
  {
70
- name: "history",
71
- description: "Browse finished turns, or continue one",
71
+ name: "mode",
72
+ args: "[auto | ask | deny]",
73
+ description: "Choose the permission mode",
72
74
  availability: "always"
73
75
  },
76
+ { name: "verbose", description: "Toggle tool detail in the stream", availability: "always" },
74
77
  {
75
- name: "sessions",
76
- description: "Switch between, or delete, saved sessions",
77
- availability: "always"
78
- },
79
- {
80
- name: "verbose",
81
- description: "Toggle tool detail in the stream",
78
+ name: "search",
79
+ args: "[text]",
80
+ description: "Search the focused stream",
82
81
  availability: "always"
83
82
  },
83
+ { name: "latest", description: "Jump to latest output", availability: "always" },
84
84
  {
85
85
  name: "retry",
86
86
  args: "[id]",
87
- description: "Re-run a failed agent (or all of them)",
87
+ description: "Choose failed agents to retry",
88
88
  availability: "idle"
89
89
  },
90
90
  {
91
91
  name: "replay",
92
92
  args: "[turn]",
93
- description: "Run an earlier turn again",
93
+ description: "Choose a turn to run again",
94
94
  availability: "idle"
95
95
  },
96
96
  {
97
- name: "fork",
97
+ name: "edit",
98
98
  args: "[turn]",
99
- description: "Edit an earlier turn's requirement",
99
+ description: "Edit a previous requirement",
100
100
  availability: "always"
101
101
  },
102
102
  {
103
103
  name: "export",
104
104
  args: "[turn] [path]",
105
- description: "Export a finished turn as Markdown",
106
- availability: "always"
107
- },
108
- {
109
- name: "search",
110
- args: "<text>",
111
- description: "Search the focused stream",
105
+ description: "Export a turn as Markdown",
112
106
  availability: "always"
113
107
  },
114
- {
115
- name: "latest",
116
- description: "Jump the stream to latest output",
117
- availability: "always"
118
- },
119
- {
120
- name: "cancel",
121
- description: "Cancel the running turn and its queue",
122
- availability: "running"
123
- },
124
- {
125
- name: "new",
126
- args: "<requirement>",
127
- description: "Start a new session with fresh agent sessions",
128
- availability: "idle"
129
- },
130
108
  {
131
109
  name: "steer",
132
- args: "<text>",
133
- description: "Send a message into the agents running right now",
110
+ args: "[text]",
111
+ description: "Send a message to running agents",
134
112
  availability: "running"
135
113
  },
136
- {
137
- name: "answer",
138
- args: "<text>",
139
- description: "Answer the leader's pending question",
140
- availability: "idle"
141
- },
142
- {
143
- name: "pause",
144
- description: "Pause queued turns, not the active agent",
145
- availability: "always"
146
- },
114
+ { name: "pause", description: "Pause queued turns", availability: "always" },
147
115
  {
148
116
  name: "continue",
149
- description: "Resume queued turns or pass a checkpoint",
117
+ description: "Resume the queue or continue a checkpoint",
150
118
  availability: "always"
151
- },
152
- {
153
- name: "modify",
154
- args: "<instruction>",
155
- description: "Provide feedback at a checkpoint and continue",
156
- availability: "running"
157
- },
158
- {
159
- name: "respond",
160
- args: "<question id> <JSON object>",
161
- description: "Answer a question in one line of JSON",
162
- availability: "always"
163
- },
164
- {
165
- name: "skip",
166
- args: "<question id>",
167
- description: "Decline a structured tool question",
168
- availability: "always"
169
- },
170
- {
171
- name: "dismiss",
172
- args: "<question id>",
173
- description: "Cancel a structured tool question",
174
- availability: "always"
175
- },
176
- {
177
- name: "approve",
178
- args: "[id] <number>",
179
- description: "Approve the request on screen, or a named one",
180
- availability: "always"
181
- },
182
- {
183
- name: "deny",
184
- args: "[id]",
185
- description: "Deny the request on screen, or a named one",
186
- availability: "always"
187
- },
188
- { name: "help", description: "Show the key map", availability: "always" },
189
- { name: "quit", description: "Exit the console", availability: "always" }
119
+ }
190
120
  ];
191
- function availableCommands(running) {
121
+ function relevant(name, context) {
122
+ if (!context) return true;
123
+ if (context.checkpoint) return name === "continue";
124
+ if (["replay", "edit", "export"].includes(name)) return context.history;
125
+ if (name === "retry") return context.retry;
126
+ if (name === "pause") return context.queue > 0 && !context.paused;
127
+ if (name === "continue") return context.queue > 0 && context.paused && !context.awaitingAnswer;
128
+ return true;
129
+ }
130
+ function availableCommands(running, context) {
192
131
  const phase = running ? "running" : "idle";
193
- return COMMANDS.filter((c) => c.availability === "always" || c.availability === phase);
132
+ return COMMANDS.filter(
133
+ (c) => (c.availability === "always" || c.availability === phase) && relevant(c.name, context)
134
+ );
194
135
  }
195
- function canRun(name, running) {
136
+ function canRun(name, running, context) {
196
137
  const command = COMMANDS.find((c) => c.name === name);
197
138
  if (!command) return true;
198
- return availableCommands(running).includes(command);
139
+ return availableCommands(running, context).includes(command);
199
140
  }
200
- function unavailableReason(name, running) {
141
+ function unavailableReason(name, running, context) {
201
142
  const command = COMMANDS.find((c) => c.name === name);
202
- if (!command || availableCommands(running).includes(command)) return void 0;
143
+ if (!command || availableCommands(running, context).includes(command)) return void 0;
144
+ if (!relevant(name, context)) return `/${name} has nothing to act on right now`;
203
145
  return running ? `/${name} waits for the current turn to finish` : `/${name} only applies while a turn is running`;
204
146
  }
205
- function matchCommands(query, running) {
147
+ function matchCommands(query, running, context) {
206
148
  const q = query.trim().toLowerCase();
207
- const pool = availableCommands(running);
149
+ const pool = availableCommands(running, context);
208
150
  if (!q) return pool;
209
151
  const byName = [];
210
152
  const byDescription = [];
@@ -464,16 +406,24 @@ function computeLayout(input) {
464
406
  const compact = columns < COMPACT_COLUMNS;
465
407
  const welcome = !input.overlay && input.turn === 0;
466
408
  const showRail = !welcome && !input.overlay && rows >= RAIL_MIN_ROWS && input.hasRail;
467
- const menuRows = Math.min(Math.max(0, input.menuRows), MAX_MENU_ROWS);
468
- const queueRows = Math.min(Math.max(0, input.queueRows ?? 0), MAX_QUEUE_ROWS);
469
409
  const promptRows = Math.max(1, input.promptRows ?? 1);
470
410
  const railRows = showRail ? Math.min(MAX_RAIL_ROWS, Math.max(1, measureRailRows(input.railCells, columns))) : 0;
411
+ const fixedRows = 4 + MIN_BODY_ROWS + promptRows + (input.notice ? 1 : 0) + (railRows > 0 ? railRows + 1 : 0);
412
+ const queueRows = Math.min(
413
+ Math.max(0, input.queueRows ?? 0),
414
+ MAX_QUEUE_ROWS,
415
+ Math.max(0, rows - fixedRows - (input.menuRows > 0 ? 1 : 0))
416
+ );
417
+ const menuRows = Math.min(
418
+ Math.max(0, input.menuRows),
419
+ MAX_MENU_ROWS,
420
+ Math.max(1, rows - fixedRows - queueRows)
421
+ );
471
422
  const chrome = 1 + // header
472
423
  1 + // rule under the header
473
424
  (railRows > 0 ? railRows + 1 : 0) + // pipeline rail plus the gap above it
474
425
  1 + // gap before the body
475
- (input.notice ? 1 : 0) + (input.viewing ? 1 : 0) + // "viewing turn N" banner
476
- queueRows + menuRows + promptRows + 1;
426
+ (input.notice ? 1 : 0) + queueRows + menuRows + promptRows + 1;
477
427
  const barRoom = Math.max(0, rows - chrome - MIN_BODY_ROWS - 1);
478
428
  const interactionRows2 = barRoom < MIN_INTERACTION_ROWS ? 0 : Math.min(Math.max(0, input.interactionRows ?? 0), MAX_INTERACTION_ROWS, barRoom);
479
429
  const reserved = chrome + (interactionRows2 > 0 ? interactionRows2 + 1 : 0);
@@ -803,14 +753,14 @@ function reduceEvent(state, event) {
803
753
  case "stage:complete": {
804
754
  const stage = state.stages.find((stage2) => stage2.stageIndex === event.payload.stageIndex);
805
755
  if (!stage || stage.outcome !== void 0) return state;
806
- const outcome2 = stageStatus(stage, state.agents);
756
+ const outcome = stageStatus(stage, state.agents);
807
757
  const memberStatuses = Object.fromEntries(
808
758
  stage.agents.map((agent) => [agent.id, state.agents[agent.id]?.status ?? "pending"])
809
759
  );
810
760
  return {
811
761
  ...state,
812
762
  stages: state.stages.map(
813
- (entry) => entry === stage ? { ...entry, outcome: outcome2, memberStatuses } : entry
763
+ (entry) => entry === stage ? { ...entry, outcome, memberStatuses } : entry
814
764
  )
815
765
  };
816
766
  }
@@ -882,8 +832,9 @@ function reduceEvent(state, event) {
882
832
  if (event.type === "agent:failed") {
883
833
  if (next.logs.length === 0 && result.output)
884
834
  next.logs = appendLog([], result.output, "text");
885
- if (result.error && !next.logs.some((s) => s.text.includes(stripAnsi(result.error)))) {
886
- next.logs = appendLog(next.logs, result.error, "system");
835
+ const error = result.error;
836
+ if (error && !next.logs.some((s) => s.text.includes(stripAnsi(error)))) {
837
+ next.logs = appendLog(next.logs, error, "system");
887
838
  }
888
839
  }
889
840
  return { ...state, agents: { ...state.agents, [result.agentId]: next } };
@@ -925,14 +876,120 @@ function reduceEvent(state, event) {
925
876
  return state;
926
877
  }
927
878
  }
879
+ var wrappedCache = /* @__PURE__ */ new WeakMap();
880
+ var MAX_CACHED_WIDTHS = 3;
928
881
  function wrapLog(logs, width) {
929
882
  if (logs.length === 0) return [];
883
+ let byWidth = wrappedCache.get(logs);
884
+ if (byWidth === void 0) {
885
+ byWidth = /* @__PURE__ */ new Map();
886
+ wrappedCache.set(logs, byWidth);
887
+ }
888
+ const cached = byWidth.get(width);
889
+ if (cached !== void 0) return cached;
930
890
  const rendered = renderSegments(logs, width);
931
- return rendered.trimEnd().split("\n").flatMap((line) => wrapLine(line, width));
891
+ const rows = rendered.trimEnd().split("\n").flatMap((line) => wrapLine(line, width));
892
+ if (byWidth.size >= MAX_CACHED_WIDTHS) byWidth.clear();
893
+ byWidth.set(width, rows);
894
+ return rows;
895
+ }
896
+ function totalRows(parts) {
897
+ let total = 0;
898
+ for (const part of parts) total += part.length;
899
+ return total;
900
+ }
901
+ function windowParts(parts, viewport, scroll) {
902
+ const height = Math.max(1, viewport);
903
+ const total = totalRows(parts);
904
+ const maxScroll = Math.max(0, total - height);
905
+ const scrolledBack = Math.min(Math.max(0, scroll), maxScroll);
906
+ const end = total - scrolledBack;
907
+ const start = Math.max(0, end - height);
908
+ const rows = [];
909
+ let offset = 0;
910
+ for (const part of parts) {
911
+ const partEnd = offset + part.length;
912
+ if (partEnd <= start) {
913
+ offset = partEnd;
914
+ continue;
915
+ }
916
+ if (offset >= end) break;
917
+ const from = Math.max(0, start - offset);
918
+ const to = Math.min(part.length, end - offset);
919
+ for (let i = from; i < to; i++) rows.push(part[i]);
920
+ offset = partEnd;
921
+ }
922
+ return { rows, scrolledBack, following: scrolledBack === 0 };
932
923
  }
933
- function focusedRowCount(turn, width) {
934
- const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
935
- return agent?.logs ? wrapLog(agent.logs, width).length : 0;
924
+ function clipToColumns(text, max) {
925
+ if (max <= 0) return "";
926
+ if (displayWidth(text) <= max) return text;
927
+ let head = "";
928
+ let used = 0;
929
+ for (const char of text) {
930
+ const width = displayWidth(char);
931
+ if (used + width > max - 1) break;
932
+ head += char;
933
+ used += width;
934
+ }
935
+ return `${head}\u2026`;
936
+ }
937
+ function turnDivider(turn, width) {
938
+ const MAX_REQ = 45;
939
+ const raw = turn.requirement?.replace(/\s+/g, " ").trim();
940
+ const req = raw ? clipToColumns(raw, MAX_REQ) : void 0;
941
+ const label = req ? ` turn ${turn.turn} \xB7 ${req} ` : ` turn ${turn.turn} `;
942
+ const fill = Math.max(1, Math.floor((width - displayWidth(label)) / 2));
943
+ const rule = `${"\u2500".repeat(fill)}${label}${"\u2500".repeat(fill)}`;
944
+ return `\x1B[2m${clipToColumns(rule, width)}\x1B[0m`;
945
+ }
946
+ function pushRows(target, rows) {
947
+ for (const row of rows) target.push(row);
948
+ }
949
+ var historyCache = /* @__PURE__ */ new WeakMap();
950
+ function historyRows(history, agentId, width) {
951
+ if (history.length === 0) return [];
952
+ let byAgent = historyCache.get(history);
953
+ if (byAgent === void 0) {
954
+ byAgent = /* @__PURE__ */ new Map();
955
+ historyCache.set(history, byAgent);
956
+ }
957
+ let byWidth = byAgent.get(agentId);
958
+ if (byWidth === void 0) {
959
+ byWidth = /* @__PURE__ */ new Map();
960
+ byAgent.set(agentId, byWidth);
961
+ }
962
+ const cached = byWidth.get(width);
963
+ if (cached !== void 0) return cached;
964
+ const rows = [];
965
+ for (const turn of history) {
966
+ rows.push(turnDivider(turn, width));
967
+ const agent = turn.agents[agentId];
968
+ if (agent && agent.logs.length > 0) pushRows(rows, wrapLog(agent.logs, width));
969
+ }
970
+ if (byWidth.size >= MAX_CACHED_WIDTHS) byWidth.clear();
971
+ byWidth.set(width, rows);
972
+ return rows;
973
+ }
974
+ function liveRows(state, agentId, width) {
975
+ const agent = state.agents[agentId];
976
+ if (!agent || agent.logs.length === 0) return [];
977
+ const rows = wrapLog(agent.logs, width);
978
+ if (state.history.length === 0) return rows;
979
+ const out = [turnDivider(state, width)];
980
+ pushRows(out, rows);
981
+ return out;
982
+ }
983
+ function timelineParts(state, agentId, width) {
984
+ const history = historyRows(state.history, agentId, width);
985
+ const live = liveRows(state, agentId, width);
986
+ if (live.length === 0) return history.length === 0 ? [] : [history];
987
+ return history.length === 0 ? [live] : [history, live];
988
+ }
989
+ function streamMatches(state, query, width) {
990
+ if (!state.focusId || !query.trim()) return [];
991
+ const needle = query.toLowerCase();
992
+ return timelineParts(state, state.focusId, width).flat().flatMap((row, index) => row.toLowerCase().includes(needle) ? [index] : []);
936
993
  }
937
994
  function snapshot(state) {
938
995
  return {
@@ -959,14 +1016,6 @@ function snapshot(state) {
959
1016
  reply: state.reply
960
1017
  };
961
1018
  }
962
- function mapShown(state, fn) {
963
- if (state.view) {
964
- const next2 = fn(state.view);
965
- return next2 === state.view ? state : { ...state, view: next2 };
966
- }
967
- const next = fn(state);
968
- return next === state ? state : { ...state, ...next };
969
- }
970
1019
  function recordedTurns(state) {
971
1020
  return [...state.history, ...state.phase === "complete" ? [snapshot(state)] : []];
972
1021
  }
@@ -1040,68 +1089,55 @@ function tuiReducer(state, action) {
1040
1089
  history,
1041
1090
  // A new requirement also replaces the previous leader metadata; leaving
1042
1091
  // it up would make the old answer or routing reason look like the
1043
- // response to the new request. Reading a past turn ends for the same
1044
- // reason — the panes belong to the run.
1092
+ // response to the new request.
1045
1093
  reason: void 0,
1046
1094
  reply: void 0,
1047
- view: void 0,
1048
1095
  // A console setting, carried across turns rather than reset with them.
1049
1096
  verbose: state.verbose
1050
1097
  };
1051
1098
  }
1052
- case "focus:cycle":
1053
- return mapShown(state, (turn) => {
1054
- const ids = Object.keys(turn.agents);
1055
- if (ids.length === 0) return turn;
1056
- const current = turn.focusId ? ids.indexOf(turn.focusId) : -1;
1057
- const nextIndex = (current + action.delta + ids.length) % ids.length;
1058
- if (nextIndex === current) return turn;
1059
- return { ...turn, focusId: ids[nextIndex], autoFollow: false, scroll: 0 };
1060
- });
1061
- case "focus:set":
1062
- return mapShown(state, (turn) => {
1063
- if (!turn.agents[action.agentId]) return turn;
1064
- if (turn.focusId === action.agentId && !turn.autoFollow && turn.scroll === 0) return turn;
1065
- return { ...turn, focusId: action.agentId, autoFollow: false, scroll: 0 };
1066
- });
1067
- case "stream:scroll":
1068
- return mapShown(state, (turn) => {
1069
- const rows = focusedRowCount(turn, action.width);
1070
- const maxScroll = Math.max(0, rows - Math.max(1, action.viewport));
1071
- const next = Math.min(Math.max(0, turn.scroll + action.delta), maxScroll);
1072
- return next === turn.scroll ? turn : { ...turn, scroll: next };
1073
- });
1074
- case "stream:search":
1075
- return mapShown(state, (turn) => {
1076
- const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
1077
- if (!agent?.logs || action.query === "") return turn;
1078
- const rows = wrapLog(agent.logs, action.width);
1079
- const needle = action.query.toLowerCase();
1080
- let index = -1;
1081
- for (let i = rows.length - 1; i >= 0; i--) {
1082
- if (rows[i].toLowerCase().includes(needle)) {
1083
- index = i;
1084
- break;
1085
- }
1086
- }
1087
- if (index === -1) return turn;
1088
- const height = Math.max(1, action.viewport);
1089
- const maxScroll = Math.max(0, rows.length - height);
1090
- const scroll = Math.min(Math.max(0, rows.length - 1 - index), maxScroll);
1091
- return scroll === turn.scroll && !turn.autoFollow ? turn : { ...turn, scroll, autoFollow: false };
1092
- });
1099
+ case "focus:cycle": {
1100
+ const ids = Object.keys(state.agents);
1101
+ if (ids.length === 0) return state;
1102
+ const current = state.focusId ? ids.indexOf(state.focusId) : -1;
1103
+ const nextIndex = (current + action.delta + ids.length) % ids.length;
1104
+ if (nextIndex === current) return state;
1105
+ return { ...state, focusId: ids[nextIndex], autoFollow: false, scroll: 0 };
1106
+ }
1107
+ case "focus:set": {
1108
+ if (!state.agents[action.agentId]) return state;
1109
+ if (state.focusId === action.agentId && !state.autoFollow && state.scroll === 0) return state;
1110
+ return { ...state, focusId: action.agentId, autoFollow: false, scroll: 0 };
1111
+ }
1112
+ case "stream:scroll": {
1113
+ const focusId = state.focusId;
1114
+ if (!focusId) return state;
1115
+ const rows = totalRows(timelineParts(state, focusId, action.width));
1116
+ const maxScroll = Math.max(0, rows - Math.max(1, action.viewport));
1117
+ const next = Math.min(Math.max(0, state.scroll + action.delta), maxScroll);
1118
+ return next === state.scroll ? state : { ...state, scroll: next };
1119
+ }
1120
+ case "stream:search": {
1121
+ const focusId = state.focusId;
1122
+ if (!focusId || action.query === "") return state;
1123
+ const parts = timelineParts(state, focusId, action.width);
1124
+ const matches = streamMatches(state, action.query, action.width);
1125
+ const index = matches[action.match ?? matches.length - 1];
1126
+ if (index === void 0) return state;
1127
+ const total = totalRows(parts);
1128
+ const height = Math.max(1, action.viewport);
1129
+ const maxScroll = Math.max(0, total - height);
1130
+ const scroll = Math.min(Math.max(0, total - 1 - index), maxScroll);
1131
+ return scroll === state.scroll && !state.autoFollow ? state : { ...state, scroll, autoFollow: false };
1132
+ }
1093
1133
  case "stream:latest":
1094
- return mapShown(
1095
- state,
1096
- (turn) => turn.scroll === 0 && turn.autoFollow ? turn : { ...turn, scroll: 0, autoFollow: true }
1097
- );
1098
- case "stream:clear":
1099
- return mapShown(state, (turn) => {
1100
- const id = turn.focusId;
1101
- const agent = id ? turn.agents[id] : void 0;
1102
- if (!id || !agent) return turn;
1103
- return { ...turn, agents: { ...turn.agents, [id]: { ...agent, logs: [] } }, scroll: 0 };
1104
- });
1134
+ return state.scroll === 0 && state.autoFollow ? state : { ...state, scroll: 0, autoFollow: true };
1135
+ case "stream:clear": {
1136
+ const id = state.focusId;
1137
+ const agent = id ? state.agents[id] : void 0;
1138
+ if (!id || !agent) return state;
1139
+ return { ...state, agents: { ...state.agents, [id]: { ...agent, logs: [] } }, scroll: 0 };
1140
+ }
1105
1141
  case "stream:note": {
1106
1142
  const agent = state.agents[action.agentId];
1107
1143
  if (!agent) return state;
@@ -1113,12 +1149,6 @@ function tuiReducer(state, action) {
1113
1149
  }
1114
1150
  };
1115
1151
  }
1116
- case "view:open": {
1117
- const record = state.history.find((turn) => turn.turn === action.turn);
1118
- return record ? { ...state, view: record } : state;
1119
- }
1120
- case "view:close":
1121
- return state.view ? { ...state, view: void 0 } : state;
1122
1152
  case "verbose:toggle":
1123
1153
  return { ...state, verbose: !state.verbose };
1124
1154
  case "session:reset":
@@ -1153,7 +1183,6 @@ function seededState(state, agents, leaderIds, history) {
1153
1183
  autoFollow: true,
1154
1184
  scroll: 0,
1155
1185
  history,
1156
- view: void 0,
1157
1186
  verbose: state.verbose
1158
1187
  };
1159
1188
  }
@@ -1189,18 +1218,8 @@ function turnElapsed(turn, now) {
1189
1218
  const end = turn.phase === "running" ? now : turn.endedAt ?? now;
1190
1219
  return Math.max(0, end - turn.startedAt);
1191
1220
  }
1192
- function visibleLog(turn, viewport, width) {
1193
- const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
1194
- const all = agent?.logs ? wrapLog(agent.logs, width) : [];
1195
- const height = Math.max(1, viewport);
1196
- const maxScroll = Math.max(0, all.length - height);
1197
- const scrolledBack = Math.min(Math.max(0, turn.scroll), maxScroll);
1198
- const end = all.length - scrolledBack;
1199
- return {
1200
- rows: all.slice(Math.max(0, end - height), end),
1201
- scrolledBack,
1202
- following: scrolledBack === 0
1203
- };
1221
+ function visibleTimeline(state, agentId, viewport, width, scroll) {
1222
+ return windowParts(timelineParts(state, agentId, width), viewport, scroll);
1204
1223
  }
1205
1224
 
1206
1225
  // src/tui/interaction.ts
@@ -1457,7 +1476,7 @@ function formComplete(fields, state) {
1457
1476
  }
1458
1477
  var MAX_FORM_FIELDS = 6;
1459
1478
  function questionBarRows(fields) {
1460
- return 2 + Math.min(fields.length, MAX_FORM_FIELDS) + 1 + 1;
1479
+ return 2 + Math.min(fields.length, MAX_FORM_FIELDS) + 1 + 2;
1461
1480
  }
1462
1481
  function interactionRows(interactions, width) {
1463
1482
  const front = interactions[0];
@@ -1485,11 +1504,11 @@ var HELP_GROUPS = [
1485
1504
  {
1486
1505
  title: "panes",
1487
1506
  rows: [
1488
- ["tab \u21E7tab", "Next / previous agent"],
1507
+ ["tab", "Next agent"],
1508
+ ["\u21E7tab", "Toggle permission mode"],
1489
1509
  ["\u2191 \u2193", "Scroll the stream one line"],
1490
1510
  ["pgup pgdn", "Scroll the stream a page"],
1491
- ["ctrl+l", "Clear the focused log"],
1492
- ["esc", "Stop reading a past turn"]
1511
+ ["ctrl+l", "Clear the current turn's output"]
1493
1512
  ]
1494
1513
  },
1495
1514
  {
@@ -1503,10 +1522,10 @@ var HELP_GROUPS = [
1503
1522
  }
1504
1523
  ];
1505
1524
  var KEY_COLUMN = 16;
1506
- var TURN_ROWS = 3;
1507
- var HISTORY_CHROME_ROWS = 4;
1508
- function historyCapacity(maxRows) {
1509
- return Math.max(1, Math.floor((maxRows - HISTORY_CHROME_ROWS) / TURN_ROWS));
1525
+ var ITEM_ROWS = 3;
1526
+ var OVERLAY_CHROME_ROWS = 4;
1527
+ function overlayCapacity(maxRows) {
1528
+ return Math.max(1, Math.floor((maxRows - OVERLAY_CHROME_ROWS) / ITEM_ROWS));
1510
1529
  }
1511
1530
  function listWindowStart(selected, total, capacity) {
1512
1531
  const maxStart = Math.max(0, total - capacity);
@@ -1532,81 +1551,6 @@ function HelpOverlay({ width, maxRows }) {
1532
1551
  /* @__PURE__ */ jsx2(Box2, { marginTop: gap, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "esc or ? to close" }) })
1533
1552
  ] });
1534
1553
  }
1535
- function outcome(turn) {
1536
- if (turn.reply) {
1537
- return turn.reply.mode === "ask" ? { text: `${GLYPH.ask} needs input`, color: COLOR.warn } : { text: `${STATUS.success.glyph} answered`, color: COLOR.ok };
1538
- }
1539
- if (turn.interrupted || turn.phase !== "complete") {
1540
- return { text: "interrupted", color: COLOR.warn };
1541
- }
1542
- if (turn.acceptance) {
1543
- switch (turn.acceptance.status) {
1544
- case "approved":
1545
- return { text: `${STATUS.success.glyph} approved`, color: COLOR.ok };
1546
- case "changes_required":
1547
- return { text: `${GLYPH.warn} changes required`, color: COLOR.warn };
1548
- case "failed":
1549
- return { text: `${STATUS.failed.glyph} acceptance failed`, color: COLOR.error };
1550
- case "cancelled":
1551
- return { text: `${STATUS.cancelled.glyph} acceptance cancelled`, color: COLOR.warn };
1552
- }
1553
- }
1554
- if (turn.cancelled) return { text: `${STATUS.cancelled.glyph} cancelled`, color: COLOR.warn };
1555
- return turn.success ? { text: `${STATUS.success.glyph} done`, color: COLOR.ok } : { text: `${STATUS.failed.glyph} failed`, color: COLOR.error };
1556
- }
1557
- function HistoryOverlay({
1558
- turns,
1559
- selected,
1560
- width,
1561
- maxRows
1562
- }) {
1563
- const capacity = historyCapacity(maxRows);
1564
- const start = listWindowStart(selected, turns.length, capacity);
1565
- const visible = turns.slice(start, start + capacity);
1566
- const range = turns.length > capacity ? ` \xB7 showing ${start + 1}\u2013${start + visible.length} of ${turns.length}` : "";
1567
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1568
- /* @__PURE__ */ jsxs2(Box2, { justifyContent: "space-between", children: [
1569
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "HISTORY" }),
1570
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1571
- turns.length,
1572
- " earlier"
1573
- ] })
1574
- ] }),
1575
- turns.length === 0 ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No earlier turns yet." }) }) : /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: visible.map((turn, offset) => {
1576
- const index = start + offset;
1577
- const focused = index === selected;
1578
- const state = outcome(turn);
1579
- const { done, total } = progress(turn);
1580
- const time = turn.totalDuration === void 0 ? "" : ` \xB7 ${formatDuration(turn.totalDuration)}`;
1581
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [
1582
- /* @__PURE__ */ jsxs2(Box2, { children: [
1583
- /* @__PURE__ */ jsx2(Text2, { color: focused ? COLOR.accent : void 0, children: focused ? `${GLYPH.caret} ` : " " }),
1584
- /* @__PURE__ */ jsxs2(Text2, { bold: focused, color: focused ? COLOR.accent : void 0, children: [
1585
- "turn ",
1586
- turn.turn
1587
- ] }),
1588
- /* @__PURE__ */ jsxs2(Text2, { color: state.color, children: [
1589
- " \xB7 ",
1590
- state.text
1591
- ] }),
1592
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1593
- time,
1594
- " \xB7 ",
1595
- done,
1596
- "/",
1597
- total,
1598
- " agents"
1599
- ] })
1600
- ] }),
1601
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, wrap: "truncate-end", children: [
1602
- " ",
1603
- truncate(turn.requirement ?? "(no requirement)", Math.max(8, width - 6))
1604
- ] })
1605
- ] }, turn.turn);
1606
- }) }),
1607
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: `\u2191\u2193 select \xB7 enter open \xB7 c continue \xB7 esc close${range}` }) })
1608
- ] });
1609
- }
1610
1554
  function SessionsOverlay({
1611
1555
  sessions,
1612
1556
  selected,
@@ -1615,7 +1559,7 @@ function SessionsOverlay({
1615
1559
  maxRows,
1616
1560
  now
1617
1561
  }) {
1618
- const capacity = historyCapacity(maxRows);
1562
+ const capacity = overlayCapacity(maxRows);
1619
1563
  const start = listWindowStart(selected, sessions.length, capacity);
1620
1564
  const visible = sessions.slice(start, start + capacity);
1621
1565
  const range = sessions.length > capacity ? ` \xB7 showing ${start + 1}\u2013${start + visible.length} of ${sessions.length}` : "";
@@ -1663,18 +1607,22 @@ function PermissionBar({
1663
1607
  others,
1664
1608
  cursor,
1665
1609
  released,
1666
- width
1610
+ width,
1611
+ maxRows = Number.POSITIVE_INFINITY
1667
1612
  }) {
1668
1613
  const choices = permissionChoices(request);
1669
- const impact = describeImpact(request, Math.max(8, width - 6));
1670
- const also = waitingLine(others);
1614
+ const capacity = Math.max(1, Math.min(choices.length, maxRows - 3));
1615
+ const start = listWindowStart(cursor ?? 0, choices.length, capacity);
1616
+ const extra = Math.max(0, maxRows - 3 - capacity);
1617
+ const impact = describeImpact(request, Math.max(8, width - 6)).slice(0, extra);
1618
+ const also = extra > impact.length ? waitingLine(others) : void 0;
1671
1619
  const tool = clean(request.toolName ?? request.kind ?? "tool");
1672
1620
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, width, children: [
1673
1621
  /* @__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` : ""}` }),
1674
1622
  /* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: ` ${truncate(clean(request.title), Math.max(8, width - 4))}` }),
1675
1623
  impact.map((line) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` ${line}` }, line)),
1676
- choices.map((choice, index) => {
1677
- const active = index === cursor;
1624
+ choices.slice(start, start + capacity).map((choice, index) => {
1625
+ const active = start + index === cursor;
1678
1626
  return /* @__PURE__ */ jsx3(
1679
1627
  Text3,
1680
1628
  {
@@ -1687,7 +1635,7 @@ function PermissionBar({
1687
1635
  );
1688
1636
  }),
1689
1637
  also === void 0 ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` ${also}` }),
1690
- /* @__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" })
1638
+ maxRows < 4 ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: released ? " typing \xB7 esc to return to permission options" : " \u2191\u2193 or 1-9 choose \xB7 enter confirm \xB7 esc to type instead" })
1691
1639
  ] });
1692
1640
  }
1693
1641
  function QuestionForm({
@@ -1696,18 +1644,26 @@ function QuestionForm({
1696
1644
  fields,
1697
1645
  form,
1698
1646
  draft,
1699
- width
1647
+ width,
1648
+ maxRows = Number.POSITIVE_INFINITY
1700
1649
  }) {
1701
1650
  const focused = fields[form.field];
1702
1651
  const titleWidth = Math.min(
1703
1652
  24,
1704
1653
  Math.max(4, ...fields.map((field) => field.title.length + (field.required ? 2 : 0)))
1705
1654
  );
1706
- const capacity = Math.min(fields.length, MAX_FORM_FIELDS);
1707
- const start = listWindowStart(form.field, fields.length, Math.max(1, capacity));
1655
+ const actions = ["Submit answers", "Skip question", "Cancel request"];
1656
+ const actionIndex = form.field - fields.length;
1657
+ const compact = maxRows < 7;
1658
+ const capacity = compact ? focused ? 1 : 0 : Math.min(fields.length, MAX_FORM_FIELDS, maxRows - 5);
1659
+ const start = listWindowStart(
1660
+ Math.min(form.field, fields.length - 1),
1661
+ fields.length,
1662
+ Math.max(1, capacity)
1663
+ );
1708
1664
  const visible = fields.slice(start, start + capacity);
1709
- const range = fields.length > capacity ? ` \xB7 ${start + 1}\u2013${start + visible.length}/${fields.length}` : "";
1710
- const detail = focused === void 0 ? [] : detailLines(focused, width);
1665
+ const range = fields.length > capacity && focused ? ` \xB7 field ${form.field + 1}/${fields.length}` : "";
1666
+ const detail = focused && (!compact || maxRows >= 5) ? detailLines(focused, width).slice(0, 1) : [];
1711
1667
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, width, children: [
1712
1668
  /* @__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` : ""}` }),
1713
1669
  /* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: clean(request.message) }),
@@ -1728,7 +1684,8 @@ function QuestionForm({
1728
1684
  );
1729
1685
  }),
1730
1686
  detail.map((line) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` ${line}` }, line)),
1731
- /* @__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` })
1687
+ compact && focused ? null : /* @__PURE__ */ jsx3(Text3, { color: actionIndex >= 0 ? COLOR.accent : void 0, wrap: "truncate-end", children: compact || width < 72 ? actionIndex >= 0 ? `\u203A ${actions[actionIndex]}` : "tab to submit / skip / cancel" : actions.map((label, index) => (actionIndex === index ? "\u203A " : " ") + label).join(" \xB7 ") }),
1688
+ maxRows < 4 ? null : /* @__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` })
1732
1689
  ] });
1733
1690
  }
1734
1691
  function fieldValue(field, form, draft, focused) {
@@ -1846,17 +1803,19 @@ function Divider() {
1846
1803
  );
1847
1804
  }
1848
1805
  function StreamPanel({
1849
- turn,
1806
+ state,
1850
1807
  height,
1851
1808
  width,
1852
1809
  now,
1853
1810
  spinner
1854
1811
  }) {
1855
- const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
1812
+ const agent = state.focusId ? state.agents[state.focusId] : void 0;
1856
1813
  const viewport = Math.max(1, height - 1);
1857
- const window = visibleLog(turn, viewport, width);
1814
+ const window = visibleTimeline(state, state.focusId ?? "", viewport, width, state.scroll);
1858
1815
  const style = agent ? STATUS[agent.status] : void 0;
1859
1816
  const elapsed = agent ? agentElapsed(agent, now) : void 0;
1817
+ const focusId = state.focusId;
1818
+ const turnCount = focusId ? recordedTurns(state).filter((turn) => (turn.agents[focusId]?.logs.length ?? 0) > 0).length : 0;
1860
1819
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexGrow: 1, height, paddingLeft: 1, children: [
1861
1820
  /* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
1862
1821
  /* @__PURE__ */ jsx4(Box4, { flexShrink: 1, children: /* @__PURE__ */ jsxs4(Text4, { wrap: "truncate-end", children: [
@@ -1883,6 +1842,11 @@ function StreamPanel({
1883
1842
  agent?.costUsd !== void 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1884
1843
  " \xB7 ",
1885
1844
  formatCost(agent.costUsd)
1845
+ ] }) : null,
1846
+ turnCount > 1 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1847
+ " \xB7 ",
1848
+ turnCount,
1849
+ " turns"
1886
1850
  ] }) : null
1887
1851
  ] }) }),
1888
1852
  /* @__PURE__ */ jsxs4(Box4, { flexShrink: 0, children: [
@@ -1893,13 +1857,14 @@ function StreamPanel({
1893
1857
  /* @__PURE__ */ jsx4(Text4, { color: window.following ? COLOR.accent : COLOR.warn, children: window.following ? GLYPH.live : `${GLYPH.paused} ${window.scrolledBack}\u2191` })
1894
1858
  ] })
1895
1859
  ] }),
1896
- window.rows.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: turn.phase === "running" ? "waiting for output\u2026" : "no output yet" }) : (
1897
- // The tail, already wrapped to `width` by `visibleLog` — the pane draws
1898
- // rows, and one log line is several of them. `visibleLog` also picks
1899
- // which rows, so the content here is exactly the pane's height and the
1900
- // oldest rows are the ones already gone: nothing is left to clip, which
1901
- // is why this box is not bottom-pinned. Pinning it would only leave a
1902
- // gap under the header whenever an agent has not said much yet.
1860
+ window.rows.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: state.phase === "running" ? "waiting for output\u2026" : "no output yet" }) : (
1861
+ // The tail, already wrapped to `width` by `visibleTimeline` — the pane
1862
+ // draws rows, and one log line is several of them. `visibleTimeline`
1863
+ // also picks which rows, so the content here is exactly the pane's
1864
+ // height and the oldest rows are the ones already gone: nothing is left
1865
+ // to clip, which is why this box is not bottom-pinned. Pinning it would
1866
+ // only leave a gap under the header whenever an agent has not said much
1867
+ // yet.
1903
1868
  //
1904
1869
  // Rows are rendered with `truncate-end` rather than letting Ink wrap
1905
1870
  // them again: they are supposed to fit, and if `width` was ever
@@ -2419,6 +2384,16 @@ var SESSION_SAVE_INTERVAL_MS = 2e3;
2419
2384
  var INPUT_PLACEHOLDER = "describe what the team should do\u2026";
2420
2385
  var RUNNING_PLACEHOLDER = "type to queue a follow-up\u2026";
2421
2386
  var EMPTY_LINE = { text: "", cursor: 0 };
2387
+ function retryCandidates(state) {
2388
+ const turn = recordedTurns(state).slice(-1)[0];
2389
+ if (!turn?.requirement) return [];
2390
+ if (turn.acceptance && turn.acceptance.status !== "approved") {
2391
+ return [
2392
+ ...new Set(turn.stages.flatMap((stage) => stage.agents.map((agent) => agent.id)))
2393
+ ].filter((id) => id !== turn.acceptance?.reviewer);
2394
+ }
2395
+ return Object.values(turn.agents).filter((agent) => agent.status === "failed").map((agent) => agent.id);
2396
+ }
2422
2397
  function pickRecordedTurn(state, raw, whenEmpty) {
2423
2398
  const turns = recordedTurns(state);
2424
2399
  if (turns.length === 0) return { error: whenEmpty };
@@ -2483,10 +2458,10 @@ function StatusLine({
2483
2458
  }
2484
2459
  if (state.phase === "complete") {
2485
2460
  const time = state.totalDuration === void 0 ? "" : ` ${formatDuration(state.totalDuration)}`;
2486
- 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}`;
2461
+ const outcome = 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}`;
2487
2462
  const cost = state.totalCostUsd === void 0 ? "" : ` \xB7 ${formatCost(state.totalCostUsd)}`;
2488
2463
  return /* @__PURE__ */ jsxs6(Text6, { color: state.success ? COLOR.ok : COLOR.error, wrap: "truncate-end", children: [
2489
- outcome2,
2464
+ outcome,
2490
2465
  cost,
2491
2466
  state.warning ? ` \xB7 ${GLYPH.warn} ${state.warning}` : ""
2492
2467
  ] });
@@ -2550,12 +2525,19 @@ function App({
2550
2525
  queueRef.current = queue;
2551
2526
  const [cancelled, setCancelled] = useState(false);
2552
2527
  const [error, setError] = useState();
2528
+ const [permissionMode, setPermissionMode] = useState(
2529
+ // Same precedence the runner uses, so the header can never announce a mode
2530
+ // the agents are not actually running under. `ask` when nobody configured
2531
+ // one, because a human is sitting here to answer.
2532
+ resolvePermissionMode(config) ?? "ask"
2533
+ );
2553
2534
  const [notice, setNotice] = useState(
2554
2535
  launchNoticeRef.current ?? restored.notice
2555
2536
  );
2556
2537
  const [helpOpen, setHelpOpen] = useState(false);
2557
- const [historyOpen, setHistoryOpen] = useState(false);
2558
- const [historyCursor, setHistoryCursor] = useState(0);
2538
+ const [picker, setPicker] = useState(null);
2539
+ const [operationInput, setOperationInput] = useState(null);
2540
+ const [searchResult, setSearchResult] = useState(null);
2559
2541
  const [sessionsOpen, setSessionsOpen] = useState(false);
2560
2542
  const [sessionCursor, setSessionCursor] = useState(0);
2561
2543
  const [sessionList, setSessionList] = useState([]);
@@ -2567,6 +2549,12 @@ function App({
2567
2549
  const sessionsRef = useRef(null);
2568
2550
  if (!sessionsRef.current) sessionsRef.current = providedSessions ?? new AgentSessionManager();
2569
2551
  const continuationRef = useRef(null);
2552
+ useEffect(() => {
2553
+ if (initialSession?.kind !== "resume" || !("id" in initialSession)) return;
2554
+ const history = restoredRef.current?.history ?? [];
2555
+ const resumable = [...history].reverse().find((turn) => Object.keys(turnSessions(turn)).length > 0);
2556
+ continuationRef.current = resumable ? { turn: resumable.turn, sessions: turnSessions(resumable) } : null;
2557
+ }, [initialSession]);
2570
2558
  const activeTurnRef = useRef(null);
2571
2559
  const reporterErrorRef = useRef(null);
2572
2560
  const closingRef = useRef(false);
@@ -2637,16 +2625,21 @@ function App({
2637
2625
  const handleCheckpoint = useCallback((info, signal) => {
2638
2626
  if (closingRef.current || signal.aborted) return Promise.resolve({ action: "cancel" });
2639
2627
  return new Promise((resolve2) => {
2640
- checkpointResolveRef.current = resolve2;
2628
+ const settle = (response) => {
2629
+ signal.removeEventListener("abort", onAbort);
2630
+ if (checkpointResolveRef.current === settle) {
2631
+ checkpointResolveRef.current = null;
2632
+ checkpointLabelRef.current = "";
2633
+ }
2634
+ resolve2(response);
2635
+ };
2636
+ checkpointResolveRef.current = settle;
2641
2637
  checkpointLabelRef.current = info.label;
2642
2638
  const names = info.completedAgents.map((a) => a.name).join(", ");
2643
2639
  setNotice(
2644
- `checkpoint: ${info.label} \u2014 ${names} done \xB7 /continue /modify <instruction> /cancel`
2640
+ `checkpoint: ${info.label} \u2014 ${names} done \xB7 /continue \xB7 type feedback \xB7 ctrl+c cancel`
2645
2641
  );
2646
- const onAbort = () => {
2647
- checkpointResolveRef.current = null;
2648
- resolve2({ action: "cancel" });
2649
- };
2642
+ const onAbort = () => settle({ action: "cancel" });
2650
2643
  signal.addEventListener("abort", onAbort, { once: true });
2651
2644
  });
2652
2645
  }, []);
@@ -2763,6 +2756,13 @@ function App({
2763
2756
  setError(void 0);
2764
2757
  setCancelled(false);
2765
2758
  setNotice(void 0);
2759
+ if (opts?.newSession && !requirement) {
2760
+ busyRef.current = false;
2761
+ setRunning(false);
2762
+ setSearchResult(null);
2763
+ setNotice("new session \u2014 type a requirement to begin");
2764
+ return;
2765
+ }
2766
2766
  const earlierTurns = opts?.newSession ? [] : conversationTurns(stateRef.current);
2767
2767
  dispatch({
2768
2768
  type: "turn:start",
@@ -2780,7 +2780,16 @@ function App({
2780
2780
  // so the leader is handed the turns behind this one as well.
2781
2781
  history: earlierTurns,
2782
2782
  logDir,
2783
- permissions: !config.permissions || config.permissions.mode === "ask" ? { mode: "ask", onRequest: requestPermission } : config.permissions,
2783
+ permissions: permissionMode === "ask" ? { mode: "ask", onRequest: requestPermission } : { mode: permissionMode },
2784
+ // The console's mode is a live decision, so it outranks any per-agent
2785
+ // `permissions` written in the file: otherwise shift+tab would relabel
2786
+ // the header while an agent that declares its own mode kept the old
2787
+ // behaviour. Per-agent modes stay a headless-run concern.
2788
+ agents: config.agents.map((agent) => ({
2789
+ ...agent,
2790
+ permissions: { mode: permissionMode }
2791
+ })),
2792
+ ...config.leader ? { leader: { ...config.leader, permissions: { mode: permissionMode } } } : {},
2784
2793
  questions: config.questions?.onRequest ? config.questions : { onRequest: requestQuestion },
2785
2794
  onCheckpoint: config.onCheckpoint ?? handleCheckpoint,
2786
2795
  // A retry names the agents to redo; a continued conversation carries only
@@ -2845,7 +2854,8 @@ function App({
2845
2854
  requestPermission,
2846
2855
  requestQuestion,
2847
2856
  handleCheckpoint,
2848
- clearInteractions
2857
+ clearInteractions,
2858
+ permissionMode
2849
2859
  ]
2850
2860
  );
2851
2861
  useEffect(() => {
@@ -2865,6 +2875,7 @@ function App({
2865
2875
  return;
2866
2876
  }
2867
2877
  if (closingRef.current) return;
2878
+ setSearchResult(null);
2868
2879
  const restored2 = restoreConsole(config.workDir, id);
2869
2880
  sessionIdRef.current = id;
2870
2881
  persistedTurnRef.current = restored2.persistedTurn;
@@ -2955,30 +2966,32 @@ function App({
2955
2966
  })();
2956
2967
  return shutdownRef.current;
2957
2968
  }, [config.workDir, exit, cleanup]);
2958
- const query = menuQuery(line.text);
2959
- const matches = useMemo(
2960
- () => query === void 0 ? [] : matchCommands(query, running),
2961
- [query, running]
2962
- );
2963
- const historyTurns = useMemo(() => [...state.history].reverse(), [state.history]);
2964
- const historyIndex = historyTurns.length === 0 ? 0 : Math.min(historyCursor, historyTurns.length - 1);
2965
- const shown = state.view ?? state;
2966
- const arg = argQuery(line.text);
2967
- const argRows = arg ? completeAgents(arg.partial, Object.keys(shown.agents)).map((id) => ({
2969
+ const commandContext = {
2970
+ history: recordedTurns(state).length > 0,
2971
+ retry: retryCandidates(state).length > 0,
2972
+ queue: queue.length,
2973
+ paused: queuePaused,
2974
+ awaitingAnswer,
2975
+ checkpoint: checkpointResolveRef.current !== null
2976
+ };
2977
+ const query = interactions[0]?.kind === "question" ? void 0 : menuQuery(line.text);
2978
+ const matches = query === void 0 ? [] : matchCommands(query, running, commandContext);
2979
+ const arg = interactions[0]?.kind === "question" ? void 0 : argQuery(line.text);
2980
+ const argRows = arg ? completeAgents(arg.partial, Object.keys(state.agents)).map((id) => ({
2968
2981
  key: id,
2969
2982
  label: id,
2970
2983
  // The static role, not the per-turn task title: while completing, the
2971
2984
  // user is asking "which agent is this", not "what is it doing now".
2972
- description: shown.agents[id]?.description ?? shown.agents[id]?.name ?? id
2985
+ description: state.agents[id]?.description ?? state.agents[id]?.name ?? id
2973
2986
  })) : [];
2974
- const allRows = arg ? argRows : paletteRows(matches);
2987
+ const allRows = picker ? picker.rows : arg ? argRows : paletteRows(matches);
2975
2988
  const paletteKey = arg ? `${arg.command.name} ${arg.partial}` : query;
2976
2989
  const paletteTotal = allRows.length;
2977
2990
  const menuIndex = menuCursor.query === paletteKey ? menuCursor.index : 0;
2978
- const selected = paletteTotal === 0 ? 0 : Math.min(menuIndex, paletteTotal - 1);
2979
- const menuOpen = !helpOpen && !historyOpen && !sessionsOpen && query !== void 0 && matches.length > 0;
2980
- const argOpen = !helpOpen && !historyOpen && !sessionsOpen && arg !== void 0 && argRows.length > 0;
2981
- const paletteOpen = menuOpen || argOpen;
2991
+ const selected = paletteTotal === 0 ? 0 : Math.min(picker?.cursor ?? menuIndex, paletteTotal - 1);
2992
+ const menuOpen = !picker && !operationInput && !helpOpen && !sessionsOpen && query !== void 0 && matches.length > 0;
2993
+ const argOpen = !picker && !operationInput && !helpOpen && !sessionsOpen && arg !== void 0 && argRows.length > 0;
2994
+ const paletteOpen = picker !== null || menuOpen || argOpen;
2982
2995
  const front = interactions[0];
2983
2996
  const menu = front?.kind === "permission" && permissionMenu.id === front.shortId ? permissionMenu : { id: front?.shortId ?? "", cursor: 0, released: false };
2984
2997
  const frontQuestion = front?.kind === "question" ? front : void 0;
@@ -2998,15 +3011,14 @@ function App({
2998
3011
  columns,
2999
3012
  rows,
3000
3013
  turn: state.turn,
3001
- overlay: helpOpen || historyOpen || sessionsOpen,
3014
+ overlay: helpOpen || sessionsOpen,
3002
3015
  menuRows: paletteOpen ? paletteTotal : 0,
3003
3016
  queueRows: queue.length,
3004
3017
  interactionRows: interactionRows(interactions, Math.max(MIN_COLUMNS, columns)),
3005
3018
  promptRows,
3006
3019
  notice: notice !== void 0 || queuePaused && queue.length > 0,
3007
- viewing: state.view !== void 0,
3008
- hasRail: shown.leaderIds.length > 0 || shown.stages.length > 0,
3009
- railCells: railCellWidths(shown)
3020
+ hasRail: state.leaderIds.length > 0 || state.stages.length > 0,
3021
+ railCells: railCellWidths(state)
3010
3022
  });
3011
3023
  const steerRunningAgents = useCallback(async (text) => {
3012
3024
  const manager = sessionsRef.current;
@@ -3031,11 +3043,11 @@ function App({
3031
3043
  setNotice(describeSteering(reports));
3032
3044
  }, []);
3033
3045
  const runCommand = useCallback(
3034
- (name, args, rawText) => {
3046
+ (name, args) => {
3035
3047
  setLine(EMPTY_LINE);
3036
- if (checkpointResolveRef.current && name !== "continue" && name !== "modify" && name !== "cancel") {
3048
+ if (checkpointResolveRef.current && name !== "continue") {
3037
3049
  setNotice(
3038
- `checkpoint: ${checkpointLabelRef.current} \xB7 /continue /modify <instruction> /cancel`
3050
+ `checkpoint: ${checkpointLabelRef.current} \xB7 /continue \xB7 type feedback \xB7 ctrl+c cancel`
3039
3051
  );
3040
3052
  return;
3041
3053
  }
@@ -3043,111 +3055,86 @@ function App({
3043
3055
  setNotice(unavailableReason(name, running));
3044
3056
  return;
3045
3057
  }
3046
- const shown2 = state.view ?? state;
3047
3058
  if (closingRef.current) return;
3048
- switch (name) {
3049
- case "respond":
3050
- case "skip":
3051
- case "dismiss": {
3052
- const [id, ...jsonParts] = args;
3053
- if (!id || (name === "respond" ? jsonParts.length === 0 : args.length !== 1)) {
3054
- setNotice(
3055
- name === "respond" ? "usage: /respond <question id> <JSON object>" : `usage: /${name} <question id>`
3056
- );
3057
- return;
3058
- }
3059
- const matches2 = interactionsRef.current.filter(
3060
- (entry2) => entry2.request.requestId === id || entry2.shortId === id
3059
+ const choose = (rows2) => {
3060
+ setPicker({ command: name, rows: rows2, cursor: 0 });
3061
+ setNotice(`choose ${name} \xB7 enter confirm \xB7 esc cancel`);
3062
+ };
3063
+ if (args.length === 0) {
3064
+ if (name === "mode") {
3065
+ choose(
3066
+ ["ask", "auto", "deny"].map((mode) => ({
3067
+ key: mode,
3068
+ label: mode,
3069
+ description: mode === "ask" ? "Ask before tools run" : mode === "auto" ? "Allow tools automatically" : "Refuse tool requests"
3070
+ }))
3061
3071
  );
3062
- const entry = matches2.length === 1 ? matches2[0] : void 0;
3063
- if (entry?.kind !== "question") {
3064
- setNotice(`unknown or stale question request: ${id}`);
3065
- return;
3066
- }
3067
- if (name !== "respond") {
3068
- entry.settle({ action: name === "skip" ? "decline" : "cancel" });
3069
- setNotice(`${name === "skip" ? "skipped" : "dismissed"} ${entry.shortId}`);
3070
- return;
3071
- }
3072
- let content;
3073
- try {
3074
- const rawJson = /^\/respond\s+\S+\s+([\s\S]+)$/.exec(rawText ?? "")?.[1] ?? "";
3075
- content = JSON.parse(rawJson);
3076
- } catch {
3077
- setNotice("response must be a valid JSON object");
3072
+ return;
3073
+ }
3074
+ if (name === "focus") {
3075
+ choose(
3076
+ Object.values(state.agents).map((agent) => ({
3077
+ key: agent.id,
3078
+ label: agent.name,
3079
+ description: agent.description ?? agent.id
3080
+ }))
3081
+ );
3082
+ return;
3083
+ }
3084
+ if (name === "retry") {
3085
+ const ids = retryCandidates(state);
3086
+ if (ids.length === 0) {
3087
+ setNotice("nothing failed");
3078
3088
  return;
3079
3089
  }
3080
- if (!validateRuntimeQuestionContent(entry.request.requestedSchema, content)) {
3081
- setNotice(
3082
- "response does not match the question schema; check required fields and choices"
3083
- );
3090
+ choose([
3091
+ { key: "all", label: "All failed / repair agents", description: ids.join(", ") },
3092
+ ...ids.map((id) => ({
3093
+ key: id,
3094
+ label: state.agents[id]?.name ?? id,
3095
+ description: state.agents[id]?.error ?? "Retry this agent"
3096
+ }))
3097
+ ]);
3098
+ return;
3099
+ }
3100
+ if (["replay", "edit", "export"].includes(name)) {
3101
+ const turns = recordedTurns(state).slice().reverse();
3102
+ if (turns.length === 0) {
3103
+ setNotice(`nothing to ${name} yet`);
3084
3104
  return;
3085
3105
  }
3086
- entry.settle({
3087
- action: "accept",
3088
- content
3089
- });
3090
- setNotice(`responded to ${entry.shortId}`);
3106
+ choose(
3107
+ turns.map((turn) => ({
3108
+ key: String(turn.turn),
3109
+ label: `Turn ${turn.turn}`,
3110
+ description: turn.requirement?.replace(/\s+/g, " ") ?? "(no requirement)"
3111
+ }))
3112
+ );
3113
+ return;
3114
+ }
3115
+ if (name === "search" || name === "steer") {
3116
+ setOperationInput(name);
3117
+ setNotice(
3118
+ name === "search" ? "search the focused stream \xB7 enter find \xB7 esc cancel" : "message running agents \xB7 enter send \xB7 esc cancel"
3119
+ );
3091
3120
  return;
3092
3121
  }
3093
- case "approve":
3094
- case "deny": {
3095
- const onScreen = interactionsRef.current[0];
3096
- const byId = (id) => {
3097
- const found = interactionsRef.current.filter(
3098
- (item) => item.request.requestId === id || item.shortId === id
3099
- );
3100
- if (found.length !== 1) setNotice(`unknown or stale permission request: ${id}`);
3101
- return found.length === 1 ? found[0] : void 0;
3102
- };
3103
- let entry;
3104
- let number;
3105
- if (name === "approve" && args.length === 1 && /^\d+$/.test(args[0])) {
3106
- number = args[0];
3107
- entry = onScreen;
3108
- } else if (name === "approve" && args.length === 2) {
3109
- number = args[1];
3110
- entry = byId(args[0]);
3111
- } else if (name === "deny" && args.length === 0) {
3112
- entry = onScreen;
3113
- } else if (name === "deny" && args.length === 1) {
3114
- entry = byId(args[0]);
3122
+ }
3123
+ switch (name) {
3124
+ case "mode": {
3125
+ const m = args[0];
3126
+ if (m === "auto" || m === "ask" || m === "deny") {
3127
+ setPermissionMode(m);
3128
+ setNotice(`permission mode: ${m}`);
3115
3129
  } else {
3116
- setNotice(
3117
- name === "approve" ? "usage: /approve [id] <option number>" : "usage: /deny [id]"
3118
- );
3119
- return;
3120
- }
3121
- if (entry === void 0) {
3122
- if (args.length === 0 || /^\d+$/.test(args[0] ?? "")) {
3123
- setNotice("no permission request on screen");
3124
- }
3125
- return;
3126
- }
3127
- if (entry.kind !== "permission") {
3128
- setNotice("the request on screen is a question, not a permission");
3129
- return;
3130
+ setNotice("usage: /mode auto | ask | deny");
3130
3131
  }
3131
- if (name === "deny") {
3132
- entry.settle();
3133
- setNotice(`denied ${entry.shortId}`);
3134
- return;
3135
- }
3136
- const option = /^\d+$/.test(number ?? "") ? entry.request.options[Number(number) - 1] : void 0;
3137
- if (!option) {
3138
- setNotice(
3139
- `invalid option for ${entry.shortId}; choose 1\u2013${entry.request.options.length}`
3140
- );
3141
- return;
3142
- }
3143
- entry.settle(option.optionId);
3144
- setNotice(`selected ${clean(option.name)} for ${entry.shortId}`);
3145
3132
  return;
3146
3133
  }
3147
3134
  case "new": {
3148
3135
  const text = args.join(" ");
3149
- if (!text) {
3150
- setNotice("usage: /new <requirement> \u2014 starts a new session with fresh agent sessions");
3136
+ if (text) {
3137
+ setNotice("/new opens an empty session \u2014 enter the requirement afterwards");
3151
3138
  return;
3152
3139
  }
3153
3140
  setQueue([]);
@@ -3156,15 +3143,6 @@ function App({
3156
3143
  void startTurn(text, { newSession: true });
3157
3144
  return;
3158
3145
  }
3159
- case "answer": {
3160
- const text = args.join(" ");
3161
- if (!awaitingAnswerRef.current || !text) {
3162
- setNotice("usage: /answer <text> \u2014 only when the leader is waiting for an answer");
3163
- return;
3164
- }
3165
- void startTurn(text);
3166
- return;
3167
- }
3168
3146
  case "steer": {
3169
3147
  const text = args.join(" ");
3170
3148
  if (!text) {
@@ -3175,6 +3153,10 @@ function App({
3175
3153
  return;
3176
3154
  }
3177
3155
  case "pause":
3156
+ if (queueRef.current.length === 0) {
3157
+ setNotice("no queued turns to pause");
3158
+ return;
3159
+ }
3178
3160
  queuePausedRef.current = true;
3179
3161
  setQueuePaused(true);
3180
3162
  setNotice("queue paused \u2014 the active turn continues");
@@ -3192,65 +3174,31 @@ function App({
3192
3174
  setNotice("answer the leader first; queued input is not an answer");
3193
3175
  return;
3194
3176
  }
3177
+ if (queueRef.current.length === 0) {
3178
+ setNotice("no queued turns to resume");
3179
+ return;
3180
+ }
3195
3181
  queuePausedRef.current = false;
3196
3182
  setQueuePaused(false);
3197
3183
  setNotice("queue resumed");
3198
3184
  return;
3199
3185
  }
3200
- case "modify": {
3201
- const instruction = args.join(" ");
3202
- if (!instruction) {
3203
- setNotice("usage: /modify <instruction> \u2014 only at a checkpoint");
3204
- return;
3205
- }
3206
- if (!checkpointResolveRef.current) {
3207
- setNotice("no checkpoint is waiting");
3208
- return;
3209
- }
3210
- const resolve2 = checkpointResolveRef.current;
3211
- checkpointResolveRef.current = null;
3212
- checkpointLabelRef.current = "";
3213
- resolve2({ action: "modify", instruction });
3214
- setNotice("modifying and continuing\u2026");
3215
- return;
3216
- }
3217
- case "agents":
3218
- setNotice(
3219
- `team: ${plannedAgents(config).map((agent) => agent.id).join(", ")}`
3220
- );
3221
- return;
3222
3186
  case "focus": {
3223
3187
  const input = args[0];
3224
3188
  if (!input) {
3225
3189
  setNotice("usage: /focus <agent id>");
3226
3190
  return;
3227
3191
  }
3228
- const resolved = resolveAgentId(input, Object.keys(shown2.agents));
3192
+ const resolved = resolveAgentId(input, Object.keys(state.agents));
3229
3193
  if ("error" in resolved) {
3230
3194
  setNotice(resolved.error);
3231
3195
  return;
3232
3196
  }
3197
+ setSearchResult(null);
3233
3198
  dispatch({ type: "focus:set", agentId: resolved.id });
3234
3199
  setNotice(`focused ${resolved.id}`);
3235
3200
  return;
3236
3201
  }
3237
- case "clear":
3238
- if (!shown2.focusId) {
3239
- setNotice("nothing focused");
3240
- return;
3241
- }
3242
- dispatch({ type: "stream:clear" });
3243
- setNotice(`cleared ${shown2.focusId}`);
3244
- return;
3245
- case "history":
3246
- if (state.history.length === 0) {
3247
- setNotice("no earlier turns yet");
3248
- return;
3249
- }
3250
- setNotice(void 0);
3251
- setHistoryCursor(0);
3252
- setHistoryOpen(true);
3253
- return;
3254
3202
  case "sessions": {
3255
3203
  const list = listSessions(config.workDir);
3256
3204
  setSessionList(list);
@@ -3265,38 +3213,44 @@ function App({
3265
3213
  setNotice(state.verbose ? "verbose off" : "verbose on \u2014 showing tool detail");
3266
3214
  return;
3267
3215
  case "retry": {
3268
- if (state.phase !== "complete" || !state.requirement) {
3216
+ const turn = recordedTurns(state).slice(-1)[0];
3217
+ if (!turn?.requirement) {
3269
3218
  setNotice("nothing to retry yet");
3270
3219
  return;
3271
3220
  }
3221
+ if (args[0] === "all") args = [];
3272
3222
  let ids;
3273
- if (state.acceptance && state.acceptance.status !== "approved" && (!args[0] || args[0] === state.acceptance.reviewer)) {
3223
+ if (turn.acceptance && turn.acceptance.status !== "approved" && (!args[0] || args[0] === turn.acceptance.reviewer)) {
3274
3224
  ids = [
3275
- ...new Set(state.stages.flatMap((stage) => stage.agents.map((agent) => agent.id)))
3276
- ].filter((id) => id !== state.acceptance?.reviewer);
3225
+ ...new Set(turn.stages.flatMap((stage) => stage.agents.map((agent) => agent.id)))
3226
+ ].filter((id) => id !== turn.acceptance?.reviewer);
3277
3227
  } else if (args[0]) {
3278
- const resolved = resolveAgentId(args[0], Object.keys(state.agents));
3228
+ const resolved = resolveAgentId(args[0], Object.keys(turn.agents));
3279
3229
  if ("error" in resolved) {
3280
3230
  setNotice(resolved.error);
3281
3231
  return;
3282
3232
  }
3233
+ if (!retryCandidates(state).includes(resolved.id)) {
3234
+ setNotice(`agent has not failed or does not need repair: ${resolved.id}`);
3235
+ return;
3236
+ }
3283
3237
  ids = [resolved.id];
3284
3238
  } else {
3285
- ids = Object.values(state.agents).filter((agent) => agent.status === "failed").map((agent) => agent.id);
3239
+ ids = Object.values(turn.agents).filter((agent) => agent.status === "failed").map((agent) => agent.id);
3286
3240
  }
3287
3241
  if (ids.length === 0) {
3288
3242
  setNotice("nothing failed");
3289
3243
  return;
3290
3244
  }
3291
3245
  const sessions = Object.fromEntries(
3292
- ids.filter((id) => state.agents[id]?.sessionId).map((id) => [id, state.agents[id].sessionId])
3246
+ ids.filter((id) => turn.agents[id]?.sessionId).map((id) => [id, turn.agents[id].sessionId])
3293
3247
  );
3294
- if (ids.some((id) => state.leaderIds.includes(id))) {
3295
- void startTurn(state.requirement);
3248
+ if (ids.some((id) => turn.leaderIds.includes(id))) {
3249
+ void startTurn(turn.requirement);
3296
3250
  setNotice("retrying leader routing");
3297
3251
  return;
3298
3252
  }
3299
- void startTurn(state.effectiveRequirement ?? state.requirement, {
3253
+ void startTurn(turn.effectiveRequirement ?? turn.requirement, {
3300
3254
  resume: {
3301
3255
  agents: ids,
3302
3256
  ...Object.keys(sessions).length > 0 ? { sessions } : {}
@@ -3321,8 +3275,8 @@ function App({
3321
3275
  setNotice(`replaying turn ${target.record.turn}`);
3322
3276
  return;
3323
3277
  }
3324
- case "fork": {
3325
- const target = pickTurn(state, args[0], "nothing to fork yet");
3278
+ case "edit": {
3279
+ const target = pickTurn(state, args[0], "nothing to edit yet");
3326
3280
  if ("error" in target) {
3327
3281
  setNotice(target.error);
3328
3282
  return;
@@ -3347,74 +3301,66 @@ function App({
3347
3301
  setNotice("usage: /search <text>");
3348
3302
  return;
3349
3303
  }
3304
+ const matches2 = streamMatches(state, text, layout.streamWidth);
3305
+ if (matches2.length === 0) {
3306
+ setSearchResult(null);
3307
+ setNotice(`no matches for ${text}`);
3308
+ return;
3309
+ }
3310
+ const index = matches2.length - 1;
3311
+ setSearchResult({ query: text, index });
3350
3312
  dispatch({
3351
3313
  type: "stream:search",
3352
3314
  query: text,
3315
+ match: index,
3353
3316
  viewport: layout.logViewport,
3354
3317
  width: layout.streamWidth
3355
3318
  });
3356
- setNotice(`searched for ${text}`);
3319
+ setNotice(
3320
+ "searched for " + text + " \xB7 match " + (index + 1) + "/" + matches2.length + " \xB7 ctrl+n next \xB7 ctrl+p previous"
3321
+ );
3357
3322
  return;
3358
3323
  }
3359
3324
  case "latest":
3325
+ setSearchResult(null);
3360
3326
  dispatch({ type: "stream:latest" });
3361
3327
  setNotice("showing latest output");
3362
3328
  return;
3363
- case "cancel": {
3364
- if (checkpointResolveRef.current) {
3365
- const resolve2 = checkpointResolveRef.current;
3366
- checkpointResolveRef.current = null;
3367
- checkpointLabelRef.current = "";
3368
- resolve2({ action: "cancel" });
3369
- setNotice("checkpoint cancelled");
3370
- return;
3371
- }
3372
- if (!abortRef.current) {
3373
- setNotice("nothing to cancel");
3374
- return;
3375
- }
3376
- const dropped = queue.length;
3377
- abortRef.current.abort();
3378
- clearInteractions();
3379
- setQueue([]);
3380
- setCancelled(true);
3381
- setNotice(dropped > 0 ? `cancelling\u2026 \xB7 dropped ${dropped} queued` : "cancelling\u2026");
3382
- return;
3383
- }
3384
- case "help":
3385
- setNotice(void 0);
3386
- setHelpOpen(true);
3387
- return;
3388
- case "quit":
3389
- void shutdown();
3390
- return;
3391
3329
  default:
3392
3330
  setNotice(`unknown command: /${name}`);
3393
3331
  }
3394
3332
  },
3395
- [
3396
- config,
3397
- shutdown,
3398
- clearInteractions,
3399
- layout.logViewport,
3400
- layout.streamWidth,
3401
- queue.length,
3402
- running,
3403
- state,
3404
- startTurn,
3405
- steerRunningAgents
3406
- ]
3333
+ [config, layout.logViewport, layout.streamWidth, running, state, startTurn, steerRunningAgents]
3407
3334
  );
3408
3335
  const submit = useCallback(() => {
3409
3336
  const text = line.text.trim();
3410
3337
  if (!text || closingRef.current) return;
3338
+ if (operationInput) {
3339
+ const operation = operationInput;
3340
+ setOperationInput(null);
3341
+ if (operation === "steer" && !running) {
3342
+ setLine(EMPTY_LINE);
3343
+ setQueue((current) => [...current, text]);
3344
+ queuePausedRef.current = true;
3345
+ setQueuePaused(true);
3346
+ setNotice("turn finished \u2014 message queued; /continue runs it");
3347
+ return;
3348
+ }
3349
+ runCommand(operation, [text]);
3350
+ return;
3351
+ }
3411
3352
  const command = parseCommand(text);
3412
3353
  if (command) {
3413
- runCommand(command.name, command.args, text);
3354
+ runCommand(command.name, command.args);
3414
3355
  return;
3415
3356
  }
3416
3357
  if (checkpointResolveRef.current) {
3417
- runCommand("modify", [text], text);
3358
+ const resolve2 = checkpointResolveRef.current;
3359
+ checkpointResolveRef.current = null;
3360
+ checkpointLabelRef.current = "";
3361
+ setLine(EMPTY_LINE);
3362
+ resolve2({ action: "modify", instruction: text });
3363
+ setNotice("modifying and continuing\u2026");
3418
3364
  return;
3419
3365
  }
3420
3366
  setLine(EMPTY_LINE);
@@ -3424,7 +3370,7 @@ function App({
3424
3370
  return;
3425
3371
  }
3426
3372
  void startTurn(text);
3427
- }, [line.text, runCommand, running, startTurn]);
3373
+ }, [line.text, operationInput, runCommand, running, startTurn]);
3428
3374
  const insert = useCallback((chunk) => {
3429
3375
  setLine((l) => ({
3430
3376
  text: l.text.slice(0, l.cursor) + chunk + l.text.slice(l.cursor),
@@ -3454,10 +3400,13 @@ function App({
3454
3400
  const spinner = SPINNER[tick % SPINNER.length];
3455
3401
  const handleKey = (char, key) => {
3456
3402
  if (key.ctrl && char === "c") {
3457
- if (helpOpen) {
3403
+ if (picker || operationInput) {
3404
+ setNotice(void 0);
3405
+ setPicker(null);
3406
+ setOperationInput(null);
3407
+ setLine(EMPTY_LINE);
3408
+ } else if (helpOpen) {
3458
3409
  setHelpOpen(false);
3459
- } else if (historyOpen) {
3460
- setHistoryOpen(false);
3461
3410
  } else if (sessionsOpen) {
3462
3411
  setSessionsOpen(false);
3463
3412
  } else if (abortRef.current) {
@@ -3477,43 +3426,6 @@ function App({
3477
3426
  if (key.escape || char === "?") setHelpOpen(false);
3478
3427
  return;
3479
3428
  }
3480
- if (historyOpen) {
3481
- if (key.escape) {
3482
- setHistoryOpen(false);
3483
- return;
3484
- }
3485
- if (historyTurns.length === 0) return;
3486
- if (key.upArrow) {
3487
- setHistoryCursor((historyIndex - 1 + historyTurns.length) % historyTurns.length);
3488
- return;
3489
- }
3490
- if (key.downArrow) {
3491
- setHistoryCursor((historyIndex + 1) % historyTurns.length);
3492
- return;
3493
- }
3494
- if (key.return) {
3495
- dispatch({ type: "view:open", turn: historyTurns[historyIndex].turn });
3496
- setHistoryOpen(false);
3497
- return;
3498
- }
3499
- if (char === "c") {
3500
- const target = historyTurns[historyIndex];
3501
- const sessions = turnSessions(target);
3502
- const count = Object.keys(sessions).length;
3503
- if (count === 0) {
3504
- setNotice(`turn ${target.turn} has no agent sessions to continue`);
3505
- return;
3506
- }
3507
- continuationRef.current = { turn: target.turn, sessions };
3508
- dispatch({ type: "view:open", turn: target.turn });
3509
- setHistoryOpen(false);
3510
- setNotice(
3511
- `continuing turn ${target.turn} \u2014 ${count} ${count === 1 ? "session" : "sessions"} \xB7 type your next instruction`
3512
- );
3513
- return;
3514
- }
3515
- return;
3516
- }
3517
3429
  if (sessionsOpen) {
3518
3430
  if (key.escape) {
3519
3431
  setSessionsOpen(false);
@@ -3558,13 +3470,61 @@ function App({
3558
3470
  }
3559
3471
  return;
3560
3472
  }
3473
+ if (picker) {
3474
+ if (key.escape) {
3475
+ setPicker(null);
3476
+ setLine(EMPTY_LINE);
3477
+ setNotice(void 0);
3478
+ return;
3479
+ }
3480
+ if (key.upArrow || key.downArrow || key.tab) {
3481
+ const delta = key.upArrow || key.tab && key.shift ? -1 : 1;
3482
+ setPicker({
3483
+ ...picker,
3484
+ cursor: (picker.cursor + delta + picker.rows.length) % picker.rows.length
3485
+ });
3486
+ return;
3487
+ }
3488
+ if (key.return) {
3489
+ const row = picker.rows[picker.cursor];
3490
+ setPicker(null);
3491
+ if (row) runCommand(picker.command, [row.key]);
3492
+ return;
3493
+ }
3494
+ if (!(key.ctrl && char === "d")) return;
3495
+ }
3496
+ if (operationInput && key.escape) {
3497
+ setOperationInput(null);
3498
+ setLine(EMPTY_LINE);
3499
+ setNotice(void 0);
3500
+ return;
3501
+ }
3502
+ if (searchResult && key.ctrl && (char === "n" || char === "p")) {
3503
+ const matches2 = streamMatches(state, searchResult.query, layout.streamWidth);
3504
+ if (matches2.length === 0) {
3505
+ setSearchResult(null);
3506
+ setNotice(`no matches for ${searchResult.query}`);
3507
+ return;
3508
+ }
3509
+ const index = (searchResult.index + (char === "n" ? 1 : -1) + matches2.length) % matches2.length;
3510
+ setSearchResult({ ...searchResult, index });
3511
+ dispatch({
3512
+ type: "stream:search",
3513
+ query: searchResult.query,
3514
+ match: index,
3515
+ viewport: layout.logViewport,
3516
+ width: layout.streamWidth
3517
+ });
3518
+ setNotice(`match ${index + 1}/${matches2.length} \xB7 ctrl+n next \xB7 ctrl+p previous`);
3519
+ return;
3520
+ }
3561
3521
  if (key.ctrl && char === "d") {
3562
3522
  if (!line.text) void shutdown();
3563
3523
  return;
3564
3524
  }
3565
3525
  if (key.ctrl && char === "l") {
3566
3526
  dispatch({ type: "stream:clear" });
3567
- setNotice(shown.focusId ? `cleared ${shown.focusId}` : "nothing focused");
3527
+ setNotice(state.focusId ? `cleared ${state.focusId}` : "nothing focused");
3568
3528
  return;
3569
3529
  }
3570
3530
  if (key.ctrl && char === "a") {
@@ -3575,9 +3535,16 @@ function App({
3575
3535
  setLine((l) => ({ ...l, cursor: l.text.length }));
3576
3536
  return;
3577
3537
  }
3578
- const formFront = front?.kind === "question" && !isCommandLine(line.text) ? front : void 0;
3538
+ const formFront = front?.kind === "question" && !operationInput ? front : void 0;
3579
3539
  if (key.tab && formFront === void 0 && !paletteOpen) {
3580
- dispatch({ type: "focus:cycle", delta: key.shift ? -1 : 1 });
3540
+ if (key.shift) {
3541
+ const next = permissionMode === "auto" ? "ask" : "auto";
3542
+ setPermissionMode(next);
3543
+ setNotice(`permission mode: ${next}`);
3544
+ return;
3545
+ }
3546
+ setSearchResult(null);
3547
+ dispatch({ type: "focus:cycle", delta: 1 });
3581
3548
  return;
3582
3549
  }
3583
3550
  if (paletteOpen) {
@@ -3609,7 +3576,7 @@ function App({
3609
3576
  return;
3610
3577
  }
3611
3578
  }
3612
- if (front?.kind === "permission" && line.text === "" && !menu.released) {
3579
+ if (!operationInput && front?.kind === "permission" && line.text === "" && !menu.released) {
3613
3580
  const total = permissionChoices(front.request).length;
3614
3581
  if (key.upArrow || key.downArrow) {
3615
3582
  setPermissionMenu({
@@ -3645,8 +3612,12 @@ function App({
3645
3612
  if (draft !== void 0) setLine({ text: draft, cursor: draft.length });
3646
3613
  };
3647
3614
  const moveFocus = (index, state2 = committed) => {
3648
- const next = focusField(questionFields, state2, index);
3649
- save(next.state, next.draft);
3615
+ const target = (index + total + 3) % (total + 3);
3616
+ if (target >= total) save({ ...state2, field: target }, "");
3617
+ else {
3618
+ const next = focusField(questionFields, state2, target);
3619
+ save(next.state, next.draft);
3620
+ }
3650
3621
  };
3651
3622
  const submit2 = (state2) => {
3652
3623
  if (!formComplete(questionFields, state2)) {
@@ -3668,15 +3639,29 @@ function App({
3668
3639
  setNotice(`answered ${front2.shortId}`);
3669
3640
  };
3670
3641
  const advance = (state2) => {
3671
- if (form.field >= total - 1) submit2(state2);
3672
- else moveFocus(form.field + 1, state2);
3642
+ moveFocus(form.field + 1, state2);
3673
3643
  };
3674
3644
  if (field === void 0) {
3675
- if (key.return) submit2(committed);
3645
+ const action = Math.max(0, form.field - total);
3646
+ if (key.tab || key.leftArrow || key.rightArrow || key.upArrow || key.downArrow) {
3647
+ const delta = key.leftArrow || key.upArrow || key.tab && key.shift ? -1 : 1;
3648
+ moveFocus(form.field + delta);
3649
+ return;
3650
+ }
3651
+ if (key.return) {
3652
+ if (action === 0) submit2(committed);
3653
+ else {
3654
+ front2.settle({ action: action === 1 ? "decline" : "cancel" });
3655
+ setFormEntry({ id: "", state: initialForm() });
3656
+ setLine(EMPTY_LINE);
3657
+ setNotice(action === 1 ? "question skipped" : "question cancelled");
3658
+ }
3659
+ }
3676
3660
  return;
3677
3661
  }
3678
3662
  if (key.tab) {
3679
- moveFocus(key.shift ? form.field - 1 : form.field + 1);
3663
+ const next = field.kind === "choice" ? commitField(questionFields, committed) : committed;
3664
+ moveFocus(key.shift ? form.field - 1 : form.field + 1, next);
3680
3665
  return;
3681
3666
  }
3682
3667
  if (field.choices.length > 0 && (key.upArrow || key.downArrow)) {
@@ -3780,12 +3765,8 @@ function App({
3780
3765
  setError(void 0);
3781
3766
  return;
3782
3767
  }
3783
- if (state.view) {
3784
- dispatch({ type: "view:close" });
3785
- return;
3786
- }
3787
3768
  if (front !== void 0) {
3788
- setPermissionMenu({ id: front.shortId, cursor: void 0, released: true });
3769
+ setPermissionMenu({ id: front.shortId, cursor: 0, released: !menu.released });
3789
3770
  }
3790
3771
  setLine(EMPTY_LINE);
3791
3772
  setNotice(void 0);
@@ -3804,17 +3785,42 @@ function App({
3804
3785
  const handlerRef = useRef(handleKey);
3805
3786
  handlerRef.current = handleKey;
3806
3787
  useInput((char, key) => handlerRef.current(char, key));
3807
- const keys = helpOpen ? "esc close" : historyOpen ? "\u2191\u2193 select \xB7 enter open \xB7 esc close" : sessionsOpen ? "\u2191\u2193 select \xB7 enter open \xB7 d delete \xB7 esc close" : argOpen ? `\u2191\u2193 select \xB7 tab complete \xB7 enter ${arg?.command.name} \xB7 esc close` : menuOpen ? "\u2191\u2193 select \xB7 tab complete \xB7 enter run \xB7 esc close" : running ? checkpointResolveRef.current ? `/continue \xB7 /modify <instruction> \xB7 /cancel` : interactions.length > 0 ? layout.interactionRows === 0 ? (
3808
- // Too short to dock the bar, so the commands are the only way in.
3809
- "decide with /approve 1 \xB7 /deny \xB7 /respond"
3810
- ) : 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";
3811
- 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" : checkpointResolveRef.current ? `checkpoint: ${checkpointLabelRef.current} \u2014 type feedback or /continue` : running ? RUNNING_PLACEHOLDER : awaitingAnswer ? "answer the leader\u2026 \xB7 /task starts a new task" : INPUT_PLACEHOLDER;
3788
+ const keys = picker ? "\u2191\u2193 select \xB7 enter confirm \xB7 esc cancel" : operationInput ? "enter submit \xB7 esc cancel" : helpOpen ? "esc close" : sessionsOpen ? "\u2191\u2193 select \xB7 enter open \xB7 d delete \xB7 esc close" : argOpen ? `\u2191\u2193 select \xB7 tab complete \xB7 enter ${arg?.command.name} \xB7 esc close` : menuOpen ? "\u2191\u2193 select \xB7 tab complete \xB7 enter run \xB7 esc close" : running ? checkpointResolveRef.current ? `/continue \xB7 type feedback \xB7 ctrl+c cancel` : interactions.length > 0 ? layout.interactionRows === 0 ? (
3789
+ // On short terminals the decision is shown in the body.
3790
+ "\u2191\u2193 choose \xB7 enter confirm \xB7 esc type \xB7 ctrl+c cancel"
3791
+ ) : interactions[0].kind === "question" ? "tab field / actions \xB7 \u2191\u2193 pick \xB7 enter confirm" : menu.released ? "esc return to permission options" : "\u2191\u2193 or 1-9 choose \xB7 enter confirm \xB7 esc to type" : "enter queues \xB7 ctrl+c cancel" : state.reply ? "enter sends \xB7 / commands \xB7 ? help" : "tab switch \xB7 shift+tab mode \xB7 / commands \xB7 ? help";
3792
+ const placeholder = picker ? "choose an option above\u2026" : operationInput ? operationInput === "search" ? "search text\u2026" : "message for running agents\u2026" : 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" : checkpointResolveRef.current ? `checkpoint: ${checkpointLabelRef.current} \u2014 type feedback or /continue` : running ? RUNNING_PLACEHOLDER : awaitingAnswer ? "answer the leader\u2026 \xB7 /new starts a new session" : INPUT_PLACEHOLDER;
3793
+ const renderInteraction = (maxRows) => {
3794
+ if (!front) return null;
3795
+ return front.kind === "permission" ? /* @__PURE__ */ jsx6(
3796
+ PermissionBar,
3797
+ {
3798
+ request: front.request,
3799
+ others: interactions.slice(1).flatMap((entry) => entry.kind === "permission" ? [entry.request] : []),
3800
+ cursor: menu.cursor,
3801
+ released: menu.released,
3802
+ width: layout.columns,
3803
+ maxRows
3804
+ }
3805
+ ) : /* @__PURE__ */ jsx6(
3806
+ QuestionForm,
3807
+ {
3808
+ request: front.request,
3809
+ others: interactions.slice(1).flatMap((entry) => entry.kind === "question" ? [entry.request] : []),
3810
+ fields: questionFields,
3811
+ form,
3812
+ draft: line.text,
3813
+ width: layout.columns,
3814
+ maxRows
3815
+ }
3816
+ );
3817
+ };
3812
3818
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: layout.rows, children: [
3813
3819
  /* @__PURE__ */ jsx6(
3814
3820
  Header,
3815
3821
  {
3816
3822
  name: state.name || config.name,
3817
- right: state.view ? `turn ${shown.turn} \xB7 viewing` : `turn ${shown.turn} \xB7 ${running ? formatDuration(turnElapsed(state, now) ?? 0) : state.phase === "complete" ? "complete" : "ready"}`
3823
+ right: `turn ${state.turn} \xB7 ${running ? formatDuration(turnElapsed(state, now) ?? 0) : state.phase === "complete" ? "complete" : "ready"} \xB7 ${permissionMode}`
3818
3824
  }
3819
3825
  ),
3820
3826
  /* @__PURE__ */ jsx6(Rule, { width: layout.columns }),
@@ -3823,9 +3829,9 @@ function App({
3823
3829
  // The rail wraps, so it can be taller than one row. Cap it at the height
3824
3830
  // the budget reserved and clip, rather than letting the extra rows push
3825
3831
  // the chrome below it off the screen.
3826
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx6(PipelineRail, { turn: shown, spinner }) })
3832
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx6(PipelineRail, { turn: state, spinner }) })
3827
3833
  ) : null,
3828
- shown.reason ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: ` reason: ${shown.reason}` }) : null,
3834
+ state.reason ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: ` reason: ${state.reason}` }) : null,
3829
3835
  /* @__PURE__ */ jsx6(
3830
3836
  Box6,
3831
3837
  {
@@ -3834,15 +3840,7 @@ function App({
3834
3840
  flexShrink: 0,
3835
3841
  flexDirection: "column",
3836
3842
  overflow: "hidden",
3837
- children: helpOpen ? /* @__PURE__ */ jsx6(HelpOverlay, { width: layout.columns, maxRows: layout.bodyHeight }) : historyOpen ? /* @__PURE__ */ jsx6(
3838
- HistoryOverlay,
3839
- {
3840
- turns: historyTurns,
3841
- selected: historyIndex,
3842
- width: layout.columns,
3843
- maxRows: layout.bodyHeight
3844
- }
3845
- ) : sessionsOpen ? /* @__PURE__ */ jsx6(
3843
+ children: helpOpen ? /* @__PURE__ */ jsx6(HelpOverlay, { width: layout.columns, maxRows: layout.bodyHeight }) : sessionsOpen ? /* @__PURE__ */ jsx6(
3846
3844
  SessionsOverlay,
3847
3845
  {
3848
3846
  sessions: sessionList,
@@ -3852,20 +3850,20 @@ function App({
3852
3850
  maxRows: layout.bodyHeight,
3853
3851
  now
3854
3852
  }
3855
- ) : layout.welcome ? /* @__PURE__ */ jsx6(WelcomePanel, { turn: state, width: layout.columns }) : error ? (
3853
+ ) : front && layout.interactionRows === 0 && !paletteOpen ? renderInteraction(layout.bodyHeight) : layout.welcome ? /* @__PURE__ */ jsx6(WelcomePanel, { turn: state, width: layout.columns }) : error ? (
3856
3854
  // The error wins over a reply: a run that stopped with an error is the
3857
3855
  // most urgent thing on screen, and a stale answer underneath it would
3858
3856
  // read as the outcome. The status line keeps a one-line version too.
3859
3857
  /* @__PURE__ */ jsx6(ErrorPanel, { message: error, width: layout.columns })
3860
- ) : shown.reply ? /* @__PURE__ */ jsx6(ReplyPanel, { reply: shown.reply, width: layout.columns }) : /* @__PURE__ */ jsxs6(Box6, { height: layout.bodyHeight, flexShrink: 0, flexDirection: "row", children: [
3858
+ ) : state.reply ? /* @__PURE__ */ jsx6(ReplyPanel, { reply: state.reply, width: layout.columns }) : /* @__PURE__ */ jsxs6(Box6, { height: layout.bodyHeight, flexShrink: 0, flexDirection: "row", children: [
3861
3859
  layout.showTeam ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
3862
- /* @__PURE__ */ jsx6(TeamPanel, { turn: shown, width: layout.teamWidth, now, spinner }),
3860
+ /* @__PURE__ */ jsx6(TeamPanel, { turn: state, width: layout.teamWidth, now, spinner }),
3863
3861
  /* @__PURE__ */ jsx6(Divider, {})
3864
3862
  ] }) : null,
3865
3863
  /* @__PURE__ */ jsx6(
3866
3864
  StreamPanel,
3867
3865
  {
3868
- turn: shown,
3866
+ state,
3869
3867
  height: layout.bodyHeight,
3870
3868
  width: layout.streamWidth,
3871
3869
  now,
@@ -3875,7 +3873,6 @@ function App({
3875
3873
  ] })
3876
3874
  }
3877
3875
  ),
3878
- 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,
3879
3876
  notice || queuePaused && queue.length > 0 ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate-end", children: [
3880
3877
  " ",
3881
3878
  notice ?? (awaitingAnswer ? "queue paused \u2014 answer the leader first" : "queue paused \u2014 /continue resumes it")
@@ -3890,26 +3887,7 @@ function App({
3890
3887
  overflow: "hidden",
3891
3888
  flexShrink: 0,
3892
3889
  flexDirection: "column",
3893
- children: front.kind === "permission" ? /* @__PURE__ */ jsx6(
3894
- PermissionBar,
3895
- {
3896
- request: front.request,
3897
- others: interactions.slice(1).flatMap((entry) => entry.kind === "permission" ? [entry.request] : []),
3898
- cursor: menu.cursor,
3899
- released: menu.released,
3900
- width: layout.columns
3901
- }
3902
- ) : /* @__PURE__ */ jsx6(
3903
- QuestionForm,
3904
- {
3905
- request: front.request,
3906
- others: interactions.slice(1).flatMap((entry) => entry.kind === "question" ? [entry.request] : []),
3907
- fields: questionFields,
3908
- form,
3909
- draft: line.text,
3910
- width: layout.columns
3911
- }
3912
- )
3890
+ children: renderInteraction(layout.interactionRows)
3913
3891
  }
3914
3892
  )
3915
3893
  ] }),
@@ -4009,8 +3987,9 @@ export {
4009
3987
  seedAgents,
4010
3988
  stageStatus,
4011
3989
  startTui,
3990
+ streamMatches,
4012
3991
  stripAnsi,
4013
3992
  tuiReducer,
4014
3993
  turnElapsed,
4015
- visibleLog
3994
+ visibleTimeline
4016
3995
  };