@bubblebrain-ai/bubble 0.0.37 → 0.0.39
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/dist/agent/internal-reminder-sanitizer.js +4 -0
- package/dist/agent/profiles.d.ts +6 -0
- package/dist/agent/profiles.js +32 -0
- package/dist/agent/task-lifecycle-reminder.d.ts +26 -0
- package/dist/agent/task-lifecycle-reminder.js +59 -0
- package/dist/agent.d.ts +43 -0
- package/dist/agent.js +70 -0
- package/dist/approval/controller.d.ts +3 -1
- package/dist/approval/controller.js +5 -2
- package/dist/approval/tool-helper.js +10 -1
- package/dist/approval/types.d.ts +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -0
- package/dist/loop/engine.d.ts +29 -0
- package/dist/loop/engine.js +63 -0
- package/dist/main.js +92 -24
- package/dist/orchestrator/default-hooks.js +94 -1
- package/dist/orchestrator/hooks.d.ts +7 -0
- package/dist/print-output.d.ts +54 -0
- package/dist/print-output.js +103 -0
- package/dist/prompt/delegation.js +6 -3
- package/dist/prompt/task-reminders.d.ts +17 -0
- package/dist/prompt/task-reminders.js +15 -0
- package/dist/session-types.d.ts +1 -1
- package/dist/tasks/manager.d.ts +136 -0
- package/dist/tasks/manager.js +530 -0
- package/dist/tasks/process-utils.d.ts +10 -0
- package/dist/tasks/process-utils.js +49 -0
- package/dist/tasks/promotion.d.ts +17 -0
- package/dist/tasks/promotion.js +30 -0
- package/dist/tasks/wake.d.ts +53 -0
- package/dist/tasks/wake.js +125 -0
- package/dist/tools/bash.d.ts +15 -1
- package/dist/tools/bash.js +103 -3
- package/dist/tools/child-tools.js +3 -1
- package/dist/tools/edit.js +4 -12
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +8 -1
- package/dist/tools/server-manager.d.ts +6 -36
- package/dist/tools/server-manager.js +6 -234
- package/dist/tools/task-tools.d.ts +10 -0
- package/dist/tools/task-tools.js +129 -0
- package/dist/tools/write.js +6 -12
- package/dist/tui-ink/app.d.ts +20 -2
- package/dist/tui-ink/app.js +307 -19
- package/dist/tui-ink/approval/approval-dialog.js +4 -4
- package/dist/tui-ink/display-history.d.ts +1 -1
- package/dist/tui-ink/footer.d.ts +3 -3
- package/dist/tui-ink/footer.js +3 -3
- package/dist/tui-ink/input-box.d.ts +6 -0
- package/dist/tui-ink/markdown.d.ts +10 -2
- package/dist/tui-ink/markdown.js +13 -12
- package/dist/tui-ink/message-list.js +14 -3
- package/dist/tui-ink/run.d.ts +6 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-inspector.d.ts +5 -1
- package/dist/tui-ink/subagent-inspector.js +23 -7
- package/dist/tui-ink/welcome.js +4 -0
- package/package.json +1 -1
package/dist/tui-ink/app.js
CHANGED
|
@@ -46,6 +46,9 @@ import { shouldContinueGoal, stopReasonNotice } from "../goal/engine.js";
|
|
|
46
46
|
import { goalCompleteNotice, goalIndicatorLine, goalSummaryText } from "../goal/format.js";
|
|
47
47
|
import { tokenUsageTotal } from "../goal/usage.js";
|
|
48
48
|
import { formatInternalContextBlock } from "../agent/internal-reminder-sanitizer.js";
|
|
49
|
+
import { MAX_ACTIVE_LOOPS, decideLoopFiring, formatInterval, formatLoopList, parseLoopCommand, } from "../loop/engine.js";
|
|
50
|
+
import { TaskWakeCoalescer, findDanglingTaskStarts, formatTaskWakeSummary, isPidAlive, shouldFireTaskWake, taskEligibleForWake, } from "../tasks/wake.js";
|
|
51
|
+
const TASK_WAKE_DEBOUNCE_MS = 2000;
|
|
49
52
|
import { collectUsageStatsBundle, formatStatsPanelBody, rangeLabel } from "../stats/usage.js";
|
|
50
53
|
import { GrokRuntimeError } from "../external-runtime/grok-errors.js";
|
|
51
54
|
import { GROK_LOCAL_SLASH_COMMANDS, classifyGrokInput, isGrokLocalSlashCommand, } from "../external-runtime/grok-input-policy.js";
|
|
@@ -76,6 +79,32 @@ function buildTips(agent, registry, grokActive = false) {
|
|
|
76
79
|
tips.push("Type / for commands and skills");
|
|
77
80
|
return tips;
|
|
78
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Slash-command results arrive as plain strings; recognize the /model
|
|
84
|
+
* confirmations by shape so they render as accent-colored UI notices — the
|
|
85
|
+
* same treatment the picker path applies — instead of assistant prose.
|
|
86
|
+
*/
|
|
87
|
+
function slashResultNoticeKind(result) {
|
|
88
|
+
return /^(Grok m|M)odel switched to /.test(result) ? "ui_notice" : undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* True when a user message is nothing but harness-injected internal blocks
|
|
92
|
+
* (goal kicks, task wakes) — hidden live, so hidden on reconstruction too.
|
|
93
|
+
*/
|
|
94
|
+
export function isInternalBlockOnlyContent(content) {
|
|
95
|
+
if (typeof content !== "string")
|
|
96
|
+
return false;
|
|
97
|
+
const trimmed = content.trim();
|
|
98
|
+
return /^<bubble_internal_(?:context|reminder)\b/.test(trimmed)
|
|
99
|
+
&& /<\/bubble_internal_(?:context|reminder)>$/.test(trimmed);
|
|
100
|
+
}
|
|
101
|
+
/** Status-row summary for running background tasks (design §2.5). */
|
|
102
|
+
function taskRowSummary(tasks, nowTick) {
|
|
103
|
+
const first = tasks[0];
|
|
104
|
+
const label = first.description?.trim() || first.command.replace(/\s+/g, " ").slice(0, 32);
|
|
105
|
+
const elapsed = Math.max(0, Math.round((nowTick - first.startedAt) / 1000));
|
|
106
|
+
return `${label} ${elapsed}s`;
|
|
107
|
+
}
|
|
79
108
|
function friendlyCwd(cwd) {
|
|
80
109
|
const home = os.homedir();
|
|
81
110
|
if (cwd === home)
|
|
@@ -95,7 +124,7 @@ function truncate(value, max) {
|
|
|
95
124
|
return value;
|
|
96
125
|
return `${value.slice(0, Math.max(0, max - 1))}…`;
|
|
97
126
|
}
|
|
98
|
-
function reconstructDisplayMessages(agentMessages) {
|
|
127
|
+
export function reconstructDisplayMessages(agentMessages) {
|
|
99
128
|
const result = [];
|
|
100
129
|
for (const m of agentMessages) {
|
|
101
130
|
if (m.role === "system" || m.role === "tool")
|
|
@@ -103,6 +132,12 @@ function reconstructDisplayMessages(agentMessages) {
|
|
|
103
132
|
if (m.role === "user") {
|
|
104
133
|
if (m.isMeta)
|
|
105
134
|
continue; // <system-reminder> injections are not user-visible
|
|
135
|
+
// Harness-initiated kicks (goal continuations, task wakes) persist as
|
|
136
|
+
// user-role messages wrapped in internal blocks so the model keeps
|
|
137
|
+
// them across resume — but they were hidden live and must stay hidden
|
|
138
|
+
// when the transcript is rebuilt.
|
|
139
|
+
if (isInternalBlockOnlyContent(m.content))
|
|
140
|
+
continue;
|
|
106
141
|
result.push({
|
|
107
142
|
key: nextDisplayMessageKey("user"),
|
|
108
143
|
role: "user",
|
|
@@ -222,8 +257,12 @@ export const INK_LOCAL_SLASH_COMMANDS = [
|
|
|
222
257
|
name: "goal",
|
|
223
258
|
description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)",
|
|
224
259
|
},
|
|
260
|
+
{
|
|
261
|
+
name: "loop",
|
|
262
|
+
description: "Run a prompt on a recurring interval (/loop 5m <prompt>|list|stop [id])",
|
|
263
|
+
},
|
|
225
264
|
];
|
|
226
|
-
export function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, externalRuntime, onExit }) {
|
|
265
|
+
export function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, processManager, tasksAutoResume, promotionChannel, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, externalRuntime, onExit }) {
|
|
227
266
|
const [sessionManager, setSessionManager] = useState(initialSessionManager);
|
|
228
267
|
const [externalRuntimeBinding, setExternalRuntimeBinding] = useState(() => initialSessionManager?.getMetadata().externalRuntime);
|
|
229
268
|
const sessionManagerRef = useRef(sessionManager);
|
|
@@ -369,17 +408,23 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
369
408
|
// 1Hz tick keeps the composer activity indicator animated while the agent is
|
|
370
409
|
// running without churning renders at idle.
|
|
371
410
|
const [nowTick, setNowTick] = useState(() => Date.now());
|
|
411
|
+
// Live background-task snapshot for the status row (design §2.5); refreshed
|
|
412
|
+
// by the manager subscription effect below.
|
|
413
|
+
const [taskSnapshot, setTaskSnapshot] = useState([]);
|
|
414
|
+
const runningTasks = useMemo(() => taskSnapshot.filter((task) => task.status === "running"), [taskSnapshot]);
|
|
372
415
|
// Timestamp of when the current agent run started. Used only for the final
|
|
373
416
|
// per-task duration summary.
|
|
374
417
|
const runStartRef = useRef(null);
|
|
375
418
|
// Mark the moment the run started; flips back to null in the finally block.
|
|
419
|
+
// Also ticks while background tasks run at idle so their elapsed counter
|
|
420
|
+
// keeps moving (design §2.5).
|
|
376
421
|
useEffect(() => {
|
|
377
|
-
if (!isRunning)
|
|
422
|
+
if (!isRunning && runningTasks.length === 0)
|
|
378
423
|
return;
|
|
379
424
|
setNowTick(Date.now());
|
|
380
425
|
const t = setInterval(() => setNowTick(Date.now()), 1000);
|
|
381
426
|
return () => clearInterval(t);
|
|
382
|
-
}, [isRunning]);
|
|
427
|
+
}, [isRunning, runningTasks.length]);
|
|
383
428
|
const userConfig = new UserConfig();
|
|
384
429
|
const safeRegistry = registry ?? new ProviderRegistry(userConfig);
|
|
385
430
|
const safeSkillRegistry = skillRegistry ?? new SkillRegistry({
|
|
@@ -592,6 +637,22 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
592
637
|
setPickerMode("slash");
|
|
593
638
|
return;
|
|
594
639
|
}
|
|
640
|
+
// Ctrl+G: promote the running foreground bash command to a background
|
|
641
|
+
// task (design §2.5). A no-op when nothing promotable is running.
|
|
642
|
+
if (isCtrlLetterInput(input, key, "g") && !pickerMode && promotionChannel) {
|
|
643
|
+
const liveTools = [
|
|
644
|
+
...streamingTools,
|
|
645
|
+
...streamingParts.flatMap((part) => (part.type === "tools" ? part.toolCalls : [])),
|
|
646
|
+
];
|
|
647
|
+
const candidate = [...liveTools].reverse().find((tool) => tool.name === "bash" && !tool.result && promotionChannel.hasPromotable(tool.id));
|
|
648
|
+
if (candidate) {
|
|
649
|
+
const taskId = promotionChannel.requestPromotion(candidate.id);
|
|
650
|
+
if (taskId) {
|
|
651
|
+
addMessage("assistant", `Moved to background: ${taskId} — the command keeps running (Ctrl+G).`, "ui_notice");
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
595
656
|
if (key.ctrl && key.shift && input.toLowerCase() === "m" && !pickerMode) {
|
|
596
657
|
if (externalSessionBound) {
|
|
597
658
|
addMessage("assistant", "MCP is unavailable in external runtime sessions (no Bubble workspace access).");
|
|
@@ -696,8 +757,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
696
757
|
reprintTranscript();
|
|
697
758
|
updateDisplayMessages(updater);
|
|
698
759
|
}, [reprintTranscript, updateDisplayMessages]);
|
|
699
|
-
const addMessage = useCallback((role, content) => {
|
|
700
|
-
updateDisplayMessages((prev) => [...prev, withMessageKey({ role, content })]);
|
|
760
|
+
const addMessage = useCallback((role, content, syntheticKind) => {
|
|
761
|
+
updateDisplayMessages((prev) => [...prev, withMessageKey(syntheticKind ? { role, content, syntheticKind } : { role, content })]);
|
|
701
762
|
}, [updateDisplayMessages]);
|
|
702
763
|
// Reflow on terminal resize. ink 7.0.3 only clears its dynamic frame when the
|
|
703
764
|
// terminal NARROWS (see its resized() handler); on widen / tmux split the
|
|
@@ -826,7 +887,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
826
887
|
manager.appendMarker("thinking_level_switch", selection.reasoningEffort);
|
|
827
888
|
refreshExternalRuntimeBinding(manager);
|
|
828
889
|
}
|
|
829
|
-
addMessage("assistant", `Grok model switched to ${selection.modelId ?? parsed.modelId}${selection.reasoningEffort !== "off" ? ` (${selection.reasoningEffort})` : ""}
|
|
890
|
+
addMessage("assistant", `Grok model switched to ${selection.modelId ?? parsed.modelId}${selection.reasoningEffort !== "off" ? ` (${selection.reasoningEffort})` : ""}.`, "ui_notice");
|
|
830
891
|
closePicker();
|
|
831
892
|
};
|
|
832
893
|
void run().catch((error) => {
|
|
@@ -895,6 +956,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
895
956
|
clearComposerDraft();
|
|
896
957
|
sessionManagerRef.current = result.manager;
|
|
897
958
|
setSessionManager(result.manager);
|
|
959
|
+
// Keep ownerSessionId on newly spawned tasks/servers accurate (§2.2c).
|
|
960
|
+
agent.setSessionID(result.manager.getSessionFile());
|
|
898
961
|
const nextExternalBinding = result.manager.getMetadata().externalRuntime;
|
|
899
962
|
externalRuntimeBindingRef.current = nextExternalBinding;
|
|
900
963
|
setExternalRuntimeBinding(nextExternalBinding);
|
|
@@ -1019,7 +1082,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1019
1082
|
const effortNote = nextThinkingLevel && nextThinkingLevel !== "off"
|
|
1020
1083
|
? (isThinkingToggle || isThinkingOnly ? " in thinking mode" : ` with ${nextThinkingLevel} effort`)
|
|
1021
1084
|
: "";
|
|
1022
|
-
addMessage("assistant", `Model switched to ${displayModel(model)}${effortNote}
|
|
1085
|
+
addMessage("assistant", `Model switched to ${displayModel(model)}${effortNote}.`, "ui_notice");
|
|
1023
1086
|
closePicker();
|
|
1024
1087
|
return nextThinkingLevel;
|
|
1025
1088
|
};
|
|
@@ -1079,7 +1142,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1079
1142
|
onExternalRuntimeChange: refreshExternalRuntimeBinding,
|
|
1080
1143
|
});
|
|
1081
1144
|
if (handled && result)
|
|
1082
|
-
addMessage("assistant", result);
|
|
1145
|
+
addMessage("assistant", result, slashResultNoticeKind(result));
|
|
1083
1146
|
}, [
|
|
1084
1147
|
addMessage,
|
|
1085
1148
|
agent,
|
|
@@ -1224,6 +1287,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1224
1287
|
closePicker();
|
|
1225
1288
|
});
|
|
1226
1289
|
}, [addMessage, agent, closePicker, createProvider, keyProviderId, safeRegistry, transitionToNative]);
|
|
1290
|
+
// /loop handler lives below handleSubmit (it needs addMessage only); the
|
|
1291
|
+
// ref breaks the declaration cycle so handleSubmit can route to it.
|
|
1292
|
+
const handleLoopCommandRef = useRef(null);
|
|
1227
1293
|
const handleSubmit = useCallback(async (payload) => {
|
|
1228
1294
|
const initialPayload = typeof payload === "string" ? { text: payload, images: [] } : payload;
|
|
1229
1295
|
const input = initialPayload.text;
|
|
@@ -1904,6 +1970,13 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1904
1970
|
}
|
|
1905
1971
|
}
|
|
1906
1972
|
};
|
|
1973
|
+
// Background-task wake (design §2.3b): an internal continuation, not a
|
|
1974
|
+
// user message — hidden from the transcript (the ✦ notice row is the
|
|
1975
|
+
// user-facing record) and wrapped as internal context for the model.
|
|
1976
|
+
if (initialPayload.internal === "task_wake") {
|
|
1977
|
+
await runAgentInput(input, "", [], { hidden: true });
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1907
1980
|
// Slash commands and skill invocations drop any attached images —
|
|
1908
1981
|
// they're meant for pure command routing.
|
|
1909
1982
|
if (input.trimStart().startsWith("/")) {
|
|
@@ -1922,6 +1995,10 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1922
1995
|
await handleGoalCommand(input);
|
|
1923
1996
|
return;
|
|
1924
1997
|
}
|
|
1998
|
+
if (/^\/loop(?:\s|$)/.test(input.trim())) {
|
|
1999
|
+
handleLoopCommandRef.current?.(input);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
1925
2002
|
const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
|
|
1926
2003
|
if (skillInvocation) {
|
|
1927
2004
|
await runAgentInput(skillInvocation.actualPrompt, displayInput);
|
|
@@ -1995,7 +2072,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1995
2072
|
]);
|
|
1996
2073
|
}
|
|
1997
2074
|
else {
|
|
1998
|
-
addMessage("assistant", result);
|
|
2075
|
+
addMessage("assistant", result, slashResultNoticeKind(result));
|
|
1999
2076
|
}
|
|
2000
2077
|
}
|
|
2001
2078
|
if (inject) {
|
|
@@ -2037,6 +2114,199 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
2037
2114
|
}
|
|
2038
2115
|
}
|
|
2039
2116
|
}, [addMessage, agent, args.cwd, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, createProvider, currentSessionFile, externalRuntime, externalRuntimeBinding, externalSessionBound, fillComposer, grokSessionBound, prepareSubmitDisplay, refreshExternalRuntimeBinding, safeRegistry, safeSkillRegistry, startFreshSession, transitionToNative, unsupportedExternalSessionBound, updateDisplayMessages, queueInput, submitSteer, requestExit, setStartingSubmit]);
|
|
2117
|
+
// --- Background tasks: markers, notices, auto-resume (design §2.3b) ---
|
|
2118
|
+
// Owner SessionManager captured at spawn so markers always land in the
|
|
2119
|
+
// owner session's jsonl, regardless of what is bound when the task ends.
|
|
2120
|
+
const taskOwnerSessionsRef = useRef(new Map());
|
|
2121
|
+
// Completions for non-current sessions, held until switch-back (§2.2c).
|
|
2122
|
+
const pendingTaskCompletionsRef = useRef(new Map());
|
|
2123
|
+
const taskWakeCoalescerRef = useRef(null);
|
|
2124
|
+
const announceTaskCompletion = useCallback((task) => {
|
|
2125
|
+
const elapsed = Math.round(((task.endedAt ?? Date.now()) - task.startedAt) / 1000);
|
|
2126
|
+
addMessage("assistant", `Background task ${task.id}${task.description ? ` (${task.description})` : ""} ${task.status}${task.exitCode != null ? ` — exit ${task.exitCode}` : ""} in ${elapsed}s.`, "ui_notice");
|
|
2127
|
+
}, [addMessage]);
|
|
2128
|
+
const fireTaskWake = useCallback((tasks) => {
|
|
2129
|
+
if (!processManager)
|
|
2130
|
+
return;
|
|
2131
|
+
// Gate at FIRE time (after the debounce): a turn that started meanwhile
|
|
2132
|
+
// or queued user input suppresses the wake — the state-change reminder
|
|
2133
|
+
// carries the completion into the next turn instead.
|
|
2134
|
+
if (!shouldFireTaskWake({
|
|
2135
|
+
autoResume: tasksAutoResume !== false,
|
|
2136
|
+
turnRunning: !!activeAbortRef.current,
|
|
2137
|
+
queuedInputs: queuedInputsRef.current.length,
|
|
2138
|
+
exiting: exitRequestedRef.current,
|
|
2139
|
+
})) {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
const summary = formatTaskWakeSummary(tasks, (id) => processManager.taskOutputTail(id, 2000));
|
|
2143
|
+
for (const task of tasks)
|
|
2144
|
+
processManager.markTaskDelivered(task.id);
|
|
2145
|
+
void handleSubmit({
|
|
2146
|
+
text: formatInternalContextBlock("task-finished", summary),
|
|
2147
|
+
images: [],
|
|
2148
|
+
internal: "task_wake",
|
|
2149
|
+
});
|
|
2150
|
+
}, [handleSubmit, processManager, tasksAutoResume]);
|
|
2151
|
+
const refreshTaskSnapshot = useCallback(() => {
|
|
2152
|
+
if (!processManager)
|
|
2153
|
+
return;
|
|
2154
|
+
setTaskSnapshot(processManager.listTasks(sessionManagerRef.current?.getSessionFile()));
|
|
2155
|
+
}, [processManager]);
|
|
2156
|
+
useEffect(() => {
|
|
2157
|
+
refreshTaskSnapshot();
|
|
2158
|
+
}, [refreshTaskSnapshot, sessionManager]);
|
|
2159
|
+
useEffect(() => {
|
|
2160
|
+
if (!processManager)
|
|
2161
|
+
return;
|
|
2162
|
+
taskWakeCoalescerRef.current = new TaskWakeCoalescer(TASK_WAKE_DEBOUNCE_MS, fireTaskWake);
|
|
2163
|
+
const unsubSnapshot = processManager.onChange(() => refreshTaskSnapshot());
|
|
2164
|
+
const unsubChange = processManager.onChange((task) => {
|
|
2165
|
+
if (task.status !== "running" || taskOwnerSessionsRef.current.has(task.id))
|
|
2166
|
+
return;
|
|
2167
|
+
const owner = sessionManagerRef.current;
|
|
2168
|
+
if (!owner || task.ownerSessionId !== owner.getSessionFile())
|
|
2169
|
+
return;
|
|
2170
|
+
taskOwnerSessionsRef.current.set(task.id, owner);
|
|
2171
|
+
owner.appendMarker("task_started", JSON.stringify({
|
|
2172
|
+
id: task.id,
|
|
2173
|
+
pid: task.pid,
|
|
2174
|
+
startedAt: task.startedAt,
|
|
2175
|
+
command: task.command,
|
|
2176
|
+
description: task.description,
|
|
2177
|
+
}));
|
|
2178
|
+
});
|
|
2179
|
+
const unsubFinish = processManager.onTaskFinished((task) => {
|
|
2180
|
+
taskOwnerSessionsRef.current.get(task.id)?.appendMarker(task.status === "killed" ? "task_killed" : "task_finished", JSON.stringify({ id: task.id, status: task.status, exitCode: task.exitCode ?? null, endedAt: task.endedAt }));
|
|
2181
|
+
const currentFile = sessionManagerRef.current?.getSessionFile();
|
|
2182
|
+
if (task.ownerSessionId !== currentFile) {
|
|
2183
|
+
if (taskEligibleForWake(task) && task.ownerSessionId) {
|
|
2184
|
+
const held = pendingTaskCompletionsRef.current.get(task.ownerSessionId) ?? [];
|
|
2185
|
+
held.push(task);
|
|
2186
|
+
pendingTaskCompletionsRef.current.set(task.ownerSessionId, held);
|
|
2187
|
+
}
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
announceTaskCompletion(task);
|
|
2191
|
+
if (taskEligibleForWake(task))
|
|
2192
|
+
taskWakeCoalescerRef.current?.add(task);
|
|
2193
|
+
});
|
|
2194
|
+
return () => {
|
|
2195
|
+
unsubSnapshot();
|
|
2196
|
+
unsubChange();
|
|
2197
|
+
unsubFinish();
|
|
2198
|
+
taskWakeCoalescerRef.current?.cancel();
|
|
2199
|
+
taskWakeCoalescerRef.current = null;
|
|
2200
|
+
};
|
|
2201
|
+
}, [announceTaskCompletion, fireTaskWake, processManager, refreshTaskSnapshot]);
|
|
2202
|
+
// Switch-back sweep: completions held for this session fire now (§2.2c).
|
|
2203
|
+
useEffect(() => {
|
|
2204
|
+
if (!processManager)
|
|
2205
|
+
return;
|
|
2206
|
+
const file = sessionManager?.getSessionFile();
|
|
2207
|
+
if (!file)
|
|
2208
|
+
return;
|
|
2209
|
+
const held = pendingTaskCompletionsRef.current.get(file);
|
|
2210
|
+
if (!held?.length)
|
|
2211
|
+
return;
|
|
2212
|
+
pendingTaskCompletionsRef.current.delete(file);
|
|
2213
|
+
for (const task of held) {
|
|
2214
|
+
announceTaskCompletion(task);
|
|
2215
|
+
taskWakeCoalescerRef.current?.add(task);
|
|
2216
|
+
}
|
|
2217
|
+
}, [announceTaskCompletion, processManager, sessionManager]);
|
|
2218
|
+
// Resume-time orphan report: dangling task_started markers from a previous
|
|
2219
|
+
// process, probed for liveness (§2.2c). Runs once per mount.
|
|
2220
|
+
const orphanProbeDoneRef = useRef(false);
|
|
2221
|
+
useEffect(() => {
|
|
2222
|
+
if (!processManager || orphanProbeDoneRef.current)
|
|
2223
|
+
return;
|
|
2224
|
+
orphanProbeDoneRef.current = true;
|
|
2225
|
+
const manager = sessionManagerRef.current;
|
|
2226
|
+
if (!manager)
|
|
2227
|
+
return;
|
|
2228
|
+
let entries;
|
|
2229
|
+
try {
|
|
2230
|
+
entries = manager.getEntries();
|
|
2231
|
+
}
|
|
2232
|
+
catch {
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
for (const orphan of findDanglingTaskStarts(entries)) {
|
|
2236
|
+
const alive = isPidAlive(orphan.pid);
|
|
2237
|
+
addMessage("assistant", alive
|
|
2238
|
+
? `Background task ${orphan.id}${orphan.description ? ` (${orphan.description})` : ""} from a previous process is still running (pid ${orphan.pid}). Kill it manually if unwanted: kill ${orphan.pid}`
|
|
2239
|
+
: `Background task ${orphan.id}${orphan.description ? ` (${orphan.description})` : ""} was orphaned by a previous process and is no longer running.`, "ui_notice");
|
|
2240
|
+
}
|
|
2241
|
+
}, [addMessage, processManager]);
|
|
2242
|
+
// --- /loop: recurring prompts (design §2.6) ---
|
|
2243
|
+
const loopsRef = useRef([]);
|
|
2244
|
+
const nextLoopIdRef = useRef(1);
|
|
2245
|
+
const handleSubmitRef = useRef(handleSubmit);
|
|
2246
|
+
handleSubmitRef.current = handleSubmit;
|
|
2247
|
+
const handleLoopCommand = useCallback((raw) => {
|
|
2248
|
+
const parsed = parseLoopCommand(raw);
|
|
2249
|
+
if (parsed.action === "help" || parsed.error) {
|
|
2250
|
+
addMessage("assistant", parsed.error ?? "Usage: /loop <interval> <prompt> (e.g. /loop 5m check CI status). Also: /loop list, /loop stop [id].");
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
if (parsed.action === "list") {
|
|
2254
|
+
addMessage("assistant", formatLoopList(loopsRef.current, Date.now()));
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
if (parsed.action === "stop") {
|
|
2258
|
+
if (parsed.stopId === undefined) {
|
|
2259
|
+
const count = loopsRef.current.length;
|
|
2260
|
+
loopsRef.current = [];
|
|
2261
|
+
addMessage("assistant", count > 0 ? `Stopped ${count} loop${count === 1 ? "" : "s"}.` : "No active loops.");
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
const before = loopsRef.current.length;
|
|
2265
|
+
loopsRef.current = loopsRef.current.filter((loop) => loop.id !== parsed.stopId);
|
|
2266
|
+
addMessage("assistant", loopsRef.current.length < before ? `Stopped loop #${parsed.stopId}.` : `No loop #${parsed.stopId}.`);
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
if (loopsRef.current.length >= MAX_ACTIVE_LOOPS) {
|
|
2270
|
+
addMessage("assistant", `Loop limit reached (${MAX_ACTIVE_LOOPS}). Stop one with /loop stop [id] first.`);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
const loop = {
|
|
2274
|
+
id: nextLoopIdRef.current++,
|
|
2275
|
+
prompt: parsed.prompt,
|
|
2276
|
+
intervalMs: parsed.intervalMs,
|
|
2277
|
+
// Fires immediately on creation (next idle tick), then repeats.
|
|
2278
|
+
nextFireAt: Date.now(),
|
|
2279
|
+
fires: 0,
|
|
2280
|
+
};
|
|
2281
|
+
loopsRef.current = [...loopsRef.current, loop];
|
|
2282
|
+
addMessage("assistant", `Loop #${loop.id} set: every ${formatInterval(loop.intervalMs)} — ${loop.prompt}\nSession-scoped; /loop stop ${loop.id} to cancel.`, "ui_notice");
|
|
2283
|
+
}, [addMessage]);
|
|
2284
|
+
handleLoopCommandRef.current = handleLoopCommand;
|
|
2285
|
+
useEffect(() => {
|
|
2286
|
+
const timer = setInterval(() => {
|
|
2287
|
+
if (loopsRef.current.length === 0 || exitRequestedRef.current)
|
|
2288
|
+
return;
|
|
2289
|
+
const now = Date.now();
|
|
2290
|
+
for (const loop of loopsRef.current) {
|
|
2291
|
+
const decision = decideLoopFiring(loop, now, !!activeAbortRef.current);
|
|
2292
|
+
if (decision === "wait")
|
|
2293
|
+
continue;
|
|
2294
|
+
loop.nextFireAt = now + loop.intervalMs;
|
|
2295
|
+
if (decision === "defer") {
|
|
2296
|
+
// Defer-not-stack (design §2.6): the previous turn is still running.
|
|
2297
|
+
addMessage("assistant", `Loop #${loop.id} skipped this interval (a turn is still running); next in ${formatInterval(loop.intervalMs)}.`, "ui_notice");
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2300
|
+
loop.fires += 1;
|
|
2301
|
+
void handleSubmitRef.current({
|
|
2302
|
+
text: loop.prompt,
|
|
2303
|
+
displayText: `⟳ loop #${loop.id}: ${loop.prompt}`,
|
|
2304
|
+
images: [],
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
}, 1000);
|
|
2308
|
+
return () => clearInterval(timer);
|
|
2309
|
+
}, [addMessage]);
|
|
2040
2310
|
// Drain the queue once the run ends and no modal needs the user first.
|
|
2041
2311
|
// The placeholder row is removed right before resubmitting — handleSubmit
|
|
2042
2312
|
// renders the message again as a regular user row.
|
|
@@ -2097,13 +2367,22 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
2097
2367
|
const showThinkingLabel = Boolean(thinkingLevel)
|
|
2098
2368
|
&& thinkingLevel !== "off"
|
|
2099
2369
|
&& (isThinkingToggle || isThinkingOnly || availableThinkingLevels.length > 2);
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2370
|
+
// The banner's model row stays live until the first settled row commits it
|
|
2371
|
+
// into <Static> scrollback (so a /model switch on the fresh launch screen
|
|
2372
|
+
// updates in place); after that the accent-colored transcript notices carry
|
|
2373
|
+
// model changes. External runtimes surface theirs via the footer runtimeLabel.
|
|
2374
|
+
const bannerTips = buildTips(agent, safeRegistry, grokSessionBound);
|
|
2375
|
+
const bannerProviderId = grokSessionBound ? "grok" : externalSessionBound ? undefined : agent.providerId || safeRegistry.getDefault()?.id;
|
|
2376
|
+
const bannerModelId = grokSessionBound ? externalRuntimeBinding?.modelId : externalSessionBound ? undefined : agent.apiModel;
|
|
2377
|
+
const bannerModelLabel = externalSessionBound
|
|
2378
|
+
? (grokSessionBound
|
|
2379
|
+
? `Grok Subscription${externalRuntimeBinding?.modelId ? ` · ${externalRuntimeBinding.modelId}` : ""}`
|
|
2380
|
+
: "Unsupported external runtime · recovery-only mode")
|
|
2381
|
+
: agent.model ? displayModel(agent.model) : undefined;
|
|
2382
|
+
const bannerThinkingLabel = grokSessionBound
|
|
2383
|
+
? externalRuntimeBinding?.reasoningEffort
|
|
2384
|
+
: !externalSessionBound && showThinkingLabel ? thinkingLevel : undefined;
|
|
2385
|
+
const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: bannerTips, updateNotice: currentUpdateNotice, cwd: friendlyCwd(args.cwd), sessionLabel: sessionBasename(currentSessionFile()), providerId: bannerProviderId, modelId: bannerModelId, modelLabel: bannerModelLabel, thinkingLabel: bannerThinkingLabel })) : null;
|
|
2107
2386
|
const commandPaletteItems = useMemo(() => buildCommandPaletteItems(safeSkillRegistry, externalSessionBound), [externalSessionBound, safeSkillRegistry]);
|
|
2108
2387
|
const mcpReconnectItems = useMemo(() => buildMcpReconnectItems(mcpManager), [mcpManager]);
|
|
2109
2388
|
// No fixed-height frame: settled rows flow into the terminal's native
|
|
@@ -2164,7 +2443,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
2164
2443
|
}, onCancel: closePicker }) })), pickerMode === "mcp-reconnect" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(McpReconnectPicker, { items: mcpReconnectItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
|
|
2165
2444
|
closePicker();
|
|
2166
2445
|
void handleSubmit(item.command);
|
|
2167
|
-
}, onCancel: closePicker }) })), pickerMode === "session" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SessionPicker, { currentCwd: args.cwd, currentSessions: SessionManager.summarizeSessionsForCwd(args.cwd), allSessions: SessionManager.listAllSessions(), onSelect: handleSessionSelect, onCancel: closePicker }) })), pickerMode === "agents" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SubagentInspector, { groups: subagentGroups, onCancel: closePicker }) })), pickerMode === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
|
|
2446
|
+
}, onCancel: closePicker }) })), pickerMode === "session" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SessionPicker, { currentCwd: args.cwd, currentSessions: SessionManager.summarizeSessionsForCwd(args.cwd), allSessions: SessionManager.listAllSessions(), onSelect: handleSessionSelect, onCancel: closePicker }) })), pickerMode === "agents" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SubagentInspector, { groups: subagentGroups, onCancel: closePicker, tasks: taskSnapshot, onKillTask: (taskId) => { void processManager?.killTask(taskId); } }) })), pickerMode === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
|
|
2168
2447
|
closePicker();
|
|
2169
2448
|
void handleSubmit(command);
|
|
2170
2449
|
}, onCancel: closePicker }) })), pickerMode === "feishu-setup" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeishuSetupPicker, { onComplete: (summary) => {
|
|
@@ -2213,7 +2492,16 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
2213
2492
|
: 0, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined, onArrowDownAtBottom: () => {
|
|
2214
2493
|
if (subagentMembers.length > 0 && !pickerMode)
|
|
2215
2494
|
setSubagentEntryFocused(true);
|
|
2216
|
-
}, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: externalSessionBound ? undefined : safeSkillRegistry, localSlashCommands: externalSessionBound ? [] : [...INK_LOCAL_SLASH_COMMANDS], allowedSlashCommands: unsupportedExternalSessionBound ? GROK_LOCAL_SLASH_COMMANDS : undefined, allowWorkspaceMentions: !unsupportedExternalSessionBound, allowImageAttachments: !externalSessionBound, terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [
|
|
2495
|
+
}, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: externalSessionBound ? undefined : safeSkillRegistry, localSlashCommands: externalSessionBound ? [] : [...INK_LOCAL_SLASH_COMMANDS], allowedSlashCommands: unsupportedExternalSessionBound ? GROK_LOCAL_SLASH_COMMANDS : undefined, allowWorkspaceMentions: !unsupportedExternalSessionBound, allowImageAttachments: !externalSessionBound, terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (subagentMembers.length > 0 || runningTasks.length > 0) && (_jsxs(Box, { paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [[
|
|
2496
|
+
subagentMembers.length > 0
|
|
2497
|
+
? `${subagentMembers.length} subagent${subagentMembers.length === 1 ? "" : "s"} · ${subagentSummary(subagentMembers)}`
|
|
2498
|
+
: undefined,
|
|
2499
|
+
runningTasks.length > 0
|
|
2500
|
+
? `${runningTasks.length} task${runningTasks.length === 1 ? "" : "s"} · ${taskRowSummary(runningTasks, nowTick)}`
|
|
2501
|
+
: undefined,
|
|
2502
|
+
].filter(Boolean).join(" · "), " \u00B7 "] }), subagentMembers.length > 0
|
|
2503
|
+
? _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })
|
|
2504
|
+
: _jsx(Text, { color: palette.accent, children: "task_output to check \u00B7 kill_task to stop" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({
|
|
2217
2505
|
mode: permissionMode,
|
|
2218
2506
|
goalLine: externalSessionBound ? undefined : goalLine,
|
|
2219
2507
|
runtimeLabel: externalSessionBound
|
|
@@ -29,7 +29,7 @@ export function ApprovalDialog({ request, onDecision, onAllowBashPrefix, }) {
|
|
|
29
29
|
const onCancel = () => onDecision({ action: "reject" });
|
|
30
30
|
const title = dialogTitle(request);
|
|
31
31
|
const question = dialogQuestion(request);
|
|
32
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RequestPreview, { request: request }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: question }) }), _jsx(Box, { marginTop: 1, children: _jsx(ApprovalSelect, { options: options, onSubmit: onSubmit, onCancel: onCancel, hint: "\u2191\u2193 choose \u00B7 Enter select \u00B7 Tab add feedback \u00B7 Esc reject" }) })] }));
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), (request.type === "edit" || request.type === "write") && request.outsideWorkspace && (_jsx(Text, { color: theme.warning, bold: true, children: "\u26A0 This path is outside the workspace" })), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RequestPreview, { request: request }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: question }) }), _jsx(Box, { marginTop: 1, children: _jsx(ApprovalSelect, { options: options, onSubmit: onSubmit, onCancel: onCancel, hint: "\u2191\u2193 choose \u00B7 Enter select \u00B7 Tab add feedback \u00B7 Esc reject" }) })] }));
|
|
33
33
|
}
|
|
34
34
|
function buildOptions(request) {
|
|
35
35
|
if (request.type === "bash") {
|
|
@@ -108,7 +108,7 @@ function basename(p) {
|
|
|
108
108
|
function RequestPreview({ request }) {
|
|
109
109
|
switch (request.type) {
|
|
110
110
|
case "bash":
|
|
111
|
-
return _jsx(BashPreview, { command: request.command, cwd: request.cwd });
|
|
111
|
+
return _jsx(BashPreview, { command: request.command, cwd: request.cwd, background: request.background });
|
|
112
112
|
case "edit":
|
|
113
113
|
return _jsx(DiffView, { diff: request.diff });
|
|
114
114
|
case "patch":
|
|
@@ -155,10 +155,10 @@ function AgentProfilePreview({ path, promptPreview }) {
|
|
|
155
155
|
const theme = useTheme();
|
|
156
156
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.muted, children: compressHome(path) }), _jsx(Text, { children: promptPreview }), _jsx(Text, { color: theme.warning, children: "This prompt comes from the repository's .bubble/agents and will drive a subagent." })] }));
|
|
157
157
|
}
|
|
158
|
-
function BashPreview({ command, cwd }) {
|
|
158
|
+
function BashPreview({ command, cwd, background }) {
|
|
159
159
|
const theme = useTheme();
|
|
160
160
|
const danger = classifyBashDanger(command);
|
|
161
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "$ " }), _jsx(Text, { children: command })] }), _jsxs(Text, { color: theme.muted, children: ["cwd: ", compressHome(cwd)] }), danger && (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.warning, bold: true, children: ["\u26A0 ", danger.pattern, ":"] }), _jsxs(Text, { color: theme.warning, children: [" ", danger.message] })] }))] }));
|
|
161
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "$ " }), _jsx(Text, { children: command })] }), _jsxs(Text, { color: theme.muted, children: ["cwd: ", compressHome(cwd)] }), background && (_jsx(Text, { color: theme.warning, children: "runs in background \u2014 keeps running after this turn ends" })), danger && (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.warning, bold: true, children: ["\u26A0 ", danger.pattern, ":"] }), _jsxs(Text, { color: theme.warning, children: [" ", danger.message] })] }))] }));
|
|
162
162
|
}
|
|
163
163
|
const MAX_WRITE_PREVIEW_LINES = 20;
|
|
164
164
|
function WritePreview({ path, content }) {
|
|
@@ -12,7 +12,7 @@ export interface DisplayMessage {
|
|
|
12
12
|
reasoning?: string;
|
|
13
13
|
toolCalls?: DisplayToolCall[];
|
|
14
14
|
parts?: DisplayMessagePart[];
|
|
15
|
-
syntheticKind?: "ui_summary" | "ui_compact_summary" | "ui_interrupt";
|
|
15
|
+
syntheticKind?: "ui_summary" | "ui_compact_summary" | "ui_interrupt" | "ui_notice";
|
|
16
16
|
/** Markdown body shown inside a `ui_compact_summary` card. */
|
|
17
17
|
compactionSummary?: string;
|
|
18
18
|
hiddenCount?: number;
|
package/dist/tui-ink/footer.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ export interface FooterData {
|
|
|
5
5
|
runtimeLabel?: string;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Bottom status line. Path / provider / model
|
|
9
|
-
* the
|
|
10
|
-
* (zero rows) in the default mode.
|
|
8
|
+
* Bottom status line. Path / provider / model live in the welcome banner and
|
|
9
|
+
* the model-switch transcript notices; the footer only surfaces the
|
|
10
|
+
* permission-mode badge, so it renders nothing (zero rows) in the default mode.
|
|
11
11
|
*/
|
|
12
12
|
export declare function FooterBar({ data }: {
|
|
13
13
|
data: FooterData;
|
package/dist/tui-ink/footer.js
CHANGED
|
@@ -3,9 +3,9 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { useTheme } from "./theme.js";
|
|
4
4
|
import { PERMISSION_MODE_INFO } from "../permission/mode.js";
|
|
5
5
|
/**
|
|
6
|
-
* Bottom status line. Path / provider / model
|
|
7
|
-
* the
|
|
8
|
-
* (zero rows) in the default mode.
|
|
6
|
+
* Bottom status line. Path / provider / model live in the welcome banner and
|
|
7
|
+
* the model-switch transcript notices; the footer only surfaces the
|
|
8
|
+
* permission-mode badge, so it renders nothing (zero rows) in the default mode.
|
|
9
9
|
*/
|
|
10
10
|
export function FooterBar({ data }) {
|
|
11
11
|
const showMode = !!data.mode && data.mode !== "default";
|
|
@@ -11,6 +11,12 @@ export interface SubmitPayload {
|
|
|
11
11
|
images: ImageAttachment[];
|
|
12
12
|
/** First UI-only [Image #N] label reserved for this submitted payload. */
|
|
13
13
|
imageDisplayStart?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Harness-initiated submission (background-tasks design §2.3b): runs
|
|
16
|
+
* hidden with the text wrapped as internal context, never rendered as a
|
|
17
|
+
* user row. Only the TUI wake path sets this.
|
|
18
|
+
*/
|
|
19
|
+
internal?: "task_wake";
|
|
14
20
|
}
|
|
15
21
|
interface InputBoxProps {
|
|
16
22
|
onSubmit: (payload: SubmitPayload) => void;
|
|
@@ -79,17 +79,25 @@ export declare function wrapInlineSegments(segments: MarkdownInlineSegment[], ma
|
|
|
79
79
|
* reset handles the rare case where `content` is replaced wholesale (e.g.,
|
|
80
80
|
* the user re-enters a turn).
|
|
81
81
|
*/
|
|
82
|
-
export declare function StreamingMarkdown({ content, maxWidth, }: {
|
|
82
|
+
export declare function StreamingMarkdown({ content, maxWidth, flushTop, }: {
|
|
83
83
|
content: string;
|
|
84
84
|
maxWidth?: number;
|
|
85
|
+
/** See MarkdownContent: applies to the true document start only. */
|
|
86
|
+
flushTop?: boolean;
|
|
85
87
|
}): import("react/jsx-runtime").JSX.Element;
|
|
86
88
|
export declare function splitListItem(line: string): {
|
|
87
89
|
prefix: string;
|
|
88
90
|
content: string;
|
|
89
91
|
indent: number;
|
|
90
92
|
} | null;
|
|
91
|
-
export declare function MarkdownContent({ content, maxWidth, }: {
|
|
93
|
+
export declare function MarkdownContent({ content, maxWidth, flushTop, }: {
|
|
92
94
|
content: string;
|
|
93
95
|
maxWidth?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Suppress the FIRST block's top margin. Set when this document renders
|
|
98
|
+
* beside an inline marker (the timeline "● " gutter): a leading heading /
|
|
99
|
+
* code block / table would otherwise start one row below the marker.
|
|
100
|
+
*/
|
|
101
|
+
flushTop?: boolean;
|
|
94
102
|
}): import("react/jsx-runtime").JSX.Element;
|
|
95
103
|
export {};
|