u-foo 2.3.31 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -213
- package/README.zh-CN.md +151 -197
- package/SKILLS/ufoo/SKILL.md +8 -8
- package/bin/uagy.js +69 -0
- package/bin/uclaude.js +2 -2
- package/bin/ucode.js +4 -4
- package/bin/ucodex.js +2 -2
- package/bin/ufoo.js +5 -23
- package/modules/AGENTS.template.md +1 -1
- package/modules/bus/SKILLS/ubus/SKILL.md +35 -10
- package/package.json +9 -5
- package/scripts/chat-app-smoke.js +30 -0
- package/scripts/global-chat-switch-benchmark.js +5 -5
- package/scripts/ink-demo.js +23 -0
- package/scripts/ink-smoke.js +30 -0
- package/scripts/ucode-app-smoke.js +36 -0
- package/src/{agent → agents/activity}/activityDetector.js +39 -2
- package/src/{agent → agents/activity}/activityStatePublisher.js +1 -1
- package/src/{agent → agents/activity}/activityStateWriter.js +2 -2
- package/src/{agent → agents/activity}/activityTracker.js +1 -1
- package/src/agents/activity/index.js +8 -0
- package/src/{agent → agents/controller}/controllerToolExecutor.js +4 -4
- package/src/agents/controller/index.js +8 -0
- package/src/{agent → agents/controller}/loopObservability.js +2 -2
- package/src/{agent → agents/controller}/loopRuntime.js +1 -1
- package/src/{agent → agents/controller}/ufooAgent.js +9 -9
- package/src/agents/index.js +10 -0
- package/src/agents/internal/index.js +3 -0
- package/src/{agent → agents/internal}/internalRunner.js +45 -22
- package/src/agents/launch/agyConversation.js +159 -0
- package/src/agents/launch/index.js +12 -0
- package/src/{agent → agents/launch}/launchEnvironment.js +2 -3
- package/src/{agent → agents/launch}/launcher.js +64 -21
- package/src/{agent → agents/launch}/notifier.js +23 -12
- package/src/{agent → agents/launch}/ptyRunner.js +44 -12
- package/src/{agent → agents/launch}/ptyWrapper.js +2 -2
- package/src/{agent → agents/launch}/publisherRouting.js +1 -1
- package/src/{agent → agents/launch}/readyDetector.js +23 -0
- package/src/{agent → agents/prompts}/defaultBootstrap.js +63 -4
- package/src/{group/bootstrap.js → agents/prompts/groupBootstrap.js} +41 -6
- package/src/agents/prompts/index.js +8 -0
- package/src/{code/prompts → agents/prompts/native}/index.js +1 -1
- package/src/{agent → agents/providers}/claudeThreadProvider.js +1 -1
- package/src/{agent → agents/providers}/codexThreadProvider.js +1 -1
- package/src/{agent → agents/providers}/directAuthStatus.js +184 -1
- package/src/agents/providers/index.js +13 -0
- package/src/{agent → agents/providers}/upstreamTransport.js +2 -2
- package/src/{chat → app/chat}/agentSockets.js +1 -1
- package/src/{chat → app/chat}/commandExecutor.js +56 -28
- package/src/{chat → app/chat}/commands.js +119 -5
- package/src/{chat → app/chat}/daemonConnection.js +1 -1
- package/src/{chat → app/chat}/daemonMessageRouter.js +54 -4
- package/src/{chat → app/chat}/daemonTransport.js +2 -1
- package/src/{chat → app/chat}/dashboardView.js +2 -21
- package/src/app/chat/index.js +6 -0
- package/src/{chat → app/chat}/inputSubmitHandler.js +38 -13
- package/src/{chat → app/chat}/internalAgentLogHistory.js +1 -1
- package/src/app/chat/multiWindow/index.js +268 -0
- package/src/app/chat/multiWindow/paneLayout.js +84 -0
- package/src/app/chat/multiWindow/paneManager.js +299 -0
- package/src/app/chat/multiWindow/renderer.js +384 -0
- package/src/app/chat/multiWindow/virtualTerminal.js +327 -0
- package/src/{chat → app/chat}/projectCloseController.js +1 -1
- package/src/app/chat/shellCommand.js +42 -0
- package/src/{chat → app/chat}/transport.js +16 -3
- package/src/{cli → app/cli}/ctxCoreCommands.js +3 -3
- package/src/{doctor/index.js → app/cli/features/doctor.js} +1 -1
- package/src/{init/index.js → app/cli/features/init.js} +14 -32
- package/src/{cli → app/cli}/groupCoreCommands.js +2 -2
- package/src/app/cli/index.js +9 -0
- package/src/{cli → app/cli}/onlineCoreCommands.js +5 -5
- package/src/{cli.js → app/cli/run.js} +62 -59
- package/src/app/index.js +6 -0
- package/src/code/agent.js +10 -9
- package/src/code/index.js +2 -0
- package/src/code/launcher/index.js +9 -0
- package/src/{agent → code/launcher}/ucode.js +7 -8
- package/src/{agent → code/launcher}/ucodeBootstrap.js +3 -3
- package/src/{agent → code/launcher}/ucodeBuild.js +2 -2
- package/src/{agent → code/launcher}/ucodeDoctor.js +2 -2
- package/src/{agent → code/launcher}/ucodeRuntimeConfig.js +1 -2
- package/src/code/nativeRunner.js +4 -4
- package/src/code/taskDecomposer.js +5 -4
- package/src/code/tui.js +39 -1997
- package/src/config.js +15 -2
- package/src/{bus → coordination/bus}/activate.js +2 -2
- package/src/{bus → coordination/bus}/daemon.js +15 -5
- package/src/coordination/bus/envelope.js +173 -0
- package/src/{bus → coordination/bus}/index.js +7 -3
- package/src/{bus → coordination/bus}/inject.js +11 -3
- package/src/{bus → coordination/bus}/message.js +1 -1
- package/src/coordination/bus/messageMeta.js +130 -0
- package/src/coordination/bus/promptEnvelope.js +65 -0
- package/src/{bus → coordination/bus}/shake.js +1 -1
- package/src/{bus → coordination/bus}/store.js +3 -3
- package/src/{bus → coordination/bus}/subscriber.js +2 -2
- package/src/{bus → coordination/bus}/utils.js +2 -2
- package/src/{history → coordination/history}/inputTimeline.js +5 -5
- package/src/coordination/index.js +10 -0
- package/src/{memory → coordination/memory}/historySearch.js +1 -1
- package/src/{memory → coordination/memory}/index.js +3 -3
- package/src/{report → coordination/report}/store.js +2 -2
- package/src/{ufoo → coordination/state}/agentRegistryDiagnostics.js +43 -0
- package/src/{status → coordination/status}/index.js +3 -3
- package/src/online/bridge.js +2 -2
- package/src/{controller → orchestration/controller}/flags.js +1 -1
- package/src/{controller → orchestration/controller}/gateRouter.js +1 -1
- package/src/orchestration/controller/index.js +10 -0
- package/src/{controller → orchestration/controller}/shadowGuard.js +1 -1
- package/src/orchestration/groups/bootstrap.js +3 -0
- package/src/orchestration/groups/index.js +10 -0
- package/src/orchestration/groups/promptProfiles.js +3 -0
- package/src/{group → orchestration/groups}/templates.js +1 -1
- package/src/{group → orchestration/groups}/validateTemplate.js +1 -1
- package/src/orchestration/index.js +7 -0
- package/src/orchestration/solo/index.js +3 -0
- package/src/{daemon → runtime/daemon}/agentProcessManager.js +1 -1
- package/src/{daemon → runtime/daemon}/cronOps.js +3 -2
- package/src/{daemon → runtime/daemon}/groupOrchestrator.js +26 -9
- package/src/{daemon → runtime/daemon}/index.js +273 -79
- package/src/{daemon → runtime/daemon}/ipcServer.js +24 -2
- package/src/{daemon → runtime/daemon}/nicknameScope.js +6 -3
- package/src/{daemon → runtime/daemon}/ops.js +48 -61
- package/src/{daemon → runtime/daemon}/promptLoop.js +1 -1
- package/src/{daemon → runtime/daemon}/promptRequest.js +13 -8
- package/src/runtime/daemon/providerSessions.js +230 -0
- package/src/{daemon → runtime/daemon}/reporting.js +4 -4
- package/src/{daemon → runtime/daemon}/run.js +12 -5
- package/src/{daemon → runtime/daemon}/soloBootstrap.js +7 -7
- package/src/{daemon → runtime/daemon}/status.js +5 -5
- package/src/runtime/index.js +10 -0
- package/src/runtime/process/nodeExecutable.js +26 -0
- package/src/{projects → runtime/projects}/registry.js +1 -1
- package/src/{projects → runtime/projects}/runtimes.js +1 -1
- package/src/{terminal → runtime/terminal}/adapterRouter.js +0 -10
- package/src/{terminal → runtime/terminal}/adapters/internalAdapter.js +0 -4
- package/src/tools/handlers/common.js +1 -1
- package/src/tools/handlers/listAgents.js +1 -1
- package/src/tools/handlers/memory.js +3 -3
- package/src/tools/handlers/readBusSummary.js +1 -1
- package/src/tools/handlers/readOpenDecisions.js +1 -1
- package/src/tools/handlers/readProjectRegistry.js +1 -1
- package/src/tools/handlers/readPromptHistory.js +2 -2
- package/src/tools/schemaFixtures.js +1 -1
- package/src/ui/MIGRATION.md +336 -0
- package/src/ui/format/index.js +974 -0
- package/src/ui/index.js +9 -0
- package/src/ui/ink/ChatApp.js +3674 -0
- package/src/ui/ink/DashboardBar.js +685 -0
- package/src/ui/ink/InkDemo.js +96 -0
- package/src/ui/ink/MultilineInput.js +612 -0
- package/src/ui/ink/UcodeApp.js +822 -0
- package/src/ui/ink/agentMirror.js +730 -0
- package/src/ui/ink/chatReducer.js +359 -0
- package/src/ui/runInk.js +57 -0
- package/src/bus/messageMeta.js +0 -52
- package/src/chat/agentViewController.js +0 -1072
- package/src/chat/chatLogController.js +0 -138
- package/src/chat/completionController.js +0 -533
- package/src/chat/dashboardKeyController.js +0 -573
- package/src/chat/index.js +0 -2214
- package/src/chat/inputHistoryController.js +0 -135
- package/src/chat/inputListenerController.js +0 -470
- package/src/chat/layout.js +0 -186
- package/src/chat/pasteController.js +0 -81
- package/src/chat/statusLineController.js +0 -223
- package/src/chat/streamTracker.js +0 -156
- package/src/code/config +0 -0
- package/src/daemon/providerSessions.js +0 -488
- package/src/terminal/adapters/internalPtyAdapter.js +0 -42
- /package/src/{code/prompts → agents/prompts/native}/actions.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/efficiency.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/environment.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/identity.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/safety.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/sections.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/system.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/tasks.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/bash.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/edit.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/read.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/write.js +0 -0
- /package/src/{code/prompts → agents/prompts/native}/ufoo.js +0 -0
- /package/src/{group → agents/prompts}/promptProfiles.js +0 -0
- /package/src/{agent → agents/providers}/claudeEventTranslator.js +0 -0
- /package/src/{agent → agents/providers}/claudeOauthTokenReader.js +0 -0
- /package/src/{agent → agents/providers}/claudeSessionFiles.js +0 -0
- /package/src/{agent → agents/providers}/codexEventTranslator.js +0 -0
- /package/src/{agent → agents/providers}/credentials/claude.js +0 -0
- /package/src/{agent → agents/providers}/credentials/codex.js +0 -0
- /package/src/{agent → agents/providers}/credentials/index.js +0 -0
- /package/src/{chat → app/chat}/agentBar.js +0 -0
- /package/src/{chat → app/chat}/agentDirectory.js +0 -0
- /package/src/{chat → app/chat}/cronScheduler.js +0 -0
- /package/src/{chat → app/chat}/daemonCoordinator.js +0 -0
- /package/src/{chat → app/chat}/daemonReconnect.js +0 -0
- /package/src/{chat → app/chat}/daemonTransportDefaults.js +0 -0
- /package/src/{chat → app/chat}/inputMath.js +0 -0
- /package/src/{chat → app/chat}/rawKeyMap.js +0 -0
- /package/src/{chat → app/chat}/settingsController.js +0 -0
- /package/src/{chat → app/chat}/text.js +0 -0
- /package/src/{chat → app/chat}/transientAgentState.js +0 -0
- /package/src/{cli → app/cli}/busCoreCommands.js +0 -0
- /package/src/{skills/index.js → app/cli/features/skills.js} +0 -0
- /package/src/{bus → coordination/bus}/nickname.js +0 -0
- /package/src/{bus → coordination/bus}/queue.js +0 -0
- /package/src/{context → coordination/context}/decisions.js +0 -0
- /package/src/{context → coordination/context}/doctor.js +0 -0
- /package/src/{context → coordination/context}/index.js +0 -0
- /package/src/{context → coordination/context}/sync.js +0 -0
- /package/src/{ufoo → coordination/state}/agentsStore.js +0 -0
- /package/src/{ufoo → coordination/state}/paths.js +0 -0
- /package/src/{controller → orchestration/controller}/launchRouting.js +0 -0
- /package/src/{controller → orchestration/controller}/routerFastPath.js +0 -0
- /package/src/{controller → orchestration/controller}/routerFinalize.js +0 -0
- /package/src/{group → orchestration/groups}/diagram.js +0 -0
- /package/src/{group → orchestration/groups}/templateValidation.js +0 -0
- /package/src/{solo → orchestration/solo}/commands.js +0 -0
- /package/src/{shared → runtime/contracts}/eventContract.js +0 -0
- /package/src/{shared → runtime/contracts}/ptySocketContract.js +0 -0
- /package/src/{providerapi → runtime/privacy}/redactor.js +0 -0
- /package/src/{providerapi → runtime/privacy}/shadowDiff.js +0 -0
- /package/src/{projects → runtime/projects}/identity.js +0 -0
- /package/src/{projects → runtime/projects}/index.js +0 -0
- /package/src/{projects → runtime/projects}/projectId.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapterContract.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/externalAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/hostAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/internalQueueAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/terminalAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/adapters/tmuxAdapter.js +0 -0
- /package/src/{terminal → runtime/terminal}/detect.js +0 -0
- /package/src/{terminal → runtime/terminal}/index.js +0 -0
- /package/src/{terminal → runtime/terminal}/iterm2.js +0 -0
- /package/src/{utils → ui/format}/banner.js +0 -0
- /package/src/{shared → ui/format}/markdownRenderer.js +0 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ink-based ucode TUI rendered via React + ink.
|
|
5
|
+
*
|
|
6
|
+
* Activation: this is the only ucode TUI.
|
|
7
|
+
*
|
|
8
|
+
* Coverage today: banner, scrolling log via <Static>, tool-call merge with
|
|
9
|
+
* Ctrl+O expand, multiline editor (see MultilineInput.js), spinner+phase
|
|
10
|
+
* status line, abortController-driven Esc cancel, input history Up/Down,
|
|
11
|
+
* agent selection footer, runSingleCommand + runNaturalLanguageTask path.
|
|
12
|
+
*
|
|
13
|
+
* Also covers blessed parity branches: background tasks, ubus, resume,
|
|
14
|
+
* nl_bg, and autoBus polling.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { runInk } = require("../runInk");
|
|
18
|
+
const fmt = require("../format");
|
|
19
|
+
const { createMultilineInput } = require("./MultilineInput");
|
|
20
|
+
|
|
21
|
+
function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
22
|
+
const { useEffect, useState, useCallback, useRef } = React;
|
|
23
|
+
const { Box, Text, useInput, useApp, useStdout } = ink;
|
|
24
|
+
const h = React.createElement;
|
|
25
|
+
const MultilineInput = createMultilineInput({ React, ink });
|
|
26
|
+
|
|
27
|
+
const banner = fmt.buildUcodeBannerLines({
|
|
28
|
+
model: (props.state && props.state.model) || process.env.UFOO_UCODE_MODEL || "",
|
|
29
|
+
engine: (props.state && props.state.engine) || "ufoo-core",
|
|
30
|
+
workspaceRoot: props.workspaceRoot,
|
|
31
|
+
sessionId: (props.state && props.state.sessionId) || "",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return function UcodeApp() {
|
|
35
|
+
const [logLines, setLogLines] = useState(() =>
|
|
36
|
+
banner.concat([""]).map((line, idx) => ({ id: `b-${idx}`, text: line }))
|
|
37
|
+
);
|
|
38
|
+
const [draft, setDraft] = useState("");
|
|
39
|
+
const [draftVersion, setDraftVersion] = useState(0);
|
|
40
|
+
// status: idle when message === "". `type` picks a STATUS_INDICATORS
|
|
41
|
+
// bucket; `showTimer` and `startedAt` reproduce the blessed spinner
|
|
42
|
+
// controls. The BG suffix is computed from backgroundTasksRef and
|
|
43
|
+
// appended by computeStatusText below.
|
|
44
|
+
const [status, setStatus] = useState({
|
|
45
|
+
message: "",
|
|
46
|
+
type: "thinking",
|
|
47
|
+
showTimer: false,
|
|
48
|
+
startedAt: 0,
|
|
49
|
+
});
|
|
50
|
+
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
51
|
+
const [, setNowTick] = useState(0);
|
|
52
|
+
const [size, setSize] = useState({ cols: 0, rows: 0 });
|
|
53
|
+
const [agents, setAgents] = useState([]);
|
|
54
|
+
const [selectedAgentIndex, setSelectedAgentIndex] = useState(-1);
|
|
55
|
+
const [agentSelectionMode, setAgentSelectionMode] = useState(false);
|
|
56
|
+
// activeMerge holds the in-flight group of consecutive tool calls.
|
|
57
|
+
// Rendered as a single live row below <Static>; promoted to <Static>
|
|
58
|
+
// and cleared whenever a non-tool log line arrives. lastMergeRef tracks
|
|
59
|
+
// the most recent group with >=2 entries so Ctrl+O can still expand it
|
|
60
|
+
// after the group has been frozen into the log.
|
|
61
|
+
const [activeMerge, setActiveMerge] = useState(null);
|
|
62
|
+
const lastMergeRef = useRef(null);
|
|
63
|
+
// pendingTaskRef holds the live AbortController for the current
|
|
64
|
+
// runNaturalLanguageTask call so Esc can cancel it. We use a ref (not
|
|
65
|
+
// state) because the value is consumed inside the run loop, not by
|
|
66
|
+
// render.
|
|
67
|
+
const pendingTaskRef = useRef(null);
|
|
68
|
+
const backgroundTasksRef = useRef(new Map());
|
|
69
|
+
const backgroundSeqRef = useRef(0);
|
|
70
|
+
const autoBusQueuedRef = useRef(false);
|
|
71
|
+
const autoBusErrorRef = useRef("");
|
|
72
|
+
const [, setBackgroundVersion] = useState(0);
|
|
73
|
+
// inputHistory mirrors blessed's flat history list. Up walks back
|
|
74
|
+
// through it when the editor reports the cursor is already on the top
|
|
75
|
+
// visual row (i.e. moveCursorVertically returned moved=false).
|
|
76
|
+
const [inputHistory, setInputHistory] = useState([]);
|
|
77
|
+
const [historyIndex, setHistoryIndex] = useState(0);
|
|
78
|
+
const { exit } = useApp();
|
|
79
|
+
const { stdout } = useStdout();
|
|
80
|
+
const lineSeqRef = useRef(banner.length + 1);
|
|
81
|
+
const mergeIdRef = useRef(0);
|
|
82
|
+
|
|
83
|
+
const targetAgent = agentSelectionMode && selectedAgentIndex >= 0
|
|
84
|
+
? agents[selectedAgentIndex]
|
|
85
|
+
: null;
|
|
86
|
+
|
|
87
|
+
const bumpBackground = useCallback(() => setBackgroundVersion((v) => v + 1), []);
|
|
88
|
+
|
|
89
|
+
const getBackgroundSuffix = useCallback(() => {
|
|
90
|
+
const tasks = backgroundTasksRef.current;
|
|
91
|
+
if (!tasks || tasks.size === 0) return "";
|
|
92
|
+
let running = 0;
|
|
93
|
+
let done = 0;
|
|
94
|
+
let failed = 0;
|
|
95
|
+
for (const task of tasks.values()) {
|
|
96
|
+
if (!task) continue;
|
|
97
|
+
if (task.status === "running") running += 1;
|
|
98
|
+
else if (task.status === "done") done += 1;
|
|
99
|
+
else if (task.status === "failed") failed += 1;
|
|
100
|
+
}
|
|
101
|
+
const parts = [];
|
|
102
|
+
if (running) parts.push(`${running} running`);
|
|
103
|
+
if (done) parts.push(`${done} done`);
|
|
104
|
+
if (failed) parts.push(`${failed} failed`);
|
|
105
|
+
return parts.length ? ` · BG ${parts.join("/")}` : "";
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const getAgentLabel = useCallback((agent) => {
|
|
109
|
+
if (!agent) return "";
|
|
110
|
+
if (agent.nickname) return agent.nickname;
|
|
111
|
+
const idTail = String(agent.id || "").slice(0, 6);
|
|
112
|
+
return idTail ? `${agent.type}:${idTail}` : agent.type;
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const ucodeModel = (props.state && props.state.model)
|
|
116
|
+
|| process.env.UFOO_UCODE_MODEL
|
|
117
|
+
|| "default";
|
|
118
|
+
let workspaceLabel = "";
|
|
119
|
+
try {
|
|
120
|
+
const os = require("os");
|
|
121
|
+
const path = require("path");
|
|
122
|
+
const root = props.workspaceRoot || process.cwd();
|
|
123
|
+
const home = os.homedir();
|
|
124
|
+
let normalized = root.startsWith(home) ? root.replace(home, "~") : root;
|
|
125
|
+
workspaceLabel = path.normalize(normalized);
|
|
126
|
+
} catch {
|
|
127
|
+
workspaceLabel = String(props.workspaceRoot || "");
|
|
128
|
+
}
|
|
129
|
+
const hintParts = [ucodeModel];
|
|
130
|
+
if (workspaceLabel) hintParts.push(workspaceLabel);
|
|
131
|
+
const agentsHint = hintParts.join(" · ");
|
|
132
|
+
|
|
133
|
+
const selfSubscriberId = String(
|
|
134
|
+
(props.autoBus && props.autoBus.subscriberId) ||
|
|
135
|
+
process.env.UFOO_SUBSCRIBER_ID ||
|
|
136
|
+
""
|
|
137
|
+
).trim();
|
|
138
|
+
|
|
139
|
+
const refreshAgents = useCallback(() => {
|
|
140
|
+
try {
|
|
141
|
+
const list = fmt.filterSelectableAgents(
|
|
142
|
+
fmt.loadActiveAgents(props.workspaceRoot),
|
|
143
|
+
selfSubscriberId
|
|
144
|
+
);
|
|
145
|
+
setAgents(list);
|
|
146
|
+
} catch {
|
|
147
|
+
// loadActiveAgents already swallows errors and returns []. This catch
|
|
148
|
+
// is just a belt-and-braces guard against future regressions.
|
|
149
|
+
}
|
|
150
|
+
}, [selfSubscriberId]);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!interactive) return undefined;
|
|
154
|
+
refreshAgents();
|
|
155
|
+
const timer = setInterval(refreshAgents, 3000);
|
|
156
|
+
return () => clearInterval(timer);
|
|
157
|
+
}, [interactive, refreshAgents]);
|
|
158
|
+
|
|
159
|
+
// Keep selection within bounds when the agents list changes.
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (selectedAgentIndex < 0) return;
|
|
162
|
+
if (agents.length === 0) {
|
|
163
|
+
setSelectedAgentIndex(-1);
|
|
164
|
+
setAgentSelectionMode(false);
|
|
165
|
+
} else if (selectedAgentIndex >= agents.length) {
|
|
166
|
+
setSelectedAgentIndex(agents.length - 1);
|
|
167
|
+
}
|
|
168
|
+
}, [agents, selectedAgentIndex]);
|
|
169
|
+
|
|
170
|
+
const onArrowDownAtEnd = useCallback((currentValue) => {
|
|
171
|
+
// History first: if we're past the bottom of a multi-line edit, walk
|
|
172
|
+
// forward through the recent history. Reaching the end clears the
|
|
173
|
+
// input the same way blessed does.
|
|
174
|
+
if (inputHistory.length > 0) {
|
|
175
|
+
const transition = fmt.resolveHistoryDownTransition({
|
|
176
|
+
inputHistory,
|
|
177
|
+
historyIndex,
|
|
178
|
+
currentValue,
|
|
179
|
+
});
|
|
180
|
+
if (transition.moved) {
|
|
181
|
+
setHistoryIndex(transition.nextHistoryIndex);
|
|
182
|
+
setDraft(transition.nextValue);
|
|
183
|
+
setDraftVersion((v) => v + 1);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (agents.length === 0) return;
|
|
188
|
+
const decision = fmt.resolveAgentSelectionOnDown({
|
|
189
|
+
agentSelectionMode,
|
|
190
|
+
selectedAgentIndex,
|
|
191
|
+
totalAgents: agents.length,
|
|
192
|
+
});
|
|
193
|
+
if (decision.action === "enter") {
|
|
194
|
+
setSelectedAgentIndex(decision.index);
|
|
195
|
+
setAgentSelectionMode(true);
|
|
196
|
+
}
|
|
197
|
+
}, [inputHistory, historyIndex, agents, agentSelectionMode, selectedAgentIndex]);
|
|
198
|
+
|
|
199
|
+
const onArrowUpAtStart = useCallback(() => {
|
|
200
|
+
// History first: if we're already on the top visual row, walk back
|
|
201
|
+
// through the recent history before doing anything else.
|
|
202
|
+
if (inputHistory.length > 0) {
|
|
203
|
+
const nextIndex = Math.max(0, historyIndex - 1);
|
|
204
|
+
if (nextIndex !== historyIndex || draft !== inputHistory[nextIndex]) {
|
|
205
|
+
setHistoryIndex(nextIndex);
|
|
206
|
+
setDraft(inputHistory[nextIndex] || "");
|
|
207
|
+
setDraftVersion((v) => v + 1);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (agentSelectionMode) {
|
|
212
|
+
setAgentSelectionMode(false);
|
|
213
|
+
setSelectedAgentIndex(-1);
|
|
214
|
+
}
|
|
215
|
+
}, [inputHistory, historyIndex, draft, agentSelectionMode]);
|
|
216
|
+
|
|
217
|
+
const onArrowSideAtEmpty = useCallback((direction) => {
|
|
218
|
+
if (!agentSelectionMode) return;
|
|
219
|
+
if (agents.length === 0) return;
|
|
220
|
+
const next = fmt.cycleAgentSelectionIndex(
|
|
221
|
+
selectedAgentIndex,
|
|
222
|
+
agents.length,
|
|
223
|
+
direction
|
|
224
|
+
);
|
|
225
|
+
setSelectedAgentIndex(next);
|
|
226
|
+
}, [agents, agentSelectionMode, selectedAgentIndex]);
|
|
227
|
+
|
|
228
|
+
const appendLogLine = useCallback((text) => {
|
|
229
|
+
setLogLines((prev) => {
|
|
230
|
+
const id = `l-${lineSeqRef.current}`;
|
|
231
|
+
lineSeqRef.current += 1;
|
|
232
|
+
const next = prev.concat([{ id, text: String(text || "") }]);
|
|
233
|
+
return next.length > 1000 ? next.slice(-1000) : next;
|
|
234
|
+
});
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
237
|
+
const renderMergeText = useCallback((merge) => {
|
|
238
|
+
if (!merge || !Array.isArray(merge.entries)) return "";
|
|
239
|
+
return fmt.buildToolMergeRowText(merge.entries);
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
// Promote the in-flight tool group (if any) to a permanent log line.
|
|
243
|
+
// Called before any non-tool text is logged, so the group "freezes"
|
|
244
|
+
// exactly the way blessed updates the line in place when the next text
|
|
245
|
+
// arrives.
|
|
246
|
+
const flushActiveMerge = useCallback(() => {
|
|
247
|
+
setActiveMerge((current) => {
|
|
248
|
+
if (!current) return null;
|
|
249
|
+
appendLogLine(renderMergeText(current));
|
|
250
|
+
return null;
|
|
251
|
+
});
|
|
252
|
+
}, [appendLogLine, renderMergeText]);
|
|
253
|
+
|
|
254
|
+
const logToolHint = useCallback((entry, payload) => {
|
|
255
|
+
const tool = String((entry && entry.tool) || "").trim().toLowerCase();
|
|
256
|
+
if (!tool) return;
|
|
257
|
+
const resObj = payload && typeof payload === "object" ? payload : (entry && entry.result) || {};
|
|
258
|
+
const phase = String((entry && entry.phase) || "").trim().toLowerCase();
|
|
259
|
+
const isError = phase === "error" || resObj.ok === false;
|
|
260
|
+
const detail = tool === "bash" ? fmt.normalizeBashToolCommand(entry && entry.args, resObj) : "";
|
|
261
|
+
const errorText = String((entry && entry.error) || resObj.error || "").trim();
|
|
262
|
+
const toolEntry = fmt.normalizeToolMergeEntry({ tool, detail, isError, errorText });
|
|
263
|
+
|
|
264
|
+
setActiveMerge((current) => {
|
|
265
|
+
let next;
|
|
266
|
+
if (current) {
|
|
267
|
+
next = { ...current, entries: current.entries.concat([toolEntry]) };
|
|
268
|
+
} else {
|
|
269
|
+
mergeIdRef.current += 1;
|
|
270
|
+
next = { id: mergeIdRef.current, entries: [toolEntry], expanded: false };
|
|
271
|
+
}
|
|
272
|
+
if (next.entries.length >= 2) lastMergeRef.current = next;
|
|
273
|
+
return next;
|
|
274
|
+
});
|
|
275
|
+
}, []);
|
|
276
|
+
|
|
277
|
+
const appendLogText = useCallback((text) => {
|
|
278
|
+
// Multi-line text → split into separate log entries so <Static> keys
|
|
279
|
+
// stay stable when streaming arrives line-by-line. Always promote any
|
|
280
|
+
// in-flight tool group first so it freezes above the new text.
|
|
281
|
+
const raw = String(text == null ? "" : text);
|
|
282
|
+
if (!raw) return;
|
|
283
|
+
flushActiveMerge();
|
|
284
|
+
const lines = raw.split(/\r?\n/);
|
|
285
|
+
for (const line of lines) appendLogLine(line);
|
|
286
|
+
}, [appendLogLine, flushActiveMerge]);
|
|
287
|
+
|
|
288
|
+
const expandLastMerge = useCallback(() => {
|
|
289
|
+
// Try the active group first; fall back to the most recent frozen one.
|
|
290
|
+
// Both paths must keep the "expand only once" guarantee that blessed
|
|
291
|
+
// enforces via group.expanded.
|
|
292
|
+
const active = activeMerge;
|
|
293
|
+
const candidate = (active && !active.expanded && active.entries.length >= 2)
|
|
294
|
+
? active
|
|
295
|
+
: (lastMergeRef.current && !lastMergeRef.current.expanded && lastMergeRef.current.entries.length >= 2
|
|
296
|
+
? lastMergeRef.current
|
|
297
|
+
: null);
|
|
298
|
+
if (!candidate) return;
|
|
299
|
+
|
|
300
|
+
const lines = fmt.buildMergedToolExpandedLines(candidate.entries);
|
|
301
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
302
|
+
const branch = i === lines.length - 1 ? "└" : "│";
|
|
303
|
+
appendLogLine(`${branch} ${lines[i]}`);
|
|
304
|
+
}
|
|
305
|
+
candidate.expanded = true;
|
|
306
|
+
if (active && active.id === candidate.id) setActiveMerge(null);
|
|
307
|
+
if (lastMergeRef.current && lastMergeRef.current.id === candidate.id) {
|
|
308
|
+
lastMergeRef.current = null;
|
|
309
|
+
}
|
|
310
|
+
}, [activeMerge, appendLogLine]);
|
|
311
|
+
|
|
312
|
+
const runChainRef = useRef(Promise.resolve());
|
|
313
|
+
|
|
314
|
+
const executeLine = useCallback(async (rawValue) => {
|
|
315
|
+
const normalized = String(rawValue || "").replace(/\r?\n/g, " ").trim();
|
|
316
|
+
if (!normalized) return;
|
|
317
|
+
appendLogLine(`› ${normalized}`);
|
|
318
|
+
|
|
319
|
+
const runtimeWorkspace = String(
|
|
320
|
+
(props.state && props.state.workspaceRoot) || props.workspaceRoot || process.cwd()
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
let result;
|
|
324
|
+
try {
|
|
325
|
+
result = props.runSingleCommand(normalized, runtimeWorkspace);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
appendLogText(`Error: ${err && err.message ? err.message : "command parse failed"}`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!result || typeof result !== "object") return;
|
|
331
|
+
|
|
332
|
+
switch (result.kind) {
|
|
333
|
+
case "empty":
|
|
334
|
+
return;
|
|
335
|
+
case "exit":
|
|
336
|
+
exit();
|
|
337
|
+
return;
|
|
338
|
+
case "probe":
|
|
339
|
+
return;
|
|
340
|
+
case "help":
|
|
341
|
+
case "error":
|
|
342
|
+
appendLogText(result.output || "");
|
|
343
|
+
return;
|
|
344
|
+
case "ubus": {
|
|
345
|
+
setStatus({ message: "Checking bus messages...", type: "typing", showTimer: false, startedAt: Date.now() });
|
|
346
|
+
try {
|
|
347
|
+
const { extractAgentNickname } = require("../../code/agent");
|
|
348
|
+
const ubusResult = await props.runUbusCommand(props.state, {
|
|
349
|
+
workspaceRoot: runtimeWorkspace,
|
|
350
|
+
onMessageReceived: (msg) => {
|
|
351
|
+
const nickname = extractAgentNickname(msg && msg.from) || (msg && msg.from) || "bus";
|
|
352
|
+
appendLogText(`${nickname}: ${(msg && msg.task) || ""}`);
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
if (!ubusResult || !ubusResult.ok) {
|
|
356
|
+
appendLogText(`Error: ${(ubusResult && ubusResult.error) || "ubus failed"}`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const exchanges = Array.isArray(ubusResult.messageExchanges) ? ubusResult.messageExchanges : [];
|
|
360
|
+
if (exchanges.length > 0) {
|
|
361
|
+
for (const exchange of exchanges) {
|
|
362
|
+
const nickname = extractAgentNickname(exchange && exchange.from) || (exchange && exchange.from) || "bus";
|
|
363
|
+
appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}`);
|
|
364
|
+
}
|
|
365
|
+
} else if (Number(ubusResult.handled) === 0) {
|
|
366
|
+
appendLogText("ubus: no pending messages.");
|
|
367
|
+
}
|
|
368
|
+
if (typeof props.persistSessionState === "function") {
|
|
369
|
+
const persisted = props.persistSessionState(props.state);
|
|
370
|
+
if (!persisted || persisted.ok === false) {
|
|
371
|
+
appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} finally {
|
|
375
|
+
setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
case "resume": {
|
|
380
|
+
if (typeof props.resumeSessionState !== "function") {
|
|
381
|
+
appendLogText("Error: resume unsupported");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const resumed = props.resumeSessionState(props.state, result.sessionId, runtimeWorkspace);
|
|
385
|
+
if (!resumed || !resumed.ok) {
|
|
386
|
+
appendLogText(`Error: ${(resumed && resumed.error) || "resume failed"}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
appendLogText(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
case "tool": {
|
|
393
|
+
const payload = result.result && typeof result.result === "object" ? result.result : {};
|
|
394
|
+
logToolHint({
|
|
395
|
+
tool: result.tool,
|
|
396
|
+
args: result.args,
|
|
397
|
+
phase: payload.ok === false ? "error" : "end",
|
|
398
|
+
error: payload.error || "",
|
|
399
|
+
}, payload);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
case "nl_bg": {
|
|
403
|
+
backgroundSeqRef.current += 1;
|
|
404
|
+
const jobId = `bg-${Date.now().toString(36)}-${backgroundSeqRef.current.toString(36)}`;
|
|
405
|
+
const taskRecord = {
|
|
406
|
+
id: jobId,
|
|
407
|
+
task: result.task,
|
|
408
|
+
status: "running",
|
|
409
|
+
startedAt: Date.now(),
|
|
410
|
+
summary: "",
|
|
411
|
+
};
|
|
412
|
+
backgroundTasksRef.current.set(jobId, taskRecord);
|
|
413
|
+
bumpBackground();
|
|
414
|
+
setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
|
|
415
|
+
appendLogText(`[${jobId}] started in background.`);
|
|
416
|
+
|
|
417
|
+
const bgState = {
|
|
418
|
+
workspaceRoot: props.state && props.state.workspaceRoot,
|
|
419
|
+
provider: props.state && props.state.provider,
|
|
420
|
+
model: props.state && props.state.model,
|
|
421
|
+
engine: props.state && props.state.engine,
|
|
422
|
+
context: props.state && props.state.context,
|
|
423
|
+
nlMessages: Array.isArray(props.state && props.state.nlMessages) ? props.state.nlMessages.slice() : [],
|
|
424
|
+
sessionId: "",
|
|
425
|
+
timeoutMs: props.state && props.state.timeoutMs,
|
|
426
|
+
jsonOutput: false,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
Promise.resolve()
|
|
430
|
+
.then(() => props.runNaturalLanguageTask(result.task, bgState))
|
|
431
|
+
.then((nlResult) => {
|
|
432
|
+
taskRecord.status = nlResult && nlResult.ok ? "done" : "failed";
|
|
433
|
+
taskRecord.finishedAt = Date.now();
|
|
434
|
+
taskRecord.summary = String(props.formatNlResult(nlResult, false) || "").trim();
|
|
435
|
+
const title = taskRecord.status === "done" ? "done" : "failed";
|
|
436
|
+
appendLogText(`[${jobId}] ${title}: ${taskRecord.summary || "no summary"}`);
|
|
437
|
+
})
|
|
438
|
+
.catch((err) => {
|
|
439
|
+
taskRecord.status = "failed";
|
|
440
|
+
taskRecord.finishedAt = Date.now();
|
|
441
|
+
taskRecord.summary = err && err.message ? String(err.message) : "background task failed";
|
|
442
|
+
appendLogText(`[${jobId}] failed: ${taskRecord.summary}`);
|
|
443
|
+
})
|
|
444
|
+
.finally(() => {
|
|
445
|
+
bumpBackground();
|
|
446
|
+
setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
case "nl": {
|
|
451
|
+
const startedAt = Date.now();
|
|
452
|
+
const abortController = new AbortController();
|
|
453
|
+
pendingTaskRef.current = { abortController, startedAt };
|
|
454
|
+
const setNlStatus = (msg) => setStatus({
|
|
455
|
+
message: msg,
|
|
456
|
+
type: "thinking",
|
|
457
|
+
showTimer: true,
|
|
458
|
+
startedAt,
|
|
459
|
+
});
|
|
460
|
+
setNlStatus("Waiting for model...");
|
|
461
|
+
let streamBuf = "";
|
|
462
|
+
let sawStreamText = false;
|
|
463
|
+
let nlResult = null;
|
|
464
|
+
try {
|
|
465
|
+
nlResult = await props.runNaturalLanguageTask(result.task, props.state, {
|
|
466
|
+
signal: abortController.signal,
|
|
467
|
+
onPhase: (event) => {
|
|
468
|
+
if (!event || typeof event !== "object") return;
|
|
469
|
+
if (event.type === "request_start") setNlStatus("Waiting for model...");
|
|
470
|
+
else if (event.type === "thinking_delta") setNlStatus("Thinking...");
|
|
471
|
+
else if (event.type === "text_delta") setNlStatus("Generating response...");
|
|
472
|
+
else if (event.type === "tool_request") {
|
|
473
|
+
const label = fmt.TOOL_LABELS[String(event.name || "").toLowerCase()] ||
|
|
474
|
+
`Calling ${event.name}`;
|
|
475
|
+
setNlStatus(`${label}...`);
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
onDelta: (delta) => {
|
|
479
|
+
const text = String(delta || "");
|
|
480
|
+
if (!text) return;
|
|
481
|
+
if (/[^\s]/.test(text)) sawStreamText = true;
|
|
482
|
+
streamBuf += text;
|
|
483
|
+
const parts = streamBuf.split(/\r?\n/);
|
|
484
|
+
while (parts.length > 1) {
|
|
485
|
+
appendLogLine(parts.shift());
|
|
486
|
+
}
|
|
487
|
+
streamBuf = parts[0];
|
|
488
|
+
},
|
|
489
|
+
onToolLog: (entry) => {
|
|
490
|
+
if (!entry || typeof entry !== "object") return;
|
|
491
|
+
if (entry.tool && entry.phase === "start") {
|
|
492
|
+
const label = fmt.TOOL_LABELS[String(entry.tool || "").toLowerCase()] ||
|
|
493
|
+
`Calling ${entry.tool}`;
|
|
494
|
+
setNlStatus(`${label}...`);
|
|
495
|
+
}
|
|
496
|
+
logToolHint(entry, entry.result);
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
} catch (err) {
|
|
500
|
+
appendLogText(`Error: ${err && err.message ? err.message : "agent loop failed"}`);
|
|
501
|
+
return;
|
|
502
|
+
} finally {
|
|
503
|
+
pendingTaskRef.current = null;
|
|
504
|
+
setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
|
|
505
|
+
}
|
|
506
|
+
if (streamBuf) {
|
|
507
|
+
if (/[^\s]/.test(streamBuf)) sawStreamText = true;
|
|
508
|
+
appendLogLine(streamBuf);
|
|
509
|
+
}
|
|
510
|
+
// Skip the summary echo when the model already streamed its
|
|
511
|
+
// response in full — otherwise the user sees the same text twice.
|
|
512
|
+
// Mirrors the shouldSkipSummary check in tui.js.
|
|
513
|
+
const streamed = Boolean(nlResult && nlResult.streamed);
|
|
514
|
+
const ok = Boolean(nlResult && nlResult.ok);
|
|
515
|
+
const shouldSkipSummary = streamed && ok && sawStreamText;
|
|
516
|
+
if (!shouldSkipSummary) {
|
|
517
|
+
const summary = props.formatNlResult(nlResult, false);
|
|
518
|
+
if (summary) appendLogText(summary);
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const persisted = props.persistSessionState(props.state);
|
|
522
|
+
if (persisted && persisted.ok === false) {
|
|
523
|
+
appendLogText(
|
|
524
|
+
`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${persisted.error || "unknown error"}`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
} catch {
|
|
528
|
+
// persistSessionState failures shouldn't crash the TUI.
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
default:
|
|
533
|
+
if (result.output) appendLogText(result.output);
|
|
534
|
+
}
|
|
535
|
+
}, [appendLogLine, appendLogText, exit, props, logToolHint]);
|
|
536
|
+
// ^ `props` is captured by the createUcodeApp closure on a single mount,
|
|
537
|
+
// so its reference is stable across renders even though it looks like a
|
|
538
|
+
// changing dep to React's exhaustive-deps lint.
|
|
539
|
+
|
|
540
|
+
const runAutoBusOnce = useCallback(async () => {
|
|
541
|
+
const autoBus = props.autoBus || {};
|
|
542
|
+
if (!autoBus.enabled || pendingTaskRef.current) return;
|
|
543
|
+
const getPendingCount = typeof autoBus.getPendingCount === "function"
|
|
544
|
+
? autoBus.getPendingCount
|
|
545
|
+
: () => 0;
|
|
546
|
+
if (Number(getPendingCount()) <= 0) {
|
|
547
|
+
autoBusErrorRef.current = "";
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const abortController = new AbortController();
|
|
552
|
+
const startedAt = Date.now();
|
|
553
|
+
pendingTaskRef.current = { abortController, startedAt };
|
|
554
|
+
setStatus({
|
|
555
|
+
message: "Processing bus messages...",
|
|
556
|
+
type: "thinking",
|
|
557
|
+
showTimer: true,
|
|
558
|
+
startedAt,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
const { extractAgentNickname } = require("../../code/agent");
|
|
563
|
+
const ubusResult = await props.runUbusCommand(props.state, {
|
|
564
|
+
workspaceRoot: props.workspaceRoot,
|
|
565
|
+
subscriberId: autoBus.subscriberId,
|
|
566
|
+
signal: abortController.signal,
|
|
567
|
+
onMessageReceived: (msg) => {
|
|
568
|
+
const nickname = extractAgentNickname(msg && msg.from) || (msg && msg.from) || "bus";
|
|
569
|
+
appendLogText(`${nickname}: ${(msg && msg.task) || ""}`);
|
|
570
|
+
setStatus({
|
|
571
|
+
message: "Working on task...",
|
|
572
|
+
type: "thinking",
|
|
573
|
+
showTimer: true,
|
|
574
|
+
startedAt,
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
if (!ubusResult || !ubusResult.ok) {
|
|
580
|
+
const nextError = String((ubusResult && ubusResult.error) || "ubus failed");
|
|
581
|
+
if (nextError !== autoBusErrorRef.current) {
|
|
582
|
+
autoBusErrorRef.current = nextError;
|
|
583
|
+
appendLogText(`Error: ${nextError}`);
|
|
584
|
+
}
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
autoBusErrorRef.current = "";
|
|
589
|
+
const exchanges = Array.isArray(ubusResult.messageExchanges) ? ubusResult.messageExchanges : [];
|
|
590
|
+
for (const exchange of exchanges) {
|
|
591
|
+
const nickname = extractAgentNickname(exchange && exchange.from) || (exchange && exchange.from) || "bus";
|
|
592
|
+
appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}`);
|
|
593
|
+
}
|
|
594
|
+
if (Number(ubusResult.handled) > 0 && typeof props.persistSessionState === "function") {
|
|
595
|
+
const persisted = props.persistSessionState(props.state);
|
|
596
|
+
if (!persisted || persisted.ok === false) {
|
|
597
|
+
appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} finally {
|
|
601
|
+
pendingTaskRef.current = null;
|
|
602
|
+
setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
|
|
603
|
+
}
|
|
604
|
+
}, [appendLogText, props]);
|
|
605
|
+
|
|
606
|
+
useEffect(() => {
|
|
607
|
+
if (!interactive || !(props.autoBus && props.autoBus.enabled)) return undefined;
|
|
608
|
+
const schedule = () => {
|
|
609
|
+
if (autoBusQueuedRef.current || pendingTaskRef.current) return;
|
|
610
|
+
const getPendingCount = typeof props.autoBus.getPendingCount === "function"
|
|
611
|
+
? props.autoBus.getPendingCount
|
|
612
|
+
: () => 0;
|
|
613
|
+
if (Number(getPendingCount()) <= 0) return;
|
|
614
|
+
autoBusQueuedRef.current = true;
|
|
615
|
+
runChainRef.current = runChainRef.current
|
|
616
|
+
.then(() => runAutoBusOnce())
|
|
617
|
+
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : "ubus failed"}`))
|
|
618
|
+
.finally(() => {
|
|
619
|
+
autoBusQueuedRef.current = false;
|
|
620
|
+
});
|
|
621
|
+
};
|
|
622
|
+
const timer = setInterval(schedule, 1500);
|
|
623
|
+
schedule();
|
|
624
|
+
return () => clearInterval(timer);
|
|
625
|
+
}, [interactive, props.autoBus, runAutoBusOnce, appendLogText]);
|
|
626
|
+
|
|
627
|
+
const submit = useCallback((submitted) => {
|
|
628
|
+
const value = String(submitted == null ? draft : submitted);
|
|
629
|
+
const trimmed = value.trim();
|
|
630
|
+
if (!trimmed) return;
|
|
631
|
+
setDraft("");
|
|
632
|
+
setDraftVersion((v) => v + 1);
|
|
633
|
+
setInputHistory((prev) => {
|
|
634
|
+
const next = prev.concat([trimmed]).slice(-200);
|
|
635
|
+
setHistoryIndex(next.length);
|
|
636
|
+
return next;
|
|
637
|
+
});
|
|
638
|
+
// Serialize executions so streaming tasks don't interleave.
|
|
639
|
+
runChainRef.current = runChainRef.current
|
|
640
|
+
.then(() => executeLine(value))
|
|
641
|
+
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`));
|
|
642
|
+
}, [draft, executeLine, appendLogText]);
|
|
643
|
+
|
|
644
|
+
useEffect(() => {
|
|
645
|
+
if (!stdout) return undefined;
|
|
646
|
+
const update = () =>
|
|
647
|
+
setSize({ cols: stdout.columns || 0, rows: stdout.rows || 0 });
|
|
648
|
+
update();
|
|
649
|
+
stdout.on("resize", update);
|
|
650
|
+
return () => stdout.off("resize", update);
|
|
651
|
+
}, [stdout]);
|
|
652
|
+
|
|
653
|
+
// Drive the spinner + elapsed-timer redraws while a task is in flight.
|
|
654
|
+
useEffect(() => {
|
|
655
|
+
if (!status.message || status.type === "none") return undefined;
|
|
656
|
+
const timer = setInterval(() => {
|
|
657
|
+
setSpinnerTick((t) => t + 1);
|
|
658
|
+
if (status.showTimer) setNowTick((t) => t + 1);
|
|
659
|
+
}, 100);
|
|
660
|
+
return () => clearInterval(timer);
|
|
661
|
+
}, [status.message, status.type, status.showTimer]);
|
|
662
|
+
|
|
663
|
+
const statusText = useMemoStatusText(React, status, spinnerTick, getBackgroundSuffix());
|
|
664
|
+
|
|
665
|
+
// Top-level only catches Ctrl+C and Ctrl+O (expand last tool group);
|
|
666
|
+
// the editor handles all text editing.
|
|
667
|
+
useInput((input, key) => {
|
|
668
|
+
if (key.ctrl && input === "c") { exit(); return; }
|
|
669
|
+
if (key.ctrl && input === "o") { expandLastMerge(); return; }
|
|
670
|
+
}, { isActive: interactive });
|
|
671
|
+
|
|
672
|
+
return h(Box, { flexDirection: "column", width: "100%" },
|
|
673
|
+
h(Box, { flexDirection: "column", width: "100%" },
|
|
674
|
+
...logLines.map((item) =>
|
|
675
|
+
h(Text, { key: item.id }, item.text || " ")
|
|
676
|
+
)
|
|
677
|
+
),
|
|
678
|
+
activeMerge ? h(Box, null,
|
|
679
|
+
h(Text, { color: activeMerge.entries.some((e) => e.isError) ? "red" : "cyan" },
|
|
680
|
+
renderMergeText(activeMerge)
|
|
681
|
+
),
|
|
682
|
+
) : null,
|
|
683
|
+
h(Box, { marginTop: 1, width: "100%" },
|
|
684
|
+
h(Text, { color: "gray" }, statusText),
|
|
685
|
+
h(Box, { flexGrow: 1 }),
|
|
686
|
+
h(Text, { color: "gray" }, `v${fmt.UCODE_VERSION}`),
|
|
687
|
+
),
|
|
688
|
+
h(Box, { width: "100%" },
|
|
689
|
+
h(MultilineInput, {
|
|
690
|
+
value: draft,
|
|
691
|
+
valueVersion: draftVersion,
|
|
692
|
+
onChange: (next) => setDraft(next),
|
|
693
|
+
onSubmit: (value) => submit(value),
|
|
694
|
+
onCancel: () => {
|
|
695
|
+
// If a task is in flight, Esc requests cancellation. Otherwise
|
|
696
|
+
// it clears the agent selection (matches blessed). The text
|
|
697
|
+
// value is left alone so the user doesn't lose what they typed.
|
|
698
|
+
const pending = pendingTaskRef.current;
|
|
699
|
+
if (pending && pending.abortController && !pending.abortController.signal.aborted) {
|
|
700
|
+
try { pending.abortController.abort(); } catch { /* ignore */ }
|
|
701
|
+
appendLogLine("⚙ Cancellation requested. Stopping the current task...");
|
|
702
|
+
setStatus({
|
|
703
|
+
message: "Cancelling...",
|
|
704
|
+
type: "waiting",
|
|
705
|
+
showTimer: true,
|
|
706
|
+
startedAt: pending.startedAt,
|
|
707
|
+
});
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (agentSelectionMode) {
|
|
711
|
+
setAgentSelectionMode(false);
|
|
712
|
+
setSelectedAgentIndex(-1);
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
onArrowDownAtBottom: onArrowDownAtEnd,
|
|
716
|
+
onArrowUpAtTop: onArrowUpAtStart,
|
|
717
|
+
onArrowLeftAtEmpty: () => onArrowSideAtEmpty("left"),
|
|
718
|
+
onArrowRightAtEmpty: () => onArrowSideAtEmpty("right"),
|
|
719
|
+
width: Math.max(20, (size.cols || 80) - 4),
|
|
720
|
+
interactive,
|
|
721
|
+
placeholder: "",
|
|
722
|
+
promptPrefix: targetAgent ? `›@${getAgentLabel(targetAgent)} ` : "› ",
|
|
723
|
+
// The agents footer is rendered below the input. Matching chat's
|
|
724
|
+
// IME parking contract keeps the hardware cursor aligned with the
|
|
725
|
+
// inverse caret instead of drifting to the bottom of the frame.
|
|
726
|
+
linesBelowInput: 1,
|
|
727
|
+
}),
|
|
728
|
+
),
|
|
729
|
+
h(Box, { width: "100%" },
|
|
730
|
+
h(Text, { wrap: "truncate", color: "gray" }, "Agents: "),
|
|
731
|
+
agents.length === 0
|
|
732
|
+
? h(Text, { wrap: "truncate", color: "cyan" }, "none")
|
|
733
|
+
: (() => {
|
|
734
|
+
const labels = agents.map((a) => `@${getAgentLabel(a)}`);
|
|
735
|
+
// Reserve 1 col for borders, the "Agents: " prefix, the hint
|
|
736
|
+
// and a few spaces for safety. We just clamp aggressively
|
|
737
|
+
// when stdout.cols is unknown.
|
|
738
|
+
const cols = size.cols || 80;
|
|
739
|
+
const reservedForHint = fmt.displayCellWidth(` · ${agentsHint}`);
|
|
740
|
+
const budget = Math.max(20, cols - 10 - reservedForHint);
|
|
741
|
+
const plan = fmt.planAgentsFooter(
|
|
742
|
+
labels,
|
|
743
|
+
agentSelectionMode ? selectedAgentIndex : -1,
|
|
744
|
+
budget
|
|
745
|
+
);
|
|
746
|
+
return h(React.Fragment, null,
|
|
747
|
+
...plan.items.map((item, idx) =>
|
|
748
|
+
h(React.Fragment, { key: idx },
|
|
749
|
+
idx > 0 ? h(Text, { color: "gray" }, " ") : null,
|
|
750
|
+
h(Text, {
|
|
751
|
+
wrap: "truncate",
|
|
752
|
+
color: item.selected ? undefined : "cyan",
|
|
753
|
+
inverse: item.selected,
|
|
754
|
+
}, item.label),
|
|
755
|
+
)
|
|
756
|
+
),
|
|
757
|
+
plan.hint
|
|
758
|
+
? h(Text, { wrap: "truncate", color: "gray" }, plan.hint)
|
|
759
|
+
: null,
|
|
760
|
+
);
|
|
761
|
+
})(),
|
|
762
|
+
h(Text, { wrap: "truncate", color: "gray" }, ` · ${agentsHint}`),
|
|
763
|
+
),
|
|
764
|
+
);
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function runUcodeInkTui(props = {}) {
|
|
769
|
+
return new Promise((resolve, reject) => {
|
|
770
|
+
runInk(
|
|
771
|
+
(React, ink) => {
|
|
772
|
+
const UcodeApp = createUcodeApp({ React, ink, props });
|
|
773
|
+
return React.createElement(UcodeApp);
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
stdin: props.stdin || process.stdin,
|
|
777
|
+
stdout: props.stdout || process.stdout,
|
|
778
|
+
exitOnCtrlC: true,
|
|
779
|
+
}
|
|
780
|
+
)
|
|
781
|
+
.then(async (handle) => {
|
|
782
|
+
try {
|
|
783
|
+
await handle.waitUntilExit();
|
|
784
|
+
resolve({ code: 0 });
|
|
785
|
+
} catch (err) {
|
|
786
|
+
reject(err);
|
|
787
|
+
}
|
|
788
|
+
})
|
|
789
|
+
.catch(reject);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
module.exports = { runUcodeInkTui, createUcodeApp, computeStatusText };
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Pure status-line text builder used by the React component (and unit
|
|
797
|
+
* tests). Returns "UCODE · Ready" while idle and a spinner+message+timer
|
|
798
|
+
* combination while a task is in flight, mirroring updateStatus() in the
|
|
799
|
+
* blessed implementation.
|
|
800
|
+
*/
|
|
801
|
+
function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
|
|
802
|
+
const message = String((status && status.message) || "");
|
|
803
|
+
const suffix = String(backgroundSuffix || "");
|
|
804
|
+
if (!message) return `UCODE · Ready${suffix}`;
|
|
805
|
+
const type = String((status && status.type) || "thinking");
|
|
806
|
+
const indicators = fmt.STATUS_INDICATORS[type] || fmt.STATUS_INDICATORS.thinking;
|
|
807
|
+
const indicator = indicators[Math.max(0, Math.floor(Number(spinnerTick) || 0)) % indicators.length];
|
|
808
|
+
const startedAt = Number.isFinite(status && status.startedAt) ? status.startedAt : 0;
|
|
809
|
+
const timerText = status && status.showTimer && startedAt
|
|
810
|
+
? ` (${fmt.formatPendingElapsed(Date.now() - startedAt)}, esc cancel)`
|
|
811
|
+
: "";
|
|
812
|
+
return `${indicator} ${message}${timerText}${suffix}`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function useMemoStatusText(React, status, spinnerTick, backgroundSuffix = "") {
|
|
816
|
+
// Dependencies intentionally include startedAt so the timer ticks even
|
|
817
|
+
// when the message string is unchanged.
|
|
818
|
+
return React.useMemo(
|
|
819
|
+
() => computeStatusText(status, spinnerTick, backgroundSuffix),
|
|
820
|
+
[status, spinnerTick, backgroundSuffix]
|
|
821
|
+
);
|
|
822
|
+
}
|