u-foo 2.5.13 → 2.5.15

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +339 -24
  4. package/src/code/commands.js +61 -0
  5. package/src/code/context/artifactGc.js +292 -0
  6. package/src/code/context/artifactIndex.js +161 -0
  7. package/src/code/context/artifacts.js +183 -0
  8. package/src/code/context/assembler.js +698 -0
  9. package/src/code/context/executionSegment.js +314 -0
  10. package/src/code/context/featureFlag.js +13 -0
  11. package/src/code/context/index.js +18 -0
  12. package/src/code/context/projectSnapshot.js +201 -0
  13. package/src/code/context/promptLayers.js +159 -0
  14. package/src/code/context/reducers.js +328 -0
  15. package/src/code/context/stableJson.js +29 -0
  16. package/src/code/context/stateCommit.js +412 -0
  17. package/src/code/context/transcript.js +182 -0
  18. package/src/code/context/transcriptSync.js +106 -0
  19. package/src/code/context/workingSet.js +323 -0
  20. package/src/code/dispatch.js +4 -1
  21. package/src/code/index.js +6 -0
  22. package/src/code/modelCommand.js +87 -0
  23. package/src/code/nativeRunner.js +187 -31
  24. package/src/code/repl.js +36 -32
  25. package/src/code/sessionStore.js +227 -15
  26. package/src/code/skills/index.js +10 -0
  27. package/src/code/skills/injection.js +65 -3
  28. package/src/code/skills/loader.js +21 -0
  29. package/src/code/skills/manifest.js +87 -0
  30. package/src/code/skills/render.js +15 -1
  31. package/src/code/taskDecomposer.js +32 -2
  32. package/src/code/tools/artifactRead.js +40 -0
  33. package/src/code/tui.js +2 -0
  34. package/src/code/usageStore.js +15 -0
  35. package/src/ui/format/index.js +260 -44
  36. package/src/ui/format/markdownRenderer.js +215 -72
  37. package/src/ui/ink/ChatApp.js +39 -8
  38. package/src/ui/ink/UcodeApp.js +408 -55
  39. package/src/ui/ink/chatLogModel.js +102 -21
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+
3
+ const { loadArtifact, readArtifactSlice } = require("../context/artifacts");
4
+
5
+ function runArtifactReadTool(args = {}, options = {}) {
6
+ const workspaceRoot = options.workspaceRoot || process.cwd();
7
+ const sessionId = String(options.sessionId || args.sessionId || "").trim();
8
+ const artifactId = String(args.artifactId || args.id || "").trim();
9
+ if (!artifactId) {
10
+ return { ok: false, error: "artifactId is required" };
11
+ }
12
+ if (!sessionId) {
13
+ return { ok: false, error: "sessionId is required for artifact_read" };
14
+ }
15
+
16
+ const loaded = loadArtifact(workspaceRoot, sessionId, artifactId);
17
+ if (!loaded.ok || !loaded.artifact) {
18
+ return { ok: false, error: loaded.error || "artifact not found", artifactId };
19
+ }
20
+
21
+ const selector = {};
22
+ if (args.startLine !== undefined) selector.startLine = args.startLine;
23
+ if (args.endLine !== undefined) selector.endLine = args.endLine;
24
+ if (args.maxChars !== undefined) selector.maxChars = args.maxChars;
25
+ if (args.tailLines !== undefined) selector.tailLines = args.tailLines;
26
+
27
+ const slice = readArtifactSlice(loaded.artifact, selector);
28
+ return {
29
+ ok: slice.ok !== false,
30
+ artifactId,
31
+ content: slice.content || "",
32
+ range: slice.range,
33
+ truncated: Boolean(slice.truncated),
34
+ error: slice.error || "",
35
+ };
36
+ }
37
+
38
+ module.exports = {
39
+ runArtifactReadTool,
40
+ };
package/src/code/tui.js CHANGED
@@ -27,6 +27,7 @@ const {
27
27
  normalizeToolMergeEntry,
28
28
  parseActiveAgentsFromBusStatus,
29
29
  renderLogLinesWithMarkdown,
30
+ renderLogLinesWithMarkdownAnsi,
30
31
  resolveAgentSelectionOnDown,
31
32
  resolveHistoryDownTransition,
32
33
  shouldClearAgentSelectionOnUp,
@@ -51,6 +52,7 @@ module.exports = {
51
52
  parseActiveAgentsFromBusStatus,
52
53
  shouldUseUcodeTui,
53
54
  renderLogLinesWithMarkdown,
55
+ renderLogLinesWithMarkdownAnsi,
54
56
  shouldEnterAgentSelection,
55
57
  resolveAgentSelectionOnDown,
56
58
  cycleAgentSelectionIndex,
@@ -92,10 +92,25 @@ function summarizeSessionUsage({ workspaceRoot = process.cwd(), sessionId = "" }
92
92
  return summary;
93
93
  }
94
94
 
95
+ function formatSessionUsageStatus(summary = {}) {
96
+ const source = summary && typeof summary === "object" ? summary : {};
97
+ const input = Number(source.input) || 0;
98
+ const output = Number(source.output) || 0;
99
+ const cacheRead = Number(source.cacheRead) || 0;
100
+ const cacheCreation = Number(source.cacheCreation) || 0;
101
+ const denominator = cacheRead + input;
102
+ const hitRate = denominator > 0 ? (cacheRead / denominator) * 100 : 0;
103
+ return [
104
+ `Session tokens: input=${input} output=${output} cache_read=${cacheRead} cache_creation=${cacheCreation}`,
105
+ `Cache hit rate: ${hitRate.toFixed(1)}% (cache_read/(cache_read+input))`,
106
+ ].join("\n");
107
+ }
108
+
95
109
  module.exports = {
96
110
  getUsageFilePath,
97
111
  buildUsageRecord,
98
112
  appendUsageRecord,
99
113
  createUsageSummary,
100
114
  summarizeSessionUsage,
115
+ formatSessionUsageStatus,
101
116
  };
@@ -41,6 +41,7 @@ const TOOL_LABELS = {
41
41
  write: "Writing file",
42
42
  edit: "Editing file",
43
43
  bash: "Running command",
44
+ artifact_read: "Reading artifact",
44
45
  };
45
46
 
46
47
  const ANSI_PATTERN = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
@@ -248,6 +249,120 @@ function renderLogLinesWithMarkdown(text = "", state = {}, escapeFn = (value) =>
248
249
  return renderMarkdownLines(text, state, escapeFn);
249
250
  }
250
251
 
252
+ function renderLogLinesWithMarkdownAnsi(text = "", state = {}) {
253
+ const { renderMarkdownLinesAnsi } = require("./markdownRenderer");
254
+ return renderMarkdownLinesAnsi(text, state);
255
+ }
256
+
257
+ function messageContentText(message = {}) {
258
+ if (!message || typeof message !== "object") return "";
259
+ const content = message.content;
260
+ if (typeof content === "string") return content;
261
+ if (Array.isArray(content)) {
262
+ return content.map((part) => {
263
+ if (typeof part === "string") return part;
264
+ if (part && typeof part === "object" && part.text != null) return String(part.text);
265
+ return "";
266
+ }).join("");
267
+ }
268
+ return "";
269
+ }
270
+
271
+ function toolMessagePreview(message = {}) {
272
+ const raw = messageContentText(message).trim();
273
+ if (!raw) return "";
274
+ try {
275
+ const parsed = JSON.parse(raw);
276
+ if (parsed && typeof parsed === "object") {
277
+ if (parsed.preview) return String(parsed.preview);
278
+ if (parsed.artifactId) return `artifact:${parsed.artifactId}`;
279
+ }
280
+ } catch {
281
+ // plain tool text
282
+ }
283
+ return raw;
284
+ }
285
+
286
+ /**
287
+ * Convert persisted nlMessages into ucode TUI log rows for resume/history.
288
+ * Applies the shared ANSI markdown renderer so restored assistant text matches
289
+ * live streaming output.
290
+ */
291
+ function buildUcodeSessionLogEntries(messages = [], options = {}) {
292
+ const list = Array.isArray(messages) ? messages : [];
293
+ const markdownState = options.markdownState && typeof options.markdownState === "object"
294
+ ? options.markdownState
295
+ : { inCodeBlock: false };
296
+ const idPrefix = String(options.idPrefix || "h");
297
+ let seq = Number.isFinite(options.startSeq) ? Math.max(0, Math.floor(options.startSeq)) : 0;
298
+ const maxToolPreviewLines = Number.isFinite(options.maxToolPreviewLines)
299
+ ? Math.max(1, Math.floor(options.maxToolPreviewLines))
300
+ : 4;
301
+ const entries = [];
302
+
303
+ const pushLines = (text, kind) => {
304
+ const source = String(text == null ? "" : text);
305
+ let lines;
306
+ if (kind === "assistant" || kind === "error") {
307
+ try {
308
+ lines = renderLogLinesWithMarkdownAnsi(source, markdownState);
309
+ if (!Array.isArray(lines) || lines.length === 0) lines = source.split(/\r?\n/);
310
+ } catch {
311
+ lines = source.split(/\r?\n/);
312
+ }
313
+ } else {
314
+ lines = source.split(/\r?\n/);
315
+ }
316
+ for (const line of lines) {
317
+ entries.push({
318
+ id: `${idPrefix}-${seq}`,
319
+ text: String(line || ""),
320
+ kind,
321
+ });
322
+ seq += 1;
323
+ }
324
+ };
325
+
326
+ for (const message of list) {
327
+ if (!message || typeof message !== "object") continue;
328
+ const role = String(message.role || "").trim().toLowerCase();
329
+ if (role === "user") {
330
+ const text = messageContentText(message);
331
+ if (!text.trim()) continue;
332
+ const lines = text.split(/\r?\n/);
333
+ lines.forEach((line, index) => {
334
+ pushLines(index === 0 ? `› ${line}` : line, "user");
335
+ });
336
+ continue;
337
+ }
338
+ if (role === "assistant") {
339
+ const text = messageContentText(message);
340
+ if (text) pushLines(text, "assistant");
341
+ const calls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
342
+ if (calls.length > 0) {
343
+ const names = calls
344
+ .map((call) => {
345
+ if (!call || typeof call !== "object") return "";
346
+ if (call.function && call.function.name) return String(call.function.name);
347
+ return String(call.name || "");
348
+ })
349
+ .map((name) => name.trim())
350
+ .filter(Boolean);
351
+ if (names.length > 0) pushLines(`⚙ ${names.join(" · ")}`, "system");
352
+ }
353
+ continue;
354
+ }
355
+ if (role === "tool") {
356
+ const preview = toolMessagePreview(message);
357
+ if (!preview.trim()) continue;
358
+ const clipped = preview.split(/\r?\n/).slice(0, maxToolPreviewLines).join("\n");
359
+ pushLines(clipped, "toolDetail");
360
+ }
361
+ }
362
+
363
+ return { entries, nextSeq: seq, markdownState };
364
+ }
365
+
251
366
  function shouldEnterAgentSelection(inputValue = "") {
252
367
  const text = String(inputValue || "");
253
368
  const trimmed = text.trim();
@@ -514,6 +629,39 @@ function normalizeBashToolCommand(args = {}, payload = {}) {
514
629
  return [command, code].filter(Boolean).join(" · ");
515
630
  }
516
631
 
632
+ function shortenPathDetail(value = "", maxChars = 72) {
633
+ const text = String(value || "").trim().replace(/\\/g, "/");
634
+ if (!text) return "";
635
+ const limit = Number.isFinite(maxChars) && maxChars > 8 ? Math.floor(maxChars) : 72;
636
+ if (text.length <= limit) return text;
637
+ return `…${text.slice(-(limit - 1))}`;
638
+ }
639
+
640
+ function normalizeToolLogDetail(tool = "", args = {}, payload = {}) {
641
+ const name = String(tool || "").trim().toLowerCase();
642
+ const argObj = args && typeof args === "object" ? args : {};
643
+ const resObj = payload && typeof payload === "object" ? payload : {};
644
+
645
+ if (name === "bash") return normalizeBashToolCommand(argObj, resObj);
646
+
647
+ if (name === "read" || name === "write" || name === "edit") {
648
+ const pathText = String(argObj.path || resObj.path || "").trim();
649
+ return shortenPathDetail(pathText);
650
+ }
651
+
652
+ if (name === "artifact_read") {
653
+ const artifactId = String(argObj.artifactId || argObj.id || resObj.artifactId || "").trim();
654
+ const rangeBits = [];
655
+ if (argObj.startLine != null) rangeBits.push(`L${argObj.startLine}`);
656
+ if (argObj.endLine != null) rangeBits.push(`L${argObj.endLine}`);
657
+ const range = rangeBits.length > 0 ? rangeBits.join("-") : "";
658
+ return [artifactId, range].filter(Boolean).join(" · ");
659
+ }
660
+
661
+ const fallback = String(argObj.path || argObj.command || argObj.cmd || "").trim();
662
+ return shortenPathDetail(fallback);
663
+ }
664
+
517
665
  function normalizeToolMergeEntry(entry = {}) {
518
666
  const source = entry && typeof entry === "object" ? entry : {};
519
667
  const tool = String(source.tool || "").trim().toLowerCase() || "tool";
@@ -822,6 +970,7 @@ function buildCompletions({
822
970
  commandTree = null,
823
971
  groupTemplates = [],
824
972
  soloProfiles = [],
973
+ argumentLists = null,
825
974
  limit = 8,
826
975
  } = {}) {
827
976
  const raw = String(text || "");
@@ -830,9 +979,14 @@ function buildCompletions({
830
979
  const endsWithWhitespace = /\s$/.test(trimmed);
831
980
 
832
981
  if (trimmed.startsWith("/")) {
833
- const parts = trimmed.split(/\s+/);
834
- const head = parts[0]; // "/launch"
835
- const tail = parts.slice(1);
982
+ const tokenParts = trimmed.trimEnd().split(/\s+/).filter(Boolean);
983
+ const head = tokenParts[0] || ""; // "/launch"
984
+ const tail = tokenParts.slice(1);
985
+ const headKey = head.startsWith("/") ? head : `/${head}`;
986
+ const headNode = commandTree && typeof commandTree === "object" ? commandTree[headKey] : null;
987
+ const argListForHead = argumentLists && typeof argumentLists === "object"
988
+ ? argumentLists[headKey]
989
+ : null;
836
990
 
837
991
  // Dynamic argument completion for /group run <alias> and
838
992
  // /solo run <profile>. These pull from runtime sources (group
@@ -842,7 +996,7 @@ function buildCompletions({
842
996
  : (head === "/solo" && tail[0] === "run")
843
997
  ? soloProfiles
844
998
  : null;
845
- if (dynList && (tail.length >= 2 || trimmed.endsWith(" "))) {
999
+ if (dynList && (tail.length >= 2 || (tail[0] === "run" && endsWithWhitespace))) {
846
1000
  const partial = String(tail[1] || "").toLowerCase();
847
1001
  const out = [];
848
1002
  for (const item of (Array.isArray(dynList) ? dynList : [])) {
@@ -867,49 +1021,91 @@ function buildCompletions({
867
1021
  }
868
1022
 
869
1023
  // Sub-command completion: "/cmd <prefix>" or "/cmd sub <prefix>".
870
- if (tail.length >= 1 && commandTree) {
871
- const headKey = head.startsWith("/") ? head : `/${head}`;
872
- let node = commandTree[headKey];
873
- if (!node || typeof node !== "object") return [];
874
- // Walk into nested children for everything but the last token.
875
- for (let i = 0; i < tail.length - 1; i += 1) {
876
- const segment = tail[i];
877
- if (!segment) return [];
878
- const next = node && node.children && node.children[segment];
879
- if (!next) return [];
880
- node = next;
1024
+ if ((tail.length >= 1 || (endsWithWhitespace && headNode && headNode.children)) && commandTree) {
1025
+ let node = headNode;
1026
+ if (!node || typeof node !== "object") {
1027
+ // fall through
1028
+ } else if (node.children && typeof node.children === "object") {
1029
+ const walkTail = endsWithWhitespace && tail.length === 0
1030
+ ? []
1031
+ : (endsWithWhitespace ? tail : tail.slice(0, -1));
1032
+ const partial = endsWithWhitespace && tail.length === 0
1033
+ ? ""
1034
+ : (endsWithWhitespace ? "" : String(tail[tail.length - 1] || "").toLowerCase());
1035
+ let walkNode = node;
1036
+ let walkOk = true;
1037
+ for (const segment of walkTail) {
1038
+ const next = walkNode.children && walkNode.children[segment];
1039
+ if (!next) {
1040
+ walkOk = false;
1041
+ break;
1042
+ }
1043
+ walkNode = next;
1044
+ }
1045
+ const children = walkOk ? (walkNode && walkNode.children) : null;
1046
+ if (children && typeof children === "object") {
1047
+ const prefixSoFar = walkTail.length > 0
1048
+ ? `${head} ${walkTail.join(" ")}`
1049
+ : head;
1050
+ const entries = Object.keys(children).map((name) => ({
1051
+ name,
1052
+ ...children[name],
1053
+ }));
1054
+ entries.sort((a, b) => {
1055
+ const orderA = Number.isFinite(a.order) ? a.order : 999;
1056
+ const orderB = Number.isFinite(b.order) ? b.order : 999;
1057
+ if (orderA !== orderB) return orderA - orderB;
1058
+ return a.name.localeCompare(b.name);
1059
+ });
1060
+ const out = [];
1061
+ for (const entry of entries) {
1062
+ if (partial && !entry.name.toLowerCase().startsWith(partial)) continue;
1063
+ const hasDynamicArguments = (head === "/group" && entry.name === "run")
1064
+ || (head === "/solo" && entry.name === "run")
1065
+ || Boolean(entry.hasArguments);
1066
+ out.push({
1067
+ kind: "subcommand",
1068
+ label: `${prefixSoFar} ${entry.name}`.trim(),
1069
+ replace: `${prefixSoFar} ${entry.name} `.replace(/^\s+/, ""),
1070
+ description: String(entry.desc || entry.summary || entry.description || ""),
1071
+ hasChildren: Boolean((entry.children && typeof entry.children === "object") || hasDynamicArguments),
1072
+ });
1073
+ if (out.length >= limit) break;
1074
+ }
1075
+ if (!endsWithWhitespace && out.length === 1) {
1076
+ const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
1077
+ if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
1078
+ }
1079
+ return out;
1080
+ }
881
1081
  }
882
- const children = node && node.children;
883
- if (!children || typeof children !== "object") return [];
884
- const partial = String(tail[tail.length - 1] || "").toLowerCase();
885
- const prefixSoFar = `${head} ${tail.slice(0, -1).join(" ")}`.replace(/\s+$/, "");
886
- // Sort by `order` (when present) then alphabetically — matches the
887
- // sortCommands helper used by the blessed completion popup.
888
- const entries = Object.keys(children).map((name) => ({
889
- name,
890
- ...children[name],
891
- }));
892
- entries.sort((a, b) => {
893
- const orderA = Number.isFinite(a.order) ? a.order : 999;
894
- const orderB = Number.isFinite(b.order) ? b.order : 999;
895
- if (orderA !== orderB) return orderA - orderB;
896
- return a.name.localeCompare(b.name);
897
- });
1082
+ }
1083
+
1084
+ // Generic top-level argument lists (e.g. /resume <session-id>).
1085
+ if (
1086
+ Array.isArray(argListForHead)
1087
+ && argListForHead.length > 0
1088
+ && !(headNode && headNode.children)
1089
+ && (endsWithWhitespace || tail.length >= 1)
1090
+ ) {
1091
+ if (tail.length > 1) return [];
1092
+ const partial = String(tail[0] || "").toLowerCase();
898
1093
  const out = [];
899
- for (const entry of entries) {
900
- if (!entry.name.toLowerCase().startsWith(partial)) continue;
901
- const hasDynamicArguments = (head === "/group" && entry.name === "run")
902
- || (head === "/solo" && entry.name === "run");
1094
+ for (const item of argListForHead) {
1095
+ const id = String((item && (item.alias || item.cmd || item.id || item.name)) || item || "");
1096
+ if (!id) continue;
1097
+ if (partial && !id.toLowerCase().startsWith(partial)) continue;
1098
+ const desc = String((item && (item.desc || item.summary || item.description || item.source)) || "");
903
1099
  out.push({
904
- kind: "subcommand",
905
- label: `${prefixSoFar} ${entry.name}`.trim(),
906
- replace: `${prefixSoFar} ${entry.name} `.replace(/^\s+/, ""),
907
- description: String(entry.desc || entry.summary || entry.description || ""),
908
- hasChildren: Boolean((entry.children && typeof entry.children === "object") || hasDynamicArguments),
1100
+ kind: "argument",
1101
+ label: `${head} ${id}`,
1102
+ replace: `${head} ${id} `,
1103
+ description: desc,
1104
+ hasChildren: false,
909
1105
  });
910
1106
  if (out.length >= limit) break;
911
1107
  }
912
- if (!endsWithWhitespace && out.length === 1) {
1108
+ if (partial && out.length === 1) {
913
1109
  const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
914
1110
  if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
915
1111
  }
@@ -920,8 +1116,16 @@ function buildCompletions({
920
1116
  const after = trimmed.slice(1);
921
1117
  const prefix = after.toLowerCase();
922
1118
  const list = Array.isArray(commands) ? commands : [];
1119
+ const sorted = list.slice().sort((a, b) => {
1120
+ const orderA = Number.isFinite(a && a.order) ? a.order : 999;
1121
+ const orderB = Number.isFinite(b && b.order) ? b.order : 999;
1122
+ if (orderA !== orderB) return orderA - orderB;
1123
+ const nameA = String((a && a.cmd) || a || "");
1124
+ const nameB = String((b && b.cmd) || b || "");
1125
+ return nameA.localeCompare(nameB);
1126
+ });
923
1127
  const out = [];
924
- for (const item of list) {
1128
+ for (const item of sorted) {
925
1129
  // Registry entries already include the leading '/' in `cmd`. Strip
926
1130
  // it before matching the user's prefix and put it back when we
927
1131
  // render so we don't end up with '//cron'.
@@ -930,18 +1134,27 @@ function buildCompletions({
930
1134
  const lower = bare.toLowerCase();
931
1135
  if (!bare) continue;
932
1136
  if (!lower.startsWith(prefix)) continue;
1137
+ const node = commandTree && commandTree[`/${bare}`];
1138
+ const hasChildren = Boolean(
1139
+ (node && node.children && typeof node.children === "object")
1140
+ || (node && node.hasArguments)
1141
+ || (argumentLists && Array.isArray(argumentLists[`/${bare}`]) && argumentLists[`/${bare}`].length > 0),
1142
+ );
933
1143
  out.push({
934
1144
  kind: "command",
935
1145
  label: `/${bare}`,
936
1146
  replace: `/${bare} `,
937
1147
  description: String((item && (item.desc || item.summary || item.description)) || ""),
938
- hasChildren: Boolean(commandTree && commandTree[`/${bare}`] && commandTree[`/${bare}`].children),
1148
+ hasChildren,
1149
+ optionalArguments: Boolean(node && node.optionalArguments),
939
1150
  });
940
1151
  if (out.length >= limit) break;
941
1152
  }
942
1153
  if (!endsWithWhitespace && out.length === 1) {
943
1154
  const candidate = String(out[0].replace || "").trim().replace(/^\//, "").toLowerCase();
944
- if (candidate === prefix && !out[0].hasChildren) return [];
1155
+ // Exact bare command: close the popup so Enter submits. Commands with
1156
+ // optionalArguments (e.g. /model) are valid both bare and with an arg.
1157
+ if (candidate === prefix && (!out[0].hasChildren || out[0].optionalArguments)) return [];
945
1158
  }
946
1159
  return out;
947
1160
  }
@@ -994,6 +1207,7 @@ module.exports = {
994
1207
  buildToolMergeRowText,
995
1208
  buildCompletions,
996
1209
  buildUcodeBannerLines,
1210
+ buildUcodeSessionLogEntries,
997
1211
  charDisplayWidth,
998
1212
  clampCursorPos,
999
1213
  createEscapeTagStripper,
@@ -1012,11 +1226,13 @@ module.exports = {
1012
1226
  moveCursorVertically,
1013
1227
  normalizeBashToolCommand,
1014
1228
  normalizeModelLabel,
1229
+ normalizeToolLogDetail,
1015
1230
  normalizeToolMergeEntry,
1016
1231
  parseActiveAgentsFromBusStatus,
1017
1232
  planAgentsFooter,
1018
1233
  planProjectsRail,
1019
1234
  renderLogLinesWithMarkdown,
1235
+ renderLogLinesWithMarkdownAnsi,
1020
1236
  resolveAgentSelectionOnDown,
1021
1237
  resolveHistoryDownTransition,
1022
1238
  shouldClearAgentSelectionOnUp,