u-foo 3.0.0 → 3.0.2

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.
Files changed (47) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/tasks.js +4 -1
  3. package/src/app/chat/commandExecutor.js +111 -1
  4. package/src/app/chat/commands.js +2 -1
  5. package/src/app/chat/daemonMessageRouter.js +1 -1
  6. package/src/app/chat/inputSubmitHandler.js +3 -2
  7. package/src/code/agent.js +17 -3
  8. package/src/code/commands.js +3 -3
  9. package/src/code/context/executionSegment.js +5 -0
  10. package/src/code/context/planMode.js +8 -1
  11. package/src/code/context/promptLayers.js +10 -9
  12. package/src/code/dispatch.js +4 -0
  13. package/src/code/index.js +2 -0
  14. package/src/code/modelCommand.js +199 -23
  15. package/src/code/nativeRunner.js +299 -225
  16. package/src/code/protocol/controlPlane.js +93 -0
  17. package/src/code/protocol/faultHarness.js +90 -0
  18. package/src/code/protocol/index.js +20 -0
  19. package/src/code/protocol/loopEvents.js +102 -0
  20. package/src/code/protocol/materialize.js +107 -0
  21. package/src/code/protocol/messageFixtures.js +116 -0
  22. package/src/code/protocol/ownership.js +147 -0
  23. package/src/code/protocol/protocolValidator.js +165 -0
  24. package/src/code/protocol/suspension.js +173 -0
  25. package/src/code/protocol/toolCallLedger.js +222 -0
  26. package/src/code/protocol/transitions.js +97 -0
  27. package/src/code/providers/anthropicMessagesTransport.js +93 -0
  28. package/src/code/providers/index.js +8 -0
  29. package/src/code/providers/modelsCatalog.js +304 -0
  30. package/src/code/providers/openaiChatTransport.js +98 -0
  31. package/src/code/providers/transportContract.js +46 -0
  32. package/src/code/repl.js +45 -29
  33. package/src/code/runtime/taskControl.js +177 -53
  34. package/src/code/runtime/taskFocus.js +30 -10
  35. package/src/code/runtime/taskLoop.js +25 -3
  36. package/src/code/runtime/taskRun.js +172 -2
  37. package/src/code/runtime/workspaceLease.js +41 -0
  38. package/src/code/sessionStore.js +1 -0
  39. package/src/code/taskRoute.js +73 -0
  40. package/src/code/thinkingLevels.js +132 -0
  41. package/src/code/tools/taskRun.js +118 -0
  42. package/src/config.js +10 -1
  43. package/src/ui/format/index.js +48 -3
  44. package/src/ui/ink/ChatApp.js +137 -25
  45. package/src/ui/ink/UcodeApp.js +38 -30
  46. package/src/ui/ink/chatLogModel.js +238 -32
  47. package/src/ui/ink/chatReducer.js +18 -6
@@ -1103,13 +1103,52 @@ function buildCompletions({
1103
1103
  }
1104
1104
  }
1105
1105
 
