@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/README.md +5 -4
- package/dist/tui/commands.d.ts +14 -6
- package/dist/tui/components/interaction.d.ts +7 -10
- package/dist/tui/components/overlays.d.ts +5 -26
- package/dist/tui/components/panels.d.ts +3 -3
- package/dist/tui/interaction.d.ts +2 -13
- package/dist/tui/layout.d.ts +0 -2
- package/dist/tui/state.d.ts +12 -20
- package/dist/tui.js +609 -630
- package/package.json +2 -2
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: "
|
|
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: "
|
|
64
|
-
description: "
|
|
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: "
|
|
71
|
-
|
|
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: "
|
|
76
|
-
|
|
77
|
-
|
|
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: "
|
|
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: "
|
|
93
|
+
description: "Choose a turn to run again",
|
|
94
94
|
availability: "idle"
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
|
-
name: "
|
|
97
|
+
name: "edit",
|
|
98
98
|
args: "[turn]",
|
|
99
|
-
description: "Edit
|
|
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
|
|
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: "
|
|
133
|
-
description: "Send a message
|
|
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
|
|
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
|
|
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(
|
|
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) +
|
|
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
|
|
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
|
|
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
|
-
|
|
886
|
-
|
|
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
|
-
|
|
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
|
|
934
|
-
|
|
935
|
-
|
|
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.
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
return
|
|
1100
|
-
|
|
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
|
|
1193
|
-
|
|
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 +
|
|
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
|
|
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
|
|
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
|
|
1507
|
-
var
|
|
1508
|
-
function
|
|
1509
|
-
return Math.max(1, Math.floor((maxRows -
|
|
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 =
|
|
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
|
|
1670
|
-
const
|
|
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
|
|
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
|
|
1707
|
-
const
|
|
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 ${
|
|
1710
|
-
const detail = focused
|
|
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, {
|
|
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
|
-
|
|
1806
|
+
state,
|
|
1850
1807
|
height,
|
|
1851
1808
|
width,
|
|
1852
1809
|
now,
|
|
1853
1810
|
spinner
|
|
1854
1811
|
}) {
|
|
1855
|
-
const agent =
|
|
1812
|
+
const agent = state.focusId ? state.agents[state.focusId] : void 0;
|
|
1856
1813
|
const viewport = Math.max(1, height - 1);
|
|
1857
|
-
const window =
|
|
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:
|
|
1897
|
-
// The tail, already wrapped to `width` by `
|
|
1898
|
-
// rows, and one log line is several of them. `
|
|
1899
|
-
// which rows, so the content here is exactly the pane's
|
|
1900
|
-
// oldest rows are the ones already gone: nothing is left
|
|
1901
|
-
// is why this box is not bottom-pinned. Pinning it would
|
|
1902
|
-
// gap under the header whenever an agent has not said much
|
|
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
|
|
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
|
-
|
|
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 [
|
|
2558
|
-
const [
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
2959
|
-
|
|
2960
|
-
()
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
const
|
|
2967
|
-
const
|
|
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:
|
|
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 = !
|
|
2980
|
-
const argOpen = !
|
|
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 ||
|
|
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
|
-
|
|
3008
|
-
|
|
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
|
|
3046
|
+
(name, args) => {
|
|
3035
3047
|
setLine(EMPTY_LINE);
|
|
3036
|
-
if (checkpointResolveRef.current && name !== "continue"
|
|
3048
|
+
if (checkpointResolveRef.current && name !== "continue") {
|
|
3037
3049
|
setNotice(
|
|
3038
|
-
`checkpoint: ${checkpointLabelRef.current} \xB7 /continue
|
|
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
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
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
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
setNotice("
|
|
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
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
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
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
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
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
const
|
|
3097
|
-
|
|
3098
|
-
|
|
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 (
|
|
3150
|
-
setNotice("
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
3223
|
+
if (turn.acceptance && turn.acceptance.status !== "approved" && (!args[0] || args[0] === turn.acceptance.reviewer)) {
|
|
3274
3224
|
ids = [
|
|
3275
|
-
...new Set(
|
|
3276
|
-
].filter((id) => id !==
|
|
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(
|
|
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(
|
|
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) =>
|
|
3246
|
+
ids.filter((id) => turn.agents[id]?.sessionId).map((id) => [id, turn.agents[id].sessionId])
|
|
3293
3247
|
);
|
|
3294
|
-
if (ids.some((id) =>
|
|
3295
|
-
void startTurn(
|
|
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(
|
|
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 "
|
|
3325
|
-
const target = pickTurn(state, args[0], "nothing to
|
|
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(
|
|
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
|
|
3354
|
+
runCommand(command.name, command.args);
|
|
3414
3355
|
return;
|
|
3415
3356
|
}
|
|
3416
3357
|
if (checkpointResolveRef.current) {
|
|
3417
|
-
|
|
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 (
|
|
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(
|
|
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" && !
|
|
3538
|
+
const formFront = front?.kind === "question" && !operationInput ? front : void 0;
|
|
3579
3539
|
if (key.tab && formFront === void 0 && !paletteOpen) {
|
|
3580
|
-
|
|
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
|
|
3649
|
-
save(
|
|
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
|
-
|
|
3672
|
-
else moveFocus(form.field + 1, state2);
|
|
3642
|
+
moveFocus(form.field + 1, state2);
|
|
3673
3643
|
};
|
|
3674
3644
|
if (field === void 0) {
|
|
3675
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
3808
|
-
//
|
|
3809
|
-
"
|
|
3810
|
-
) : interactions[0].kind === "question" ? "tab field \xB7 \u2191\u2193 pick \xB7 enter
|
|
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 /
|
|
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:
|
|
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:
|
|
3832
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx6(PipelineRail, { turn: state, spinner }) })
|
|
3827
3833
|
) : null,
|
|
3828
|
-
|
|
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 }) :
|
|
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
|
-
) :
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
3994
|
+
visibleTimeline
|
|
4016
3995
|
};
|