@multiclawcli/dashboard 0.12.0 → 0.14.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/panels.d.ts +4 -4
- package/dist/tui/interaction.d.ts +2 -13
- package/dist/tui/layout.d.ts +2 -2
- package/dist/tui/session.d.ts +4 -0
- package/dist/tui/state.d.ts +20 -21
- package/dist/tui.js +753 -524
- 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,149 +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
|
-
|
|
78
|
+
name: "search",
|
|
79
|
+
args: "[text]",
|
|
80
|
+
description: "Search the focused stream",
|
|
77
81
|
availability: "always"
|
|
78
82
|
},
|
|
83
|
+
{ name: "latest", description: "Jump to latest output", availability: "always" },
|
|
79
84
|
{
|
|
80
85
|
name: "retry",
|
|
81
86
|
args: "[id]",
|
|
82
|
-
description: "
|
|
87
|
+
description: "Choose failed agents to retry",
|
|
83
88
|
availability: "idle"
|
|
84
89
|
},
|
|
85
90
|
{
|
|
86
91
|
name: "replay",
|
|
87
92
|
args: "[turn]",
|
|
88
|
-
description: "
|
|
93
|
+
description: "Choose a turn to run again",
|
|
89
94
|
availability: "idle"
|
|
90
95
|
},
|
|
91
96
|
{
|
|
92
|
-
name: "
|
|
97
|
+
name: "edit",
|
|
93
98
|
args: "[turn]",
|
|
94
|
-
description: "Edit
|
|
99
|
+
description: "Edit a previous requirement",
|
|
95
100
|
availability: "always"
|
|
96
101
|
},
|
|
97
102
|
{
|
|
98
103
|
name: "export",
|
|
99
104
|
args: "[turn] [path]",
|
|
100
|
-
description: "Export a
|
|
101
|
-
availability: "always"
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
name: "search",
|
|
105
|
-
args: "<text>",
|
|
106
|
-
description: "Search the focused stream",
|
|
107
|
-
availability: "always"
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
name: "latest",
|
|
111
|
-
description: "Jump the stream to latest output",
|
|
105
|
+
description: "Export a turn as Markdown",
|
|
112
106
|
availability: "always"
|
|
113
107
|
},
|
|
114
|
-
{
|
|
115
|
-
name: "cancel",
|
|
116
|
-
description: "Cancel the running turn and its queue",
|
|
117
|
-
availability: "running"
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
name: "new",
|
|
121
|
-
args: "<requirement>",
|
|
122
|
-
description: "Start a new session with fresh agent sessions",
|
|
123
|
-
availability: "idle"
|
|
124
|
-
},
|
|
125
108
|
{
|
|
126
109
|
name: "steer",
|
|
127
|
-
args: "
|
|
128
|
-
description: "Send a message
|
|
110
|
+
args: "[text]",
|
|
111
|
+
description: "Send a message to running agents",
|
|
129
112
|
availability: "running"
|
|
130
113
|
},
|
|
131
|
-
{
|
|
132
|
-
name: "answer",
|
|
133
|
-
args: "<text>",
|
|
134
|
-
description: "Answer the leader's pending question",
|
|
135
|
-
availability: "idle"
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
name: "pause",
|
|
139
|
-
description: "Pause queued turns, not the active agent",
|
|
140
|
-
availability: "always"
|
|
141
|
-
},
|
|
114
|
+
{ name: "pause", description: "Pause queued turns", availability: "always" },
|
|
142
115
|
{
|
|
143
116
|
name: "continue",
|
|
144
|
-
description: "Resume
|
|
145
|
-
availability: "always"
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
name: "modify",
|
|
149
|
-
args: "<instruction>",
|
|
150
|
-
description: "Provide feedback at a checkpoint and continue",
|
|
151
|
-
availability: "running"
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
name: "respond",
|
|
155
|
-
args: "<question id> <JSON object>",
|
|
156
|
-
description: "Answer a question in one line of JSON",
|
|
157
|
-
availability: "always"
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
name: "skip",
|
|
161
|
-
args: "<question id>",
|
|
162
|
-
description: "Decline a structured tool question",
|
|
163
|
-
availability: "always"
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
name: "dismiss",
|
|
167
|
-
args: "<question id>",
|
|
168
|
-
description: "Cancel a structured tool question",
|
|
169
|
-
availability: "always"
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
name: "approve",
|
|
173
|
-
args: "[id] <number>",
|
|
174
|
-
description: "Approve the request on screen, or a named one",
|
|
117
|
+
description: "Resume the queue or continue a checkpoint",
|
|
175
118
|
availability: "always"
|
|
176
|
-
}
|
|
177
|
-
{
|
|
178
|
-
name: "deny",
|
|
179
|
-
args: "[id]",
|
|
180
|
-
description: "Deny the request on screen, or a named one",
|
|
181
|
-
availability: "always"
|
|
182
|
-
},
|
|
183
|
-
{ name: "help", description: "Show the key map", availability: "always" },
|
|
184
|
-
{ name: "quit", description: "Exit the console", availability: "always" }
|
|
119
|
+
}
|
|
185
120
|
];
|
|
186
|
-
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) {
|
|
187
131
|
const phase = running ? "running" : "idle";
|
|
188
|
-
return COMMANDS.filter(
|
|
132
|
+
return COMMANDS.filter(
|
|
133
|
+
(c) => (c.availability === "always" || c.availability === phase) && relevant(c.name, context)
|
|
134
|
+
);
|
|
189
135
|
}
|
|
190
|
-
function canRun(name, running) {
|
|
136
|
+
function canRun(name, running, context) {
|
|
191
137
|
const command = COMMANDS.find((c) => c.name === name);
|
|
192
138
|
if (!command) return true;
|
|
193
|
-
return availableCommands(running).includes(command);
|
|
139
|
+
return availableCommands(running, context).includes(command);
|
|
194
140
|
}
|
|
195
|
-
function unavailableReason(name, running) {
|
|
141
|
+
function unavailableReason(name, running, context) {
|
|
196
142
|
const command = COMMANDS.find((c) => c.name === name);
|
|
197
|
-
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`;
|
|
198
145
|
return running ? `/${name} waits for the current turn to finish` : `/${name} only applies while a turn is running`;
|
|
199
146
|
}
|
|
200
|
-
function matchCommands(query, running) {
|
|
147
|
+
function matchCommands(query, running, context) {
|
|
201
148
|
const q = query.trim().toLowerCase();
|
|
202
|
-
const pool = availableCommands(running);
|
|
149
|
+
const pool = availableCommands(running, context);
|
|
203
150
|
if (!q) return pool;
|
|
204
151
|
const byName = [];
|
|
205
152
|
const byDescription = [];
|
|
@@ -459,16 +406,24 @@ function computeLayout(input) {
|
|
|
459
406
|
const compact = columns < COMPACT_COLUMNS;
|
|
460
407
|
const welcome = !input.overlay && input.turn === 0;
|
|
461
408
|
const showRail = !welcome && !input.overlay && rows >= RAIL_MIN_ROWS && input.hasRail;
|
|
462
|
-
const menuRows = Math.min(Math.max(0, input.menuRows), MAX_MENU_ROWS);
|
|
463
|
-
const queueRows = Math.min(Math.max(0, input.queueRows ?? 0), MAX_QUEUE_ROWS);
|
|
464
409
|
const promptRows = Math.max(1, input.promptRows ?? 1);
|
|
465
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.details ? 1 : 0) + (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
|
+
);
|
|
466
422
|
const chrome = 1 + // header
|
|
467
423
|
1 + // rule under the header
|
|
468
424
|
(railRows > 0 ? railRows + 1 : 0) + // pipeline rail plus the gap above it
|
|
469
425
|
1 + // gap before the body
|
|
470
|
-
(input.notice ? 1 : 0) + (input.
|
|
471
|
-
queueRows + menuRows + promptRows + 1;
|
|
426
|
+
(input.notice ? 1 : 0) + (input.details ? 1 : 0) + queueRows + menuRows + promptRows + 1;
|
|
472
427
|
const barRoom = Math.max(0, rows - chrome - MIN_BODY_ROWS - 1);
|
|
473
428
|
const interactionRows2 = barRoom < MIN_INTERACTION_ROWS ? 0 : Math.min(Math.max(0, input.interactionRows ?? 0), MAX_INTERACTION_ROWS, barRoom);
|
|
474
429
|
const reserved = chrome + (interactionRows2 > 0 ? interactionRows2 + 1 : 0);
|
|
@@ -508,6 +463,7 @@ var STATUS = {
|
|
|
508
463
|
pending: { glyph: "\u25CB", color: COLOR.muted, dim: true, label: "queued" },
|
|
509
464
|
running: { glyph: "\u25D0", color: COLOR.accent, label: "running" },
|
|
510
465
|
retrying: { glyph: "\u21BB", color: COLOR.warn, label: "retrying" },
|
|
466
|
+
handed_back: { glyph: "\u21A9", color: COLOR.warn, label: "handed back" },
|
|
511
467
|
success: { glyph: "\u2713", color: COLOR.ok, label: "done" },
|
|
512
468
|
failed: { glyph: "\u2717", color: COLOR.error, label: "failed" },
|
|
513
469
|
// Stopped, not broken: a filled square reads as "halted" and shares the muted
|
|
@@ -857,11 +813,55 @@ function reduceEvent(state, event) {
|
|
|
857
813
|
}
|
|
858
814
|
};
|
|
859
815
|
}
|
|
816
|
+
case "agent:session": {
|
|
817
|
+
const { record, action } = event.payload;
|
|
818
|
+
const existing = state.agents[record.agentId];
|
|
819
|
+
const isCurrentWork = record.workId ? state.work?.id === record.workId : state.leaderIds.includes(record.agentId);
|
|
820
|
+
const startsBinding = action === "opening" || action === "load_requested";
|
|
821
|
+
const matchesBinding = !existing?.session || existing.session.bindingId === record.bindingId || startsBinding;
|
|
822
|
+
const history = state.history.map((turn) => {
|
|
823
|
+
if (!record.workId || turn.work?.id !== record.workId) return turn;
|
|
824
|
+
const member = turn.work.agents.find((item) => item.agentId === record.agentId);
|
|
825
|
+
if (member?.session?.bindingId !== record.bindingId) return turn;
|
|
826
|
+
return {
|
|
827
|
+
...turn,
|
|
828
|
+
agents: turn.agents[record.agentId]?.session?.bindingId === record.bindingId ? {
|
|
829
|
+
...turn.agents,
|
|
830
|
+
[record.agentId]: { ...turn.agents[record.agentId], session: record }
|
|
831
|
+
} : turn.agents,
|
|
832
|
+
work: {
|
|
833
|
+
...turn.work,
|
|
834
|
+
agents: turn.work.agents.map(
|
|
835
|
+
(item) => item.agentId === record.agentId ? { ...item, session: record } : item
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
const updatesCurrent = isCurrentWork && matchesBinding && existing;
|
|
841
|
+
const work = updatesCurrent && state.work && state.work.id === record.workId ? {
|
|
842
|
+
...state.work,
|
|
843
|
+
agents: state.work.agents.map(
|
|
844
|
+
(item) => item.agentId === record.agentId ? {
|
|
845
|
+
...item,
|
|
846
|
+
session: record.sessionId || !item.session?.sessionId || record.failure?.code === "cleanup_failed" ? record : item.session
|
|
847
|
+
} : item
|
|
848
|
+
)
|
|
849
|
+
} : state.work;
|
|
850
|
+
return {
|
|
851
|
+
...state,
|
|
852
|
+
work,
|
|
853
|
+
history,
|
|
854
|
+
sessionEvents: [...state.sessionEvents ?? [], event.payload],
|
|
855
|
+
agents: updatesCurrent ? { ...state.agents, [record.agentId]: { ...existing, session: record } } : state.agents
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
case "agent:handed-back":
|
|
860
859
|
case "agent:complete":
|
|
861
860
|
case "agent:failed": {
|
|
862
861
|
const result = event.payload;
|
|
863
862
|
const patch = {
|
|
864
863
|
status: result.status,
|
|
864
|
+
handoffReason: result.handoffReason,
|
|
865
865
|
duration: result.duration,
|
|
866
866
|
attempts: result.attempts,
|
|
867
867
|
retry: void 0,
|
|
@@ -870,18 +870,25 @@ function reduceEvent(state, event) {
|
|
|
870
870
|
costUsd: result.costUsd,
|
|
871
871
|
// Present only on the success path; a failed agent has no session to
|
|
872
872
|
// continue, so `/retry` re-runs it cold.
|
|
873
|
-
|
|
873
|
+
session: result.session ?? state.agents[result.agentId]?.session
|
|
874
874
|
};
|
|
875
875
|
const existing = state.agents[result.agentId];
|
|
876
876
|
const next = existing ? { ...existing, ...patch } : blankAgent(result.agentId, result.agentName, patch);
|
|
877
|
+
if (event.type === "agent:handed-back")
|
|
878
|
+
next.logs = appendLog(next.logs, "[handed back] " + result.handoffReason, "system");
|
|
877
879
|
if (event.type === "agent:failed") {
|
|
878
880
|
if (next.logs.length === 0 && result.output)
|
|
879
881
|
next.logs = appendLog([], result.output, "text");
|
|
880
|
-
|
|
881
|
-
|
|
882
|
+
const error = result.error;
|
|
883
|
+
if (error && !next.logs.some((s) => s.text.includes(stripAnsi(error)))) {
|
|
884
|
+
next.logs = appendLog(next.logs, error, "system");
|
|
882
885
|
}
|
|
883
886
|
}
|
|
884
|
-
return {
|
|
887
|
+
return {
|
|
888
|
+
...state,
|
|
889
|
+
agentResults: [...state.agentResults ?? [], result],
|
|
890
|
+
agents: { ...state.agents, [result.agentId]: next }
|
|
891
|
+
};
|
|
885
892
|
}
|
|
886
893
|
case "agent:skipped": {
|
|
887
894
|
const { agentId, agentName, icon } = event.payload;
|
|
@@ -901,6 +908,15 @@ function reduceEvent(state, event) {
|
|
|
901
908
|
});
|
|
902
909
|
return { ...state, agents: { ...state.agents, [agentId]: next } };
|
|
903
910
|
}
|
|
911
|
+
case "orchestration:route":
|
|
912
|
+
return {
|
|
913
|
+
...state,
|
|
914
|
+
routing: event.payload.routing,
|
|
915
|
+
work: event.payload.work,
|
|
916
|
+
warning: event.payload.routing.reroutes ? "Worker handed work back \u2014 rerouting once" : state.warning
|
|
917
|
+
};
|
|
918
|
+
case "orchestration:routing-error":
|
|
919
|
+
return { ...state, warning: event.payload.message };
|
|
904
920
|
case "orchestration:warning":
|
|
905
921
|
return { ...state, warning: event.payload.message };
|
|
906
922
|
case "orchestration:complete":
|
|
@@ -908,6 +924,10 @@ function reduceEvent(state, event) {
|
|
|
908
924
|
...state,
|
|
909
925
|
phase: "complete",
|
|
910
926
|
success: event.payload.success,
|
|
927
|
+
work: event.payload.work,
|
|
928
|
+
routing: event.payload.routing,
|
|
929
|
+
agentResults: event.payload.agentResults,
|
|
930
|
+
effectiveAgentResults: event.payload.effectiveAgentResults,
|
|
911
931
|
acceptance: event.payload.acceptance,
|
|
912
932
|
cancelled: event.payload.cancelled,
|
|
913
933
|
effectiveRequirement: event.payload.requirement ?? state.effectiveRequirement,
|
|
@@ -920,14 +940,120 @@ function reduceEvent(state, event) {
|
|
|
920
940
|
return state;
|
|
921
941
|
}
|
|
922
942
|
}
|
|
943
|
+
var wrappedCache = /* @__PURE__ */ new WeakMap();
|
|
944
|
+
var MAX_CACHED_WIDTHS = 3;
|
|
923
945
|
function wrapLog(logs, width) {
|
|
924
946
|
if (logs.length === 0) return [];
|
|
947
|
+
let byWidth = wrappedCache.get(logs);
|
|
948
|
+
if (byWidth === void 0) {
|
|
949
|
+
byWidth = /* @__PURE__ */ new Map();
|
|
950
|
+
wrappedCache.set(logs, byWidth);
|
|
951
|
+
}
|
|
952
|
+
const cached = byWidth.get(width);
|
|
953
|
+
if (cached !== void 0) return cached;
|
|
925
954
|
const rendered = renderSegments(logs, width);
|
|
926
|
-
|
|
955
|
+
const rows = rendered.trimEnd().split("\n").flatMap((line) => wrapLine(line, width));
|
|
956
|
+
if (byWidth.size >= MAX_CACHED_WIDTHS) byWidth.clear();
|
|
957
|
+
byWidth.set(width, rows);
|
|
958
|
+
return rows;
|
|
927
959
|
}
|
|
928
|
-
function
|
|
929
|
-
|
|
930
|
-
|
|
960
|
+
function totalRows(parts) {
|
|
961
|
+
let total = 0;
|
|
962
|
+
for (const part of parts) total += part.length;
|
|
963
|
+
return total;
|
|
964
|
+
}
|
|
965
|
+
function windowParts(parts, viewport, scroll) {
|
|
966
|
+
const height = Math.max(1, viewport);
|
|
967
|
+
const total = totalRows(parts);
|
|
968
|
+
const maxScroll = Math.max(0, total - height);
|
|
969
|
+
const scrolledBack = Math.min(Math.max(0, scroll), maxScroll);
|
|
970
|
+
const end = total - scrolledBack;
|
|
971
|
+
const start = Math.max(0, end - height);
|
|
972
|
+
const rows = [];
|
|
973
|
+
let offset = 0;
|
|
974
|
+
for (const part of parts) {
|
|
975
|
+
const partEnd = offset + part.length;
|
|
976
|
+
if (partEnd <= start) {
|
|
977
|
+
offset = partEnd;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (offset >= end) break;
|
|
981
|
+
const from = Math.max(0, start - offset);
|
|
982
|
+
const to = Math.min(part.length, end - offset);
|
|
983
|
+
for (let i = from; i < to; i++) rows.push(part[i]);
|
|
984
|
+
offset = partEnd;
|
|
985
|
+
}
|
|
986
|
+
return { rows, scrolledBack, following: scrolledBack === 0 };
|
|
987
|
+
}
|
|
988
|
+
function clipToColumns(text, max) {
|
|
989
|
+
if (max <= 0) return "";
|
|
990
|
+
if (displayWidth(text) <= max) return text;
|
|
991
|
+
let head = "";
|
|
992
|
+
let used = 0;
|
|
993
|
+
for (const char of text) {
|
|
994
|
+
const width = displayWidth(char);
|
|
995
|
+
if (used + width > max - 1) break;
|
|
996
|
+
head += char;
|
|
997
|
+
used += width;
|
|
998
|
+
}
|
|
999
|
+
return `${head}\u2026`;
|
|
1000
|
+
}
|
|
1001
|
+
function turnDivider(turn, width) {
|
|
1002
|
+
const MAX_REQ = 45;
|
|
1003
|
+
const raw = turn.requirement?.replace(/\s+/g, " ").trim();
|
|
1004
|
+
const req = raw ? clipToColumns(raw, MAX_REQ) : void 0;
|
|
1005
|
+
const label = req ? ` turn ${turn.turn} \xB7 ${req} ` : ` turn ${turn.turn} `;
|
|
1006
|
+
const fill = Math.max(1, Math.floor((width - displayWidth(label)) / 2));
|
|
1007
|
+
const rule = `${"\u2500".repeat(fill)}${label}${"\u2500".repeat(fill)}`;
|
|
1008
|
+
return `\x1B[2m${clipToColumns(rule, width)}\x1B[0m`;
|
|
1009
|
+
}
|
|
1010
|
+
function pushRows(target, rows) {
|
|
1011
|
+
for (const row of rows) target.push(row);
|
|
1012
|
+
}
|
|
1013
|
+
var historyCache = /* @__PURE__ */ new WeakMap();
|
|
1014
|
+
function historyRows(history, agentId, width) {
|
|
1015
|
+
if (history.length === 0) return [];
|
|
1016
|
+
let byAgent = historyCache.get(history);
|
|
1017
|
+
if (byAgent === void 0) {
|
|
1018
|
+
byAgent = /* @__PURE__ */ new Map();
|
|
1019
|
+
historyCache.set(history, byAgent);
|
|
1020
|
+
}
|
|
1021
|
+
let byWidth = byAgent.get(agentId);
|
|
1022
|
+
if (byWidth === void 0) {
|
|
1023
|
+
byWidth = /* @__PURE__ */ new Map();
|
|
1024
|
+
byAgent.set(agentId, byWidth);
|
|
1025
|
+
}
|
|
1026
|
+
const cached = byWidth.get(width);
|
|
1027
|
+
if (cached !== void 0) return cached;
|
|
1028
|
+
const rows = [];
|
|
1029
|
+
for (const turn of history) {
|
|
1030
|
+
rows.push(turnDivider(turn, width));
|
|
1031
|
+
const agent = turn.agents[agentId];
|
|
1032
|
+
if (agent && agent.logs.length > 0) pushRows(rows, wrapLog(agent.logs, width));
|
|
1033
|
+
}
|
|
1034
|
+
if (byWidth.size >= MAX_CACHED_WIDTHS) byWidth.clear();
|
|
1035
|
+
byWidth.set(width, rows);
|
|
1036
|
+
return rows;
|
|
1037
|
+
}
|
|
1038
|
+
function liveRows(state, agentId, width) {
|
|
1039
|
+
const agent = state.agents[agentId];
|
|
1040
|
+
if (!agent || agent.logs.length === 0) return [];
|
|
1041
|
+
const rows = wrapLog(agent.logs, width);
|
|
1042
|
+
if (state.history.length === 0) return rows;
|
|
1043
|
+
const out = [turnDivider(state, width)];
|
|
1044
|
+
pushRows(out, rows);
|
|
1045
|
+
return out;
|
|
1046
|
+
}
|
|
1047
|
+
function timelineParts(state, agentId, width) {
|
|
1048
|
+
const history = historyRows(state.history, agentId, width);
|
|
1049
|
+
const live = liveRows(state, agentId, width);
|
|
1050
|
+
if (live.length === 0) return history.length === 0 ? [] : [history];
|
|
1051
|
+
return history.length === 0 ? [live] : [history, live];
|
|
1052
|
+
}
|
|
1053
|
+
function streamMatches(state, query, width) {
|
|
1054
|
+
if (!state.focusId || !query.trim()) return [];
|
|
1055
|
+
const needle = query.toLowerCase();
|
|
1056
|
+
return timelineParts(state, state.focusId, width).flat().flatMap((row, index) => row.toLowerCase().includes(needle) ? [index] : []);
|
|
931
1057
|
}
|
|
932
1058
|
function snapshot(state) {
|
|
933
1059
|
return {
|
|
@@ -946,6 +1072,11 @@ function snapshot(state) {
|
|
|
946
1072
|
autoFollow: state.autoFollow,
|
|
947
1073
|
scroll: state.scroll,
|
|
948
1074
|
success: state.success,
|
|
1075
|
+
work: state.work,
|
|
1076
|
+
routing: state.routing,
|
|
1077
|
+
sessionEvents: state.sessionEvents,
|
|
1078
|
+
agentResults: state.agentResults,
|
|
1079
|
+
effectiveAgentResults: state.effectiveAgentResults,
|
|
949
1080
|
acceptance: state.acceptance,
|
|
950
1081
|
totalDuration: state.totalDuration,
|
|
951
1082
|
totalCostUsd: state.totalCostUsd,
|
|
@@ -954,14 +1085,6 @@ function snapshot(state) {
|
|
|
954
1085
|
reply: state.reply
|
|
955
1086
|
};
|
|
956
1087
|
}
|
|
957
|
-
function mapShown(state, fn) {
|
|
958
|
-
if (state.view) {
|
|
959
|
-
const next2 = fn(state.view);
|
|
960
|
-
return next2 === state.view ? state : { ...state, view: next2 };
|
|
961
|
-
}
|
|
962
|
-
const next = fn(state);
|
|
963
|
-
return next === state ? state : { ...state, ...next };
|
|
964
|
-
}
|
|
965
1088
|
function recordedTurns(state) {
|
|
966
1089
|
return [...state.history, ...state.phase === "complete" ? [snapshot(state)] : []];
|
|
967
1090
|
}
|
|
@@ -970,12 +1093,18 @@ function conversationTurns(state) {
|
|
|
970
1093
|
for (const turn of recordedTurns(state)) {
|
|
971
1094
|
if (turn.phase !== "complete" || turn.cancelled || !turn.requirement) continue;
|
|
972
1095
|
turns.push(
|
|
973
|
-
turn.reply ? { requirement: turn.requirement, reply: turn.reply } : { requirement: turn.requirement, outcome: conversationOutcome(turn) }
|
|
1096
|
+
turn.reply ? { requirement: turn.requirement, reply: turn.reply } : { requirement: turn.requirement, outcome: conversationOutcome(turn), work: turn.work }
|
|
974
1097
|
);
|
|
975
1098
|
}
|
|
976
1099
|
return turns;
|
|
977
1100
|
}
|
|
1101
|
+
function latestWorkFromTurns(turns) {
|
|
1102
|
+
const latest = [...turns].reverse().find((turn) => turn.work || !turn.reply && turn.stages.length > 0);
|
|
1103
|
+
if (!latest?.work || latest.cancelled) return void 0;
|
|
1104
|
+
return { ...latest.work, outcome: conversationOutcome(latest) };
|
|
1105
|
+
}
|
|
978
1106
|
function conversationOutcome(turn) {
|
|
1107
|
+
if (!turn.interrupted && !turn.cancelled && turn.work?.outcome) return turn.work.outcome;
|
|
979
1108
|
const reviewer = turn.acceptance?.reviewer;
|
|
980
1109
|
const isWorker = (id) => id !== reviewer && !turn.leaderIds.includes(id);
|
|
981
1110
|
const workers = Object.values(turn.agents).filter((agent) => isWorker(agent.id));
|
|
@@ -1035,68 +1164,55 @@ function tuiReducer(state, action) {
|
|
|
1035
1164
|
history,
|
|
1036
1165
|
// A new requirement also replaces the previous leader metadata; leaving
|
|
1037
1166
|
// it up would make the old answer or routing reason look like the
|
|
1038
|
-
// response to the new request.
|
|
1039
|
-
// reason — the panes belong to the run.
|
|
1167
|
+
// response to the new request.
|
|
1040
1168
|
reason: void 0,
|
|
1041
1169
|
reply: void 0,
|
|
1042
|
-
view: void 0,
|
|
1043
1170
|
// A console setting, carried across turns rather than reset with them.
|
|
1044
1171
|
verbose: state.verbose
|
|
1045
1172
|
};
|
|
1046
1173
|
}
|
|
1047
|
-
case "focus:cycle":
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
if (index === -1) return turn;
|
|
1083
|
-
const height = Math.max(1, action.viewport);
|
|
1084
|
-
const maxScroll = Math.max(0, rows.length - height);
|
|
1085
|
-
const scroll = Math.min(Math.max(0, rows.length - 1 - index), maxScroll);
|
|
1086
|
-
return scroll === turn.scroll && !turn.autoFollow ? turn : { ...turn, scroll, autoFollow: false };
|
|
1087
|
-
});
|
|
1174
|
+
case "focus:cycle": {
|
|
1175
|
+
const ids = Object.keys(state.agents);
|
|
1176
|
+
if (ids.length === 0) return state;
|
|
1177
|
+
const current = state.focusId ? ids.indexOf(state.focusId) : -1;
|
|
1178
|
+
const nextIndex = (current + action.delta + ids.length) % ids.length;
|
|
1179
|
+
if (nextIndex === current) return state;
|
|
1180
|
+
return { ...state, focusId: ids[nextIndex], autoFollow: false, scroll: 0 };
|
|
1181
|
+
}
|
|
1182
|
+
case "focus:set": {
|
|
1183
|
+
if (!state.agents[action.agentId]) return state;
|
|
1184
|
+
if (state.focusId === action.agentId && !state.autoFollow && state.scroll === 0) return state;
|
|
1185
|
+
return { ...state, focusId: action.agentId, autoFollow: false, scroll: 0 };
|
|
1186
|
+
}
|
|
1187
|
+
case "stream:scroll": {
|
|
1188
|
+
const focusId = state.focusId;
|
|
1189
|
+
if (!focusId) return state;
|
|
1190
|
+
const rows = totalRows(timelineParts(state, focusId, action.width));
|
|
1191
|
+
const maxScroll = Math.max(0, rows - Math.max(1, action.viewport));
|
|
1192
|
+
const next = Math.min(Math.max(0, state.scroll + action.delta), maxScroll);
|
|
1193
|
+
return next === state.scroll ? state : { ...state, scroll: next };
|
|
1194
|
+
}
|
|
1195
|
+
case "stream:search": {
|
|
1196
|
+
const focusId = state.focusId;
|
|
1197
|
+
if (!focusId || action.query === "") return state;
|
|
1198
|
+
const parts = timelineParts(state, focusId, action.width);
|
|
1199
|
+
const matches = streamMatches(state, action.query, action.width);
|
|
1200
|
+
const index = matches[action.match ?? matches.length - 1];
|
|
1201
|
+
if (index === void 0) return state;
|
|
1202
|
+
const total = totalRows(parts);
|
|
1203
|
+
const height = Math.max(1, action.viewport);
|
|
1204
|
+
const maxScroll = Math.max(0, total - height);
|
|
1205
|
+
const scroll = Math.min(Math.max(0, total - 1 - index), maxScroll);
|
|
1206
|
+
return scroll === state.scroll && !state.autoFollow ? state : { ...state, scroll, autoFollow: false };
|
|
1207
|
+
}
|
|
1088
1208
|
case "stream:latest":
|
|
1089
|
-
return
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
return
|
|
1095
|
-
|
|
1096
|
-
const agent = id ? turn.agents[id] : void 0;
|
|
1097
|
-
if (!id || !agent) return turn;
|
|
1098
|
-
return { ...turn, agents: { ...turn.agents, [id]: { ...agent, logs: [] } }, scroll: 0 };
|
|
1099
|
-
});
|
|
1209
|
+
return state.scroll === 0 && state.autoFollow ? state : { ...state, scroll: 0, autoFollow: true };
|
|
1210
|
+
case "stream:clear": {
|
|
1211
|
+
const id = state.focusId;
|
|
1212
|
+
const agent = id ? state.agents[id] : void 0;
|
|
1213
|
+
if (!id || !agent) return state;
|
|
1214
|
+
return { ...state, agents: { ...state.agents, [id]: { ...agent, logs: [] } }, scroll: 0 };
|
|
1215
|
+
}
|
|
1100
1216
|
case "stream:note": {
|
|
1101
1217
|
const agent = state.agents[action.agentId];
|
|
1102
1218
|
if (!agent) return state;
|
|
@@ -1108,12 +1224,6 @@ function tuiReducer(state, action) {
|
|
|
1108
1224
|
}
|
|
1109
1225
|
};
|
|
1110
1226
|
}
|
|
1111
|
-
case "view:open": {
|
|
1112
|
-
const record = state.history.find((turn) => turn.turn === action.turn);
|
|
1113
|
-
return record ? { ...state, view: record } : state;
|
|
1114
|
-
}
|
|
1115
|
-
case "view:close":
|
|
1116
|
-
return state.view ? { ...state, view: void 0 } : state;
|
|
1117
1227
|
case "verbose:toggle":
|
|
1118
1228
|
return { ...state, verbose: !state.verbose };
|
|
1119
1229
|
case "session:reset":
|
|
@@ -1148,7 +1258,6 @@ function seededState(state, agents, leaderIds, history) {
|
|
|
1148
1258
|
autoFollow: true,
|
|
1149
1259
|
scroll: 0,
|
|
1150
1260
|
history,
|
|
1151
|
-
view: void 0,
|
|
1152
1261
|
verbose: state.verbose
|
|
1153
1262
|
};
|
|
1154
1263
|
}
|
|
@@ -1156,6 +1265,7 @@ function stageStatus(stage, agents) {
|
|
|
1156
1265
|
if (stage.outcome !== void 0) return stage.outcome;
|
|
1157
1266
|
const statuses = stage.agents.map((a) => agents[a.id]?.status ?? "pending");
|
|
1158
1267
|
if (statuses.some((s) => s === "failed")) return "failed";
|
|
1268
|
+
if (statuses.some((s) => s === "handed_back")) return "handed_back";
|
|
1159
1269
|
if (statuses.some((s) => s === "running" || s === "retrying")) return "running";
|
|
1160
1270
|
if (statuses.length > 0 && statuses.every((s) => s === "success")) return "success";
|
|
1161
1271
|
if (statuses.length > 0 && statuses.every((s) => s === "skipped")) return "skipped";
|
|
@@ -1163,6 +1273,7 @@ function stageStatus(stage, agents) {
|
|
|
1163
1273
|
return "pending";
|
|
1164
1274
|
}
|
|
1165
1275
|
var SETTLED = /* @__PURE__ */ new Set([
|
|
1276
|
+
"handed_back",
|
|
1166
1277
|
"success",
|
|
1167
1278
|
"failed",
|
|
1168
1279
|
"cancelled",
|
|
@@ -1184,18 +1295,8 @@ function turnElapsed(turn, now) {
|
|
|
1184
1295
|
const end = turn.phase === "running" ? now : turn.endedAt ?? now;
|
|
1185
1296
|
return Math.max(0, end - turn.startedAt);
|
|
1186
1297
|
}
|
|
1187
|
-
function
|
|
1188
|
-
|
|
1189
|
-
const all = agent?.logs ? wrapLog(agent.logs, width) : [];
|
|
1190
|
-
const height = Math.max(1, viewport);
|
|
1191
|
-
const maxScroll = Math.max(0, all.length - height);
|
|
1192
|
-
const scrolledBack = Math.min(Math.max(0, turn.scroll), maxScroll);
|
|
1193
|
-
const end = all.length - scrolledBack;
|
|
1194
|
-
return {
|
|
1195
|
-
rows: all.slice(Math.max(0, end - height), end),
|
|
1196
|
-
scrolledBack,
|
|
1197
|
-
following: scrolledBack === 0
|
|
1198
|
-
};
|
|
1298
|
+
function visibleTimeline(state, agentId, viewport, width, scroll) {
|
|
1299
|
+
return windowParts(timelineParts(state, agentId, width), viewport, scroll);
|
|
1199
1300
|
}
|
|
1200
1301
|
|
|
1201
1302
|
// src/tui/interaction.ts
|
|
@@ -1452,7 +1553,7 @@ function formComplete(fields, state) {
|
|
|
1452
1553
|
}
|
|
1453
1554
|
var MAX_FORM_FIELDS = 6;
|
|
1454
1555
|
function questionBarRows(fields) {
|
|
1455
|
-
return 2 + Math.min(fields.length, MAX_FORM_FIELDS) + 1 +
|
|
1556
|
+
return 2 + Math.min(fields.length, MAX_FORM_FIELDS) + 1 + 2;
|
|
1456
1557
|
}
|
|
1457
1558
|
function interactionRows(interactions, width) {
|
|
1458
1559
|
const front = interactions[0];
|
|
@@ -1480,11 +1581,11 @@ var HELP_GROUPS = [
|
|
|
1480
1581
|
{
|
|
1481
1582
|
title: "panes",
|
|
1482
1583
|
rows: [
|
|
1483
|
-
["tab
|
|
1584
|
+
["tab", "Next agent"],
|
|
1585
|
+
["\u21E7tab", "Toggle permission mode"],
|
|
1484
1586
|
["\u2191 \u2193", "Scroll the stream one line"],
|
|
1485
1587
|
["pgup pgdn", "Scroll the stream a page"],
|
|
1486
|
-
["ctrl+l", "Clear the
|
|
1487
|
-
["esc", "Stop reading a past turn"]
|
|
1588
|
+
["ctrl+l", "Clear the current turn's output"]
|
|
1488
1589
|
]
|
|
1489
1590
|
},
|
|
1490
1591
|
{
|
|
@@ -1583,18 +1684,22 @@ function PermissionBar({
|
|
|
1583
1684
|
others,
|
|
1584
1685
|
cursor,
|
|
1585
1686
|
released,
|
|
1586
|
-
width
|
|
1687
|
+
width,
|
|
1688
|
+
maxRows = Number.POSITIVE_INFINITY
|
|
1587
1689
|
}) {
|
|
1588
1690
|
const choices = permissionChoices(request);
|
|
1589
|
-
const
|
|
1590
|
-
const
|
|
1691
|
+
const capacity = Math.max(1, Math.min(choices.length, maxRows - 3));
|
|
1692
|
+
const start = listWindowStart(cursor ?? 0, choices.length, capacity);
|
|
1693
|
+
const extra = Math.max(0, maxRows - 3 - capacity);
|
|
1694
|
+
const impact = describeImpact(request, Math.max(8, width - 6)).slice(0, extra);
|
|
1695
|
+
const also = extra > impact.length ? waitingLine(others) : void 0;
|
|
1591
1696
|
const tool = clean(request.toolName ?? request.kind ?? "tool");
|
|
1592
1697
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, width, children: [
|
|
1593
1698
|
/* @__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` : ""}` }),
|
|
1594
1699
|
/* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: ` ${truncate(clean(request.title), Math.max(8, width - 4))}` }),
|
|
1595
1700
|
impact.map((line) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` ${line}` }, line)),
|
|
1596
|
-
choices.map((choice, index) => {
|
|
1597
|
-
const active = index === cursor;
|
|
1701
|
+
choices.slice(start, start + capacity).map((choice, index) => {
|
|
1702
|
+
const active = start + index === cursor;
|
|
1598
1703
|
return /* @__PURE__ */ jsx3(
|
|
1599
1704
|
Text3,
|
|
1600
1705
|
{
|
|
@@ -1607,7 +1712,7 @@ function PermissionBar({
|
|
|
1607
1712
|
);
|
|
1608
1713
|
}),
|
|
1609
1714
|
also === void 0 ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` ${also}` }),
|
|
1610
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: released ? " typing \xB7
|
|
1715
|
+
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" })
|
|
1611
1716
|
] });
|
|
1612
1717
|
}
|
|
1613
1718
|
function QuestionForm({
|
|
@@ -1616,18 +1721,26 @@ function QuestionForm({
|
|
|
1616
1721
|
fields,
|
|
1617
1722
|
form,
|
|
1618
1723
|
draft,
|
|
1619
|
-
width
|
|
1724
|
+
width,
|
|
1725
|
+
maxRows = Number.POSITIVE_INFINITY
|
|
1620
1726
|
}) {
|
|
1621
1727
|
const focused = fields[form.field];
|
|
1622
1728
|
const titleWidth = Math.min(
|
|
1623
1729
|
24,
|
|
1624
1730
|
Math.max(4, ...fields.map((field) => field.title.length + (field.required ? 2 : 0)))
|
|
1625
1731
|
);
|
|
1626
|
-
const
|
|
1627
|
-
const
|
|
1732
|
+
const actions = ["Submit answers", "Skip question", "Cancel request"];
|
|
1733
|
+
const actionIndex = form.field - fields.length;
|
|
1734
|
+
const compact = maxRows < 7;
|
|
1735
|
+
const capacity = compact ? focused ? 1 : 0 : Math.min(fields.length, MAX_FORM_FIELDS, maxRows - 5);
|
|
1736
|
+
const start = listWindowStart(
|
|
1737
|
+
Math.min(form.field, fields.length - 1),
|
|
1738
|
+
fields.length,
|
|
1739
|
+
Math.max(1, capacity)
|
|
1740
|
+
);
|
|
1628
1741
|
const visible = fields.slice(start, start + capacity);
|
|
1629
|
-
const range = fields.length > capacity ? ` \xB7 ${
|
|
1630
|
-
const detail = focused
|
|
1742
|
+
const range = fields.length > capacity && focused ? ` \xB7 field ${form.field + 1}/${fields.length}` : "";
|
|
1743
|
+
const detail = focused && (!compact || maxRows >= 5) ? detailLines(focused, width).slice(0, 1) : [];
|
|
1631
1744
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, width, children: [
|
|
1632
1745
|
/* @__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` : ""}` }),
|
|
1633
1746
|
/* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: clean(request.message) }),
|
|
@@ -1648,7 +1761,8 @@ function QuestionForm({
|
|
|
1648
1761
|
);
|
|
1649
1762
|
}),
|
|
1650
1763
|
detail.map((line) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, wrap: "truncate-end", children: ` ${line}` }, line)),
|
|
1651
|
-
/* @__PURE__ */ jsx3(Text3, {
|
|
1764
|
+
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 ") }),
|
|
1765
|
+
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` })
|
|
1652
1766
|
] });
|
|
1653
1767
|
}
|
|
1654
1768
|
function fieldValue(field, form, draft, focused) {
|
|
@@ -1678,6 +1792,7 @@ function detailLines(field, width) {
|
|
|
1678
1792
|
}
|
|
1679
1793
|
|
|
1680
1794
|
// src/tui/components/panels.tsx
|
|
1795
|
+
import { describeSession } from "@multiclawcli/core";
|
|
1681
1796
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
1682
1797
|
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1683
1798
|
function rightColumn(agent, now) {
|
|
@@ -1766,21 +1881,27 @@ function Divider() {
|
|
|
1766
1881
|
);
|
|
1767
1882
|
}
|
|
1768
1883
|
function StreamPanel({
|
|
1769
|
-
|
|
1884
|
+
state,
|
|
1770
1885
|
height,
|
|
1771
1886
|
width,
|
|
1772
1887
|
now,
|
|
1773
1888
|
spinner
|
|
1774
1889
|
}) {
|
|
1775
|
-
const agent =
|
|
1890
|
+
const agent = state.focusId ? state.agents[state.focusId] : void 0;
|
|
1776
1891
|
const viewport = Math.max(1, height - 1);
|
|
1777
|
-
const window =
|
|
1892
|
+
const window = visibleTimeline(state, state.focusId ?? "", viewport, width, state.scroll);
|
|
1778
1893
|
const style = agent ? STATUS[agent.status] : void 0;
|
|
1779
1894
|
const elapsed = agent ? agentElapsed(agent, now) : void 0;
|
|
1895
|
+
const focusId = state.focusId;
|
|
1896
|
+
const turnCount = focusId ? recordedTurns(state).filter((turn) => (turn.agents[focusId]?.logs.length ?? 0) > 0).length : 0;
|
|
1780
1897
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexGrow: 1, height, paddingLeft: 1, children: [
|
|
1781
1898
|
/* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
|
|
1782
1899
|
/* @__PURE__ */ jsx4(Box4, { flexShrink: 1, children: /* @__PURE__ */ jsxs4(Text4, { wrap: "truncate-end", children: [
|
|
1783
1900
|
/* @__PURE__ */ jsx4(Text4, { bold: true, children: agent?.name ?? "\u2014" }),
|
|
1901
|
+
agent?.session ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1902
|
+
" \xB7 ",
|
|
1903
|
+
describeSession(agent.session)
|
|
1904
|
+
] }) : null,
|
|
1784
1905
|
agent?.model ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1785
1906
|
" \xB7 ",
|
|
1786
1907
|
agent.model
|
|
@@ -1803,6 +1924,11 @@ function StreamPanel({
|
|
|
1803
1924
|
agent?.costUsd !== void 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1804
1925
|
" \xB7 ",
|
|
1805
1926
|
formatCost(agent.costUsd)
|
|
1927
|
+
] }) : null,
|
|
1928
|
+
turnCount > 1 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1929
|
+
" \xB7 ",
|
|
1930
|
+
turnCount,
|
|
1931
|
+
" turns"
|
|
1806
1932
|
] }) : null
|
|
1807
1933
|
] }) }),
|
|
1808
1934
|
/* @__PURE__ */ jsxs4(Box4, { flexShrink: 0, children: [
|
|
@@ -1813,13 +1939,14 @@ function StreamPanel({
|
|
|
1813
1939
|
/* @__PURE__ */ jsx4(Text4, { color: window.following ? COLOR.accent : COLOR.warn, children: window.following ? GLYPH.live : `${GLYPH.paused} ${window.scrolledBack}\u2191` })
|
|
1814
1940
|
] })
|
|
1815
1941
|
] }),
|
|
1816
|
-
window.rows.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children:
|
|
1817
|
-
// The tail, already wrapped to `width` by `
|
|
1818
|
-
// rows, and one log line is several of them. `
|
|
1819
|
-
// which rows, so the content here is exactly the pane's
|
|
1820
|
-
// oldest rows are the ones already gone: nothing is left
|
|
1821
|
-
// is why this box is not bottom-pinned. Pinning it would
|
|
1822
|
-
// gap under the header whenever an agent has not said much
|
|
1942
|
+
window.rows.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: state.phase === "running" ? "waiting for output\u2026" : "no output yet" }) : (
|
|
1943
|
+
// The tail, already wrapped to `width` by `visibleTimeline` — the pane
|
|
1944
|
+
// draws rows, and one log line is several of them. `visibleTimeline`
|
|
1945
|
+
// also picks which rows, so the content here is exactly the pane's
|
|
1946
|
+
// height and the oldest rows are the ones already gone: nothing is left
|
|
1947
|
+
// to clip, which is why this box is not bottom-pinned. Pinning it would
|
|
1948
|
+
// only leave a gap under the header whenever an agent has not said much
|
|
1949
|
+
// yet.
|
|
1823
1950
|
//
|
|
1824
1951
|
// Rows are rendered with `truncate-end` rather than letting Ink wrap
|
|
1825
1952
|
// them again: they are supposed to fit, and if `width` was ever
|
|
@@ -1996,6 +2123,7 @@ function CommandMenu({ rows, selected }) {
|
|
|
1996
2123
|
// src/tui/history.ts
|
|
1997
2124
|
import * as fs3 from "fs";
|
|
1998
2125
|
import * as path3 from "path";
|
|
2126
|
+
import { describeSession as describeSession2, isSessionEvent, isSessionRecord, isWorkContext } from "@multiclawcli/core";
|
|
1999
2127
|
|
|
2000
2128
|
// src/tui/sessions.ts
|
|
2001
2129
|
import { randomUUID } from "crypto";
|
|
@@ -2094,14 +2222,24 @@ function isAcceptance(value) {
|
|
|
2094
2222
|
function isTurnView(value) {
|
|
2095
2223
|
if (!isObject(value)) return false;
|
|
2096
2224
|
const turn = value;
|
|
2097
|
-
return typeof turn.turn === "number" && typeof turn.phase === "string" && (turn.cancelled === void 0 || typeof turn.cancelled === "boolean") && (turn.interrupted === void 0 || typeof turn.interrupted === "boolean") && (turn.acceptance === void 0 || isAcceptance(turn.acceptance)) && (turn.effectiveRequirement === void 0 || typeof turn.effectiveRequirement === "string") && isObject(turn.agents) && Object.values(turn.agents).every(
|
|
2098
|
-
(
|
|
2099
|
-
|
|
2100
|
-
)) && (stage.
|
|
2225
|
+
return typeof turn.turn === "number" && typeof turn.phase === "string" && (turn.cancelled === void 0 || typeof turn.cancelled === "boolean") && (turn.interrupted === void 0 || typeof turn.interrupted === "boolean") && (turn.work === void 0 || isWorkContext(turn.work)) && (turn.sessionEvents === void 0 || Array.isArray(turn.sessionEvents) && turn.sessionEvents.every(isSessionEvent)) && (turn.acceptance === void 0 || isAcceptance(turn.acceptance)) && (turn.effectiveRequirement === void 0 || typeof turn.effectiveRequirement === "string") && isObject(turn.agents) && Object.values(turn.agents).every(
|
|
2226
|
+
(agent) => isObject(agent) && (agent.session === void 0 || isSessionRecord(agent.session))
|
|
2227
|
+
) && Array.isArray(turn.stages) && turn.stages.every(
|
|
2228
|
+
(stage) => isObject(stage) && (stage.outcome === void 0 || [
|
|
2229
|
+
"pending",
|
|
2230
|
+
"running",
|
|
2231
|
+
"retrying",
|
|
2232
|
+
"handed_back",
|
|
2233
|
+
"success",
|
|
2234
|
+
"failed",
|
|
2235
|
+
"cancelled",
|
|
2236
|
+
"skipped"
|
|
2237
|
+
].includes(stage.outcome)) && (stage.memberStatuses === void 0 || isObject(stage.memberStatuses) && Object.values(stage.memberStatuses).every(
|
|
2101
2238
|
(status) => typeof status === "string" && [
|
|
2102
2239
|
"pending",
|
|
2103
2240
|
"running",
|
|
2104
2241
|
"retrying",
|
|
2242
|
+
"handed_back",
|
|
2105
2243
|
"success",
|
|
2106
2244
|
"failed",
|
|
2107
2245
|
"cancelled",
|
|
@@ -2144,6 +2282,27 @@ async function exportTurn(turn, filePath) {
|
|
|
2144
2282
|
function formatTurnMarkdown(turn) {
|
|
2145
2283
|
const lines = [`# Turn ${turn.turn}`, ""];
|
|
2146
2284
|
if (turn.requirement) lines.push("## Requirement", "", turn.requirement, "");
|
|
2285
|
+
if (turn.effectiveRequirement && turn.effectiveRequirement !== turn.requirement)
|
|
2286
|
+
lines.push("## Complete requirement", "", turn.effectiveRequirement, "");
|
|
2287
|
+
if (turn.routing) {
|
|
2288
|
+
lines.push(
|
|
2289
|
+
"## Routing",
|
|
2290
|
+
"",
|
|
2291
|
+
"Type: " + turn.routing.mode,
|
|
2292
|
+
"Work: " + (turn.routing.workId ?? "none"),
|
|
2293
|
+
"Reroutes: " + turn.routing.reroutes,
|
|
2294
|
+
""
|
|
2295
|
+
);
|
|
2296
|
+
if (turn.routing.error) lines.push("Incomplete: " + turn.routing.error, "");
|
|
2297
|
+
}
|
|
2298
|
+
if (turn.agentResults?.length) {
|
|
2299
|
+
lines.push("## Execution attempts", "");
|
|
2300
|
+
for (const result of turn.agentResults)
|
|
2301
|
+
lines.push(
|
|
2302
|
+
"- " + result.agentId + ": " + result.status + (result.handoffReason ? " \u2014 " + result.handoffReason : "") + (result.error ? " \u2014 " + result.error : "")
|
|
2303
|
+
);
|
|
2304
|
+
lines.push("");
|
|
2305
|
+
}
|
|
2147
2306
|
if (turn.reply) {
|
|
2148
2307
|
lines.push("## Reply", "", `_${turn.reply.mode}_`, "", turn.reply.message, "");
|
|
2149
2308
|
} else {
|
|
@@ -2164,11 +2323,20 @@ function formatTurnMarkdown(turn) {
|
|
|
2164
2323
|
}
|
|
2165
2324
|
}
|
|
2166
2325
|
}
|
|
2326
|
+
if (turn.sessionEvents?.length) {
|
|
2327
|
+
lines.push("## Session lifecycle", "");
|
|
2328
|
+
for (const event of turn.sessionEvents)
|
|
2329
|
+
lines.push(
|
|
2330
|
+
"- " + event.record.agentId + ": " + event.action + (event.requestedSessionId ? " target=" + event.requestedSessionId : "") + " \u2014 " + describeSession2(event.record) + (event.reason ? " \u2014 " + event.reason : "")
|
|
2331
|
+
);
|
|
2332
|
+
lines.push("");
|
|
2333
|
+
}
|
|
2167
2334
|
const agents = Object.values(turn.agents);
|
|
2168
2335
|
if (agents.length > 0) {
|
|
2169
2336
|
lines.push("## Agents", "");
|
|
2170
2337
|
for (const agent of agents) {
|
|
2171
2338
|
lines.push(`- ${agent.id}: ${agent.name} \u2014 ${agent.status}`);
|
|
2339
|
+
if (agent.session) lines.push(" " + describeSession2(agent.session));
|
|
2172
2340
|
}
|
|
2173
2341
|
lines.push("");
|
|
2174
2342
|
lines.push("## Logs", "");
|
|
@@ -2184,6 +2352,7 @@ function formatTurnMarkdown(turn) {
|
|
|
2184
2352
|
// src/tui/session.ts
|
|
2185
2353
|
import * as fs4 from "fs";
|
|
2186
2354
|
import * as path4 from "path";
|
|
2355
|
+
import { isSessionEvent as isSessionEvent2, isSessionRecord as isSessionRecord2, isWorkContext as isWorkContext2 } from "@multiclawcli/core";
|
|
2187
2356
|
var SESSION_FILE = "session.json";
|
|
2188
2357
|
var SESSION_VERSION = 1;
|
|
2189
2358
|
var MAX_JOURNAL_LOG_CHARS = 1200;
|
|
@@ -2197,6 +2366,7 @@ var STATUSES = /* @__PURE__ */ new Set([
|
|
|
2197
2366
|
"pending",
|
|
2198
2367
|
"running",
|
|
2199
2368
|
"retrying",
|
|
2369
|
+
"handed_back",
|
|
2200
2370
|
"success",
|
|
2201
2371
|
"failed",
|
|
2202
2372
|
"cancelled",
|
|
@@ -2204,11 +2374,11 @@ var STATUSES = /* @__PURE__ */ new Set([
|
|
|
2204
2374
|
]);
|
|
2205
2375
|
function isAgentView(value) {
|
|
2206
2376
|
if (!isObject2(value)) return false;
|
|
2207
|
-
return typeof value.id === "string" && typeof value.name === "string" && Array.isArray(value.logs) && typeof value.status === "string" && STATUSES.has(value.status);
|
|
2377
|
+
return typeof value.id === "string" && typeof value.name === "string" && (value.session === void 0 || isSessionRecord2(value.session)) && Array.isArray(value.logs) && typeof value.status === "string" && STATUSES.has(value.status);
|
|
2208
2378
|
}
|
|
2209
2379
|
function isJournalTurn(value) {
|
|
2210
2380
|
if (!isObject2(value)) return false;
|
|
2211
|
-
return Number.isInteger(value.turn) && typeof value.requirement === "string" && typeof value.savedAt === "number" && Number.isFinite(value.savedAt) && (value.startedAt === void 0 || typeof value.startedAt === "number") && (value.effectiveRequirement === void 0 || typeof value.effectiveRequirement === "string") && (value.reason === void 0 || typeof value.reason === "string") && (value.warning === void 0 || typeof value.warning === "string") && (value.focusId === void 0 || typeof value.focusId === "string") && isObject2(value.agents) && Object.values(value.agents).every(isAgentView) && Array.isArray(value.leaderIds) && value.leaderIds.every((id) => typeof id === "string") && Array.isArray(value.stages) && value.stages.every(isObject2) && (value.pending === void 0 || isObject2(value.pending) && Number.isInteger(value.pending.permissions) && Number.isInteger(value.pending.questions));
|
|
2381
|
+
return Number.isInteger(value.turn) && typeof value.requirement === "string" && typeof value.savedAt === "number" && Number.isFinite(value.savedAt) && (value.work === void 0 || isWorkContext2(value.work)) && (value.sessionEvents === void 0 || Array.isArray(value.sessionEvents) && value.sessionEvents.every(isSessionEvent2)) && (value.startedAt === void 0 || typeof value.startedAt === "number") && (value.effectiveRequirement === void 0 || typeof value.effectiveRequirement === "string") && (value.reason === void 0 || typeof value.reason === "string") && (value.warning === void 0 || typeof value.warning === "string") && (value.focusId === void 0 || typeof value.focusId === "string") && isObject2(value.agents) && Object.values(value.agents).every(isAgentView) && Array.isArray(value.leaderIds) && value.leaderIds.every((id) => typeof id === "string") && Array.isArray(value.stages) && value.stages.every(isObject2) && (value.pending === void 0 || isObject2(value.pending) && Number.isInteger(value.pending.permissions) && Number.isInteger(value.pending.questions));
|
|
2212
2382
|
}
|
|
2213
2383
|
function loadJournal(workDir, sessionId) {
|
|
2214
2384
|
try {
|
|
@@ -2256,6 +2426,10 @@ function journalTurn(state, pending) {
|
|
|
2256
2426
|
}
|
|
2257
2427
|
return {
|
|
2258
2428
|
turn: state.turn,
|
|
2429
|
+
work: state.work,
|
|
2430
|
+
routing: state.routing,
|
|
2431
|
+
sessionEvents: state.sessionEvents,
|
|
2432
|
+
agentResults: state.agentResults,
|
|
2259
2433
|
requirement: state.requirement ?? "",
|
|
2260
2434
|
startedAt: state.startedAt,
|
|
2261
2435
|
effectiveRequirement: state.effectiveRequirement,
|
|
@@ -2273,10 +2447,24 @@ var UNSETTLED = /* @__PURE__ */ new Set(["pending", "running", "retrying"]);
|
|
|
2273
2447
|
function interruptedTurn(entry) {
|
|
2274
2448
|
const agents = {};
|
|
2275
2449
|
for (const [id, agent] of Object.entries(entry.agents)) {
|
|
2276
|
-
agents[id] =
|
|
2450
|
+
agents[id] = {
|
|
2451
|
+
...agent,
|
|
2452
|
+
...UNSETTLED.has(agent.status) ? { status: "cancelled" } : {},
|
|
2453
|
+
session: agent.session ? { ...agent.session, state: agent.session.state === "closed" ? "closed" : "unknown" } : void 0
|
|
2454
|
+
};
|
|
2277
2455
|
}
|
|
2278
2456
|
return {
|
|
2279
2457
|
turn: entry.turn,
|
|
2458
|
+
work: entry.work ? {
|
|
2459
|
+
...entry.work,
|
|
2460
|
+
agents: entry.work.agents.map((agent) => ({
|
|
2461
|
+
...agent,
|
|
2462
|
+
session: agent.session ? { ...agent.session, state: agent.session.state === "closed" ? "closed" : "unknown" } : void 0
|
|
2463
|
+
}))
|
|
2464
|
+
} : void 0,
|
|
2465
|
+
routing: entry.routing,
|
|
2466
|
+
sessionEvents: entry.sessionEvents,
|
|
2467
|
+
agentResults: entry.agentResults,
|
|
2280
2468
|
requirement: entry.requirement,
|
|
2281
2469
|
effectiveRequirement: entry.effectiveRequirement,
|
|
2282
2470
|
reason: entry.reason,
|
|
@@ -2339,6 +2527,16 @@ var SESSION_SAVE_INTERVAL_MS = 2e3;
|
|
|
2339
2527
|
var INPUT_PLACEHOLDER = "describe what the team should do\u2026";
|
|
2340
2528
|
var RUNNING_PLACEHOLDER = "type to queue a follow-up\u2026";
|
|
2341
2529
|
var EMPTY_LINE = { text: "", cursor: 0 };
|
|
2530
|
+
function retryCandidates(state) {
|
|
2531
|
+
const turn = recordedTurns(state).slice(-1)[0];
|
|
2532
|
+
if (!turn?.requirement) return [];
|
|
2533
|
+
if (turn.acceptance && turn.acceptance.status !== "approved") {
|
|
2534
|
+
return [
|
|
2535
|
+
...new Set(turn.stages.flatMap((stage) => stage.agents.map((agent) => agent.id)))
|
|
2536
|
+
].filter((id) => id !== turn.acceptance?.reviewer);
|
|
2537
|
+
}
|
|
2538
|
+
return Object.values(turn.agents).filter((agent) => agent.status === "failed").map((agent) => agent.id);
|
|
2539
|
+
}
|
|
2342
2540
|
function pickRecordedTurn(state, raw, whenEmpty) {
|
|
2343
2541
|
const turns = recordedTurns(state);
|
|
2344
2542
|
if (turns.length === 0) return { error: whenEmpty };
|
|
@@ -2352,20 +2550,6 @@ function pickTurn(state, raw, whenEmpty) {
|
|
|
2352
2550
|
if (!target.record.requirement) return { error: `turn ${target.record.turn} has no requirement` };
|
|
2353
2551
|
return { record: target.record, requirement: target.record.requirement };
|
|
2354
2552
|
}
|
|
2355
|
-
function turnSessions(turn) {
|
|
2356
|
-
const sessions = {};
|
|
2357
|
-
for (const agent of Object.values(turn.agents)) {
|
|
2358
|
-
if (agent.sessionId) sessions[agent.id] = agent.sessionId;
|
|
2359
|
-
}
|
|
2360
|
-
return sessions;
|
|
2361
|
-
}
|
|
2362
|
-
function continuationResume(continuation, live) {
|
|
2363
|
-
if (!continuation) return void 0;
|
|
2364
|
-
const sessions = Object.fromEntries(
|
|
2365
|
-
Object.entries(continuation.sessions).filter(([id]) => !live?.has(id))
|
|
2366
|
-
);
|
|
2367
|
-
return Object.keys(sessions).length > 0 ? { sessions } : void 0;
|
|
2368
|
-
}
|
|
2369
2553
|
function StatusLine({
|
|
2370
2554
|
state,
|
|
2371
2555
|
running,
|
|
@@ -2382,6 +2566,8 @@ function StatusLine({
|
|
|
2382
2566
|
return /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, wrap: "truncate-end", children: state.warning });
|
|
2383
2567
|
if ((cancelled || state.cancelled) && !running)
|
|
2384
2568
|
return /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, children: "cancelled \xB7 workspace changes are retained" });
|
|
2569
|
+
if (state.routing?.error && !running)
|
|
2570
|
+
return /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, wrap: "truncate-end", children: state.routing.error });
|
|
2385
2571
|
if (state.reply) {
|
|
2386
2572
|
const asking = state.reply.mode === "ask";
|
|
2387
2573
|
return /* @__PURE__ */ jsx6(Text6, { color: asking ? COLOR.warn : COLOR.ok, wrap: "truncate-end", children: asking ? `${GLYPH.ask} needs your input` : `${STATUS.success.glyph} answered` });
|
|
@@ -2470,10 +2656,19 @@ function App({
|
|
|
2470
2656
|
queueRef.current = queue;
|
|
2471
2657
|
const [cancelled, setCancelled] = useState(false);
|
|
2472
2658
|
const [error, setError] = useState();
|
|
2659
|
+
const [permissionMode, setPermissionMode] = useState(
|
|
2660
|
+
// Same precedence the runner uses, so the header can never announce a mode
|
|
2661
|
+
// the agents are not actually running under. `ask` when nobody configured
|
|
2662
|
+
// one, because a human is sitting here to answer.
|
|
2663
|
+
resolvePermissionMode(config) ?? "ask"
|
|
2664
|
+
);
|
|
2473
2665
|
const [notice, setNotice] = useState(
|
|
2474
2666
|
launchNoticeRef.current ?? restored.notice
|
|
2475
2667
|
);
|
|
2476
2668
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
2669
|
+
const [picker, setPicker] = useState(null);
|
|
2670
|
+
const [operationInput, setOperationInput] = useState(null);
|
|
2671
|
+
const [searchResult, setSearchResult] = useState(null);
|
|
2477
2672
|
const [sessionsOpen, setSessionsOpen] = useState(false);
|
|
2478
2673
|
const [sessionCursor, setSessionCursor] = useState(0);
|
|
2479
2674
|
const [sessionList, setSessionList] = useState([]);
|
|
@@ -2484,15 +2679,6 @@ function App({
|
|
|
2484
2679
|
const abortRef = useRef(null);
|
|
2485
2680
|
const sessionsRef = useRef(null);
|
|
2486
2681
|
if (!sessionsRef.current) sessionsRef.current = providedSessions ?? new AgentSessionManager();
|
|
2487
|
-
const continuationRef = useRef(null);
|
|
2488
|
-
useEffect(() => {
|
|
2489
|
-
if (initialSession?.kind !== "resume" || !("id" in initialSession)) return;
|
|
2490
|
-
const history = restoredRef.current?.history ?? [];
|
|
2491
|
-
const last = history[history.length - 1];
|
|
2492
|
-
if (last) dispatch({ type: "view:open", turn: last.turn });
|
|
2493
|
-
const resumable = [...history].reverse().find((turn) => Object.keys(turnSessions(turn)).length > 0);
|
|
2494
|
-
continuationRef.current = resumable ? { turn: resumable.turn, sessions: turnSessions(resumable) } : null;
|
|
2495
|
-
}, [initialSession]);
|
|
2496
2682
|
const activeTurnRef = useRef(null);
|
|
2497
2683
|
const reporterErrorRef = useRef(null);
|
|
2498
2684
|
const closingRef = useRef(false);
|
|
@@ -2563,16 +2749,21 @@ function App({
|
|
|
2563
2749
|
const handleCheckpoint = useCallback((info, signal) => {
|
|
2564
2750
|
if (closingRef.current || signal.aborted) return Promise.resolve({ action: "cancel" });
|
|
2565
2751
|
return new Promise((resolve2) => {
|
|
2566
|
-
|
|
2752
|
+
const settle = (response) => {
|
|
2753
|
+
signal.removeEventListener("abort", onAbort);
|
|
2754
|
+
if (checkpointResolveRef.current === settle) {
|
|
2755
|
+
checkpointResolveRef.current = null;
|
|
2756
|
+
checkpointLabelRef.current = "";
|
|
2757
|
+
}
|
|
2758
|
+
resolve2(response);
|
|
2759
|
+
};
|
|
2760
|
+
checkpointResolveRef.current = settle;
|
|
2567
2761
|
checkpointLabelRef.current = info.label;
|
|
2568
2762
|
const names = info.completedAgents.map((a) => a.name).join(", ");
|
|
2569
2763
|
setNotice(
|
|
2570
|
-
`checkpoint: ${info.label} \u2014 ${names} done \xB7 /continue
|
|
2764
|
+
`checkpoint: ${info.label} \u2014 ${names} done \xB7 /continue \xB7 type feedback \xB7 ctrl+c cancel`
|
|
2571
2765
|
);
|
|
2572
|
-
const onAbort = () => {
|
|
2573
|
-
checkpointResolveRef.current = null;
|
|
2574
|
-
resolve2({ action: "cancel" });
|
|
2575
|
-
};
|
|
2766
|
+
const onAbort = () => settle({ action: "cancel" });
|
|
2576
2767
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2577
2768
|
});
|
|
2578
2769
|
}, []);
|
|
@@ -2653,9 +2844,7 @@ function App({
|
|
|
2653
2844
|
if (closingRef.current || busyRef.current) return;
|
|
2654
2845
|
busyRef.current = true;
|
|
2655
2846
|
setRunning(true);
|
|
2656
|
-
const
|
|
2657
|
-
continuationRef.current = null;
|
|
2658
|
-
const resume = opts?.resume ?? continuationResume(continuation, sessionsRef.current);
|
|
2847
|
+
const resume = opts?.resume;
|
|
2659
2848
|
if (opts?.newSession) {
|
|
2660
2849
|
try {
|
|
2661
2850
|
await sessionsRef.current?.reset();
|
|
@@ -2689,6 +2878,13 @@ function App({
|
|
|
2689
2878
|
setError(void 0);
|
|
2690
2879
|
setCancelled(false);
|
|
2691
2880
|
setNotice(void 0);
|
|
2881
|
+
if (opts?.newSession && !requirement) {
|
|
2882
|
+
busyRef.current = false;
|
|
2883
|
+
setRunning(false);
|
|
2884
|
+
setSearchResult(null);
|
|
2885
|
+
setNotice("new session \u2014 type a requirement to begin");
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2692
2888
|
const earlierTurns = opts?.newSession ? [] : conversationTurns(stateRef.current);
|
|
2693
2889
|
dispatch({
|
|
2694
2890
|
type: "turn:start",
|
|
@@ -2705,8 +2901,18 @@ function App({
|
|
|
2705
2901
|
// An answer only reads as a requirement next to the question it answers,
|
|
2706
2902
|
// so the leader is handed the turns behind this one as well.
|
|
2707
2903
|
history: earlierTurns,
|
|
2904
|
+
latestWork: opts?.newSession ? void 0 : opts?.work ?? latestWorkFromTurns(recordedTurns(stateRef.current)),
|
|
2708
2905
|
logDir,
|
|
2709
|
-
permissions:
|
|
2906
|
+
permissions: permissionMode === "ask" ? { mode: "ask", onRequest: requestPermission } : { mode: permissionMode },
|
|
2907
|
+
// The console's mode is a live decision, so it outranks any per-agent
|
|
2908
|
+
// `permissions` written in the file: otherwise shift+tab would relabel
|
|
2909
|
+
// the header while an agent that declares its own mode kept the old
|
|
2910
|
+
// behaviour. Per-agent modes stay a headless-run concern.
|
|
2911
|
+
agents: config.agents.map((agent) => ({
|
|
2912
|
+
...agent,
|
|
2913
|
+
permissions: { mode: permissionMode }
|
|
2914
|
+
})),
|
|
2915
|
+
...config.leader ? { leader: { ...config.leader, permissions: { mode: permissionMode } } } : {},
|
|
2710
2916
|
questions: config.questions?.onRequest ? config.questions : { onRequest: requestQuestion },
|
|
2711
2917
|
onCheckpoint: config.onCheckpoint ?? handleCheckpoint,
|
|
2712
2918
|
// A retry names the agents to redo; a continued conversation carries only
|
|
@@ -2771,7 +2977,8 @@ function App({
|
|
|
2771
2977
|
requestPermission,
|
|
2772
2978
|
requestQuestion,
|
|
2773
2979
|
handleCheckpoint,
|
|
2774
|
-
clearInteractions
|
|
2980
|
+
clearInteractions,
|
|
2981
|
+
permissionMode
|
|
2775
2982
|
]
|
|
2776
2983
|
);
|
|
2777
2984
|
useEffect(() => {
|
|
@@ -2791,21 +2998,16 @@ function App({
|
|
|
2791
2998
|
return;
|
|
2792
2999
|
}
|
|
2793
3000
|
if (closingRef.current) return;
|
|
3001
|
+
setSearchResult(null);
|
|
2794
3002
|
const restored2 = restoreConsole(config.workDir, id);
|
|
2795
3003
|
sessionIdRef.current = id;
|
|
2796
3004
|
persistedTurnRef.current = restored2.persistedTurn;
|
|
2797
|
-
const resumable = [...restored2.history].reverse().find((turn) => Object.keys(turnSessions(turn)).length > 0);
|
|
2798
|
-
continuationRef.current = resumable ? { turn: resumable.turn, sessions: turnSessions(resumable) } : null;
|
|
2799
3005
|
dispatch({
|
|
2800
3006
|
type: "session:load",
|
|
2801
3007
|
agents: seedAgents(config),
|
|
2802
3008
|
leaderIds: leaderAgentIds(config),
|
|
2803
3009
|
history: restored2.history
|
|
2804
3010
|
});
|
|
2805
|
-
const lastTurn = restored2.history[restored2.history.length - 1];
|
|
2806
|
-
if (lastTurn) {
|
|
2807
|
-
dispatch({ type: "view:open", turn: lastTurn.turn });
|
|
2808
|
-
}
|
|
2809
3011
|
setQueue(restored2.queue);
|
|
2810
3012
|
queuePausedRef.current = restored2.queue.length > 0;
|
|
2811
3013
|
setQueuePaused(restored2.queue.length > 0);
|
|
@@ -2813,9 +3015,7 @@ function App({
|
|
|
2813
3015
|
setAwaitingAnswer(restored2.awaitingAnswer);
|
|
2814
3016
|
setError(void 0);
|
|
2815
3017
|
setCancelled(false);
|
|
2816
|
-
setNotice(
|
|
2817
|
-
restored2.notice ?? (resumable ? `switched session \xB7 continue with your next instruction` : `switched session \xB7 ${restored2.history.length} turn${restored2.history.length === 1 ? "" : "s"}`)
|
|
2818
|
-
);
|
|
3018
|
+
setNotice(restored2.notice ?? "switched session \xB7 route your next instruction");
|
|
2819
3019
|
},
|
|
2820
3020
|
[config]
|
|
2821
3021
|
);
|
|
@@ -2885,28 +3085,32 @@ function App({
|
|
|
2885
3085
|
})();
|
|
2886
3086
|
return shutdownRef.current;
|
|
2887
3087
|
}, [config.workDir, exit, cleanup]);
|
|
2888
|
-
const
|
|
2889
|
-
|
|
2890
|
-
()
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
3088
|
+
const commandContext = {
|
|
3089
|
+
history: recordedTurns(state).length > 0,
|
|
3090
|
+
retry: retryCandidates(state).length > 0,
|
|
3091
|
+
queue: queue.length,
|
|
3092
|
+
paused: queuePaused,
|
|
3093
|
+
awaitingAnswer,
|
|
3094
|
+
checkpoint: checkpointResolveRef.current !== null
|
|
3095
|
+
};
|
|
3096
|
+
const query = interactions[0]?.kind === "question" ? void 0 : menuQuery(line.text);
|
|
3097
|
+
const matches = query === void 0 ? [] : matchCommands(query, running, commandContext);
|
|
3098
|
+
const arg = interactions[0]?.kind === "question" ? void 0 : argQuery(line.text);
|
|
3099
|
+
const argRows = arg ? completeAgents(arg.partial, Object.keys(state.agents)).map((id) => ({
|
|
2896
3100
|
key: id,
|
|
2897
3101
|
label: id,
|
|
2898
3102
|
// The static role, not the per-turn task title: while completing, the
|
|
2899
3103
|
// user is asking "which agent is this", not "what is it doing now".
|
|
2900
|
-
description:
|
|
3104
|
+
description: state.agents[id]?.description ?? state.agents[id]?.name ?? id
|
|
2901
3105
|
})) : [];
|
|
2902
|
-
const allRows = arg ? argRows : paletteRows(matches);
|
|
3106
|
+
const allRows = picker ? picker.rows : arg ? argRows : paletteRows(matches);
|
|
2903
3107
|
const paletteKey = arg ? `${arg.command.name} ${arg.partial}` : query;
|
|
2904
3108
|
const paletteTotal = allRows.length;
|
|
2905
3109
|
const menuIndex = menuCursor.query === paletteKey ? menuCursor.index : 0;
|
|
2906
|
-
const selected = paletteTotal === 0 ? 0 : Math.min(menuIndex, paletteTotal - 1);
|
|
2907
|
-
const menuOpen = !helpOpen && !sessionsOpen && query !== void 0 && matches.length > 0;
|
|
2908
|
-
const argOpen = !helpOpen && !sessionsOpen && arg !== void 0 && argRows.length > 0;
|
|
2909
|
-
const paletteOpen = menuOpen || argOpen;
|
|
3110
|
+
const selected = paletteTotal === 0 ? 0 : Math.min(picker?.cursor ?? menuIndex, paletteTotal - 1);
|
|
3111
|
+
const menuOpen = !picker && !operationInput && !helpOpen && !sessionsOpen && query !== void 0 && matches.length > 0;
|
|
3112
|
+
const argOpen = !picker && !operationInput && !helpOpen && !sessionsOpen && arg !== void 0 && argRows.length > 0;
|
|
3113
|
+
const paletteOpen = picker !== null || menuOpen || argOpen;
|
|
2910
3114
|
const front = interactions[0];
|
|
2911
3115
|
const menu = front?.kind === "permission" && permissionMenu.id === front.shortId ? permissionMenu : { id: front?.shortId ?? "", cursor: 0, released: false };
|
|
2912
3116
|
const frontQuestion = front?.kind === "question" ? front : void 0;
|
|
@@ -2931,10 +3135,10 @@ function App({
|
|
|
2931
3135
|
queueRows: queue.length,
|
|
2932
3136
|
interactionRows: interactionRows(interactions, Math.max(MIN_COLUMNS, columns)),
|
|
2933
3137
|
promptRows,
|
|
3138
|
+
details: !!(state.reason || state.routing),
|
|
2934
3139
|
notice: notice !== void 0 || queuePaused && queue.length > 0,
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
railCells: railCellWidths(shown)
|
|
3140
|
+
hasRail: state.leaderIds.length > 0 || state.stages.length > 0,
|
|
3141
|
+
railCells: railCellWidths(state)
|
|
2938
3142
|
});
|
|
2939
3143
|
const steerRunningAgents = useCallback(async (text) => {
|
|
2940
3144
|
const manager = sessionsRef.current;
|
|
@@ -2959,11 +3163,11 @@ function App({
|
|
|
2959
3163
|
setNotice(describeSteering(reports));
|
|
2960
3164
|
}, []);
|
|
2961
3165
|
const runCommand = useCallback(
|
|
2962
|
-
(name, args
|
|
3166
|
+
(name, args) => {
|
|
2963
3167
|
setLine(EMPTY_LINE);
|
|
2964
|
-
if (checkpointResolveRef.current && name !== "continue"
|
|
3168
|
+
if (checkpointResolveRef.current && name !== "continue") {
|
|
2965
3169
|
setNotice(
|
|
2966
|
-
`checkpoint: ${checkpointLabelRef.current} \xB7 /continue
|
|
3170
|
+
`checkpoint: ${checkpointLabelRef.current} \xB7 /continue \xB7 type feedback \xB7 ctrl+c cancel`
|
|
2967
3171
|
);
|
|
2968
3172
|
return;
|
|
2969
3173
|
}
|
|
@@ -2971,111 +3175,86 @@ function App({
|
|
|
2971
3175
|
setNotice(unavailableReason(name, running));
|
|
2972
3176
|
return;
|
|
2973
3177
|
}
|
|
2974
|
-
const shown2 = state.view ?? state;
|
|
2975
3178
|
if (closingRef.current) return;
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
(entry2) => entry2.request.requestId === id || entry2.shortId === id
|
|
3179
|
+
const choose = (rows2) => {
|
|
3180
|
+
setPicker({ command: name, rows: rows2, cursor: 0 });
|
|
3181
|
+
setNotice(`choose ${name} \xB7 enter confirm \xB7 esc cancel`);
|
|
3182
|
+
};
|
|
3183
|
+
if (args.length === 0) {
|
|
3184
|
+
if (name === "mode") {
|
|
3185
|
+
choose(
|
|
3186
|
+
["ask", "auto", "deny"].map((mode) => ({
|
|
3187
|
+
key: mode,
|
|
3188
|
+
label: mode,
|
|
3189
|
+
description: mode === "ask" ? "Ask before tools run" : mode === "auto" ? "Allow tools automatically" : "Refuse tool requests"
|
|
3190
|
+
}))
|
|
2989
3191
|
);
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
setNotice("
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
if (name === "focus") {
|
|
3195
|
+
choose(
|
|
3196
|
+
Object.values(state.agents).map((agent) => ({
|
|
3197
|
+
key: agent.id,
|
|
3198
|
+
label: agent.name,
|
|
3199
|
+
description: agent.description ?? agent.id
|
|
3200
|
+
}))
|
|
3201
|
+
);
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
if (name === "retry") {
|
|
3205
|
+
const ids = retryCandidates(state);
|
|
3206
|
+
if (ids.length === 0) {
|
|
3207
|
+
setNotice("nothing failed");
|
|
3006
3208
|
return;
|
|
3007
3209
|
}
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3210
|
+
choose([
|
|
3211
|
+
{ key: "all", label: "All failed / repair agents", description: ids.join(", ") },
|
|
3212
|
+
...ids.map((id) => ({
|
|
3213
|
+
key: id,
|
|
3214
|
+
label: state.agents[id]?.name ?? id,
|
|
3215
|
+
description: state.agents[id]?.error ?? "Retry this agent"
|
|
3216
|
+
}))
|
|
3217
|
+
]);
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
if (["replay", "edit", "export"].includes(name)) {
|
|
3221
|
+
const turns = recordedTurns(state).slice().reverse();
|
|
3222
|
+
if (turns.length === 0) {
|
|
3223
|
+
setNotice(`nothing to ${name} yet`);
|
|
3012
3224
|
return;
|
|
3013
3225
|
}
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3226
|
+
choose(
|
|
3227
|
+
turns.map((turn) => ({
|
|
3228
|
+
key: String(turn.turn),
|
|
3229
|
+
label: `Turn ${turn.turn}`,
|
|
3230
|
+
description: turn.requirement?.replace(/\s+/g, " ") ?? "(no requirement)"
|
|
3231
|
+
}))
|
|
3232
|
+
);
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
if (name === "search" || name === "steer") {
|
|
3236
|
+
setOperationInput(name);
|
|
3237
|
+
setNotice(
|
|
3238
|
+
name === "search" ? "search the focused stream \xB7 enter find \xB7 esc cancel" : "message running agents \xB7 enter send \xB7 esc cancel"
|
|
3239
|
+
);
|
|
3019
3240
|
return;
|
|
3020
3241
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
const
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
);
|
|
3028
|
-
if (found.length !== 1) setNotice(`unknown or stale permission request: ${id}`);
|
|
3029
|
-
return found.length === 1 ? found[0] : void 0;
|
|
3030
|
-
};
|
|
3031
|
-
let entry;
|
|
3032
|
-
let number;
|
|
3033
|
-
if (name === "approve" && args.length === 1 && /^\d+$/.test(args[0])) {
|
|
3034
|
-
number = args[0];
|
|
3035
|
-
entry = onScreen;
|
|
3036
|
-
} else if (name === "approve" && args.length === 2) {
|
|
3037
|
-
number = args[1];
|
|
3038
|
-
entry = byId(args[0]);
|
|
3039
|
-
} else if (name === "deny" && args.length === 0) {
|
|
3040
|
-
entry = onScreen;
|
|
3041
|
-
} else if (name === "deny" && args.length === 1) {
|
|
3042
|
-
entry = byId(args[0]);
|
|
3242
|
+
}
|
|
3243
|
+
switch (name) {
|
|
3244
|
+
case "mode": {
|
|
3245
|
+
const m = args[0];
|
|
3246
|
+
if (m === "auto" || m === "ask" || m === "deny") {
|
|
3247
|
+
setPermissionMode(m);
|
|
3248
|
+
setNotice(`permission mode: ${m}`);
|
|
3043
3249
|
} else {
|
|
3044
|
-
setNotice(
|
|
3045
|
-
name === "approve" ? "usage: /approve [id] <option number>" : "usage: /deny [id]"
|
|
3046
|
-
);
|
|
3047
|
-
return;
|
|
3048
|
-
}
|
|
3049
|
-
if (entry === void 0) {
|
|
3050
|
-
if (args.length === 0 || /^\d+$/.test(args[0] ?? "")) {
|
|
3051
|
-
setNotice("no permission request on screen");
|
|
3052
|
-
}
|
|
3053
|
-
return;
|
|
3054
|
-
}
|
|
3055
|
-
if (entry.kind !== "permission") {
|
|
3056
|
-
setNotice("the request on screen is a question, not a permission");
|
|
3057
|
-
return;
|
|
3058
|
-
}
|
|
3059
|
-
if (name === "deny") {
|
|
3060
|
-
entry.settle();
|
|
3061
|
-
setNotice(`denied ${entry.shortId}`);
|
|
3062
|
-
return;
|
|
3063
|
-
}
|
|
3064
|
-
const option = /^\d+$/.test(number ?? "") ? entry.request.options[Number(number) - 1] : void 0;
|
|
3065
|
-
if (!option) {
|
|
3066
|
-
setNotice(
|
|
3067
|
-
`invalid option for ${entry.shortId}; choose 1\u2013${entry.request.options.length}`
|
|
3068
|
-
);
|
|
3069
|
-
return;
|
|
3250
|
+
setNotice("usage: /mode auto | ask | deny");
|
|
3070
3251
|
}
|
|
3071
|
-
entry.settle(option.optionId);
|
|
3072
|
-
setNotice(`selected ${clean(option.name)} for ${entry.shortId}`);
|
|
3073
3252
|
return;
|
|
3074
3253
|
}
|
|
3075
3254
|
case "new": {
|
|
3076
3255
|
const text = args.join(" ");
|
|
3077
|
-
if (
|
|
3078
|
-
setNotice("
|
|
3256
|
+
if (text) {
|
|
3257
|
+
setNotice("/new opens an empty session \u2014 enter the requirement afterwards");
|
|
3079
3258
|
return;
|
|
3080
3259
|
}
|
|
3081
3260
|
setQueue([]);
|
|
@@ -3084,15 +3263,6 @@ function App({
|
|
|
3084
3263
|
void startTurn(text, { newSession: true });
|
|
3085
3264
|
return;
|
|
3086
3265
|
}
|
|
3087
|
-
case "answer": {
|
|
3088
|
-
const text = args.join(" ");
|
|
3089
|
-
if (!awaitingAnswerRef.current || !text) {
|
|
3090
|
-
setNotice("usage: /answer <text> \u2014 only when the leader is waiting for an answer");
|
|
3091
|
-
return;
|
|
3092
|
-
}
|
|
3093
|
-
void startTurn(text);
|
|
3094
|
-
return;
|
|
3095
|
-
}
|
|
3096
3266
|
case "steer": {
|
|
3097
3267
|
const text = args.join(" ");
|
|
3098
3268
|
if (!text) {
|
|
@@ -3103,6 +3273,10 @@ function App({
|
|
|
3103
3273
|
return;
|
|
3104
3274
|
}
|
|
3105
3275
|
case "pause":
|
|
3276
|
+
if (queueRef.current.length === 0) {
|
|
3277
|
+
setNotice("no queued turns to pause");
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3106
3280
|
queuePausedRef.current = true;
|
|
3107
3281
|
setQueuePaused(true);
|
|
3108
3282
|
setNotice("queue paused \u2014 the active turn continues");
|
|
@@ -3120,56 +3294,31 @@ function App({
|
|
|
3120
3294
|
setNotice("answer the leader first; queued input is not an answer");
|
|
3121
3295
|
return;
|
|
3122
3296
|
}
|
|
3297
|
+
if (queueRef.current.length === 0) {
|
|
3298
|
+
setNotice("no queued turns to resume");
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3123
3301
|
queuePausedRef.current = false;
|
|
3124
3302
|
setQueuePaused(false);
|
|
3125
3303
|
setNotice("queue resumed");
|
|
3126
3304
|
return;
|
|
3127
3305
|
}
|
|
3128
|
-
case "modify": {
|
|
3129
|
-
const instruction = args.join(" ");
|
|
3130
|
-
if (!instruction) {
|
|
3131
|
-
setNotice("usage: /modify <instruction> \u2014 only at a checkpoint");
|
|
3132
|
-
return;
|
|
3133
|
-
}
|
|
3134
|
-
if (!checkpointResolveRef.current) {
|
|
3135
|
-
setNotice("no checkpoint is waiting");
|
|
3136
|
-
return;
|
|
3137
|
-
}
|
|
3138
|
-
const resolve2 = checkpointResolveRef.current;
|
|
3139
|
-
checkpointResolveRef.current = null;
|
|
3140
|
-
checkpointLabelRef.current = "";
|
|
3141
|
-
resolve2({ action: "modify", instruction });
|
|
3142
|
-
setNotice("modifying and continuing\u2026");
|
|
3143
|
-
return;
|
|
3144
|
-
}
|
|
3145
|
-
case "agents":
|
|
3146
|
-
setNotice(
|
|
3147
|
-
`team: ${plannedAgents(config).map((agent) => agent.id).join(", ")}`
|
|
3148
|
-
);
|
|
3149
|
-
return;
|
|
3150
3306
|
case "focus": {
|
|
3151
3307
|
const input = args[0];
|
|
3152
3308
|
if (!input) {
|
|
3153
3309
|
setNotice("usage: /focus <agent id>");
|
|
3154
3310
|
return;
|
|
3155
3311
|
}
|
|
3156
|
-
const resolved = resolveAgentId(input, Object.keys(
|
|
3312
|
+
const resolved = resolveAgentId(input, Object.keys(state.agents));
|
|
3157
3313
|
if ("error" in resolved) {
|
|
3158
3314
|
setNotice(resolved.error);
|
|
3159
3315
|
return;
|
|
3160
3316
|
}
|
|
3317
|
+
setSearchResult(null);
|
|
3161
3318
|
dispatch({ type: "focus:set", agentId: resolved.id });
|
|
3162
3319
|
setNotice(`focused ${resolved.id}`);
|
|
3163
3320
|
return;
|
|
3164
3321
|
}
|
|
3165
|
-
case "clear":
|
|
3166
|
-
if (!shown2.focusId) {
|
|
3167
|
-
setNotice("nothing focused");
|
|
3168
|
-
return;
|
|
3169
|
-
}
|
|
3170
|
-
dispatch({ type: "stream:clear" });
|
|
3171
|
-
setNotice(`cleared ${shown2.focusId}`);
|
|
3172
|
-
return;
|
|
3173
3322
|
case "sessions": {
|
|
3174
3323
|
const list = listSessions(config.workDir);
|
|
3175
3324
|
setSessionList(list);
|
|
@@ -3184,38 +3333,45 @@ function App({
|
|
|
3184
3333
|
setNotice(state.verbose ? "verbose off" : "verbose on \u2014 showing tool detail");
|
|
3185
3334
|
return;
|
|
3186
3335
|
case "retry": {
|
|
3187
|
-
|
|
3336
|
+
const turn = recordedTurns(state).slice(-1)[0];
|
|
3337
|
+
if (!turn?.requirement) {
|
|
3188
3338
|
setNotice("nothing to retry yet");
|
|
3189
3339
|
return;
|
|
3190
3340
|
}
|
|
3341
|
+
if (args[0] === "all") args = [];
|
|
3191
3342
|
let ids;
|
|
3192
|
-
if (
|
|
3343
|
+
if (turn.acceptance && turn.acceptance.status !== "approved" && (!args[0] || args[0] === turn.acceptance.reviewer)) {
|
|
3193
3344
|
ids = [
|
|
3194
|
-
...new Set(
|
|
3195
|
-
].filter((id) => id !==
|
|
3345
|
+
...new Set(turn.stages.flatMap((stage) => stage.agents.map((agent) => agent.id)))
|
|
3346
|
+
].filter((id) => id !== turn.acceptance?.reviewer);
|
|
3196
3347
|
} else if (args[0]) {
|
|
3197
|
-
const resolved = resolveAgentId(args[0], Object.keys(
|
|
3348
|
+
const resolved = resolveAgentId(args[0], Object.keys(turn.agents));
|
|
3198
3349
|
if ("error" in resolved) {
|
|
3199
3350
|
setNotice(resolved.error);
|
|
3200
3351
|
return;
|
|
3201
3352
|
}
|
|
3353
|
+
if (!retryCandidates(state).includes(resolved.id)) {
|
|
3354
|
+
setNotice(`agent has not failed or does not need repair: ${resolved.id}`);
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3202
3357
|
ids = [resolved.id];
|
|
3203
3358
|
} else {
|
|
3204
|
-
ids = Object.values(
|
|
3359
|
+
ids = Object.values(turn.agents).filter((agent) => agent.status === "failed").map((agent) => agent.id);
|
|
3205
3360
|
}
|
|
3206
3361
|
if (ids.length === 0) {
|
|
3207
3362
|
setNotice("nothing failed");
|
|
3208
3363
|
return;
|
|
3209
3364
|
}
|
|
3210
3365
|
const sessions = Object.fromEntries(
|
|
3211
|
-
ids.filter((id) =>
|
|
3366
|
+
ids.filter((id) => turn.agents[id]?.session?.sessionId).map((id) => [id, turn.agents[id].session])
|
|
3212
3367
|
);
|
|
3213
|
-
if (ids.some((id) =>
|
|
3214
|
-
void startTurn(
|
|
3368
|
+
if (ids.some((id) => turn.leaderIds.includes(id))) {
|
|
3369
|
+
void startTurn(turn.requirement);
|
|
3215
3370
|
setNotice("retrying leader routing");
|
|
3216
3371
|
return;
|
|
3217
3372
|
}
|
|
3218
|
-
void startTurn(
|
|
3373
|
+
void startTurn(turn.effectiveRequirement ?? turn.requirement, {
|
|
3374
|
+
work: turn.work,
|
|
3219
3375
|
resume: {
|
|
3220
3376
|
agents: ids,
|
|
3221
3377
|
...Object.keys(sessions).length > 0 ? { sessions } : {}
|
|
@@ -3240,8 +3396,8 @@ function App({
|
|
|
3240
3396
|
setNotice(`replaying turn ${target.record.turn}`);
|
|
3241
3397
|
return;
|
|
3242
3398
|
}
|
|
3243
|
-
case "
|
|
3244
|
-
const target = pickTurn(state, args[0], "nothing to
|
|
3399
|
+
case "edit": {
|
|
3400
|
+
const target = pickTurn(state, args[0], "nothing to edit yet");
|
|
3245
3401
|
if ("error" in target) {
|
|
3246
3402
|
setNotice(target.error);
|
|
3247
3403
|
return;
|
|
@@ -3266,74 +3422,66 @@ function App({
|
|
|
3266
3422
|
setNotice("usage: /search <text>");
|
|
3267
3423
|
return;
|
|
3268
3424
|
}
|
|
3425
|
+
const matches2 = streamMatches(state, text, layout.streamWidth);
|
|
3426
|
+
if (matches2.length === 0) {
|
|
3427
|
+
setSearchResult(null);
|
|
3428
|
+
setNotice(`no matches for ${text}`);
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
const index = matches2.length - 1;
|
|
3432
|
+
setSearchResult({ query: text, index });
|
|
3269
3433
|
dispatch({
|
|
3270
3434
|
type: "stream:search",
|
|
3271
3435
|
query: text,
|
|
3436
|
+
match: index,
|
|
3272
3437
|
viewport: layout.logViewport,
|
|
3273
3438
|
width: layout.streamWidth
|
|
3274
3439
|
});
|
|
3275
|
-
setNotice(
|
|
3440
|
+
setNotice(
|
|
3441
|
+
"searched for " + text + " \xB7 match " + (index + 1) + "/" + matches2.length + " \xB7 ctrl+n next \xB7 ctrl+p previous"
|
|
3442
|
+
);
|
|
3276
3443
|
return;
|
|
3277
3444
|
}
|
|
3278
3445
|
case "latest":
|
|
3446
|
+
setSearchResult(null);
|
|
3279
3447
|
dispatch({ type: "stream:latest" });
|
|
3280
3448
|
setNotice("showing latest output");
|
|
3281
3449
|
return;
|
|
3282
|
-
case "cancel": {
|
|
3283
|
-
if (checkpointResolveRef.current) {
|
|
3284
|
-
const resolve2 = checkpointResolveRef.current;
|
|
3285
|
-
checkpointResolveRef.current = null;
|
|
3286
|
-
checkpointLabelRef.current = "";
|
|
3287
|
-
resolve2({ action: "cancel" });
|
|
3288
|
-
setNotice("checkpoint cancelled");
|
|
3289
|
-
return;
|
|
3290
|
-
}
|
|
3291
|
-
if (!abortRef.current) {
|
|
3292
|
-
setNotice("nothing to cancel");
|
|
3293
|
-
return;
|
|
3294
|
-
}
|
|
3295
|
-
const dropped = queue.length;
|
|
3296
|
-
abortRef.current.abort();
|
|
3297
|
-
clearInteractions();
|
|
3298
|
-
setQueue([]);
|
|
3299
|
-
setCancelled(true);
|
|
3300
|
-
setNotice(dropped > 0 ? `cancelling\u2026 \xB7 dropped ${dropped} queued` : "cancelling\u2026");
|
|
3301
|
-
return;
|
|
3302
|
-
}
|
|
3303
|
-
case "help":
|
|
3304
|
-
setNotice(void 0);
|
|
3305
|
-
setHelpOpen(true);
|
|
3306
|
-
return;
|
|
3307
|
-
case "quit":
|
|
3308
|
-
void shutdown();
|
|
3309
|
-
return;
|
|
3310
3450
|
default:
|
|
3311
3451
|
setNotice(`unknown command: /${name}`);
|
|
3312
3452
|
}
|
|
3313
3453
|
},
|
|
3314
|
-
[
|
|
3315
|
-
config,
|
|
3316
|
-
shutdown,
|
|
3317
|
-
clearInteractions,
|
|
3318
|
-
layout.logViewport,
|
|
3319
|
-
layout.streamWidth,
|
|
3320
|
-
queue.length,
|
|
3321
|
-
running,
|
|
3322
|
-
state,
|
|
3323
|
-
startTurn,
|
|
3324
|
-
steerRunningAgents
|
|
3325
|
-
]
|
|
3454
|
+
[config, layout.logViewport, layout.streamWidth, running, state, startTurn, steerRunningAgents]
|
|
3326
3455
|
);
|
|
3327
3456
|
const submit = useCallback(() => {
|
|
3328
3457
|
const text = line.text.trim();
|
|
3329
3458
|
if (!text || closingRef.current) return;
|
|
3459
|
+
if (operationInput) {
|
|
3460
|
+
const operation = operationInput;
|
|
3461
|
+
setOperationInput(null);
|
|
3462
|
+
if (operation === "steer" && !running) {
|
|
3463
|
+
setLine(EMPTY_LINE);
|
|
3464
|
+
setQueue((current) => [...current, text]);
|
|
3465
|
+
queuePausedRef.current = true;
|
|
3466
|
+
setQueuePaused(true);
|
|
3467
|
+
setNotice("turn finished \u2014 message queued; /continue runs it");
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
runCommand(operation, [text]);
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3330
3473
|
const command = parseCommand(text);
|
|
3331
3474
|
if (command) {
|
|
3332
|
-
runCommand(command.name, command.args
|
|
3475
|
+
runCommand(command.name, command.args);
|
|
3333
3476
|
return;
|
|
3334
3477
|
}
|
|
3335
3478
|
if (checkpointResolveRef.current) {
|
|
3336
|
-
|
|
3479
|
+
const resolve2 = checkpointResolveRef.current;
|
|
3480
|
+
checkpointResolveRef.current = null;
|
|
3481
|
+
checkpointLabelRef.current = "";
|
|
3482
|
+
setLine(EMPTY_LINE);
|
|
3483
|
+
resolve2({ action: "modify", instruction: text });
|
|
3484
|
+
setNotice("modifying and continuing\u2026");
|
|
3337
3485
|
return;
|
|
3338
3486
|
}
|
|
3339
3487
|
setLine(EMPTY_LINE);
|
|
@@ -3343,7 +3491,7 @@ function App({
|
|
|
3343
3491
|
return;
|
|
3344
3492
|
}
|
|
3345
3493
|
void startTurn(text);
|
|
3346
|
-
}, [line.text, runCommand, running, startTurn]);
|
|
3494
|
+
}, [line.text, operationInput, runCommand, running, startTurn]);
|
|
3347
3495
|
const insert = useCallback((chunk) => {
|
|
3348
3496
|
setLine((l) => ({
|
|
3349
3497
|
text: l.text.slice(0, l.cursor) + chunk + l.text.slice(l.cursor),
|
|
@@ -3373,7 +3521,12 @@ function App({
|
|
|
3373
3521
|
const spinner = SPINNER[tick % SPINNER.length];
|
|
3374
3522
|
const handleKey = (char, key) => {
|
|
3375
3523
|
if (key.ctrl && char === "c") {
|
|
3376
|
-
if (
|
|
3524
|
+
if (picker || operationInput) {
|
|
3525
|
+
setNotice(void 0);
|
|
3526
|
+
setPicker(null);
|
|
3527
|
+
setOperationInput(null);
|
|
3528
|
+
setLine(EMPTY_LINE);
|
|
3529
|
+
} else if (helpOpen) {
|
|
3377
3530
|
setHelpOpen(false);
|
|
3378
3531
|
} else if (sessionsOpen) {
|
|
3379
3532
|
setSessionsOpen(false);
|
|
@@ -3438,13 +3591,61 @@ function App({
|
|
|
3438
3591
|
}
|
|
3439
3592
|
return;
|
|
3440
3593
|
}
|
|
3594
|
+
if (picker) {
|
|
3595
|
+
if (key.escape) {
|
|
3596
|
+
setPicker(null);
|
|
3597
|
+
setLine(EMPTY_LINE);
|
|
3598
|
+
setNotice(void 0);
|
|
3599
|
+
return;
|
|
3600
|
+
}
|
|
3601
|
+
if (key.upArrow || key.downArrow || key.tab) {
|
|
3602
|
+
const delta = key.upArrow || key.tab && key.shift ? -1 : 1;
|
|
3603
|
+
setPicker({
|
|
3604
|
+
...picker,
|
|
3605
|
+
cursor: (picker.cursor + delta + picker.rows.length) % picker.rows.length
|
|
3606
|
+
});
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
if (key.return) {
|
|
3610
|
+
const row = picker.rows[picker.cursor];
|
|
3611
|
+
setPicker(null);
|
|
3612
|
+
if (row) runCommand(picker.command, [row.key]);
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
if (!(key.ctrl && char === "d")) return;
|
|
3616
|
+
}
|
|
3617
|
+
if (operationInput && key.escape) {
|
|
3618
|
+
setOperationInput(null);
|
|
3619
|
+
setLine(EMPTY_LINE);
|
|
3620
|
+
setNotice(void 0);
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
if (searchResult && key.ctrl && (char === "n" || char === "p")) {
|
|
3624
|
+
const matches2 = streamMatches(state, searchResult.query, layout.streamWidth);
|
|
3625
|
+
if (matches2.length === 0) {
|
|
3626
|
+
setSearchResult(null);
|
|
3627
|
+
setNotice(`no matches for ${searchResult.query}`);
|
|
3628
|
+
return;
|
|
3629
|
+
}
|
|
3630
|
+
const index = (searchResult.index + (char === "n" ? 1 : -1) + matches2.length) % matches2.length;
|
|
3631
|
+
setSearchResult({ ...searchResult, index });
|
|
3632
|
+
dispatch({
|
|
3633
|
+
type: "stream:search",
|
|
3634
|
+
query: searchResult.query,
|
|
3635
|
+
match: index,
|
|
3636
|
+
viewport: layout.logViewport,
|
|
3637
|
+
width: layout.streamWidth
|
|
3638
|
+
});
|
|
3639
|
+
setNotice(`match ${index + 1}/${matches2.length} \xB7 ctrl+n next \xB7 ctrl+p previous`);
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3441
3642
|
if (key.ctrl && char === "d") {
|
|
3442
3643
|
if (!line.text) void shutdown();
|
|
3443
3644
|
return;
|
|
3444
3645
|
}
|
|
3445
3646
|
if (key.ctrl && char === "l") {
|
|
3446
3647
|
dispatch({ type: "stream:clear" });
|
|
3447
|
-
setNotice(
|
|
3648
|
+
setNotice(state.focusId ? `cleared ${state.focusId}` : "nothing focused");
|
|
3448
3649
|
return;
|
|
3449
3650
|
}
|
|
3450
3651
|
if (key.ctrl && char === "a") {
|
|
@@ -3455,9 +3656,16 @@ function App({
|
|
|
3455
3656
|
setLine((l) => ({ ...l, cursor: l.text.length }));
|
|
3456
3657
|
return;
|
|
3457
3658
|
}
|
|
3458
|
-
const formFront = front?.kind === "question" && !
|
|
3659
|
+
const formFront = front?.kind === "question" && !operationInput ? front : void 0;
|
|
3459
3660
|
if (key.tab && formFront === void 0 && !paletteOpen) {
|
|
3460
|
-
|
|
3661
|
+
if (key.shift) {
|
|
3662
|
+
const next = permissionMode === "auto" ? "ask" : "auto";
|
|
3663
|
+
setPermissionMode(next);
|
|
3664
|
+
setNotice(`permission mode: ${next}`);
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
setSearchResult(null);
|
|
3668
|
+
dispatch({ type: "focus:cycle", delta: 1 });
|
|
3461
3669
|
return;
|
|
3462
3670
|
}
|
|
3463
3671
|
if (paletteOpen) {
|
|
@@ -3489,7 +3697,7 @@ function App({
|
|
|
3489
3697
|
return;
|
|
3490
3698
|
}
|
|
3491
3699
|
}
|
|
3492
|
-
if (front?.kind === "permission" && line.text === "" && !menu.released) {
|
|
3700
|
+
if (!operationInput && front?.kind === "permission" && line.text === "" && !menu.released) {
|
|
3493
3701
|
const total = permissionChoices(front.request).length;
|
|
3494
3702
|
if (key.upArrow || key.downArrow) {
|
|
3495
3703
|
setPermissionMenu({
|
|
@@ -3525,8 +3733,12 @@ function App({
|
|
|
3525
3733
|
if (draft !== void 0) setLine({ text: draft, cursor: draft.length });
|
|
3526
3734
|
};
|
|
3527
3735
|
const moveFocus = (index, state2 = committed) => {
|
|
3528
|
-
const
|
|
3529
|
-
save(
|
|
3736
|
+
const target = (index + total + 3) % (total + 3);
|
|
3737
|
+
if (target >= total) save({ ...state2, field: target }, "");
|
|
3738
|
+
else {
|
|
3739
|
+
const next = focusField(questionFields, state2, target);
|
|
3740
|
+
save(next.state, next.draft);
|
|
3741
|
+
}
|
|
3530
3742
|
};
|
|
3531
3743
|
const submit2 = (state2) => {
|
|
3532
3744
|
if (!formComplete(questionFields, state2)) {
|
|
@@ -3548,15 +3760,29 @@ function App({
|
|
|
3548
3760
|
setNotice(`answered ${front2.shortId}`);
|
|
3549
3761
|
};
|
|
3550
3762
|
const advance = (state2) => {
|
|
3551
|
-
|
|
3552
|
-
else moveFocus(form.field + 1, state2);
|
|
3763
|
+
moveFocus(form.field + 1, state2);
|
|
3553
3764
|
};
|
|
3554
3765
|
if (field === void 0) {
|
|
3555
|
-
|
|
3766
|
+
const action = Math.max(0, form.field - total);
|
|
3767
|
+
if (key.tab || key.leftArrow || key.rightArrow || key.upArrow || key.downArrow) {
|
|
3768
|
+
const delta = key.leftArrow || key.upArrow || key.tab && key.shift ? -1 : 1;
|
|
3769
|
+
moveFocus(form.field + delta);
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
if (key.return) {
|
|
3773
|
+
if (action === 0) submit2(committed);
|
|
3774
|
+
else {
|
|
3775
|
+
front2.settle({ action: action === 1 ? "decline" : "cancel" });
|
|
3776
|
+
setFormEntry({ id: "", state: initialForm() });
|
|
3777
|
+
setLine(EMPTY_LINE);
|
|
3778
|
+
setNotice(action === 1 ? "question skipped" : "question cancelled");
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3556
3781
|
return;
|
|
3557
3782
|
}
|
|
3558
3783
|
if (key.tab) {
|
|
3559
|
-
|
|
3784
|
+
const next = field.kind === "choice" ? commitField(questionFields, committed) : committed;
|
|
3785
|
+
moveFocus(key.shift ? form.field - 1 : form.field + 1, next);
|
|
3560
3786
|
return;
|
|
3561
3787
|
}
|
|
3562
3788
|
if (field.choices.length > 0 && (key.upArrow || key.downArrow)) {
|
|
@@ -3660,12 +3886,8 @@ function App({
|
|
|
3660
3886
|
setError(void 0);
|
|
3661
3887
|
return;
|
|
3662
3888
|
}
|
|
3663
|
-
if (state.view) {
|
|
3664
|
-
dispatch({ type: "view:close" });
|
|
3665
|
-
return;
|
|
3666
|
-
}
|
|
3667
3889
|
if (front !== void 0) {
|
|
3668
|
-
setPermissionMenu({ id: front.shortId, cursor:
|
|
3890
|
+
setPermissionMenu({ id: front.shortId, cursor: 0, released: !menu.released });
|
|
3669
3891
|
}
|
|
3670
3892
|
setLine(EMPTY_LINE);
|
|
3671
3893
|
setNotice(void 0);
|
|
@@ -3684,17 +3906,42 @@ function App({
|
|
|
3684
3906
|
const handlerRef = useRef(handleKey);
|
|
3685
3907
|
handlerRef.current = handleKey;
|
|
3686
3908
|
useInput((char, key) => handlerRef.current(char, key));
|
|
3687
|
-
const keys = 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
|
|
3688
|
-
//
|
|
3689
|
-
"
|
|
3690
|
-
) : interactions[0].kind === "question" ? "tab field \xB7 \u2191\u2193 pick \xB7 enter
|
|
3691
|
-
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 /
|
|
3909
|
+
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 ? (
|
|
3910
|
+
// On short terminals the decision is shown in the body.
|
|
3911
|
+
"\u2191\u2193 choose \xB7 enter confirm \xB7 esc type \xB7 ctrl+c cancel"
|
|
3912
|
+
) : 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";
|
|
3913
|
+
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;
|
|
3914
|
+
const renderInteraction = (maxRows) => {
|
|
3915
|
+
if (!front) return null;
|
|
3916
|
+
return front.kind === "permission" ? /* @__PURE__ */ jsx6(
|
|
3917
|
+
PermissionBar,
|
|
3918
|
+
{
|
|
3919
|
+
request: front.request,
|
|
3920
|
+
others: interactions.slice(1).flatMap((entry) => entry.kind === "permission" ? [entry.request] : []),
|
|
3921
|
+
cursor: menu.cursor,
|
|
3922
|
+
released: menu.released,
|
|
3923
|
+
width: layout.columns,
|
|
3924
|
+
maxRows
|
|
3925
|
+
}
|
|
3926
|
+
) : /* @__PURE__ */ jsx6(
|
|
3927
|
+
QuestionForm,
|
|
3928
|
+
{
|
|
3929
|
+
request: front.request,
|
|
3930
|
+
others: interactions.slice(1).flatMap((entry) => entry.kind === "question" ? [entry.request] : []),
|
|
3931
|
+
fields: questionFields,
|
|
3932
|
+
form,
|
|
3933
|
+
draft: line.text,
|
|
3934
|
+
width: layout.columns,
|
|
3935
|
+
maxRows
|
|
3936
|
+
}
|
|
3937
|
+
);
|
|
3938
|
+
};
|
|
3692
3939
|
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: layout.rows, children: [
|
|
3693
3940
|
/* @__PURE__ */ jsx6(
|
|
3694
3941
|
Header,
|
|
3695
3942
|
{
|
|
3696
3943
|
name: state.name || config.name,
|
|
3697
|
-
right:
|
|
3944
|
+
right: `turn ${state.turn} \xB7 ${running ? formatDuration(turnElapsed(state, now) ?? 0) : state.phase === "complete" ? "complete" : "ready"} \xB7 ${permissionMode}`
|
|
3698
3945
|
}
|
|
3699
3946
|
),
|
|
3700
3947
|
/* @__PURE__ */ jsx6(Rule, { width: layout.columns }),
|
|
@@ -3703,9 +3950,9 @@ function App({
|
|
|
3703
3950
|
// The rail wraps, so it can be taller than one row. Cap it at the height
|
|
3704
3951
|
// the budget reserved and clip, rather than letting the extra rows push
|
|
3705
3952
|
// the chrome below it off the screen.
|
|
3706
|
-
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx6(PipelineRail, { turn:
|
|
3953
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx6(PipelineRail, { turn: state, spinner }) })
|
|
3707
3954
|
) : null,
|
|
3708
|
-
|
|
3955
|
+
state.reason || state.routing ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: (state.routing ? " route: " + state.routing.mode + " \xB7 " + state.routing.workId + (state.routing.reroutes ? " \xB7 rerouted" : "") : "") + (state.reason ? " reason: " + state.reason : "") }) : null,
|
|
3709
3956
|
/* @__PURE__ */ jsx6(
|
|
3710
3957
|
Box6,
|
|
3711
3958
|
{
|
|
@@ -3724,20 +3971,20 @@ function App({
|
|
|
3724
3971
|
maxRows: layout.bodyHeight,
|
|
3725
3972
|
now
|
|
3726
3973
|
}
|
|
3727
|
-
) : layout.welcome ? /* @__PURE__ */ jsx6(WelcomePanel, { turn: state, width: layout.columns }) : error ? (
|
|
3974
|
+
) : front && layout.interactionRows === 0 && !paletteOpen ? renderInteraction(layout.bodyHeight) : layout.welcome ? /* @__PURE__ */ jsx6(WelcomePanel, { turn: state, width: layout.columns }) : error ? (
|
|
3728
3975
|
// The error wins over a reply: a run that stopped with an error is the
|
|
3729
3976
|
// most urgent thing on screen, and a stale answer underneath it would
|
|
3730
3977
|
// read as the outcome. The status line keeps a one-line version too.
|
|
3731
3978
|
/* @__PURE__ */ jsx6(ErrorPanel, { message: error, width: layout.columns })
|
|
3732
|
-
) :
|
|
3979
|
+
) : state.reply ? /* @__PURE__ */ jsx6(ReplyPanel, { reply: state.reply, width: layout.columns }) : /* @__PURE__ */ jsxs6(Box6, { height: layout.bodyHeight, flexShrink: 0, flexDirection: "row", children: [
|
|
3733
3980
|
layout.showTeam ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
3734
|
-
/* @__PURE__ */ jsx6(TeamPanel, { turn:
|
|
3981
|
+
/* @__PURE__ */ jsx6(TeamPanel, { turn: state, width: layout.teamWidth, now, spinner }),
|
|
3735
3982
|
/* @__PURE__ */ jsx6(Divider, {})
|
|
3736
3983
|
] }) : null,
|
|
3737
3984
|
/* @__PURE__ */ jsx6(
|
|
3738
3985
|
StreamPanel,
|
|
3739
3986
|
{
|
|
3740
|
-
|
|
3987
|
+
state,
|
|
3741
3988
|
height: layout.bodyHeight,
|
|
3742
3989
|
width: layout.streamWidth,
|
|
3743
3990
|
now,
|
|
@@ -3747,7 +3994,6 @@ function App({
|
|
|
3747
3994
|
] })
|
|
3748
3995
|
}
|
|
3749
3996
|
),
|
|
3750
|
-
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,
|
|
3751
3997
|
notice || queuePaused && queue.length > 0 ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate-end", children: [
|
|
3752
3998
|
" ",
|
|
3753
3999
|
notice ?? (awaitingAnswer ? "queue paused \u2014 answer the leader first" : "queue paused \u2014 /continue resumes it")
|
|
@@ -3762,26 +4008,7 @@ function App({
|
|
|
3762
4008
|
overflow: "hidden",
|
|
3763
4009
|
flexShrink: 0,
|
|
3764
4010
|
flexDirection: "column",
|
|
3765
|
-
children:
|
|
3766
|
-
PermissionBar,
|
|
3767
|
-
{
|
|
3768
|
-
request: front.request,
|
|
3769
|
-
others: interactions.slice(1).flatMap((entry) => entry.kind === "permission" ? [entry.request] : []),
|
|
3770
|
-
cursor: menu.cursor,
|
|
3771
|
-
released: menu.released,
|
|
3772
|
-
width: layout.columns
|
|
3773
|
-
}
|
|
3774
|
-
) : /* @__PURE__ */ jsx6(
|
|
3775
|
-
QuestionForm,
|
|
3776
|
-
{
|
|
3777
|
-
request: front.request,
|
|
3778
|
-
others: interactions.slice(1).flatMap((entry) => entry.kind === "question" ? [entry.request] : []),
|
|
3779
|
-
fields: questionFields,
|
|
3780
|
-
form,
|
|
3781
|
-
draft: line.text,
|
|
3782
|
-
width: layout.columns
|
|
3783
|
-
}
|
|
3784
|
-
)
|
|
4011
|
+
children: renderInteraction(layout.interactionRows)
|
|
3785
4012
|
}
|
|
3786
4013
|
)
|
|
3787
4014
|
] }),
|
|
@@ -3872,6 +4099,7 @@ export {
|
|
|
3872
4099
|
conversationTurns,
|
|
3873
4100
|
createInitialState,
|
|
3874
4101
|
initialState,
|
|
4102
|
+
latestWorkFromTurns,
|
|
3875
4103
|
leaderAgentIds,
|
|
3876
4104
|
measureRailRows,
|
|
3877
4105
|
plannedAgents,
|
|
@@ -3881,8 +4109,9 @@ export {
|
|
|
3881
4109
|
seedAgents,
|
|
3882
4110
|
stageStatus,
|
|
3883
4111
|
startTui,
|
|
4112
|
+
streamMatches,
|
|
3884
4113
|
stripAnsi,
|
|
3885
4114
|
tuiReducer,
|
|
3886
4115
|
turnElapsed,
|
|
3887
|
-
|
|
4116
|
+
visibleTimeline
|
|
3888
4117
|
};
|