u-foo 3.0.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -11,7 +11,10 @@ function getDoingTasksSection() {
11
11
  - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs).
12
12
  - Don't create helpers, utilities, or abstractions for one-time operations. Three similar lines of code is better than a premature abstraction.
13
13
  - Follow workspace conventions and project instructions (AGENTS.md) when present.
14
- - Prefer concrete code edits and verifiable outcomes over explanations.`;
14
+ - Prefer concrete code edits and verifiable outcomes over explanations.
15
+ - For simple, single-goal work, execute directly with read/write/edit/bash.
16
+ - For complex work — multiple goals, several subsystems, long multi-step delivery, or clear parallel tracks — automatically decompose before diving in: split into concrete sub-objectives, then start one or more TaskRuns via task_run (standalone; no Plan Mode required). Use plan_graph only when durable dependencies, checkpoints, or a shared executable plan are needed.
17
+ - When decomposing, each TaskRun objective should be independently verifiable; prefer a few sharp tasks over one vague mega-task.`;
15
18
  }
16
19
 
17
20
  module.exports = { getDoingTasksSection };
@@ -166,6 +166,7 @@ function createCommandExecutor(options = {}) {
166
166
  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
167
167
  schedule = (fn, ms) => setTimeout(fn, ms),
168
168
  clearLog = null,
169
+ fetchModelsImpl = null,
169
170
  } = options;
170
171
 
