@duetso/agent 0.1.59 → 0.1.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +1 -1
- package/dist/src/memory/observational.d.ts.map +1 -1
- package/dist/src/memory/observational.js +24 -10
- package/dist/src/memory/observational.js.map +1 -1
- package/dist/src/session/session.d.ts +13 -1
- package/dist/src/session/session.d.ts.map +1 -1
- package/dist/src/session/session.js +2 -0
- package/dist/src/session/session.js.map +1 -1
- package/dist/src/tui/app.d.ts +1 -1
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +225 -37
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/autocomplete.d.ts +49 -6
- package/dist/src/tui/autocomplete.d.ts.map +1 -1
- package/dist/src/tui/autocomplete.js +94 -14
- package/dist/src/tui/autocomplete.js.map +1 -1
- package/dist/src/turn-runner/tools.d.ts +1 -0
- package/dist/src/turn-runner/tools.d.ts.map +1 -1
- package/dist/src/turn-runner/tools.js +25 -1
- package/dist/src/turn-runner/tools.js.map +1 -1
- package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
- package/dist/src/turn-runner/turn-runner.js +12 -6
- package/dist/src/turn-runner/turn-runner.js.map +1 -1
- package/dist/src/types/memory.d.ts +9 -0
- package/dist/src/types/memory.d.ts.map +1 -1
- package/dist/src/types/protocol.d.ts +18 -1
- package/dist/src/types/protocol.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/src/tui/app.js
CHANGED
|
@@ -2,7 +2,7 @@ import { BoxRenderable, createCliRenderer, decodePasteBytes, fg, ScrollBoxRender
|
|
|
2
2
|
import { writeClipboardText } from "./clipboard.js";
|
|
3
3
|
import { describeMacClipboardTypes, loadImageFromPath, looksLikeImageFilePath, persistPastedImage, sniffImageMimeType, tryReadClipboardImage, tryReadClipboardText, } from "./paste.js";
|
|
4
4
|
import { parseCopyArgument, selectCopyText } from "./transcript-log.js";
|
|
5
|
-
import { activeFileAutocompleteToken, activeSkillAutocompleteToken, AUTOCOMPLETE_LIMITS, BUILT_IN_SLASH_COMMANDS, fileAutocompleteMatches, formatQuestionOptionDescription, formatSkillAutocompleteDescription,
|
|
5
|
+
import { activeFileAutocompleteToken, activeSkillAutocompleteToken, AUTOCOMPLETE_LIMITS, BUILT_IN_SLASH_COMMANDS, commitActiveAnswer, fileAutocompleteMatches, formatQuestionOptionDescription, formatSkillAutocompleteDescription, moveQuestionHighlight, moveSkillAutocompleteSelection, NO_HIGHLIGHT, restoreSavedAnswer, skillAutocompleteMatches, } from "./autocomplete.js";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { buildFileIndex } from "./file-index.js";
|
|
8
8
|
import { DUET_BANNER_LINES_COMPACT, historyDisplayBlocks, limitHistoryDisplayMessages, } from "./history.js";
|
|
@@ -12,7 +12,7 @@ import { orderedSelectableStarters, selectStarters } from "./starters.js";
|
|
|
12
12
|
import { COLORS, HINT_IDLE, HINT_RUNNING, HINT_SELECTION_COPY } from "./theme.js";
|
|
13
13
|
// Re-exports preserve the historical `tui/app.js` entry point used by tests
|
|
14
14
|
// and external callers; the implementations live in focused leaf modules.
|
|
15
|
-
export { activeFileAutocompleteToken, activeSkillAutocompleteToken, fileAutocompleteMatches, formatQuestionOptionDescription, formatSkillAutocompleteDescription,
|
|
15
|
+
export { activeFileAutocompleteToken, activeSkillAutocompleteToken, commitActiveAnswer, fileAutocompleteMatches, formatQuestionOptionDescription, formatSkillAutocompleteDescription, moveQuestionHighlight, moveSkillAutocompleteSelection, NO_HIGHLIGHT, questionPickerAnswer, replaceFileAutocompleteToken, replaceSkillAutocompleteToken, restoreSavedAnswer, skillAutocompleteMatches, } from "./autocomplete.js";
|
|
16
16
|
export { formatSkillAutocompleteItem } from "./autocomplete.js";
|
|
17
17
|
export { DUET_BANNER_LINES, historyDisplayBlocks, limitHistoryDisplayMessages, startupHeaderLines, } from "./history.js";
|
|
18
18
|
import { assembleToolBlock, formatToolBlock, truncateToolText } from "./tool-formatters.js";
|
|
@@ -872,11 +872,16 @@ export async function runTui(input) {
|
|
|
872
872
|
}
|
|
873
873
|
}
|
|
874
874
|
function formatMemoryEventBody(event) {
|
|
875
|
-
|
|
875
|
+
const hasObservations = Boolean(event.observations && event.observations.length > 0);
|
|
876
|
+
const hasBumps = Boolean(event.usageBumpedObservations && event.usageBumpedObservations.length > 0);
|
|
877
|
+
if (!hasObservations && !hasBumps) {
|
|
876
878
|
return "";
|
|
877
879
|
}
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
+
const sections = [event.message];
|
|
881
|
+
if (hasObservations) {
|
|
882
|
+
sections.push(event.observations.map((observation) => observation.content).join("\n\n"));
|
|
883
|
+
}
|
|
884
|
+
return truncateToolText(sections.join("\n"));
|
|
880
885
|
}
|
|
881
886
|
// ---- input handling --------------------------------------------------------
|
|
882
887
|
let skillAutocompleteSkills = [];
|
|
@@ -892,7 +897,20 @@ export async function runTui(input) {
|
|
|
892
897
|
let fileAutocompleteItems = [];
|
|
893
898
|
let fileAutocompleteSelectedIndex = 0;
|
|
894
899
|
let pendingQuestions = [];
|
|
895
|
-
let
|
|
900
|
+
let questionActiveIndex = 0;
|
|
901
|
+
// `NO_HIGHLIGHT` (-1) means no row is highlighted yet — the user must press
|
|
902
|
+
// Up/Down to land on a concrete row. Single-select live-records the
|
|
903
|
+
// highlight as the answer; multi-select uses highlight purely for
|
|
904
|
+
// navigation and toggles the checked set on Space/Enter.
|
|
905
|
+
let questionOptionSelectedIndex = NO_HIGHLIGHT;
|
|
906
|
+
// Per-question checked indices for the active multi-select question. Reset
|
|
907
|
+
// when the picker advances to the next question; single-select questions
|
|
908
|
+
// simply ignore this set and use `questionOptionSelectedIndex` instead.
|
|
909
|
+
let questionMultiSelectChecked = new Set();
|
|
910
|
+
// Answers collected while walking the picker, keyed by question text. We
|
|
911
|
+
// dispatch the full map once the user finishes the last question, or flush
|
|
912
|
+
// it early when they decide to type a free-form prompt instead.
|
|
913
|
+
let questionAccumulatedAnswers = {};
|
|
896
914
|
let suppressNextEscapeExit = false;
|
|
897
915
|
function skillAutocompleteIsOpen() {
|
|
898
916
|
return Boolean(skillAutocompleteToken && skillAutocompleteItems.length > 0);
|
|
@@ -901,9 +919,12 @@ export async function runTui(input) {
|
|
|
901
919
|
return Boolean(fileAutocompleteToken && fileAutocompleteItems.length > 0);
|
|
902
920
|
}
|
|
903
921
|
function questionPickerIsOpen() {
|
|
904
|
-
const question = pendingQuestions[
|
|
922
|
+
const question = pendingQuestions[questionActiveIndex];
|
|
905
923
|
return Boolean(question && question.options.length > 0);
|
|
906
924
|
}
|
|
925
|
+
function activeQuestion() {
|
|
926
|
+
return pendingQuestions[questionActiveIndex];
|
|
927
|
+
}
|
|
907
928
|
function hideSkillAutocomplete() {
|
|
908
929
|
skillAutocompleteToken = undefined;
|
|
909
930
|
skillAutocompleteItems = [];
|
|
@@ -928,7 +949,10 @@ export async function runTui(input) {
|
|
|
928
949
|
}
|
|
929
950
|
function hideQuestions() {
|
|
930
951
|
pendingQuestions = [];
|
|
931
|
-
|
|
952
|
+
questionActiveIndex = 0;
|
|
953
|
+
questionOptionSelectedIndex = NO_HIGHLIGHT;
|
|
954
|
+
questionMultiSelectChecked = new Set();
|
|
955
|
+
questionAccumulatedAnswers = {};
|
|
932
956
|
questionPanel.visible = false;
|
|
933
957
|
for (const row of questionRows) {
|
|
934
958
|
row.visible = false;
|
|
@@ -937,46 +961,176 @@ export async function runTui(input) {
|
|
|
937
961
|
}
|
|
938
962
|
function showQuestions(questions) {
|
|
939
963
|
pendingQuestions = questions;
|
|
940
|
-
|
|
964
|
+
questionActiveIndex = 0;
|
|
965
|
+
questionOptionSelectedIndex = NO_HIGHLIGHT;
|
|
966
|
+
questionMultiSelectChecked = new Set();
|
|
967
|
+
questionAccumulatedAnswers = {};
|
|
941
968
|
renderQuestions();
|
|
942
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* Total navigable rows for the active question. The Up/Down handler clamps
|
|
972
|
+
* navigation to what is actually rendered on screen so a user cannot land
|
|
973
|
+
* the highlight on a row they cannot see.
|
|
974
|
+
*/
|
|
975
|
+
function activeRowCount() {
|
|
976
|
+
const question = activeQuestion();
|
|
977
|
+
if (!question)
|
|
978
|
+
return 0;
|
|
979
|
+
const optionLimit = question.multiSelect ? QUESTION_OPTION_LIMIT - 1 : QUESTION_OPTION_LIMIT;
|
|
980
|
+
const visibleOptionCount = Math.min(question.options.length, optionLimit);
|
|
981
|
+
return visibleOptionCount + (question.multiSelect ? 1 : 0);
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Row index of the synthetic Done row when the active question is
|
|
985
|
+
* multi-select; `undefined` otherwise so callers don't compare against a
|
|
986
|
+
* sentinel value. The Done row sits one past the last visible option,
|
|
987
|
+
* clamped to the same limit `renderQuestions` uses.
|
|
988
|
+
*/
|
|
989
|
+
function activeQuestionDoneIndex() {
|
|
990
|
+
const question = activeQuestion();
|
|
991
|
+
if (!question?.multiSelect)
|
|
992
|
+
return undefined;
|
|
993
|
+
const optionLimit = QUESTION_OPTION_LIMIT - 1;
|
|
994
|
+
return Math.min(question.options.length, optionLimit);
|
|
995
|
+
}
|
|
943
996
|
function renderQuestions() {
|
|
944
|
-
const question =
|
|
997
|
+
const question = activeQuestion();
|
|
945
998
|
if (!question || question.options.length === 0) {
|
|
946
999
|
hideQuestions();
|
|
947
1000
|
return;
|
|
948
1001
|
}
|
|
949
1002
|
questionPanel.visible = true;
|
|
950
|
-
|
|
1003
|
+
const baseTitle = question.header
|
|
951
1004
|
? `${question.header}: ${question.question}`
|
|
952
1005
|
: question.question;
|
|
953
|
-
const
|
|
1006
|
+
const positionPrefix = pendingQuestions.length > 1 ? `(${questionActiveIndex + 1}/${pendingQuestions.length}) ` : "";
|
|
1007
|
+
const navHint = pendingQuestions.length > 1 ? " [←/→ navigate]" : "";
|
|
1008
|
+
questionTitle.content = `${positionPrefix}${baseTitle}${navHint}`;
|
|
1009
|
+
const optionLimit = question.multiSelect ? QUESTION_OPTION_LIMIT - 1 : QUESTION_OPTION_LIMIT;
|
|
1010
|
+
const visibleOptions = question.options.slice(0, optionLimit);
|
|
1011
|
+
const doneIndex = activeQuestionDoneIndex();
|
|
954
1012
|
for (const [index, row] of questionRows.entries()) {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1013
|
+
if (index < visibleOptions.length) {
|
|
1014
|
+
const option = visibleOptions[index];
|
|
1015
|
+
const highlighted = index === questionOptionSelectedIndex;
|
|
1016
|
+
const checkbox = question.multiSelect
|
|
1017
|
+
? questionMultiSelectChecked.has(index)
|
|
1018
|
+
? "[x] "
|
|
1019
|
+
: "[ ] "
|
|
1020
|
+
: "";
|
|
1021
|
+
const labelColor = highlighted ? COLORS.status : COLORS.user;
|
|
1022
|
+
const description = formatQuestionOptionDescription(option.description);
|
|
1023
|
+
const labelLine = `${checkbox}${option.label}`;
|
|
1024
|
+
row.content = description
|
|
1025
|
+
? t `${fg(labelColor)(labelLine)}\n${description}`
|
|
1026
|
+
: t `${fg(labelColor)(labelLine)}`;
|
|
1027
|
+
row.fg = highlighted ? COLORS.agent : COLORS.hint;
|
|
1028
|
+
row.visible = true;
|
|
959
1029
|
continue;
|
|
960
1030
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
?
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1031
|
+
if (question.multiSelect && index === visibleOptions.length) {
|
|
1032
|
+
// Synthetic Done row carries no checkbox prefix and self-documents
|
|
1033
|
+
// its purpose so users discover how to advance from a multi-select.
|
|
1034
|
+
const highlighted = doneIndex === questionOptionSelectedIndex;
|
|
1035
|
+
const labelColor = highlighted ? COLORS.status : COLORS.user;
|
|
1036
|
+
const description = formatQuestionOptionDescription("Advance to next question");
|
|
1037
|
+
row.content = t `${fg(labelColor)("Done")}\n${description}`;
|
|
1038
|
+
row.fg = highlighted ? COLORS.agent : COLORS.hint;
|
|
1039
|
+
row.visible = true;
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
row.visible = false;
|
|
1043
|
+
row.content = "";
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function moveActiveQuestionHighlight(direction) {
|
|
1047
|
+
const question = activeQuestion();
|
|
1048
|
+
if (!question)
|
|
1049
|
+
return;
|
|
1050
|
+
questionOptionSelectedIndex = moveQuestionHighlight(questionOptionSelectedIndex, activeRowCount(), direction);
|
|
1051
|
+
// Single-select live-records the highlight as the answer so a
|
|
1052
|
+
// prompt-flush or arrow-nav captures it without requiring Space/Enter.
|
|
1053
|
+
// Multi-select keeps highlight separate from the toggled set.
|
|
1054
|
+
if (!question.multiSelect) {
|
|
1055
|
+
questionAccumulatedAnswers = commitActiveAnswer(question, questionOptionSelectedIndex, questionMultiSelectChecked, questionAccumulatedAnswers);
|
|
1056
|
+
}
|
|
1057
|
+
renderQuestions();
|
|
1058
|
+
}
|
|
1059
|
+
function toggleActiveMultiSelectOption() {
|
|
1060
|
+
const question = activeQuestion();
|
|
1061
|
+
if (!question?.multiSelect)
|
|
1062
|
+
return;
|
|
1063
|
+
if (questionMultiSelectChecked.has(questionOptionSelectedIndex)) {
|
|
1064
|
+
questionMultiSelectChecked.delete(questionOptionSelectedIndex);
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
questionMultiSelectChecked.add(questionOptionSelectedIndex);
|
|
969
1068
|
}
|
|
1069
|
+
questionAccumulatedAnswers = commitActiveAnswer(question, questionOptionSelectedIndex, questionMultiSelectChecked, questionAccumulatedAnswers);
|
|
1070
|
+
renderQuestions();
|
|
1071
|
+
}
|
|
1072
|
+
function navigateActiveQuestion(direction) {
|
|
1073
|
+
if (pendingQuestions.length <= 1)
|
|
1074
|
+
return false;
|
|
1075
|
+
const nextIndex = questionActiveIndex + direction;
|
|
1076
|
+
if (nextIndex < 0 || nextIndex >= pendingQuestions.length)
|
|
1077
|
+
return false;
|
|
1078
|
+
questionAccumulatedAnswers = commitActiveAnswer(activeQuestion(), questionOptionSelectedIndex, questionMultiSelectChecked, questionAccumulatedAnswers);
|
|
1079
|
+
questionActiveIndex = nextIndex;
|
|
1080
|
+
const restored = restoreSavedAnswer(activeQuestion(), questionAccumulatedAnswers);
|
|
1081
|
+
questionOptionSelectedIndex = restored.selectedIndex;
|
|
1082
|
+
questionMultiSelectChecked = restored.checked;
|
|
1083
|
+
renderQuestions();
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
function describeAnswerLabels(question, labels) {
|
|
1087
|
+
if (labels.length === 0)
|
|
1088
|
+
return question.multiSelect ? "(no selection)" : "";
|
|
1089
|
+
return labels.join(", ");
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Handle Space/Enter when the picker is open and the composer is empty.
|
|
1093
|
+
* Multi-select on a regular row toggles; multi-select on the Done row
|
|
1094
|
+
* advances. Single-select always advances (highlight = answer is already
|
|
1095
|
+
* live-recorded by Up/Down). No-op when nothing is highlighted yet so the
|
|
1096
|
+
* user is forced to make an explicit choice (or skip via Right-arrow).
|
|
1097
|
+
*/
|
|
1098
|
+
function confirmActiveSelection() {
|
|
1099
|
+
const question = activeQuestion();
|
|
1100
|
+
if (!question)
|
|
1101
|
+
return false;
|
|
1102
|
+
if (questionOptionSelectedIndex === NO_HIGHLIGHT)
|
|
1103
|
+
return false;
|
|
1104
|
+
if (question.multiSelect && questionOptionSelectedIndex !== activeQuestionDoneIndex()) {
|
|
1105
|
+
toggleActiveMultiSelectOption();
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
return advanceOrSubmit();
|
|
970
1109
|
}
|
|
971
|
-
function
|
|
972
|
-
const
|
|
973
|
-
if (!
|
|
1110
|
+
function advanceOrSubmit() {
|
|
1111
|
+
const question = activeQuestion();
|
|
1112
|
+
if (!question)
|
|
974
1113
|
return false;
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
1114
|
+
const accumulatedForActive = questionAccumulatedAnswers[question.question] ?? [];
|
|
1115
|
+
const transcriptText = describeAnswerLabels(question, accumulatedForActive);
|
|
1116
|
+
if (transcriptText) {
|
|
1117
|
+
recordTranscriptEntry("user", transcriptText);
|
|
1118
|
+
appendBlock("you:", transcriptText, COLORS.user);
|
|
1119
|
+
}
|
|
1120
|
+
if (questionActiveIndex < pendingQuestions.length - 1) {
|
|
1121
|
+
questionActiveIndex += 1;
|
|
1122
|
+
const restored = restoreSavedAnswer(activeQuestion(), questionAccumulatedAnswers);
|
|
1123
|
+
questionOptionSelectedIndex = restored.selectedIndex;
|
|
1124
|
+
questionMultiSelectChecked = restored.checked;
|
|
1125
|
+
renderQuestions();
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
978
1128
|
void input.session
|
|
979
|
-
.answer({
|
|
1129
|
+
.answer({
|
|
1130
|
+
questions: pendingQuestions,
|
|
1131
|
+
answers: questionAccumulatedAnswers,
|
|
1132
|
+
behavior: "follow_up",
|
|
1133
|
+
})
|
|
980
1134
|
.catch(reportError);
|
|
981
1135
|
hideQuestions();
|
|
982
1136
|
markRunning();
|
|
@@ -1314,17 +1468,33 @@ export async function runTui(input) {
|
|
|
1314
1468
|
}
|
|
1315
1469
|
if (questionPickerIsOpen()) {
|
|
1316
1470
|
if (key.name === "up") {
|
|
1317
|
-
|
|
1318
|
-
renderQuestions();
|
|
1471
|
+
moveActiveQuestionHighlight(-1);
|
|
1319
1472
|
key.preventDefault();
|
|
1320
1473
|
return;
|
|
1321
1474
|
}
|
|
1322
1475
|
if (key.name === "down") {
|
|
1323
|
-
|
|
1324
|
-
|
|
1476
|
+
moveActiveQuestionHighlight(1);
|
|
1477
|
+
key.preventDefault();
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
// Space confirms the active selection only when the composer is empty
|
|
1481
|
+
// so users can still type a free-form prompt that includes spaces.
|
|
1482
|
+
if (key.name === "space" && inputField.plainText.length === 0) {
|
|
1325
1483
|
key.preventDefault();
|
|
1484
|
+
confirmActiveSelection();
|
|
1326
1485
|
return;
|
|
1327
1486
|
}
|
|
1487
|
+
// Left/Right navigate between questions, but only when the composer is
|
|
1488
|
+
// empty so editing a typed prompt with arrow keys still works.
|
|
1489
|
+
if ((key.name === "left" || key.name === "right") &&
|
|
1490
|
+
inputField.plainText.length === 0 &&
|
|
1491
|
+
pendingQuestions.length > 1) {
|
|
1492
|
+
const direction = key.name === "left" ? -1 : 1;
|
|
1493
|
+
if (navigateActiveQuestion(direction)) {
|
|
1494
|
+
key.preventDefault();
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1328
1498
|
if (key.name === "escape") {
|
|
1329
1499
|
key.preventDefault();
|
|
1330
1500
|
suppressNextEscapeExit = true;
|
|
@@ -1348,7 +1518,7 @@ export async function runTui(input) {
|
|
|
1348
1518
|
submit(value);
|
|
1349
1519
|
}
|
|
1350
1520
|
else if (questionPickerIsOpen()) {
|
|
1351
|
-
|
|
1521
|
+
confirmActiveSelection();
|
|
1352
1522
|
}
|
|
1353
1523
|
else if (startersAreVisible()) {
|
|
1354
1524
|
submitHighlightedStarter();
|
|
@@ -1631,11 +1801,29 @@ export async function runTui(input) {
|
|
|
1631
1801
|
const lines = submittedImages.map((p) => `📎 ${p.label}: ${p.path}`).join("\n");
|
|
1632
1802
|
appendBlock(null, lines, COLORS.hint);
|
|
1633
1803
|
}
|
|
1634
|
-
hideQuestions();
|
|
1635
1804
|
const images = submittedImages.map((p) => p.attachment);
|
|
1636
1805
|
// Reset before dispatch so an in-flight error does not leave the user
|
|
1637
1806
|
// double-charged with the same attachments on retry.
|
|
1638
1807
|
clearPendingImages();
|
|
1808
|
+
// If the question picker is open, treat the typed message as a flush:
|
|
1809
|
+
// dispatch whatever answers were already collected together with the new
|
|
1810
|
+
// prompt text so the model sees one combined turn instead of dropping
|
|
1811
|
+
// the partial answers on the floor.
|
|
1812
|
+
if (pendingQuestions.length > 0) {
|
|
1813
|
+
void input.session
|
|
1814
|
+
.answer({
|
|
1815
|
+
questions: pendingQuestions,
|
|
1816
|
+
answers: questionAccumulatedAnswers,
|
|
1817
|
+
behavior: "follow_up",
|
|
1818
|
+
message,
|
|
1819
|
+
images,
|
|
1820
|
+
})
|
|
1821
|
+
.catch(reportError);
|
|
1822
|
+
hideQuestions();
|
|
1823
|
+
if (!running)
|
|
1824
|
+
markRunning();
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1639
1827
|
// Every submit — running or idle — is a follow_up. While the agent is
|
|
1640
1828
|
// running this queues; while idle it kicks off a fresh turn. Single
|
|
1641
1829
|
// mental model: type, press Enter, your message lands.
|