1106
- // Generic top-level argument lists (e.g. /resume <session-id>).
1106
+ // Generic top-level argument lists (e.g. /resume <session-id>,
1107
+ // /model <id> [thinking]). /model supports a secondary intensity menu.
1107
1108
  if (
1108
1109
  Array.isArray(argListForHead)
1109
1110
  && argListForHead.length > 0
1110
1111
  && !(headNode && headNode.children)
1111
1112
  && (endsWithWhitespace || tail.length >= 1)
1112
1113
  ) {
1114
+ const secondaryList = argumentLists && typeof argumentLists === "object"
1115
+ ? argumentLists[`${headKey}/thinking`] || argumentLists[`${headKey}/think`]
1116
+ : null;
1117
+ const supportsThinkingMenu = head === "/model" && Array.isArray(secondaryList) && secondaryList.length > 0;
1118
+
1119
+ // Secondary menu: "/model <id> " or "/model <id> <partial>"
1120
+ if (supportsThinkingMenu && (
1121
+ (tail.length === 1 && endsWithWhitespace)
1122
+ || tail.length >= 2
1123
+ )) {
1124
+ if (tail.length > 2) return [];
1125
+ const modelId = String(tail[0] || "").trim();
1126
+ if (!modelId) return [];
1127
+ const partial = tail.length >= 2 && !endsWithWhitespace
1128
+ ? String(tail[1] || "").toLowerCase()
1129
+ : "";
1130
+ const out = [];
1131
+ for (const item of secondaryList) {
1132
+ const id = String((item && (item.alias || item.cmd || item.id || item.name)) || item || "");
1133
+ if (!id) continue;
1134
+ if (partial && !id.toLowerCase().startsWith(partial)) continue;
1135
+ const desc = String((item && (item.desc || item.summary || item.description || item.source)) || "");
1136
+ out.push({
1137
+ kind: "argument",
1138
+ label: `${head} ${modelId} ${id}`,
1139
+ replace: `${head} ${modelId} ${id}`,
1140
+ description: desc,
1141
+ hasChildren: false,
1142
+ });
1143
+ if (out.length >= limit) break;
1144
+ }
1145
+ if (partial && out.length === 1) {
1146
+ const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
1147
+ if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
1148
+ }
1149
+ return out;
1150
+ }
1151
+
1113
1152
  if (tail.length > 1) return [];
1114
1153
  const partial = String(tail[0] || "").toLowerCase();
1115
1154
  const out = [];
@@ -1118,17 +1157,23 @@ function buildCompletions({
1118
1157
  if (!id) continue;
1119
1158
  if (partial && !id.toLowerCase().startsWith(partial)) continue;
1120
1159
  const desc = String((item && (item.desc || item.summary || item.description || item.source)) || "");
1160
+ const hasChildren = Boolean(
1161
+ (item && item.hasChildren)
1162
+ || supportsThinkingMenu
1163
+ );
1121
1164
  out.push({
1122
1165
  kind: "argument",
1123
1166
  label: `${head} ${id}`,
1124
- replace: `${head} ${id} `,
1167
+ // Trailing space keeps the popup open for the thinking submenu.
1168
+ replace: hasChildren ? `${head} ${id} ` : `${head} ${id} `,
1125
1169
  description: desc,
1126
- hasChildren: false,
1170
+ hasChildren,
1127
1171
  });
1128
1172
  if (out.length >= limit) break;
1129
1173
  }
1130
1174
  if (partial && out.length === 1) {
1131
1175
  const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
1176
+ // Keep the popup open when the sole match still has a submenu.
1132
1177
  if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
1133
1178
  }
1134
1179
  return out;
@@ -165,38 +165,58 @@ function loadChatHistory(projectRoot, cap = 200, options = {}) {
165
165
  const raw = fs.readFileSync(file, "utf8");
166
166
  const lines = raw.split(/\r?\n/).filter(Boolean);
167
167
  const out = [];
168
- const pushLine = (line = "") => {
168
+ const pushLine = (line = "", sourceType = "") => {
169
169
  const value = String(line || "");
170
170
  if (!value.trim()) {
171
- if (out.length > 0 && out[out.length - 1] !== "") out.push("");
171
+ if (out.length > 0) {
172
+ const last = out[out.length - 1];
173
+ const lastText = typeof last === "object" ? last.text : last;
174
+ if (lastText !== "") out.push({ text: "", sourceType: sourceType || "system" });
175
+ }
172
176
  return;
173
177
  }
174
- out.push(value);
178
+ out.push(sourceType ? { text: value, sourceType } : value);
175
179
  };
176
180
  for (const line of lines) {
177
181
  try {
178
182
  const entry = JSON.parse(line);
179
183
  if (!entry) continue;
180
184
  if (entry.type === "spacer") {
181
- pushLine("");
185
+ pushLine("", "system");
182
186
  continue;
183
187
  }
184
188
  const text = String(entry.text || "");
185
189
  if (!text) continue;
190
+ const sourceType = String(entry.type || "");
186
191
  // Strip blessed-tag markup that the legacy log writer used; ink
187
192
  // can't render those tags and we don't want them shown literally.
188
193
  const stripped = text.replace(/\{[^{}]+\}/g, "");
189
194
  for (const renderedLine of normalizeInkLogLines(stripped)) {
190
- pushLine(renderedLine);
195
+ pushLine(renderedLine, sourceType);
191
196
  }
192
197
  } catch {
193
198
  // ignore malformed lines
194
199
  }
195
200
  }
196
- while (out.length > 0 && out[0] === "") out.shift();
197
- while (out.length > 0 && out[out.length - 1] === "") out.pop();
201
+ while (out.length > 0) {
202
+ const first = out[0];
203
+ const firstText = typeof first === "object" ? first.text : first;
204
+ if (firstText !== "") break;
205
+ out.shift();
206
+ }
207
+ while (out.length > 0) {
208
+ const last = out[out.length - 1];
209
+ const lastText = typeof last === "object" ? last.text : last;
210
+ if (lastText !== "") break;
211
+ out.pop();
212
+ }
198
213
  const capped = out.slice(-cap);
199
- while (capped.length > 0 && capped[0] === "") capped.shift();
214
+ while (capped.length > 0) {
215
+ const first = capped[0];
216
+ const firstText = typeof first === "object" ? first.text : first;
217
+ if (firstText !== "") break;
218
+ capped.shift();
219
+ }
200
220
  return capped;
201
221
  } catch {
202
222
  return [];
@@ -435,12 +455,26 @@ function createThrottledSender(send, windowMs = 500) {
435
455
  // Kinds whose log entries render as a margin-bottom "transcript cell" in
436
456
  // buildChatLogGroups. Kept in sync with canAppendToChatLogGroup in
437
457
  // chatLogModel.js.
438
- const STATIC_GROUPABLE_KINDS = new Set(["assistant", "agent", "success", "error", "meta", "plain"]);
458
+ const STATIC_GROUPABLE_KINDS = new Set([
459
+ "assistant",
460
+ "agent",
461
+ "report",
462
+ "success",
463
+ "error",
464
+ "meta",
465
+ "system",
466
+ "plain",
467
+ ]);
439
468
 
440
469
  // Shared row colors for both the dynamic (stream) and <Static> renderers.
470
+ // Aligned with ucode LOG_LINE_TEXT_PROPS: user green+bold, system dim gray,
471
+ // team bus/agent cyan, ufoo assistant white/bold marker.
441
472
  const CHAT_LOG_ROW_PALETTE = {
473
+ user: { marker: "green", speaker: "green", body: "green", bold: true },
442
474
  assistant: { marker: "cyan", speaker: "white", body: undefined, bold: true },
443
475
  agent: { marker: "cyan", speaker: "cyan", body: undefined, bold: false },
476
+ report: { marker: "yellow", speaker: "yellow", body: undefined, bold: false },
477
+ system: { marker: "gray", speaker: "gray", body: "gray", bold: false, dim: true },
444
478
  error: { marker: "red", speaker: "red", body: "red", bold: true },
445
479
  success: { marker: "green", speaker: "green", body: "green", bold: false },
446
480
  divider: { marker: "gray", speaker: "gray", body: "gray", bold: false },
@@ -460,21 +494,39 @@ function decorateStaticLogEntry(prev, entry) {
460
494
  const markdownState = prev && prev.markdownState && typeof prev.markdownState === "object"
461
495
  ? { inCodeBlock: Boolean(prev.markdownState.inCodeBlock) }
462
496
  : { inCodeBlock: false };
463
- const sourceText = entry && typeof entry === "object" && entry.text != null
464
- ? String(entry.text)
465
- : entry;
466
- const row = buildChatLogLineModel(sourceText, { markdownState });
497
+ const source = entry && typeof entry === "object" ? entry : { text: entry };
498
+ const sourceText = source.text != null ? String(source.text) : String(entry || "");
499
+ const sourceType = String(source.sourceType || source.type || "");
500
+ const meta = source.meta && typeof source.meta === "object" ? source.meta : {};
501
+ const row = buildChatLogLineModel({
502
+ ...source,
503
+ text: sourceText,
504
+ sourceType,
505
+ meta,
506
+ }, { markdownState, sourceType, meta });
467
507
  const continuation = Boolean(
468
508
  prev
469
- && (row.kind === "plain" || row.kind === "spacer")
470
- && STATIC_GROUPABLE_KINDS.has(prev.groupKind)
509
+ && (
510
+ ((row.kind === "plain" || row.kind === "spacer") && STATIC_GROUPABLE_KINDS.has(prev.groupKind))
511
+ || (prev.groupKind === "user" && row.kind === "user" && row.marker !== "›")
512
+ )
471
513
  );
472
514
  const groupKind = continuation ? prev.groupKind : row.kind;
473
515
  // A gap belongs between visual blocks: only on entries that START a new
474
516
  // block, and only when the previous block was a transcript group (whose
475
517
  // old dynamic renderer contributed a trailing marginBottom).
476
- const marginBefore = Boolean(!continuation && prev && STATIC_GROUPABLE_KINDS.has(prev.groupKind));
477
- return { entry, row, groupKind, continuation, marginBefore, markdownState };
518
+ // User turns also get a leading gap so › prompts don't sit flush against
519
+ // the previous transcript cell (ucode parity).
520
+ const marginBefore = Boolean(
521
+ !continuation
522
+ && prev
523
+ && (
524
+ STATIC_GROUPABLE_KINDS.has(prev.groupKind)
525
+ || prev.groupKind === "user"
526
+ || row.kind === "user"
527
+ )
528
+ );
529
+ return { entry: source, row, groupKind, continuation, marginBefore, markdownState };
478
530
  }
479
531
 
480
532
  function createInkStreamState({
@@ -1315,7 +1367,15 @@ function createChatApp({ React, ink, props, interactive = true }) {
1315
1367
  }
1316
1368
  const lines = normalizeInkLogLines(text);
1317
1369
  if (lines.length === 0) return;
1318
- dispatch({ type: "log/appendMany", lines });
1370
+ const payload = lines.map((line, index) => ({
1371
+ text: line,
1372
+ type,
1373
+ sourceType: type,
1374
+ // Attach router meta only on the first physical line so multi-line
1375
+ // bus/reply bodies don't duplicate publisher payloads.
1376
+ meta: index === 0 && meta && typeof meta === "object" ? meta : {},
1377
+ }));
1378
+ dispatch({ type: "log/appendMany", lines: payload });
1319
1379
  appendScopedHistory(type, stripBlessedTags(text), meta);
1320
1380
  }, [appendScopedHistory, setStatusText]);
1321
1381
 
@@ -3505,6 +3565,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
3505
3565
  return buildChatLogGroups(lines.map((line, idx) => ({
3506
3566
  id: `s-${idx}`,
3507
3567
  text: idx === 0 ? `${prefix}${line}` : ` ${line}`,
3568
+ sourceType: "bus",
3569
+ type: "bus",
3508
3570
  })));
3509
3571
  }, [state.activeStream]);
3510
3572
 
@@ -3512,6 +3574,18 @@ function createChatApp({ React, ink, props, interactive = true }) {
3512
3574
  return null;
3513
3575
  }
3514
3576
 
3577
+ const renderUserLogBody = (bodyText = "") => {
3578
+ const body = String(bodyText || "");
3579
+ const atMatch = body.match(/^@([^\s]+)\s+(.*)$/);
3580
+ if (atMatch) {
3581
+ return {
3582
+ at: atMatch[1],
3583
+ rest: atMatch[2] || "",
3584
+ };
3585
+ }
3586
+ return { at: "", rest: body };
3587
+ };
3588
+
3515
3589
  const renderChatLogEntry = (entry, group) => {
3516
3590
  const row = entry && entry.row ? entry.row : buildChatLogLineModel("");
3517
3591
  const key = entry && entry.id ? entry.id : `log-${row.body}`;
@@ -3529,12 +3603,31 @@ function createChatApp({ React, ink, props, interactive = true }) {
3529
3603
  h(Text, { color: colors.body, bold: true, wrap: "truncate" }, row.body),
3530
3604
  );
3531
3605
  }
3606
+ if (row.kind === "user") {
3607
+ const userBody = renderUserLogBody(row.bodyText);
3608
+ return h(Box, { key, width: "100%", marginBottom: 1 },
3609
+ h(Text, { color: "green", bold: true }, row.markerText || "› "),
3610
+ userBody.at
3611
+ ? h(Text, { color: "magenta", bold: true }, `@${userBody.at} `)
3612
+ : null,
3613
+ h(Text, { color: "green", bold: true, wrap: "wrap" }, userBody.rest),
3614
+ );
3615
+ }
3532
3616
  const markerText = entry && entry.continuation
3533
- ? (group && (group.kind === "assistant" || group.kind === "agent") ? " " : " ")
3617
+ ? (group && (group.kind === "assistant" || group.kind === "agent" || group.kind === "report") ? " " : " ")
3534
3618
  : row.markerText;
3619
+ const bodyProps = {
3620
+ color: colors.body,
3621
+ wrap: "wrap",
3622
+ };
3623
+ if (colors.dim) bodyProps.dimColor = true;
3535
3624
  return h(Box, { key, width: "100%" },
3536
- h(Text, { color: colors.marker, bold: row.kind === "error" }, markerText),
3537
- h(Text, { color: colors.body, wrap: "wrap" },
3625
+ h(Text, {
3626
+ color: colors.marker,
3627
+ bold: row.kind === "error" || row.kind === "assistant",
3628
+ dimColor: Boolean(colors.dim),
3629
+ }, markerText),
3630
+ h(Text, bodyProps,
3538
3631
  row.speaker && !(entry && entry.continuation)
3539
3632
  ? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
3540
3633
  : null,
@@ -3551,7 +3644,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
3551
3644
  if (entries.length === 0) return null;
3552
3645
  const first = entries[0] || {};
3553
3646
  const row = first.row || buildChatLogLineModel("");
3554
- if (row.kind === "spacer" || row.kind === "banner" || row.kind === "divider") {
3647
+ if (row.kind === "spacer" || row.kind === "banner" || row.kind === "divider" || row.kind === "user") {
3555
3648
  return renderChatLogEntry(first, group);
3556
3649
  }
3557
3650
  return h(Box, {
@@ -3587,12 +3680,31 @@ function createChatApp({ React, ink, props, interactive = true }) {
3587
3680
  h(Text, { color: colors.body, bold: true, wrap: "truncate" }, row.body),
3588
3681
  );
3589
3682
  }
3683
+ if (row.kind === "user") {
3684
+ const userBody = renderUserLogBody(row.bodyText);
3685
+ return h(Box, { key, width: "100%", marginTop, marginBottom: 1 },
3686
+ h(Text, { color: "green", bold: true }, row.markerText || "› "),
3687
+ userBody.at
3688
+ ? h(Text, { color: "magenta", bold: true }, `@${userBody.at} `)
3689
+ : null,
3690
+ h(Text, { color: "green", bold: true, wrap: "wrap" }, userBody.rest),
3691
+ );
3692
+ }
3590
3693
  const markerText = continuation
3591
- ? (groupKind === "assistant" || groupKind === "agent" ? " " : " ")
3694
+ ? (groupKind === "assistant" || groupKind === "agent" || groupKind === "report" ? " " : " ")
3592
3695
  : row.markerText;
3696
+ const bodyProps = {
3697
+ color: colors.body,
3698
+ wrap: "wrap",
3699
+ };
3700
+ if (colors.dim) bodyProps.dimColor = true;
3593
3701
  return h(Box, { key, width: "100%", marginTop },
3594
- h(Text, { color: colors.marker, bold: row.kind === "error" }, markerText),
3595
- h(Text, { color: colors.body, wrap: "wrap" },
3702
+ h(Text, {
3703
+ color: colors.marker,
3704
+ bold: row.kind === "error" || row.kind === "assistant",
3705
+ dimColor: Boolean(colors.dim),
3706
+ }, markerText),
3707
+ h(Text, bodyProps,
3596
3708
  row.speaker && !continuation
3597
3709
  ? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
3598
3710
  : null,
@@ -323,14 +323,36 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
323
323
 
324
324
  const { UCODE_COMMAND_REGISTRY, UCODE_COMMAND_TREE } = require("../../code/commands");
325
325
  const { listSessionSummaries } = require("../../code/sessionStore");
326
- const { suggestUcodeModels, applyUcodeModelCommand } = require("../../code/modelCommand");
326
+ const { suggestUcodeModels, suggestUcodeThinkingLevels, applyUcodeModelCommand, listUcodeModels } = require("../../code/modelCommand");
327
327
  let resumeSessions = [];
328
328
  try {
329
329
  resumeSessions = listSessionSummaries(props.workspaceRoot || process.cwd(), { limit: 40 });
330
330
  } catch {
331
331
  resumeSessions = [];
332
332
  }
333
- const modelSuggestions = suggestUcodeModels(props.state || {});
333
+ const [remoteModels, setRemoteModels] = useState([]);
334
+ useEffect(() => {
335
+ let cancelled = false;
336
+ (async () => {
337
+ try {
338
+ const listed = await listUcodeModels(props.state || {}, {
339
+ workspaceRoot: props.workspaceRoot || process.cwd(),
340
+ });
341
+ if (!cancelled && listed.ok) {
342
+ setRemoteModels(Array.isArray(listed.models) ? listed.models : []);
343
+ }
344
+ } catch {
345
+ if (!cancelled) setRemoteModels([]);
346
+ }
347
+ })();
348
+ return () => { cancelled = true; };
349
+ }, [
350
+ props.workspaceRoot,
351
+ props.state && props.state.provider,
352
+ props.state && props.state.model,
353
+ ]);
354
+ const modelSuggestions = suggestUcodeModels(props.state || {}, { models: remoteModels });
355
+ const thinkingSuggestions = suggestUcodeThinkingLevels(props.state || {});
334
356
 
335
357
  const completions = fmt.buildCompletions({
336
358
  text: draft,
@@ -341,6 +363,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
341
363
  argumentLists: {
342
364
  "/resume": resumeSessions,
343
365
  "/model": modelSuggestions,
366
+ "/model/thinking": thinkingSuggestions,
344
367
  },
345
368
  limit: 20,
346
369
  });
@@ -541,7 +564,9 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
541
564
  return;
542
565
  }
543
566
  case "model": {
544
- const applied = applyUcodeModelCommand(props.state || {}, result);
567
+ const applied = await applyUcodeModelCommand(props.state || {}, result, {
568
+ workspaceRoot: runtimeWorkspace,
569
+ });
545
570
  appendLogText(applied.output || "", applied.ok ? "system" : "error");
546
571
  if (applied.ok && result.action === "set" && typeof props.persistSessionState === "function") {
547
572
  try {
@@ -950,18 +975,8 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
950
975
 
951
976
  // Pending approval/choice/chat takes priority over nudge / new NL.
952
977
  try {
953
- const {
954
- hasPendingUserInteraction,
955
- parseUserInteractionInput,
956
- getPendingUserInteraction,
957
- } = require("../../code/context/userInteraction");
978
+ const { hasPendingUserInteraction } = require("../../code/context/userInteraction");
958
979
  if (props.state && props.state.executionState && hasPendingUserInteraction(props.state.executionState)) {
959
- const pending = getPendingUserInteraction(props.state.executionState);
960
- const parsed = parseUserInteractionInput(pending, trimmed);
961
- if (!parsed.ok) {
962
- appendLogText(parsed.error || "Invalid reply", "error");
963
- return;
964
- }
965
980
  appendLogText(`› ${trimmed}`, "user");
966
981
  const startedAt = Date.now();
967
982
  setStatus({
@@ -972,14 +987,14 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
972
987
  });
973
988
  runChainRef.current = runChainRef.current
974
989
  .then(async () => {
975
- const resume = typeof props.resumeAfterUserInteraction === "function"
976
- ? props.resumeAfterUserInteraction
977
- : require("../../code/agent").resumeAfterUserInteraction;
990
+ const submit = typeof props.submitUserInteractionAnswer === "function"
991
+ ? props.submitUserInteractionAnswer
992
+ : require("../../code/protocol").submitUserInteractionAnswer;
978
993
  let streamBuf = "";
979
994
  let sawStreamText = false;
980
995
  let streamStarted = false;
981
996
  let dropLeadingStreamBlank = false;
982
- const result = await resume(trimmed, props.state, {
997
+ const result = await submit(trimmed, props.state, {
983
998
  onDelta: (delta) => {
984
999
  const text = String(delta || "");
985
1000
  if (!text) return;
@@ -1006,19 +1021,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1006
1021
  }
1007
1022
  flushTableBuffer();
1008
1023
  refreshPlanUi();
1009
- if (result && result.waitingUserInteraction) {
1010
- appendLogText("Still waiting for your reply.", "system");
1011
- setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
1012
- return;
1013
- }
1014
1024
  if (!result || result.ok === false) {
1015
1025
  appendLogText(`Error: ${(result && result.error) || "resume failed"}`, "error");
1016
- } else {
1017
- // Skip summary echo when deltas were already rendered (mirrors NL path).
1018
- const shouldSkipSummary = Boolean(result.streamed && result.ok && sawStreamText);
1019
- if (result.summary && !shouldSkipSummary) {
1020
- appendLogText(result.summary);
1021
- }
1026
+ } else if (result.shouldEchoSummary) {
1027
+ appendLogText(result.echoSummaryText || result.summary || "", result.waitingUserInteraction ? "system" : "assistant");
1028
+ } else if (result.waitingUserInteraction) {
1029
+ appendLogText("Still waiting for your reply.", "system");
1022
1030
  }
1023
1031
  setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
1024
1032
  })
@@ -1067,7 +1075,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1067
1075
  flushActiveMerge,
1068
1076
  flushTableBuffer,
1069
1077
  props.state,
1070
- props.resumeAfterUserInteraction,
1078
+ props.submitUserInteractionAnswer,
1071
1079
  refreshPlanUi,
1072
1080
  ]);
1073
1081