u-foo 2.5.14 → 3.0.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.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +517 -112
  4. package/src/code/commands.js +77 -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 +703 -0
  9. package/src/code/context/executionSegment.js +292 -0
  10. package/src/code/context/index.js +28 -0
  11. package/src/code/context/planGraph.js +1410 -0
  12. package/src/code/context/planGraphService.js +857 -0
  13. package/src/code/context/planMode.js +398 -0
  14. package/src/code/context/planProjection.js +432 -0
  15. package/src/code/context/projectSnapshot.js +201 -0
  16. package/src/code/context/promptLayers.js +175 -0
  17. package/src/code/context/reducers.js +328 -0
  18. package/src/code/context/stableJson.js +29 -0
  19. package/src/code/context/stateCommit.js +414 -0
  20. package/src/code/context/toolRuntime.js +172 -0
  21. package/src/code/context/transcript.js +182 -0
  22. package/src/code/context/transcriptSync.js +106 -0
  23. package/src/code/context/userInteraction.js +457 -0
  24. package/src/code/context/userNudge.js +116 -0
  25. package/src/code/context/workingSet.js +323 -0
  26. package/src/code/dispatch.js +20 -1
  27. package/src/code/index.js +8 -0
  28. package/src/code/modelCommand.js +87 -0
  29. package/src/code/nativeRunner.js +625 -34
  30. package/src/code/repl.js +196 -50
  31. package/src/code/runtime/agentWakeup.js +58 -0
  32. package/src/code/runtime/graphOwner.js +41 -0
  33. package/src/code/runtime/graphYieldRouter.js +42 -0
  34. package/src/code/runtime/index.js +15 -0
  35. package/src/code/runtime/loopMailbox.js +124 -0
  36. package/src/code/runtime/runtimeEvents.js +39 -0
  37. package/src/code/runtime/taskControl.js +565 -0
  38. package/src/code/runtime/taskFocus.js +165 -0
  39. package/src/code/runtime/taskLoop.js +383 -0
  40. package/src/code/runtime/taskRun.js +187 -0
  41. package/src/code/runtime/toolProvenance.js +70 -0
  42. package/src/code/runtime/workspaceLease.js +208 -0
  43. package/src/code/sessionStore.js +217 -15
  44. package/src/code/skills/index.js +10 -0
  45. package/src/code/skills/injection.js +66 -3
  46. package/src/code/skills/loader.js +21 -0
  47. package/src/code/skills/manifest.js +87 -0
  48. package/src/code/skills/render.js +15 -1
  49. package/src/code/taskDecomposer.js +56 -2
  50. package/src/code/tools/artifactRead.js +40 -0
  51. package/src/code/tools/askUser.js +11 -0
  52. package/src/code/tools/planGraph.js +29 -0
  53. package/src/code/tui.js +2 -0
  54. package/src/code/usageStore.js +15 -0
  55. package/src/ui/format/index.js +285 -45
  56. package/src/ui/format/markdownRenderer.js +436 -71
  57. package/src/ui/ink/ChatApp.js +39 -8
  58. package/src/ui/ink/UcodeApp.js +592 -43
  59. 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
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ const { runAskUserTool } = require("../context/userInteraction");
4
+
5
+ function runAskUserToolDispatch(args = {}, options = {}) {
6
+ return runAskUserTool(args, options);
7
+ }
8
+
9
+ module.exports = {
10
+ runAskUserTool: runAskUserToolDispatch,
11
+ };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ const {
4
+ runPlanGraphCommand,
5
+ normalizePlanGraphCommand,
6
+ } = require("../context/planGraphService");
7
+
8
+ function runPlanGraphTool(args = {}, options = {}) {
9
+ const command = normalizePlanGraphCommand(args) || args;
10
+ const result = runPlanGraphCommand(command, {
11
+ executionState: options.executionState,
12
+ runTool: options.runTool,
13
+ autoAdvance: options.autoAdvance !== false,
14
+ parallel: options.parallel !== false,
15
+ knownTools: options.knownTools,
16
+ maxNodeRuns: options.maxNodeRuns,
17
+ });
18
+
19
+ const payload = result.modelPayload || result;
20
+ return {
21
+ ok: payload.status === "accepted",
22
+ ...payload,
23
+ executionState: result.executionState,
24
+ };
25
+ }
26
+
27
+ module.exports = {
28
+ runPlanGraphTool,
29
+ };
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;
@@ -128,7 +129,16 @@ function normalizeModelLabel(model = "") {
128
129
  return "default";
129
130
  }
