@bubblebrain-ai/bubble 0.0.22 → 0.0.24
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/README.md +197 -34
- package/dist/agent/internal-reminder-sanitizer.js +29 -9
- package/dist/goal/command.d.ts +20 -0
- package/dist/goal/command.js +71 -0
- package/dist/goal/engine.d.ts +33 -0
- package/dist/goal/engine.js +65 -0
- package/dist/goal/format.d.ts +18 -0
- package/dist/goal/format.js +82 -0
- package/dist/goal/prompts.d.ts +13 -0
- package/dist/goal/prompts.js +84 -0
- package/dist/goal/store.d.ts +61 -0
- package/dist/goal/store.js +161 -0
- package/dist/goal/tools.d.ts +10 -0
- package/dist/goal/tools.js +70 -0
- package/dist/main.js +10 -2
- package/dist/model-catalog.js +17 -0
- package/dist/provider-transform.js +31 -0
- package/dist/session-types.d.ts +3 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +2 -0
- package/dist/tui/run.d.ts +8 -0
- package/dist/tui/run.js +318 -29
- package/dist/tui/trace-groups.js +41 -5
- package/dist/tui-ink/run.d.ts +2 -0
- package/dist/update/index.d.ts +18 -4
- package/dist/update/index.js +41 -19
- package/package.json +1 -1
package/dist/tui/run.js
CHANGED
|
@@ -38,6 +38,11 @@ import { getNextPermissionMode, PERMISSION_MODE_INFO } from "../permission/mode.
|
|
|
38
38
|
import { getContextBudget } from "../context/budget.js";
|
|
39
39
|
import { getLspService } from "../lsp/index.js";
|
|
40
40
|
import { inferBashPrefix } from "../approval/session-cache.js";
|
|
41
|
+
import { parseGoalCommand } from "../goal/command.js";
|
|
42
|
+
import { continuationPrompt, initialPrompt } from "../goal/prompts.js";
|
|
43
|
+
import { shouldContinueGoal, stopReasonNotice } from "../goal/engine.js";
|
|
44
|
+
import { goalSummaryText, goalIndicatorLine, goalCompleteNotice } from "../goal/format.js";
|
|
45
|
+
import { formatInternalContextBlock } from "../agent/internal-reminder-sanitizer.js";
|
|
41
46
|
import { collectFeedback } from "../feedback/collect.js";
|
|
42
47
|
import { submitFeedback, FeedbackSubmitError } from "../feedback/submit.js";
|
|
43
48
|
import { createFrames } from "./opencode-spinner.js";
|
|
@@ -101,6 +106,7 @@ const DEFAULT_THEME = {
|
|
|
101
106
|
toolRead: "#9d7cd8",
|
|
102
107
|
toolWrite: "#f5a742",
|
|
103
108
|
toolSearch: "#5c9cf5",
|
|
109
|
+
toolMcp: "#d479c9",
|
|
104
110
|
diffAdded: "#7fd88f",
|
|
105
111
|
diffRemoved: "#e06c75",
|
|
106
112
|
diffContext: "#a6acb8",
|
|
@@ -145,6 +151,7 @@ const LIGHT_THEME = {
|
|
|
145
151
|
toolRead: "#6F55AE",
|
|
146
152
|
toolWrite: "#8B4A00",
|
|
147
153
|
toolSearch: "#356FD2",
|
|
154
|
+
toolMcp: "#A03595",
|
|
148
155
|
diffAdded: "#1E725C",
|
|
149
156
|
diffRemoved: "#B62633",
|
|
150
157
|
diffContext: "#6F7377",
|
|
@@ -166,6 +173,10 @@ const LOCAL_SLASH_COMMANDS = [
|
|
|
166
173
|
name: "toggle-thinking",
|
|
167
174
|
description: "Toggle thinking block visibility",
|
|
168
175
|
},
|
|
176
|
+
{
|
|
177
|
+
name: "goal",
|
|
178
|
+
description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)",
|
|
179
|
+
},
|
|
169
180
|
{
|
|
170
181
|
name: "trace",
|
|
171
182
|
description: "Toggle verbose trace output",
|
|
@@ -514,6 +525,28 @@ function OpenTuiApp(props) {
|
|
|
514
525
|
const [todos, setTodos] = createSignal(props.agent.getTodos());
|
|
515
526
|
const [mode, setMode] = createSignal(props.agent.mode);
|
|
516
527
|
const [notice, setNotice] = createSignal("");
|
|
528
|
+
// Autonomous-goal feature: shared store (also backs the get_goal/update_goal
|
|
529
|
+
// tools), a reactive indicator line, the consecutive auto-continuation
|
|
530
|
+
// counter, and a flag to suppress persistence while loading from the session.
|
|
531
|
+
const goalStore = props.options.goalStore;
|
|
532
|
+
const [goalLine, setGoalLine] = createSignal("");
|
|
533
|
+
let goalPersistSuspended = false;
|
|
534
|
+
if (goalStore) {
|
|
535
|
+
goalStore.onChange((goal) => {
|
|
536
|
+
setGoalLine(goal ? goalIndicatorLine(goal) : "");
|
|
537
|
+
syncSidebarGoal();
|
|
538
|
+
if (!goalPersistSuspended)
|
|
539
|
+
persistGoal(goal);
|
|
540
|
+
});
|
|
541
|
+
const persisted = props.options.sessionManager?.getMetadata().goal;
|
|
542
|
+
if (persisted) {
|
|
543
|
+
goalPersistSuspended = true;
|
|
544
|
+
// Resume-safety: a loaded active goal is parked as paused so it never
|
|
545
|
+
// silently resumes (and spends tokens) on session load. /goal resume runs it.
|
|
546
|
+
goalStore.loadFrom(persisted.status === "active" ? { ...persisted, status: "paused" } : persisted);
|
|
547
|
+
goalPersistSuspended = false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
517
550
|
let copyToastClearTimer;
|
|
518
551
|
let copyToastRoot;
|
|
519
552
|
let copyToastText;
|
|
@@ -564,6 +597,9 @@ function OpenTuiApp(props) {
|
|
|
564
597
|
let rootBox;
|
|
565
598
|
let sidebarShell;
|
|
566
599
|
let homeSurfaceShell;
|
|
600
|
+
let homeUpdateNotice = props.options.updateNotice;
|
|
601
|
+
let homeUpdateNoticeBox;
|
|
602
|
+
let homeUpdateNoticeText;
|
|
567
603
|
let transcriptHost;
|
|
568
604
|
const transcriptState = {
|
|
569
605
|
entries: [],
|
|
@@ -665,6 +701,8 @@ function OpenTuiApp(props) {
|
|
|
665
701
|
const sidebarTodoRows = [];
|
|
666
702
|
const sidebarTodoMarkers = [];
|
|
667
703
|
const sidebarTodoLabels = [];
|
|
704
|
+
let sidebarGoalSection;
|
|
705
|
+
let sidebarGoalText;
|
|
668
706
|
const sidebarFileRows = [];
|
|
669
707
|
const sidebarFileLabels = [];
|
|
670
708
|
const sidebarFileAdditions = [];
|
|
@@ -971,6 +1009,29 @@ function OpenTuiApp(props) {
|
|
|
971
1009
|
syncSidebarFiles();
|
|
972
1010
|
bumpSidebar();
|
|
973
1011
|
}
|
|
1012
|
+
function syncSidebarGoal() {
|
|
1013
|
+
const line = goalLine();
|
|
1014
|
+
if (sidebarGoalSection)
|
|
1015
|
+
sidebarGoalSection.visible = !!line;
|
|
1016
|
+
if (sidebarGoalText) {
|
|
1017
|
+
sidebarGoalText.content = line || "";
|
|
1018
|
+
sidebarGoalText.requestRender();
|
|
1019
|
+
}
|
|
1020
|
+
sidebarShell?.requestRender();
|
|
1021
|
+
rootBox?.requestRender();
|
|
1022
|
+
}
|
|
1023
|
+
function persistGoal(goal) {
|
|
1024
|
+
const sessionManager = props.options.sessionManager;
|
|
1025
|
+
if (!sessionManager)
|
|
1026
|
+
return;
|
|
1027
|
+
try {
|
|
1028
|
+
const metadata = sessionManager.getMetadata();
|
|
1029
|
+
sessionManager.setMetadata({ ...metadata, goal: goal ?? undefined });
|
|
1030
|
+
}
|
|
1031
|
+
catch {
|
|
1032
|
+
// Persistence is best-effort; never break the run loop over it.
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
974
1035
|
function syncSidebarChrome() {
|
|
975
1036
|
const visible = sidebarVisible();
|
|
976
1037
|
if (sidebarShell) {
|
|
@@ -4912,6 +4973,10 @@ function OpenTuiApp(props) {
|
|
|
4912
4973
|
toggleThinkingVisibility();
|
|
4913
4974
|
return true;
|
|
4914
4975
|
}
|
|
4976
|
+
if (/^\/goal(?:\s|$)/.test(input.trim())) {
|
|
4977
|
+
await handleGoalCommand(input);
|
|
4978
|
+
return true;
|
|
4979
|
+
}
|
|
4915
4980
|
if (/^\/(?:trace|verbose|debug)(?:\s|$)/.test(input.trim())) {
|
|
4916
4981
|
toggleVerboseTrace();
|
|
4917
4982
|
return true;
|
|
@@ -5462,20 +5527,31 @@ function OpenTuiApp(props) {
|
|
|
5462
5527
|
addMessage("error", "No model selected. Use /model after /login or provider setup.");
|
|
5463
5528
|
return;
|
|
5464
5529
|
}
|
|
5465
|
-
|
|
5466
|
-
//
|
|
5467
|
-
//
|
|
5468
|
-
const
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5530
|
+
// Goal continuation turns are "hidden": their input is an internal context
|
|
5531
|
+
// block (stripped from the model echo) and must not render a user bubble or
|
|
5532
|
+
// pollute prompt history.
|
|
5533
|
+
const isGoalRun = !!options.goalRun;
|
|
5534
|
+
if (!options.hidden) {
|
|
5535
|
+
rememberPromptHistory(displayInput);
|
|
5536
|
+
// History keeps the short marker (it expands again on resend); the
|
|
5537
|
+
// transcript shows the full pasted content once the message is sent.
|
|
5538
|
+
const displayContent = expandComposerPastedTexts(displayInput);
|
|
5539
|
+
const reusedQueuedDisplay = promoteQueuedUserDisplay(options.displayId, displayContent);
|
|
5540
|
+
const nextMessages = reusedQueuedDisplay
|
|
5541
|
+
? displayMessages
|
|
5542
|
+
: [...displayMessages, { role: "user", content: displayContent }];
|
|
5543
|
+
if (!reusedQueuedDisplay)
|
|
5544
|
+
displayMessages = nextMessages;
|
|
5545
|
+
streamingDisplay = undefined;
|
|
5546
|
+
// The user just sent this message — re-engage bottom-follow so the new
|
|
5547
|
+
// turn is visible even if they had scrolled up to read earlier history.
|
|
5548
|
+
redrawTranscript(undefined, nextMessages, { forceFollow: true });
|
|
5549
|
+
}
|
|
5550
|
+
else {
|
|
5551
|
+
streamingDisplay = undefined;
|
|
5552
|
+
redrawTranscript(undefined, displayMessages, { forceFollow: true });
|
|
5553
|
+
}
|
|
5554
|
+
let goalRunTokens = 0;
|
|
5479
5555
|
const taskStartedAt = Date.now();
|
|
5480
5556
|
const run = beginAgentRun();
|
|
5481
5557
|
traceEvent("tui_agent_run_begin", {
|
|
@@ -5743,6 +5819,8 @@ function OpenTuiApp(props) {
|
|
|
5743
5819
|
reasoningTokens: current.reasoningTokens + (event.usage.reasoningTokens ?? 0),
|
|
5744
5820
|
turns: current.turns + 1,
|
|
5745
5821
|
}));
|
|
5822
|
+
// Accumulate billed tokens (input + output) toward the goal budget.
|
|
5823
|
+
goalRunTokens += (event.usage.promptTokens || 0) + (event.usage.completionTokens || 0);
|
|
5746
5824
|
}
|
|
5747
5825
|
bumpSidebar();
|
|
5748
5826
|
const currentParts = snapshotDisplayParts(assistantParts);
|
|
@@ -5833,6 +5911,143 @@ function OpenTuiApp(props) {
|
|
|
5833
5911
|
setTimeout(() => activePrompt()?.focus(), 0);
|
|
5834
5912
|
if (queuedInputCount() > 0)
|
|
5835
5913
|
scheduleQueuedInputDrain();
|
|
5914
|
+
maybeContinueGoal({ runCancelled, runErrored: !!runError, isGoalRun, runTokens: goalRunTokens });
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
/**
|
|
5918
|
+
* Drives the autonomous goal loop. Called after every agent run finishes:
|
|
5919
|
+
* accounts the goal turn, decides whether to auto-continue, and either fires
|
|
5920
|
+
* the next hidden continuation turn or stops with an explanatory notice.
|
|
5921
|
+
*/
|
|
5922
|
+
function maybeContinueGoal(input) {
|
|
5923
|
+
if (!goalStore)
|
|
5924
|
+
return;
|
|
5925
|
+
const current = goalStore.snapshot();
|
|
5926
|
+
if (!current)
|
|
5927
|
+
return;
|
|
5928
|
+
// User interrupt or a provider/run error (out of quota, network down, API
|
|
5929
|
+
// failure) stops the autonomous loop. Pause an active goal so it never
|
|
5930
|
+
// silently retries into a broken provider; the user fixes it and resumes.
|
|
5931
|
+
if (input.runCancelled || input.runErrored) {
|
|
5932
|
+
if (current.status === "active") {
|
|
5933
|
+
goalStore.pause();
|
|
5934
|
+
setNotice(stopReasonNotice(input.runErrored ? "error" : "cancelled"));
|
|
5935
|
+
}
|
|
5936
|
+
return;
|
|
5937
|
+
}
|
|
5938
|
+
// Account the goal turn that just finished (token spend + turn count).
|
|
5939
|
+
if (input.isGoalRun) {
|
|
5940
|
+
if (input.runTokens > 0)
|
|
5941
|
+
goalStore.addTokens(input.runTokens);
|
|
5942
|
+
goalStore.incrementTurn();
|
|
5943
|
+
}
|
|
5944
|
+
const goal = goalStore.snapshot();
|
|
5945
|
+
const decision = shouldContinueGoal({ goal, queuedInputs: queuedInputCount() });
|
|
5946
|
+
if (decision.continue) {
|
|
5947
|
+
const text = formatInternalContextBlock("goal", continuationPrompt(goal));
|
|
5948
|
+
// Start the next turn after this run has fully unwound.
|
|
5949
|
+
queueMicrotask(() => { void runAgentInput(text, "", { hidden: true, goalRun: true }); });
|
|
5950
|
+
return;
|
|
5951
|
+
}
|
|
5952
|
+
if (decision.reason === "budget" && goal.status === "active") {
|
|
5953
|
+
goalStore.markBudgetLimited();
|
|
5954
|
+
}
|
|
5955
|
+
// tokensUsed is now accurate (addTokens ran above), so the completion notice
|
|
5956
|
+
// carries the real final spend — which update_goal could not report mid-run.
|
|
5957
|
+
if (decision.reason === "complete") {
|
|
5958
|
+
setNotice(goalCompleteNotice(goal));
|
|
5959
|
+
return;
|
|
5960
|
+
}
|
|
5961
|
+
const note = stopReasonNotice(decision.reason);
|
|
5962
|
+
if (note)
|
|
5963
|
+
setNotice(note);
|
|
5964
|
+
}
|
|
5965
|
+
// Starts a goal turn unless a run is already in flight (which will continue
|
|
5966
|
+
// the goal when it finishes). When `displayInput` is given (the initial /goal
|
|
5967
|
+
// set), the objective renders as a visible message so the user sees what they
|
|
5968
|
+
// asked for; otherwise the turn is hidden (silent auto-continuation/resume).
|
|
5969
|
+
function kickGoalTurn(prompt, displayInput) {
|
|
5970
|
+
if (isRunning())
|
|
5971
|
+
return;
|
|
5972
|
+
queueMicrotask(() => {
|
|
5973
|
+
void runAgentInput(prompt, displayInput ?? "", { hidden: displayInput === undefined, goalRun: true });
|
|
5974
|
+
});
|
|
5975
|
+
}
|
|
5976
|
+
async function handleGoalCommand(input) {
|
|
5977
|
+
if (!goalStore) {
|
|
5978
|
+
setNotice("Goals are not available in this session");
|
|
5979
|
+
return;
|
|
5980
|
+
}
|
|
5981
|
+
const command = parseGoalCommand(input);
|
|
5982
|
+
if (command.error) {
|
|
5983
|
+
addMessage("error", command.error);
|
|
5984
|
+
return;
|
|
5985
|
+
}
|
|
5986
|
+
const existing = goalStore.snapshot();
|
|
5987
|
+
switch (command.kind) {
|
|
5988
|
+
case "show": {
|
|
5989
|
+
if (!existing) {
|
|
5990
|
+
setNotice("No active goal. Set one with /goal <objective>");
|
|
5991
|
+
}
|
|
5992
|
+
else {
|
|
5993
|
+
setNotice(goalSummaryText(existing));
|
|
5994
|
+
}
|
|
5995
|
+
return;
|
|
5996
|
+
}
|
|
5997
|
+
case "clear": {
|
|
5998
|
+
if (!existing) {
|
|
5999
|
+
setNotice("No active goal to clear");
|
|
6000
|
+
return;
|
|
6001
|
+
}
|
|
6002
|
+
goalStore.clear();
|
|
6003
|
+
setNotice("Goal cleared");
|
|
6004
|
+
return;
|
|
6005
|
+
}
|
|
6006
|
+
case "pause": {
|
|
6007
|
+
if (!existing) {
|
|
6008
|
+
setNotice("No active goal to pause");
|
|
6009
|
+
return;
|
|
6010
|
+
}
|
|
6011
|
+
goalStore.pause();
|
|
6012
|
+
setNotice("Goal paused — /goal resume to continue");
|
|
6013
|
+
return;
|
|
6014
|
+
}
|
|
6015
|
+
case "resume": {
|
|
6016
|
+
if (!existing) {
|
|
6017
|
+
setNotice("No goal to resume. Set one with /goal <objective>");
|
|
6018
|
+
return;
|
|
6019
|
+
}
|
|
6020
|
+
const resumed = goalStore.resume();
|
|
6021
|
+
if (resumed?.status === "active") {
|
|
6022
|
+
setNotice("Goal resumed");
|
|
6023
|
+
kickGoalTurn(formatInternalContextBlock("goal", continuationPrompt(resumed)));
|
|
6024
|
+
}
|
|
6025
|
+
else {
|
|
6026
|
+
setNotice("Goal cannot be resumed (already complete)");
|
|
6027
|
+
}
|
|
6028
|
+
return;
|
|
6029
|
+
}
|
|
6030
|
+
case "edit": {
|
|
6031
|
+
if (!existing) {
|
|
6032
|
+
setNotice("No active goal to edit. Set one with /goal <objective>");
|
|
6033
|
+
return;
|
|
6034
|
+
}
|
|
6035
|
+
goalStore.edit(command.objective);
|
|
6036
|
+
if (command.tokenBudget !== undefined)
|
|
6037
|
+
goalStore.setBudget(command.tokenBudget);
|
|
6038
|
+
setNotice(`Goal updated: ${truncate(goalStore.snapshot().objective, 60)}`);
|
|
6039
|
+
return;
|
|
6040
|
+
}
|
|
6041
|
+
case "set": {
|
|
6042
|
+
const goal = goalStore.set(command.objective, { tokenBudget: command.tokenBudget });
|
|
6043
|
+
const budgetNote = goal.tokenBudget !== undefined ? ` (budget ${goal.tokenBudget} tok)` : "";
|
|
6044
|
+
setNotice(`Goal set${budgetNote} — working autonomously. /goal pause to stop.`);
|
|
6045
|
+
// Echo the full `/goal …` command the user typed as their visible
|
|
6046
|
+
// message (so the transcript and prompt history reflect the invocation);
|
|
6047
|
+
// the model receives the (hidden) initial goal prompt as the turn input.
|
|
6048
|
+
kickGoalTurn(formatInternalContextBlock("goal", initialPrompt(goal)), input.trim());
|
|
6049
|
+
return;
|
|
6050
|
+
}
|
|
5836
6051
|
}
|
|
5837
6052
|
}
|
|
5838
6053
|
function promptUiKeyDown(event) {
|
|
@@ -5922,11 +6137,49 @@ function OpenTuiApp(props) {
|
|
|
5922
6137
|
}, [
|
|
5923
6138
|
h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, ...logoLines.map((line) => renderHomeLogoLine(line))),
|
|
5924
6139
|
h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center", paddingTop: 1 }, h("text", { fg: theme.textMuted, content: `v${getCurrentVersion()}` })),
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
:
|
|
6140
|
+
// Always mounted so a late registry check can reveal it mid-session.
|
|
6141
|
+
h("box", {
|
|
6142
|
+
ref: (ref) => {
|
|
6143
|
+
homeUpdateNoticeBox = ref;
|
|
6144
|
+
ref.visible = !!homeUpdateNotice;
|
|
6145
|
+
},
|
|
6146
|
+
visible: !!homeUpdateNotice,
|
|
6147
|
+
flexShrink: 0,
|
|
6148
|
+
flexDirection: "column",
|
|
6149
|
+
alignItems: "center",
|
|
6150
|
+
}, h("text", {
|
|
6151
|
+
ref: (ref) => { homeUpdateNoticeText = ref; },
|
|
6152
|
+
fg: theme.accent,
|
|
6153
|
+
content: homeUpdateNotice ?? "",
|
|
6154
|
+
})),
|
|
5928
6155
|
]);
|
|
5929
6156
|
}
|
|
6157
|
+
function watchUpdateNoticeRefresh() {
|
|
6158
|
+
const refresh = props.options.updateNoticeRefresh;
|
|
6159
|
+
if (!refresh)
|
|
6160
|
+
return;
|
|
6161
|
+
refresh.then((notice) => {
|
|
6162
|
+
if (!notice || uiDisposed)
|
|
6163
|
+
return;
|
|
6164
|
+
homeUpdateNotice = notice;
|
|
6165
|
+
if (homeUpdateNoticeText)
|
|
6166
|
+
homeUpdateNoticeText.content = notice;
|
|
6167
|
+
if (homeUpdateNoticeBox)
|
|
6168
|
+
homeUpdateNoticeBox.visible = true;
|
|
6169
|
+
// Already chatting (or resumed straight into a transcript): the home
|
|
6170
|
+
// banner is hidden, so surface the nudge as a transcript line instead.
|
|
6171
|
+
// (Not setNotice: the notice() row in renderSessionView is evaluated
|
|
6172
|
+
// once at initial render and never materializes afterwards.)
|
|
6173
|
+
if (!isHomeSurfaceActive(streamingDisplay))
|
|
6174
|
+
addMessage("assistant", notice);
|
|
6175
|
+
rootBox?.requestRender();
|
|
6176
|
+
}).catch(() => {
|
|
6177
|
+
// The check is best-effort; never disturb the session over it.
|
|
6178
|
+
});
|
|
6179
|
+
}
|
|
6180
|
+
// Component body, not onMount: the onMount callback never fires under the
|
|
6181
|
+
// current @opentui/solid runtime, so anything registered there is dead code.
|
|
6182
|
+
watchUpdateNoticeRefresh();
|
|
5930
6183
|
function renderQuestionPanelHost() {
|
|
5931
6184
|
return h("box", {
|
|
5932
6185
|
ref: (ref) => {
|
|
@@ -6684,7 +6937,7 @@ function OpenTuiApp(props) {
|
|
|
6684
6937
|
visible: sidebarVisible(),
|
|
6685
6938
|
flexDirection: "column",
|
|
6686
6939
|
}, [
|
|
6687
|
-
h("scrollbox", { flexGrow: 1, minHeight: 0 }, h("box", { flexDirection: "column", gap: 1, paddingRight: 1 }, renderSidebarTitle(), renderSidebarSection("Context", [
|
|
6940
|
+
h("scrollbox", { flexGrow: 1, minHeight: 0 }, h("box", { flexDirection: "column", gap: 1, paddingRight: 1 }, renderSidebarTitle(), renderSidebarGoal(), renderSidebarSection("Context", [
|
|
6688
6941
|
h("text", {
|
|
6689
6942
|
fg: theme.textMuted,
|
|
6690
6943
|
flexShrink: 0,
|
|
@@ -6845,6 +7098,25 @@ function OpenTuiApp(props) {
|
|
|
6845
7098
|
}),
|
|
6846
7099
|
]);
|
|
6847
7100
|
}
|
|
7101
|
+
function renderSidebarGoal() {
|
|
7102
|
+
const line = goalLine();
|
|
7103
|
+
return h("box", {
|
|
7104
|
+
flexDirection: "column",
|
|
7105
|
+
flexShrink: 0,
|
|
7106
|
+
visible: !!line,
|
|
7107
|
+
ref: (ref) => {
|
|
7108
|
+
sidebarGoalSection = ref;
|
|
7109
|
+
syncSidebarGoal();
|
|
7110
|
+
},
|
|
7111
|
+
}, h("text", { fg: theme.text }, "Goal"), h("text", {
|
|
7112
|
+
fg: theme.accent,
|
|
7113
|
+
wrapMode: "word",
|
|
7114
|
+
ref: (ref) => {
|
|
7115
|
+
sidebarGoalText = ref;
|
|
7116
|
+
ref.content = goalLine() || "";
|
|
7117
|
+
},
|
|
7118
|
+
}));
|
|
7119
|
+
}
|
|
6848
7120
|
function renderSidebarTodos(todos) {
|
|
6849
7121
|
const visible = todos.slice(0, 8);
|
|
6850
7122
|
return h("box", {
|
|
@@ -7864,7 +8136,7 @@ function createTraceGroupRenderable(ctx, group, syntaxStyle, width = 80) {
|
|
|
7864
8136
|
}))));
|
|
7865
8137
|
}
|
|
7866
8138
|
if (group.omitted > 0) {
|
|
7867
|
-
children.push(createText(ctx,
|
|
8139
|
+
children.push(createText(ctx, traceGroupOmittedLabel(group), {
|
|
7868
8140
|
fg: theme.textMuted,
|
|
7869
8141
|
wrapMode: "word",
|
|
7870
8142
|
}));
|
|
@@ -7882,6 +8154,15 @@ function shouldRenderTraceGroupAsRawTool(tool) {
|
|
|
7882
8154
|
function traceGroupDetailLines(group) {
|
|
7883
8155
|
return group.previewLines.length > 0 ? group.previewLines : group.items;
|
|
7884
8156
|
}
|
|
8157
|
+
// Overflow hint under a trace group. Line-based details (tool output) read as
|
|
8158
|
+
// "N more lines"; item-based details (file lists) stay as "N more".
|
|
8159
|
+
function traceGroupOmittedLabel(group) {
|
|
8160
|
+
if (group.previewLines.length > 0) {
|
|
8161
|
+
const noun = group.omitted === 1 ? "line" : "lines";
|
|
8162
|
+
return ` ... ${group.omitted} more ${noun}, Ctrl+O to expand`;
|
|
8163
|
+
}
|
|
8164
|
+
return ` ... ${group.omitted} more, Ctrl+O to expand`;
|
|
8165
|
+
}
|
|
7885
8166
|
const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
|
|
7886
8167
|
function executeInlineBudget(group, width) {
|
|
7887
8168
|
return Math.max(14, width - group.title.length - 20);
|
|
@@ -7958,9 +8239,14 @@ function traceGroupTitleColor(group) {
|
|
|
7958
8239
|
case "edit": return theme.toolWrite;
|
|
7959
8240
|
case "subagent": return theme.accent;
|
|
7960
8241
|
case "list": return theme.secondary;
|
|
7961
|
-
default: return theme.toolText;
|
|
8242
|
+
default: return isMcpTraceGroup(group) ? theme.toolMcp : theme.toolText;
|
|
7962
8243
|
}
|
|
7963
8244
|
}
|
|
8245
|
+
// An "other" group whose single tool is an MCP call (`mcp__<server>__<tool>`).
|
|
8246
|
+
function isMcpTraceGroup(group) {
|
|
8247
|
+
const name = group.raw[0]?.name;
|
|
8248
|
+
return typeof name === "string" && name.startsWith("mcp__");
|
|
8249
|
+
}
|
|
7964
8250
|
function traceGroupKey(group) {
|
|
7965
8251
|
return `group:${group.kind}:${group.raw.map((tool) => tool.id).join(":")}`;
|
|
7966
8252
|
}
|
|
@@ -8749,7 +9035,7 @@ function renderTraceGroup(group, syntaxStyle, width = 80) {
|
|
|
8749
9035
|
wrapMode: "word",
|
|
8750
9036
|
}, `${index === 0 ? "↳ " : " "}${truncate(line, detailWidth)}`)))
|
|
8751
9037
|
: null, group.omitted > 0
|
|
8752
|
-
? h("text", { fg: theme.textMuted, wrapMode: "word" },
|
|
9038
|
+
? h("text", { fg: theme.textMuted, wrapMode: "word" }, traceGroupOmittedLabel(group))
|
|
8753
9039
|
: null);
|
|
8754
9040
|
}
|
|
8755
9041
|
function renderTool(tool, syntaxStyle, width = 80) {
|
|
@@ -8864,14 +9150,17 @@ function pickerTitle(kind, providerId) {
|
|
|
8864
9150
|
function getModelPickerReasoningLevels(providerId, modelId) {
|
|
8865
9151
|
// Only expand into one picker row per effort for models that genuinely have a
|
|
8866
9152
|
// reasoning-effort spectrum: OpenAI's reasoning models (codex gpt-5.x:
|
|
8867
|
-
// off/minimal/low/medium/high/xhigh), DeepSeek's v4 models,
|
|
8868
|
-
// Step Plan models.
|
|
8869
|
-
//
|
|
8870
|
-
//
|
|
9153
|
+
// off/minimal/low/medium/high/xhigh), DeepSeek's v4 models, StepFun
|
|
9154
|
+
// Step Plan models, and GLM-5.2 (the only GLM that accepts `reasoning_effort`:
|
|
9155
|
+
// none/minimal/low/medium/high/xhigh/max). Other providers — including older
|
|
9156
|
+
// GLM (5.1/4.7/4.6/5-turbo) and Moonshot/Kimi — only have a thinking on/off
|
|
9157
|
+
// toggle, not an effort control, so they stay as a single row.
|
|
8871
9158
|
const isOpenAIReasoning = providerId === "openai" || providerId === "openai-codex";
|
|
8872
9159
|
const isDeepseekReasoning = providerId === "deepseek" && (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro");
|
|
8873
9160
|
const isStepFunReasoning = providerId === "stepfun";
|
|
8874
|
-
|
|
9161
|
+
const isGlm52Reasoning = ["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(providerId)
|
|
9162
|
+
&& modelId === "glm-5.2";
|
|
9163
|
+
if (!isOpenAIReasoning && !isDeepseekReasoning && !isStepFunReasoning && !isGlm52Reasoning)
|
|
8875
9164
|
return [];
|
|
8876
9165
|
const levels = getAvailableThinkingLevels(providerId, modelId);
|
|
8877
9166
|
// gpt-4o and friends report only ["off"] — keep those as a single row too.
|
|
@@ -8884,9 +9173,9 @@ function displayModelWithThinking(model, thinkingLevel) {
|
|
|
8884
9173
|
if (!providerId)
|
|
8885
9174
|
return displayModel(model);
|
|
8886
9175
|
// Use the same scoping as the picker: only models with a real reasoning-effort
|
|
8887
|
-
// spectrum (OpenAI codex gpt-5.x, deepseek v4, StepFun Step Plan) get
|
|
8888
|
-
// "(level)" suffix. The on/off thinking toggle on GLM / Moonshot(Kimi)
|
|
8889
|
-
// not an effort control.
|
|
9176
|
+
// spectrum (OpenAI codex gpt-5.x, deepseek v4, StepFun Step Plan, GLM-5.2) get
|
|
9177
|
+
// the "(level)" suffix. The on/off thinking toggle on older GLM / Moonshot(Kimi)
|
|
9178
|
+
// is not an effort control.
|
|
8890
9179
|
const levels = getModelPickerReasoningLevels(providerId, modelId);
|
|
8891
9180
|
if (levels.length > 1 && thinkingLevel !== "off") {
|
|
8892
9181
|
return `${displayModel(model)} (${thinkingLevel})`;
|
package/dist/tui/trace-groups.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import { getEditDiffDetails } from "./edit-diff.js";
|
|
3
3
|
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
4
|
+
import { mcpInfoFromString } from "../mcp/name.js";
|
|
4
5
|
const DEFAULT_MAX_ITEMS = 6;
|
|
5
6
|
const DEFAULT_MAX_PREVIEW_LINES = 8;
|
|
6
7
|
export function buildTraceGroups(toolCalls, options = {}) {
|
|
@@ -120,13 +121,18 @@ function classifyTool(toolCall) {
|
|
|
120
121
|
return { kind: "edit", title: "Edit", bucketKey: `edit:${toolCall.id}`, groupable: false };
|
|
121
122
|
case "write":
|
|
122
123
|
return { kind: "write", title: "Write", bucketKey: "write", groupable: true };
|
|
123
|
-
default:
|
|
124
|
+
default: {
|
|
125
|
+
const mcp = mcpInfoFromString(toolCall.name);
|
|
126
|
+
const title = mcp
|
|
127
|
+
? `${mcp.serverName.toUpperCase()}: ${mcp.toolName}`
|
|
128
|
+
: displayToolName(toolCall.name);
|
|
124
129
|
return {
|
|
125
130
|
kind: "other",
|
|
126
|
-
title
|
|
131
|
+
title,
|
|
127
132
|
bucketKey: `${toolCall.name}:${toolCall.id}`,
|
|
128
133
|
groupable: false,
|
|
129
134
|
};
|
|
135
|
+
}
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
138
|
function buildTraceGroup(classifier, raw, options) {
|
|
@@ -345,15 +351,23 @@ function buildSubagentGroup(classifier, tool, options, pending, startedAt) {
|
|
|
345
351
|
}
|
|
346
352
|
function buildOtherGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
347
353
|
const tool = raw[0];
|
|
348
|
-
const
|
|
354
|
+
const mcp = mcpInfoFromString(tool.name);
|
|
355
|
+
// MCP tools carry arbitrary args, so render them as `key: value` pairs inline
|
|
356
|
+
// (via the `command` slot) instead of the path-based header used for builtins.
|
|
357
|
+
const header = mcp ? undefined : toolHeader(tool, options.homeDir);
|
|
358
|
+
const argsLabel = mcp ? mcpArgsLabel(tool.args) : "";
|
|
359
|
+
// Suppress the "N calls" fallback for MCP tools — the title already names the
|
|
360
|
+
// tool, and args (when present) ride alongside it.
|
|
361
|
+
const hasInline = mcp || !!header;
|
|
349
362
|
const preview = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
350
363
|
const { shown, omitted } = take(preview, options.maxPreviewLines);
|
|
351
364
|
return {
|
|
352
365
|
kind: "other",
|
|
353
366
|
title: classifier.title,
|
|
354
367
|
raw,
|
|
355
|
-
|
|
356
|
-
|
|
368
|
+
command: argsLabel || undefined,
|
|
369
|
+
count: hasInline ? undefined : raw.length,
|
|
370
|
+
noun: hasInline ? undefined : plural(raw.length, "call", "calls"),
|
|
357
371
|
items: header ? [header] : [],
|
|
358
372
|
previewLines: shown,
|
|
359
373
|
errorLines: [],
|
|
@@ -469,6 +483,28 @@ function displayToolName(name) {
|
|
|
469
483
|
return "Tool";
|
|
470
484
|
return name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, " ");
|
|
471
485
|
}
|
|
486
|
+
/** Compact `key: value, key: value` rendering of an MCP tool's arguments. */
|
|
487
|
+
function mcpArgsLabel(args) {
|
|
488
|
+
if (!args || typeof args !== "object")
|
|
489
|
+
return "";
|
|
490
|
+
return Object.entries(args)
|
|
491
|
+
.filter(([, value]) => value !== undefined)
|
|
492
|
+
.map(([key, value]) => `${key}: ${formatMcpArgValue(value)}`)
|
|
493
|
+
.join(", ");
|
|
494
|
+
}
|
|
495
|
+
function formatMcpArgValue(value) {
|
|
496
|
+
if (typeof value === "string")
|
|
497
|
+
return JSON.stringify(value);
|
|
498
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") {
|
|
499
|
+
return String(value);
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
return JSON.stringify(value);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return String(value);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
472
508
|
function toolHeader(tool, homeDir) {
|
|
473
509
|
const args = tool.args || {};
|
|
474
510
|
for (const key of ["path", "command", "pattern", "query", "url"]) {
|
package/dist/tui-ink/run.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface RunTuiOptions {
|
|
|
25
25
|
settingsManager?: SettingsManager;
|
|
26
26
|
lspService?: LspService;
|
|
27
27
|
mcpManager?: McpManager;
|
|
28
|
+
/** Accepted for compatibility with the shared options bag; the goal feature is OpenTUI-only. */
|
|
29
|
+
goalStore?: import("../goal/store.js").GoalStore;
|
|
28
30
|
themeMode?: ThemeMode;
|
|
29
31
|
themeOverrides?: Record<string, string>;
|
|
30
32
|
detectedTheme?: ResolvedTheme;
|
package/dist/update/index.d.ts
CHANGED
|
@@ -37,10 +37,24 @@ export declare function upgradeCommandFor(manager: PackageManager): {
|
|
|
37
37
|
export declare function runUpdateCommand(opts?: {
|
|
38
38
|
checkOnly?: boolean;
|
|
39
39
|
}): Promise<number>;
|
|
40
|
+
export interface StartupUpdateCheck {
|
|
41
|
+
/** Notice derived from the local cache — available immediately, no network. */
|
|
42
|
+
notice: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves once the background registry check completes: a notice string
|
|
45
|
+
* when it finds a version newer than both the running one and the cached
|
|
46
|
+
* `notice`, otherwise null. Never rejects.
|
|
47
|
+
*/
|
|
48
|
+
refreshed: Promise<string | null>;
|
|
49
|
+
}
|
|
40
50
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
51
|
+
* Startup "update available" check. The immediate `notice` comes from the
|
|
52
|
+
* local cache file (fast, no network on the hot path). A registry refresh
|
|
53
|
+
* always runs in the background (throttled to once per 30 minutes) so a
|
|
54
|
+
* release published since the last launch surfaces in the *current* session
|
|
55
|
+
* via `refreshed`, instead of only after the cache TTL plus another restart.
|
|
56
|
+
* Never throws.
|
|
45
57
|
*/
|
|
58
|
+
export declare function startStartupUpdateCheck(): Promise<StartupUpdateCheck>;
|
|
59
|
+
/** Cache-only variant of {@link startStartupUpdateCheck} (still refreshes in the background). */
|
|
46
60
|
export declare function getStartupUpdateNotice(): Promise<string | null>;
|