171
172
  if (!projectRoot) {
@@ -1623,7 +1624,35 @@ function createCommandExecutor(options = {}) {
1623
1624
  logMessage("system", ` • url: ${url || "(unset)"}`);
1624
1625
  logMessage("system", ` • key: ${maskSecret(key)}`);
1625
1626
  logMessage("system", ` • transport: ${transport} (auto)`);
1627
+ try {
1628
+ const { currentThinkingLevel } = require("../../code/modelCommand");
1629
+ logMessage("system", ` • thinking: ${currentThinkingLevel({})}`);
1630
+ } catch {
1631
+ // ignore
1632
+ }
1626
1633
  logMessage("system", " • tip: url supports generic gateway base, transport is auto-detected");
1634
+ try {
1635
+ const { listUcodeModels } = require("../../code/modelCommand");
1636
+ const listed = await listUcodeModels({
1637
+ provider,
1638
+ model,
1639
+ }, {
1640
+ workspaceRoot: getActiveProjectRoot() || projectRoot,
1641
+ fetchImpl: fetchModelsImpl || undefined,
1642
+ });
1643
+ if (listed.ok && listed.models.length > 0) {
1644
+ const sample = listed.models.slice(0, 10);
1645
+ logMessage("system", ` • models route: ${listed.models.length} available`);
1646
+ logMessage("system", ` • models: ${sample.join(", ")}${listed.models.length > 10 ? "…" : ""}`);
1647
+ if (model && !listed.models.includes(model)) {
1648
+ logMessage("system", ` • warning: configured model "${model}" is not in the provider catalog`);
1649
+ }
1650
+ } else if (listed.error) {
1651
+ logMessage("system", ` • models route: ${listed.error}`);
1652
+ }
1653
+ } catch (err) {
1654
+ logMessage("system", ` • models route: ${err && err.message ? err.message : "unavailable"}`);
1655
+ }
1627
1656
  return;
1628
1657
  }
1629
1658
 
@@ -1645,6 +1674,52 @@ function createCommandExecutor(options = {}) {
1645
1674
  logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings ucode set provider=<openai|anthropic> model=<id> url=<baseUrl> key=<apiKey>");
1646
1675
  return;
1647
1676
  }
1677
+
1678
+ // Preview the config that would apply after this set, then confirm the
1679
+ // model id against the provider /models route when a model is present.
1680
+ const preview = {
1681
+ ...(loadUcodeConfig() || {}),
1682
+ ...updates,
1683
+ };
1684
+ const modelToConfirm = String(preview.ucodeModel || "").trim();
1685
+ if (modelToConfirm) {
1686
+ try {
1687
+ const { confirmModelSupported } = require("../../code/providers/modelsCatalog");
1688
+ const { resolveRuntimeConfig } = require("../../code/nativeRunner");
1689
+ const runtime = resolveRuntimeConfig({
1690
+ workspaceRoot: getActiveProjectRoot() || projectRoot,
1691
+ provider: preview.ucodeProvider || "",
1692
+ model: modelToConfirm,
1693
+ });
1694
+ // Prefer the previewed url/key over env-only resolution so a single
1695
+ // set line that changes url+model validates against the new gateway.
1696
+ const confirmation = await confirmModelSupported({
1697
+ provider: runtime.provider,
1698
+ transport: inferUcodeTransport(preview.ucodeProvider, preview.ucodeBaseUrl || runtime.baseUrl),
1699
+ baseUrl: String(preview.ucodeBaseUrl || runtime.baseUrl || "").trim(),
1700
+ apiKey: String(preview.ucodeApiKey || runtime.apiKey || "").trim(),
1701
+ model: modelToConfirm,
1702
+ strict: false,
1703
+ fetchImpl: fetchModelsImpl || undefined,
1704
+ });
1705
+ if (!confirmation.allowed) {
1706
+ logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(confirmation.error || "model not supported")}`);
1707
+ if (confirmation.models && confirmation.models.length > 0) {
1708
+ const sample = confirmation.models.slice(0, 10).join(", ");
1709
+ logMessage("system", ` • available: ${escapeBlessed(sample)}${confirmation.models.length > 10 ? "…" : ""}`);
1710
+ }
1711
+ return;
1712
+ }
1713
+ if (confirmation.warning) {
1714
+ logMessage("system", `{gray-fg}note:{/gray-fg} ${escapeBlessed(confirmation.warning)}`);
1715
+ } else if (confirmation.ok) {
1716
+ logMessage("system", `{gray-fg}models route:{/gray-fg} confirmed ${escapeBlessed(modelToConfirm)}`);
1717
+ }
1718
+ } catch (err) {
1719
+ logMessage("system", `{gray-fg}note:{/gray-fg} models route check skipped (${escapeBlessed(err && err.message ? err.message : "unavailable")})`);
1720
+ }
1721
+ }
1722
+
1648
1723
  saveUcodeConfig(updates);
1649
1724
  logMessage("system", "{white-fg}✓{/white-fg} ucode config updated (global)");
1650
1725
  if (Object.prototype.hasOwnProperty.call(updates, "ucodeProvider")) {
@@ -1664,6 +1739,41 @@ function createCommandExecutor(options = {}) {
1664
1739
  return;
1665
1740
  }
1666
1741
 
1742
+ if (action === "models") {
1743
+ try {
1744
+ const { listUcodeModels } = require("../../code/modelCommand");
1745
+ const config = loadUcodeConfig() || {};
1746
+ const listed = await listUcodeModels({
1747
+ provider: config.ucodeProvider || "",
1748
+ model: config.ucodeModel || "",
1749
+ }, {
1750
+ workspaceRoot: getActiveProjectRoot() || projectRoot,
1751
+ skipCache: true,
1752
+ fetchImpl: fetchModelsImpl || undefined,
1753
+ });
1754
+ if (!listed.ok) {
1755
+ logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(listed.error || "models route failed")}`);
1756
+ if (listed.url) logMessage("system", ` • url: ${escapeBlessed(listed.url)}`);
1757
+ return;
1758
+ }
1759
+ logMessage("system", `{cyan-fg}ucode models:{/cyan-fg} ${listed.models.length} from ${escapeBlessed(listed.url)}`);
1760
+ if (listed.models.length === 0) {
1761
+ logMessage("system", " • (empty catalog)");
1762
+ return;
1763
+ }
1764
+ listed.models.slice(0, 40).forEach((id) => {
1765
+ const current = String(config.ucodeModel || "").trim() === id ? " {gray-fg}(current){/gray-fg}" : "";
1766
+ logMessage("system", ` • ${escapeBlessed(id)}${current}`);
1767
+ });
1768
+ if (listed.models.length > 40) {
1769
+ logMessage("system", ` • … ${listed.models.length - 40} more`);
1770
+ }
1771
+ } catch (err) {
1772
+ logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(err && err.message ? err.message : "models route failed")}`);
1773
+ }
1774
+ return;
1775
+ }
1776
+
1667
1777
  if (action === "clear") {
1668
1778
  const fieldsRaw = args.slice(1).map((item) => String(item || "").trim().toLowerCase()).filter(Boolean);
1669
1779
  const fields = fieldsRaw.length === 0 ? ["all"] : fieldsRaw;
@@ -1682,7 +1792,7 @@ function createCommandExecutor(options = {}) {
1682
1792
  return;
1683
1793
  }
1684
1794
 
1685
- logMessage("error", "{white-fg}✗{/white-fg} Unknown settings ucode action. Use: show, set, clear");
1795
+ logMessage("error", "{white-fg}✗{/white-fg} Unknown settings ucode action. Use: show, set, models, clear");
1686
1796
  }
1687
1797
 
1688
1798
  async function executeCommand(text) {
@@ -135,7 +135,8 @@ const COMMAND_TREE = {
135
135
  children: {
136
136
  show: { desc: "Show ucode provider/model/url/key", order: 1 },
137
137
  set: { desc: "Set ucode provider/model/url/key", order: 2 },
138
- clear: { desc: "Clear ucode provider/model/url/key", order: 3 },
138
+ models: { desc: "List models from the provider /models route", order: 3 },
139
+ clear: { desc: "Clear ucode provider/model/url/key", order: 4 },
139
140
  },
140
141
  },
141
142
  },
@@ -416,7 +416,7 @@ function createDaemonMessageRouter(options = {}) {
416
416
  const publisher = report.agent_id || data.publisher || "ufoo-agent";
417
417
  const displayName = resolveAgentDisplayName(publisher);
418
418
  const detail = report.summary || report.message || data.message || report.task_id || "report";
419
- logMessage("bus", `${speakerPrefix(displayName)}${escapeBlessed(detail)}`);
419
+ logMessage("report", `${speakerPrefix(displayName)}${escapeBlessed(detail)}`, data);
420
420
  requestStatus();
421
421
  renderScreen();
422
422
  return true;
@@ -33,8 +33,9 @@ function createInputSubmitHandler(options = {}) {
33
33
 
34
34
  function userEcho(text, targetLabel = "") {
35
35
  const body = escapeBlessed(text);
36
- if (!targetLabel) return body;
37
- return `{magenta-fg}@${escapeBlessed(targetLabel)}{/magenta-fg} ${body}`;
36
+ // Match ucode › prompt echo so history reload and live log share a prefix.
37
+ if (!targetLabel) return `› ${body}`;
38
+ return `› {magenta-fg}@${escapeBlessed(targetLabel)}{/magenta-fg} ${body}`;
38
39
  }
39
40
 
40
41
  async function tryActivateTargetAgent(agentId) {
@@ -7,7 +7,7 @@
7
7
  const UCODE_COMMAND_REGISTRY = [
8
8
  { cmd: "/help", desc: "Show available commands", order: 10 },
9
9
  { cmd: "/status", desc: "Show session / usage status", order: 20 },
10
- { cmd: "/model", desc: "Show or switch the active model", order: 25 },
10
+ { cmd: "/model", desc: "Show or switch model (+ thinking intensity)", order: 25 },
11
11
  { cmd: "/plan", desc: "Show plan progress or set plan mode", order: 27 },
12
12
  { cmd: "/ubus", desc: "Check pending bus messages", order: 30 },
13
13
  { cmd: "/resume", desc: "Resume a saved session", order: 40 },
@@ -20,7 +20,7 @@ const UCODE_COMMAND_TREE = {
20
20
  "/help": { desc: "Show available commands" },
21
21
  "/status": { desc: "Show session / usage status" },
22
22
  "/model": {
23
- desc: "Show or switch the active model",
23
+ desc: "Show or switch model, then pick thinking intensity",
24
24
  hasArguments: true,
25
25
  optionalArguments: true,
26
26
  },
@@ -59,7 +59,7 @@ function listUcodeCommandsForHelp() {
59
59
  " /exit|/quit",
60
60
  " /ubus",
61
61
  " /status",
62
- " /model [model-id]",
62
+ " /model [model-id] [off|low|medium|high|max]",
63
63
  " /plan [on|off|show|hide|focus|debug|clear]",
64
64
  " /skills [list]",
65
65
  " /skills show <name>",
@@ -31,7 +31,7 @@ const {
31
31
  } = require("../skills");
32
32
  const { hashContent } = require("./artifacts");
33
33
 
34
- const PROMPT_VERSION = "native-v5";
34
+ const PROMPT_VERSION = "native-v6";
35
35
 
36
36
  function buildImmutablePrefix() {
37
37
  return [
@@ -44,19 +44,20 @@ function buildImmutablePrefix() {
44
44
  getOutputEfficiencySection(),
45
45
  [
46
46
  "Tool calling grammar:",
47
- "- Use read, write, edit, bash, and artifact_read for direct work, even when it takes several tool calls. Use plan_graph only when the work needs a durable semantic/executable plan, explicit dependencies or checkpoints, or asynchronous TaskRuns.",
48
- "- Plan Mode is a runtime posture for the Agent Loop, not an agent tool. While Plan Mode is ON, direct write, edit, and bash calls from the Agent Loop are blocked; read and artifact_read remain available.",
47
+ "- Use read, write, edit, bash, and artifact_read for direct, single-goal work, even when it takes several tool calls.",
48
+ "- TaskRuns are orthogonal to Plan Mode. A TaskRun does not require Plan Mode or a plan_graph. Use task_run operation=start with an objective for a standalone single-point TaskRun; it starts asynchronously and returns immediately.",
49
+ "- On complex or multi-goal requests, automatically decompose into concrete sub-objectives and start TaskRun(s) via task_run. Prefer task_run for independent or loosely coupled tracks; use plan_graph only when you need durable dependencies, checkpoints, or a shared executable plan.",
50
+ "- Use plan_graph for durable graph structure: create, patch, inspect, cancel_graph, and control. Graph-bound TaskRuns use plan_graph control.start_task on execution.kind=task_loop nodes.",
51
+ "- Plan Mode is a runtime posture for the Agent Loop, not an agent tool. While Plan Mode is ON, direct write, edit, and bash calls from the Agent Loop are blocked; read and artifact_read remain available. TaskRuns still run independently of Plan Mode.",
49
52
  "- In the Agent Loop, plan_graph operation=create automatically enables Plan Mode. The user may also use /plan on or /plan off.",
50
- "- Turning Plan Mode off does not cancel an existing graph or running TaskRuns. Use plan_graph operation=cancel_graph or operation=control with cancel_task to stop them.",
53
+ "- Turning Plan Mode off does not cancel an existing graph or running TaskRuns. Cancel with task_run (standalone) or plan_graph operation=cancel_graph / control.cancel_task (graph-bound).",
51
54
  "- When the user enables Plan Mode and no active graph exists, create a plan_graph before performing side effects.",
52
- "- Use plan_graph for durable graph structure and TaskRun lifecycle: create, patch, inspect, cancel_graph, and control.",
53
55
  "- After an accepted plan_graph create or patch, Runtime automatically advances ready tool nodes. Never invent or request an execute_graph tool.",
54
- "- Do not call plan_graph together with read, write, edit, bash, or artifact_read in the same assistant turn.",
56
+ "- Do not call plan_graph or task_run together with read, write, edit, bash, or artifact_read in the same assistant turn.",
55
57
  "- When an active graph is waiting on a task, advance that node through plan_graph instead of bypassing it with direct workspace tools: use patch.expand_node for execution.kind=expand, control.complete_task (nodeId) for execution.kind=inline_llm, or control.start_task for execution.kind=task_loop.",
56
- "- control.complete_task with nodeId completes a waiting_llm inline_llm task for the current Graph owner. control.complete_task with taskRunId is reserved for the owning TaskLoop. Do not directly complete expand or aggregate tasks.",
57
- "- While Plan Mode is ON, workspace mutations must be represented as plan_graph tool nodes or performed inside a running task_loop.",
58
+ "- control.complete_task with nodeId completes a waiting_llm inline_llm task for the current Graph owner. control.complete_task with taskRunId (or task_run complete) is reserved for the owning TaskLoop. Do not directly complete expand or aggregate tasks.",
59
+ "- While Plan Mode is ON, workspace mutations must be represented as plan_graph tool nodes or performed inside a running TaskRun/task_loop.",
58
60
  "- Treat a User reminder as the latest user instruction. Reconcile it before continuing from tool results. If it is compatible with the active plan, resume the waiting plan node; otherwise patch, cancel, or replan first.",
59
- "- Use execution.kind=task_loop for work that should continue asynchronously without occupying the Agent Loop. plan_graph operation=control action=start_task starts the TaskRun and returns immediately.",
60
61
  "- TaskLoops do not consume User reminders. The Agent Loop is woken by runtime task_started, task_succeeded, task_failed, and task_cancelled events.",
61
62
  "- Runtime enforces TaskRun concurrency limits and workspace write leases. Direct Agent write, edit, or bash calls may be rejected while writing TaskRuns are active.",
62
63
  "- Tool results may contain an artifactId. Use artifact_read to hydrate raw stored output or a slice of it; use read for workspace file paths.",
@@ -6,6 +6,7 @@ const { runEditTool } = require("./tools/edit");
6
6
  const { runBashTool } = require("./tools/bash");
7
7
  const { runArtifactReadTool } = require("./tools/artifactRead");
8
8
  const { runPlanGraphTool } = require("./tools/planGraph");
9
+ const { runTaskRunTool } = require("./tools/taskRun");
9
10
  const { runAskUserTool } = require("./tools/askUser");
10
11
 
11
12
  const TOOL_NAMES = [
@@ -15,6 +16,7 @@ const TOOL_NAMES = [
15
16
  "bash",
16
17
  "artifact_read",
17
18
  "plan_graph",
19
+ "task_run",
18
20
  "ask_user",
19
21
  ];
20
22
 
@@ -26,6 +28,7 @@ function normalizeToolName(value = "") {
26
28
  if (text === "bash") return "bash";
27
29
  if (text === "artifact_read" || text === "artifact-read" || text === "artifactread") return "artifact_read";
28
30
  if (text === "plan_graph" || text === "plan-graph" || text === "plangraph") return "plan_graph";
31
+ if (text === "task_run" || text === "task-run" || text === "taskrun") return "task_run";
29
32
  if (text === "ask_user" || text === "ask-user" || text === "askuser") return "ask_user";
30
33
  return "";
31
34
  }
@@ -45,6 +48,7 @@ function runToolCall(input = {}, options = {}) {
45
48
  if (tool === "edit") return runEditTool(args, options);
46
49
  if (tool === "artifact_read") return runArtifactReadTool(args, options);
47
50
  if (tool === "plan_graph") return runPlanGraphTool(args, options);
51
+ if (tool === "task_run") return runTaskRunTool(args, options);
48
52
  if (tool === "ask_user") return runAskUserTool(args, options);
49
53
  return runBashTool(args, options);
50
54
  }
@@ -1,37 +1,176 @@
1
1
  "use strict";
2
2
 
3
- const { saveGlobalUcodeConfig } = require("../config");
3
+ const { saveGlobalUcodeConfig, loadGlobalUcodeConfig } = require("../config");
4
+ const {
5
+ listProviderModels,
6
+ confirmModelSupported,
7
+ } = require("./providers/modelsCatalog");
8
+ const {
9
+ normalizeThinkingLevel,
10
+ suggestThinkingLevels,
11
+ applyThinkingLevelToEnv,
12
+ resolveThinkingFromEnvAndConfig,
13
+ DEFAULT_THINKING_LEVEL,
14
+ } = require("./thinkingLevels");
15
+
16
+ function fallbackModelSuggestions(provider = "") {
17
+ const text = String(provider || "").trim().toLowerCase();
18
+ if (text.includes("anthropic") || text.includes("claude")) {
19
+ return ["claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"];
20
+ }
21
+ if (text.includes("kimi") || text.includes("moonshot")) {
22
+ return ["k3", "kimi-k2.5", "moonshot-v1-128k"];
23
+ }
24
+ return ["gpt-5.4", "gpt-5.3", "o3", "o4-mini"];
25
+ }
26
+
27
+ function resolveModelRuntime(state = {}, options = {}) {
28
+ // Lazy require: nativeRunner pulls agent/repl paths that can load modelCommand.
29
+ const { resolveRuntimeConfig } = require("./nativeRunner");
30
+ return resolveRuntimeConfig({
31
+ workspaceRoot: options.workspaceRoot || process.cwd(),
32
+ provider: options.provider || (state && state.provider) || "",
33
+ model: options.model || (state && state.model) || "",
34
+ });
35
+ }
36
+
37
+ function currentThinkingLevel(state = {}) {
38
+ let configLevel = "";
39
+ try {
40
+ configLevel = String((loadGlobalUcodeConfig() || {}).ucodeThinking || "").trim();
41
+ } catch {
42
+ configLevel = "";
43
+ }
44
+ const fromState = normalizeThinkingLevel(state && state.thinking);
45
+ const resolved = resolveThinkingFromEnvAndConfig({
46
+ env: process.env,
47
+ configLevel: fromState || configLevel,
48
+ });
49
+ if (resolved.level) return resolved.level;
50
+ if (resolved.source === "env-budget") {
51
+ // Approximate a named level for the secondary menu highlight.
52
+ const budget = Number(resolved.budgetTokens) || 0;
53
+ if (budget <= 0) return "off";
54
+ if (budget <= 3000) return "low";
55
+ if (budget <= 16000) return "medium";
56
+ if (budget <= 40000) return "high";
57
+ return "max";
58
+ }
59
+ return DEFAULT_THINKING_LEVEL;
60
+ }
61
+
62
+ function persistThinkingLevel(state = {}, level = "") {
63
+ const normalized = normalizeThinkingLevel(level);
64
+ if (!normalized) return "";
65
+ if (state && typeof state === "object") state.thinking = normalized;
66
+ try {
67
+ saveGlobalUcodeConfig({ ucodeThinking: normalized });
68
+ } catch {
69
+ // best-effort
70
+ }
71
+ applyThinkingLevelToEnv(normalized, process.env);
72
+ return normalized;
73
+ }
74
+
75
+ /**
76
+ * Fetch models from the configured provider's /models route.
77
+ */
78
+ async function listUcodeModels(state = {}, options = {}) {
79
+ const runtime = resolveModelRuntime(state, options);
80
+ const listed = await listProviderModels({
81
+ ...runtime,
82
+ fetchImpl: options.fetchImpl,
83
+ timeoutMs: options.timeoutMs,
84
+ skipCache: options.skipCache === true,
85
+ });
86
+ return {
87
+ ...listed,
88
+ provider: runtime.provider,
89
+ transport: runtime.transport,
90
+ baseUrl: runtime.baseUrl,
91
+ };
92
+ }
4
93
 
5
94
  /**
6
95
  * Apply /model show|set against the live session state.
7
- * Persists ucodeModel to the global config so the next launch keeps it.
96
+ * Persists ucodeModel / ucodeThinking so the next launch keeps them.
97
+ * Set validates against the provider models route when available.
98
+ *
99
+ * result.shape:
100
+ * { action: "show" }
101
+ * { action: "set", model, thinking? }
8
102
  */
9
- function applyUcodeModelCommand(state = {}, result = {}) {
103
+ async function applyUcodeModelCommand(state = {}, result = {}, options = {}) {
10
104
  const action = String((result && result.action) || "").trim().toLowerCase();
11
105
  if (action === "show") {
12
106
  const model = String((state && state.model) || "").trim() || "(unset)";
13
107
  const provider = String((state && state.provider) || "").trim() || "(unset)";
108
+ const thinking = currentThinkingLevel(state);
109
+ const lines = [
110
+ `model: ${model}`,
111
+ `provider: ${provider}`,
112
+ `thinking: ${thinking}`,
113
+ "usage: /model <model-id> [off|low|medium|high|max]",
114
+ ];
115
+ try {
116
+ const listed = await listUcodeModels(state, options);
117
+ if (listed.ok && listed.models.length > 0) {
118
+ const sample = listed.models.slice(0, 12);
119
+ lines.push(`models route: ${listed.url}`);
120
+ lines.push(`available (${listed.models.length}): ${sample.join(", ")}${listed.models.length > 12 ? "…" : ""}`);
121
+ } else if (listed.error) {
122
+ lines.push(`models route: ${listed.error}`);
123
+ }
124
+ } catch (err) {
125
+ lines.push(`models route: ${err && err.message ? err.message : "unavailable"}`);
126
+ }
14
127
  return {
15
128
  ok: true,
16
129
  error: "",
17
- output: [
18
- `model: ${model}`,
19
- `provider: ${provider}`,
20
- "usage: /model <model-id>",
21
- ].join("\n"),
130
+ output: lines.join("\n"),
22
131
  model: String((state && state.model) || "").trim(),
132
+ thinking,
23
133
  };
24
134
  }
25
135
  if (action === "set") {
26
136
  const next = String((result && result.model) || "").trim();
137
+ const thinkingRaw = String((result && result.thinking) || "").trim();
138
+ const thinkingNext = normalizeThinkingLevel(thinkingRaw);
27
139
  if (!next) {
28
140
  return {
29
141
  ok: false,
30
- error: "usage: /model [model-id]",
31
- output: "usage: /model [model-id]",
142
+ error: "usage: /model [model-id] [off|low|medium|high|max]",
143
+ output: "usage: /model [model-id] [off|low|medium|high|max]",
32
144
  };
33
145
  }
146
+ if (thinkingRaw && !thinkingNext) {
147
+ return {
148
+ ok: false,
149
+ error: `unknown thinking level "${thinkingRaw}" (use off|low|medium|high|max)`,
150
+ output: `unknown thinking level "${thinkingRaw}" (use off|low|medium|high|max)`,
151
+ };
152
+ }
153
+
154
+ const runtime = resolveModelRuntime(state, { ...options, model: next });
155
+ const confirmation = await confirmModelSupported({
156
+ ...runtime,
157
+ model: next,
158
+ fetchImpl: options.fetchImpl,
159
+ timeoutMs: options.timeoutMs,
160
+ skipCache: options.skipCache === true,
161
+ strict: options.strict === true,
162
+ });
163
+ if (!confirmation.allowed) {
164
+ return {
165
+ ok: false,
166
+ error: confirmation.error || `model "${next}" is not supported`,
167
+ output: confirmation.error || `model "${next}" is not supported`,
168
+ models: confirmation.models,
169
+ };
170
+ }
171
+
34
172
  const previous = String((state && state.model) || "").trim();
173
+ const previousThinking = currentThinkingLevel(state);
35
174
  if (state && typeof state === "object") state.model = next;
36
175
  try {
37
176
  saveGlobalUcodeConfig({ ucodeModel: next });
@@ -43,45 +182,82 @@ function applyUcodeModelCommand(state = {}, result = {}) {
43
182
  } catch {
44
183
  // ignore env write failures
45
184
  }
46
- const output = previous && previous !== next
185
+
186
+ const lines = [];
187
+ const modelOutput = previous && previous !== next
47
188
  ? `model switched: ${previous} → ${next}`
48
189
  : `model set: ${next}`;
190
+ lines.push(modelOutput);
191
+
192
+ let appliedThinking = "";
193
+ if (thinkingNext) {
194
+ appliedThinking = persistThinkingLevel(state, thinkingNext);
195
+ if (previousThinking && previousThinking !== appliedThinking) {
196
+ lines.push(`thinking: ${previousThinking} → ${appliedThinking}`);
197
+ } else {
198
+ lines.push(`thinking: ${appliedThinking}`);
199
+ }
200
+ }
201
+
202
+ if (confirmation.warning) lines.push(`note: ${confirmation.warning}`);
203
+ if (confirmation.ok && confirmation.models.length > 0) {
204
+ lines.push(`confirmed via models route (${confirmation.models.length} available)`);
205
+ }
49
206
  return {
50
207
  ok: true,
51
208
  error: "",
52
- output,
209
+ output: lines.join("\n"),
53
210
  model: next,
54
211
  previous,
212
+ thinking: appliedThinking || previousThinking,
213
+ warning: confirmation.warning || "",
214
+ models: confirmation.models,
55
215
  };
56
216
  }
57
217
  return {
58
218
  ok: false,
59
- error: "usage: /model [model-id]",
60
- output: "usage: /model [model-id]",
219
+ error: "usage: /model [model-id] [off|low|medium|high|max]",
220
+ output: "usage: /model [model-id] [off|low|medium|high|max]",
61
221
  };
62
222
  }
63
223
 
64
- function suggestUcodeModels(state = {}) {
224
+ /**
225
+ * Build /model completion rows. Prefer a live models-route catalog when
226
+ * provided; otherwise fall back to a small hardcoded list.
227
+ * Models are marked hasChildren so the TUI opens a thinking-intensity
228
+ * secondary menu after the id is chosen.
229
+ */
230
+ function suggestUcodeModels(state = {}, options = {}) {
65
231
  const current = String((state && state.model) || "").trim();
66
232
  const provider = String((state && state.provider) || "").trim().toLowerCase();
67
- let defaults = ["gpt-5.4", "gpt-5.3", "o3", "o4-mini"];
68
- if (provider.includes("anthropic") || provider.includes("claude")) {
69
- defaults = ["claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"];
70
- } else if (provider.includes("kimi") || provider.includes("moonshot")) {
71
- defaults = ["kimi-k2.5", "moonshot-v1-128k"];
72
- }
233
+ const remote = Array.isArray(options.models)
234
+ ? options.models.map((item) => String(item || "").trim()).filter(Boolean)
235
+ : [];
236
+ const defaults = remote.length > 0 ? remote : fallbackModelSuggestions(provider);
73
237
  const ids = [];
74
238
  if (current) ids.push(current);
75
239
  for (const id of defaults) {
76
240
  if (id && !ids.includes(id)) ids.push(id);
77
241
  }
78
- return ids.map((id) => ({
242
+ return ids.slice(0, 40).map((id) => ({
79
243
  id,
80
- desc: id === current ? "current" : "",
244
+ desc: id === current
245
+ ? "current · pick thinking next"
246
+ : (remote.length > 0 ? "models route · pick thinking next" : "pick thinking next"),
247
+ hasChildren: true,
81
248
  }));
82
249
  }
83
250
 
251
+ function suggestUcodeThinkingLevels(state = {}) {
252
+ return suggestThinkingLevels({ current: currentThinkingLevel(state) });
253
+ }
254
+
84
255
  module.exports = {
85
256
  applyUcodeModelCommand,
86
257
  suggestUcodeModels,
258
+ suggestUcodeThinkingLevels,
259
+ listUcodeModels,
260
+ fallbackModelSuggestions,
261
+ currentThinkingLevel,
262
+ persistThinkingLevel,
87
263
  };