130
131
 
131
- function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = "", agentId = "", workspaceRoot = "", sessionId = "", width = 0 } = {}) {
132
+ function buildUcodeBannerLines({
133
+ model = "",
134
+ engine = "ufoo-core",
135
+ nickname = "",
136
+ agentId = "",
137
+ workspaceRoot = "",
138
+ sessionId = "",
139
+ width = 0,
140
+ planMode = false,
141
+ } = {}) {
132
142
  const modelLabel = normalizeModelLabel(model);
133
143
  void width;
134
144
  void engine;
@@ -150,6 +160,9 @@ function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = ""
150
160
  const infoLines = [];
151
161
  infoLines.push(`${chalk.dim("Version:")} ${chalk.cyan.bold(UCODE_VERSION)}`);
152
162
  infoLines.push(`${chalk.dim("Model:")} ${chalk.yellow(modelLabel)}`);
163
+ if (planMode) {
164
+ infoLines.push(`${chalk.dim("Mode:")} ${chalk.magenta.bold("PLAN")}`);
165
+ }
153
166
  infoLines.push(`${chalk.dim("Dictionary:")} ${chalk.gray(shortPath)}`);
154
167
  const normalizedSessionId = String(sessionId || "").trim();
155
168
  if (normalizedSessionId) {
@@ -248,6 +261,130 @@ function renderLogLinesWithMarkdown(text = "", state = {}, escapeFn = (value) =>
248
261
  return renderMarkdownLines(text, state, escapeFn);
249
262
  }
250
263
 
264
+ function renderLogLinesWithMarkdownAnsi(text = "", state = {}) {
265
+ const { renderMarkdownLinesAnsi } = require("./markdownRenderer");
266
+ return renderMarkdownLinesAnsi(text, state);
267
+ }
268
+
269
+ function createMarkdownTableBuffer() {
270
+ const { createMarkdownTableBuffer: create } = require("./markdownRenderer");
271
+ return create();
272
+ }
273
+
274
+ function isTableRowLine(line = "") {
275
+ const { isTableRowLine: check } = require("./markdownRenderer");
276
+ return check(line);
277
+ }
278
+
279
+ function messageContentText(message = {}) {
280
+ if (!message || typeof message !== "object") return "";
281
+ const content = message.content;
282
+ if (typeof content === "string") return content;
283
+ if (Array.isArray(content)) {
284
+ return content.map((part) => {
285
+ if (typeof part === "string") return part;
286
+ if (part && typeof part === "object" && part.text != null) return String(part.text);
287
+ return "";
288
+ }).join("");
289
+ }
290
+ return "";
291
+ }
292
+
293
+ function toolMessagePreview(message = {}) {
294
+ const raw = messageContentText(message).trim();
295
+ if (!raw) return "";
296
+ try {
297
+ const parsed = JSON.parse(raw);
298
+ if (parsed && typeof parsed === "object") {
299
+ if (parsed.preview) return String(parsed.preview);
300
+ if (parsed.artifactId) return `artifact:${parsed.artifactId}`;
301
+ }
302
+ } catch {
303
+ // plain tool text
304
+ }
305
+ return raw;
306
+ }
307
+
308
+ /**
309
+ * Convert persisted nlMessages into ucode TUI log rows for resume/history.
310
+ * Applies the shared ANSI markdown renderer so restored assistant text matches
311
+ * live streaming output.
312
+ */
313
+ function buildUcodeSessionLogEntries(messages = [], options = {}) {
314
+ const list = Array.isArray(messages) ? messages : [];
315
+ const markdownState = options.markdownState && typeof options.markdownState === "object"
316
+ ? options.markdownState
317
+ : { inCodeBlock: false };
318
+ const idPrefix = String(options.idPrefix || "h");
319
+ let seq = Number.isFinite(options.startSeq) ? Math.max(0, Math.floor(options.startSeq)) : 0;
320
+ const maxToolPreviewLines = Number.isFinite(options.maxToolPreviewLines)
321
+ ? Math.max(1, Math.floor(options.maxToolPreviewLines))
322
+ : 4;
323
+ const entries = [];
324
+
325
+ const pushLines = (text, kind) => {
326
+ const source = String(text == null ? "" : text);
327
+ let lines;
328
+ if (kind === "assistant" || kind === "error") {
329
+ try {
330
+ lines = renderLogLinesWithMarkdownAnsi(source, markdownState);
331
+ if (!Array.isArray(lines) || lines.length === 0) lines = source.split(/\r?\n/);
332
+ } catch {
333
+ lines = source.split(/\r?\n/);
334
+ }
335
+ } else {
336
+ lines = source.split(/\r?\n/);
337
+ }
338
+ for (const line of lines) {
339
+ entries.push({
340
+ id: `${idPrefix}-${seq}`,
341
+ text: String(line || ""),
342
+ kind,
343
+ });
344
+ seq += 1;
345
+ }
346
+ };
347
+
348
+ for (const message of list) {
349
+ if (!message || typeof message !== "object") continue;
350
+ const role = String(message.role || "").trim().toLowerCase();
351
+ if (role === "user") {
352
+ const text = messageContentText(message);
353
+ if (!text.trim()) continue;
354
+ const lines = text.split(/\r?\n/);
355
+ lines.forEach((line, index) => {
356
+ pushLines(index === 0 ? `› ${line}` : line, "user");
357
+ });
358
+ continue;
359
+ }
360
+ if (role === "assistant") {
361
+ const text = messageContentText(message);
362
+ if (text) pushLines(text, "assistant");
363
+ const calls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
364
+ if (calls.length > 0) {
365
+ const names = calls
366
+ .map((call) => {
367
+ if (!call || typeof call !== "object") return "";
368
+ if (call.function && call.function.name) return String(call.function.name);
369
+ return String(call.name || "");
370
+ })
371
+ .map((name) => name.trim())
372
+ .filter(Boolean);
373
+ if (names.length > 0) pushLines(`⚙ ${names.join(" · ")}`, "system");
374
+ }
375
+ continue;
376
+ }
377
+ if (role === "tool") {
378
+ const preview = toolMessagePreview(message);
379
+ if (!preview.trim()) continue;
380
+ const clipped = preview.split(/\r?\n/).slice(0, maxToolPreviewLines).join("\n");
381
+ pushLines(clipped, "toolDetail");
382
+ }
383
+ }
384
+
385
+ return { entries, nextSeq: seq, markdownState };
386
+ }
387
+
251
388
  function shouldEnterAgentSelection(inputValue = "") {
252
389
  const text = String(inputValue || "");
253
390
  const trimmed = text.trim();
@@ -514,6 +651,39 @@ function normalizeBashToolCommand(args = {}, payload = {}) {
514
651
  return [command, code].filter(Boolean).join(" · ");
515
652
  }
516
653
 
654
+ function shortenPathDetail(value = "", maxChars = 72) {
655
+ const text = String(value || "").trim().replace(/\\/g, "/");
656
+ if (!text) return "";
657
+ const limit = Number.isFinite(maxChars) && maxChars > 8 ? Math.floor(maxChars) : 72;
658
+ if (text.length <= limit) return text;
659
+ return `…${text.slice(-(limit - 1))}`;
660
+ }
661
+
662
+ function normalizeToolLogDetail(tool = "", args = {}, payload = {}) {
663
+ const name = String(tool || "").trim().toLowerCase();
664
+ const argObj = args && typeof args === "object" ? args : {};
665
+ const resObj = payload && typeof payload === "object" ? payload : {};
666
+
667
+ if (name === "bash") return normalizeBashToolCommand(argObj, resObj);
668
+
669
+ if (name === "read" || name === "write" || name === "edit") {
670
+ const pathText = String(argObj.path || resObj.path || "").trim();
671
+ return shortenPathDetail(pathText);
672
+ }
673
+
674
+ if (name === "artifact_read") {
675
+ const artifactId = String(argObj.artifactId || argObj.id || resObj.artifactId || "").trim();
676
+ const rangeBits = [];
677
+ if (argObj.startLine != null) rangeBits.push(`L${argObj.startLine}`);
678
+ if (argObj.endLine != null) rangeBits.push(`L${argObj.endLine}`);
679
+ const range = rangeBits.length > 0 ? rangeBits.join("-") : "";
680
+ return [artifactId, range].filter(Boolean).join(" · ");
681
+ }
682
+
683
+ const fallback = String(argObj.path || argObj.command || argObj.cmd || "").trim();
684
+ return shortenPathDetail(fallback);
685
+ }
686
+
517
687
  function normalizeToolMergeEntry(entry = {}) {
518
688
  const source = entry && typeof entry === "object" ? entry : {};
519
689
  const tool = String(source.tool || "").trim().toLowerCase() || "tool";
@@ -822,6 +992,7 @@ function buildCompletions({
822
992
  commandTree = null,
823
993
  groupTemplates = [],
824
994
  soloProfiles = [],
995
+ argumentLists = null,
825
996
  limit = 8,
826
997
  } = {}) {
827
998
  const raw = String(text || "");
@@ -830,9 +1001,14 @@ function buildCompletions({
830
1001
  const endsWithWhitespace = /\s$/.test(trimmed);
831
1002
 
832
1003
  if (trimmed.startsWith("/")) {
833
- const parts = trimmed.split(/\s+/);
834
- const head = parts[0]; // "/launch"
835
- const tail = parts.slice(1);
1004
+ const tokenParts = trimmed.trimEnd().split(/\s+/).filter(Boolean);
1005
+ const head = tokenParts[0] || ""; // "/launch"
1006
+ const tail = tokenParts.slice(1);
1007
+ const headKey = head.startsWith("/") ? head : `/${head}`;
1008
+ const headNode = commandTree && typeof commandTree === "object" ? commandTree[headKey] : null;
1009
+ const argListForHead = argumentLists && typeof argumentLists === "object"
1010
+ ? argumentLists[headKey]
1011
+ : null;
836
1012
 
837
1013
  // Dynamic argument completion for /group run <alias> and
838
1014
  // /solo run <profile>. These pull from runtime sources (group
@@ -842,7 +1018,7 @@ function buildCompletions({
842
1018
  : (head === "/solo" && tail[0] === "run")
843
1019
  ? soloProfiles
844
1020
  : null;
845
- if (dynList && (tail.length >= 2 || trimmed.endsWith(" "))) {
1021
+ if (dynList && (tail.length >= 2 || (tail[0] === "run" && endsWithWhitespace))) {
846
1022
  const partial = String(tail[1] || "").toLowerCase();
847
1023
  const out = [];
848
1024
  for (const item of (Array.isArray(dynList) ? dynList : [])) {
@@ -867,49 +1043,91 @@ function buildCompletions({
867
1043
  }
868
1044
 
869
1045
  // 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;
1046
+ if ((tail.length >= 1 || (endsWithWhitespace && headNode && headNode.children)) && commandTree) {
1047
+ let node = headNode;
1048
+ if (!node || typeof node !== "object") {
1049
+ // fall through
1050
+ } else if (node.children && typeof node.children === "object") {
1051
+ const walkTail = endsWithWhitespace && tail.length === 0
1052
+ ? []
1053
+ : (endsWithWhitespace ? tail : tail.slice(0, -1));
1054
+ const partial = endsWithWhitespace && tail.length === 0
1055
+ ? ""
1056
+ : (endsWithWhitespace ? "" : String(tail[tail.length - 1] || "").toLowerCase());
1057
+ let walkNode = node;
1058
+ let walkOk = true;
1059
+ for (const segment of walkTail) {
1060
+ const next = walkNode.children && walkNode.children[segment];
1061
+ if (!next) {
1062
+ walkOk = false;
1063
+ break;
1064
+ }
1065
+ walkNode = next;
1066
+ }
1067
+ const children = walkOk ? (walkNode && walkNode.children) : null;
1068
+ if (children && typeof children === "object") {
1069
+ const prefixSoFar = walkTail.length > 0
1070
+ ? `${head} ${walkTail.join(" ")}`
1071
+ : head;
1072
+ const entries = Object.keys(children).map((name) => ({
1073
+ name,
1074
+ ...children[name],
1075
+ }));
1076
+ entries.sort((a, b) => {
1077
+ const orderA = Number.isFinite(a.order) ? a.order : 999;
1078
+ const orderB = Number.isFinite(b.order) ? b.order : 999;
1079
+ if (orderA !== orderB) return orderA - orderB;
1080
+ return a.name.localeCompare(b.name);
1081
+ });
1082
+ const out = [];
1083
+ for (const entry of entries) {
1084
+ if (partial && !entry.name.toLowerCase().startsWith(partial)) continue;
1085
+ const hasDynamicArguments = (head === "/group" && entry.name === "run")
1086
+ || (head === "/solo" && entry.name === "run")
1087
+ || Boolean(entry.hasArguments);
1088
+ out.push({
1089
+ kind: "subcommand",
1090
+ label: `${prefixSoFar} ${entry.name}`.trim(),
1091
+ replace: `${prefixSoFar} ${entry.name} `.replace(/^\s+/, ""),
1092
+ description: String(entry.desc || entry.summary || entry.description || ""),
1093
+ hasChildren: Boolean((entry.children && typeof entry.children === "object") || hasDynamicArguments),
1094
+ });
1095
+ if (out.length >= limit) break;
1096
+ }
1097
+ if (!endsWithWhitespace && out.length === 1) {
1098
+ const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
1099
+ if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
1100
+ }
1101
+ return out;
1102
+ }
881
1103
  }
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
- });
1104
+ }
1105
+
1106
+ // Generic top-level argument lists (e.g. /resume <session-id>).
1107
+ if (
1108
+ Array.isArray(argListForHead)
1109
+ && argListForHead.length > 0
1110
+ && !(headNode && headNode.children)
1111
+ && (endsWithWhitespace || tail.length >= 1)
1112
+ ) {
1113
+ if (tail.length > 1) return [];
1114
+ const partial = String(tail[0] || "").toLowerCase();
898
1115
  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");
1116
+ for (const item of argListForHead) {
1117
+ const id = String((item && (item.alias || item.cmd || item.id || item.name)) || item || "");
1118
+ if (!id) continue;
1119
+ if (partial && !id.toLowerCase().startsWith(partial)) continue;
1120
+ const desc = String((item && (item.desc || item.summary || item.description || item.source)) || "");
903
1121
  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),
1122
+ kind: "argument",
1123
+ label: `${head} ${id}`,
1124
+ replace: `${head} ${id} `,
1125
+ description: desc,
1126
+ hasChildren: false,
909
1127
  });
910
1128
  if (out.length >= limit) break;
911
1129
  }
912
- if (!endsWithWhitespace && out.length === 1) {
1130
+ if (partial && out.length === 1) {
913
1131
  const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
914
1132
  if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
915
1133
  }
@@ -920,8 +1138,16 @@ function buildCompletions({
920
1138
  const after = trimmed.slice(1);
921
1139
  const prefix = after.toLowerCase();
922
1140
  const list = Array.isArray(commands) ? commands : [];
1141
+ const sorted = list.slice().sort((a, b) => {
1142
+ const orderA = Number.isFinite(a && a.order) ? a.order : 999;
1143
+ const orderB = Number.isFinite(b && b.order) ? b.order : 999;
1144
+ if (orderA !== orderB) return orderA - orderB;
1145
+ const nameA = String((a && a.cmd) || a || "");
1146
+ const nameB = String((b && b.cmd) || b || "");
1147
+ return nameA.localeCompare(nameB);
1148
+ });
923
1149
  const out = [];
924
- for (const item of list) {
1150
+ for (const item of sorted) {
925
1151
  // Registry entries already include the leading '/' in `cmd`. Strip
926
1152
  // it before matching the user's prefix and put it back when we
927
1153
  // render so we don't end up with '//cron'.
@@ -930,18 +1156,27 @@ function buildCompletions({
930
1156
  const lower = bare.toLowerCase();
931
1157
  if (!bare) continue;
932
1158
  if (!lower.startsWith(prefix)) continue;
1159
+ const node = commandTree && commandTree[`/${bare}`];
1160
+ const hasChildren = Boolean(
1161
+ (node && node.children && typeof node.children === "object")
1162
+ || (node && node.hasArguments)
1163
+ || (argumentLists && Array.isArray(argumentLists[`/${bare}`]) && argumentLists[`/${bare}`].length > 0),
1164
+ );
933
1165
  out.push({
934
1166
  kind: "command",
935
1167
  label: `/${bare}`,
936
1168
  replace: `/${bare} `,
937
1169
  description: String((item && (item.desc || item.summary || item.description)) || ""),
938
- hasChildren: Boolean(commandTree && commandTree[`/${bare}`] && commandTree[`/${bare}`].children),
1170
+ hasChildren,
1171
+ optionalArguments: Boolean(node && node.optionalArguments),
939
1172
  });
940
1173
  if (out.length >= limit) break;
941
1174
  }
942
1175
  if (!endsWithWhitespace && out.length === 1) {
943
1176
  const candidate = String(out[0].replace || "").trim().replace(/^\//, "").toLowerCase();
944
- if (candidate === prefix && !out[0].hasChildren) return [];
1177
+ // Exact bare command: close the popup so Enter submits. Commands with
1178
+ // optionalArguments (e.g. /model) are valid both bare and with an arg.
1179
+ if (candidate === prefix && (!out[0].hasChildren || out[0].optionalArguments)) return [];
945
1180
  }
946
1181
  return out;
947
1182
  }
@@ -994,6 +1229,7 @@ module.exports = {
994
1229
  buildToolMergeRowText,
995
1230
  buildCompletions,
996
1231
  buildUcodeBannerLines,
1232
+ buildUcodeSessionLogEntries,
997
1233
  charDisplayWidth,
998
1234
  clampCursorPos,
999
1235
  createEscapeTagStripper,
@@ -1012,11 +1248,15 @@ module.exports = {
1012
1248
  moveCursorVertically,
1013
1249
  normalizeBashToolCommand,
1014
1250
  normalizeModelLabel,
1251
+ normalizeToolLogDetail,
1015
1252
  normalizeToolMergeEntry,
1016
1253
  parseActiveAgentsFromBusStatus,
1017
1254
  planAgentsFooter,
1018
1255
  planProjectsRail,
1019
1256
  renderLogLinesWithMarkdown,
1257
+ renderLogLinesWithMarkdownAnsi,
1258
+ createMarkdownTableBuffer,
1259
+ isTableRowLine,
1020
1260
  resolveAgentSelectionOnDown,
1021
1261
  resolveHistoryDownTransition,
1022
1262
  shouldClearAgentSelectionOnUp,